added enabled tts learning setting, give user a warning message when tts not available for target language (#1227)
This commit is contained in:
parent
43040c4010
commit
9444aecfd3
9 changed files with 236 additions and 124 deletions
|
|
@ -4058,7 +4058,7 @@
|
|||
"roomDataMissing": "Some data may be missing from rooms in which you are not a member.",
|
||||
"updatePhoneOS": "You may need to update your device's OS version.",
|
||||
"wordsPerMinute": "Words per minute",
|
||||
"autoIGCToolName": "Run Language Assistance Automatically",
|
||||
"autoIGCToolName": "Run language assistance automatically",
|
||||
"autoIGCToolDescription": "Automatically run language assistance after typing messages",
|
||||
"runGrammarCorrection": "Check message",
|
||||
"grammarCorrectionFailed": "Issues to address",
|
||||
|
|
@ -4623,5 +4623,9 @@
|
|||
"maxXP": {}
|
||||
}
|
||||
},
|
||||
"registrationEmailMessage": "Please verify your email with a link sent there. In some cases, the email takes up to 5 minutes to arrive. Please also check your spam folder."
|
||||
"registrationEmailMessage": "Please verify your email with a link sent there. In some cases, the email takes up to 5 minutes to arrive. Please also check your spam folder.",
|
||||
"enableTTSToolName": "Enabled text-to-speech",
|
||||
"enableTTSToolDescription": "Allow the app to generate text-to-speech output for portions of text in your target language",
|
||||
"couldNotFindTTS": "We couldn't find a text-to-speech engine for your current target language. ",
|
||||
"ttsInstructionsHyperlink": "Click here to view instructions for downloading a new voice on your device."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ abstract class AppConfig {
|
|||
static String androidUpdateURL =
|
||||
"https://play.google.com/store/apps/details?id=com.talktolearn.chat";
|
||||
static String iosUpdateURL = "itms-apps://itunes.apple.com/app/id1445118630";
|
||||
|
||||
static String windowsTTSDownloadInstructions =
|
||||
"https://support.microsoft.com/en-us/topic/download-languages-and-voices-for-immersive-reader-read-mode-and-read-aloud-4c83a8d8-7486-42f7-8e46-2b0fdf753130";
|
||||
static String androidTTSDownloadInstructions =
|
||||
"https://support.google.com/accessibility/android/answer/6006983?hl=en";
|
||||
// Pangea#
|
||||
|
||||
static void loadFromJson(Map<String, dynamic> json) {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ enum ToolSetting {
|
|||
definitions,
|
||||
// translations,
|
||||
autoIGC,
|
||||
enableTTS,
|
||||
}
|
||||
|
||||
extension SettingCopy on ToolSetting {
|
||||
|
|
@ -249,6 +250,8 @@ extension SettingCopy on ToolSetting {
|
|||
// return L10n.of(context).messageTranslationsToolName;
|
||||
case ToolSetting.autoIGC:
|
||||
return L10n.of(context).autoIGCToolName;
|
||||
case ToolSetting.enableTTS:
|
||||
return L10n.of(context).enableTTSToolName;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +270,8 @@ extension SettingCopy on ToolSetting {
|
|||
// return L10n.of(context).translationsToolDescrption;
|
||||
case ToolSetting.autoIGC:
|
||||
return L10n.of(context).autoIGCToolDescription;
|
||||
case ToolSetting.enableTTS:
|
||||
return L10n.of(context).enableTTSToolDescription;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,6 +283,7 @@ extension SettingCopy on ToolSetting {
|
|||
case ToolSetting.immersionMode:
|
||||
return false;
|
||||
case ToolSetting.autoIGC:
|
||||
case ToolSetting.enableTTS:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ class UserToolSettings {
|
|||
bool immersionMode;
|
||||
bool definitions;
|
||||
bool autoIGC;
|
||||
bool enableTTS;
|
||||
|
||||
UserToolSettings({
|
||||
this.interactiveTranslator = true,
|
||||
|
|
@ -133,6 +134,7 @@ class UserToolSettings {
|
|||
this.immersionMode = false,
|
||||
this.definitions = true,
|
||||
this.autoIGC = true,
|
||||
this.enableTTS = true,
|
||||
});
|
||||
|
||||
factory UserToolSettings.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
@ -144,6 +146,7 @@ class UserToolSettings {
|
|||
immersionMode: false,
|
||||
definitions: json[ToolSetting.definitions.toString()] ?? true,
|
||||
autoIGC: json[ToolSetting.autoIGC.toString()] ?? true,
|
||||
enableTTS: json[ToolSetting.enableTTS.toString()] ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -153,6 +156,7 @@ class UserToolSettings {
|
|||
data[ToolSetting.immersionMode.toString()] = immersionMode;
|
||||
data[ToolSetting.definitions.toString()] = definitions;
|
||||
data[ToolSetting.autoIGC.toString()] = autoIGC;
|
||||
data[ToolSetting.enableTTS.toString()] = enableTTS;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
|||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/user_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning_view.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -16,6 +17,19 @@ class SettingsLearning extends StatefulWidget {
|
|||
|
||||
class SettingsLearningController extends State<SettingsLearning> {
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
final tts = TtsController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tts.setupTTS().then((_) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tts.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
setPublicProfile(bool isPublic) {
|
||||
pangeaController.userController.updateProfile((profile) {
|
||||
|
|
@ -50,6 +64,8 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
return profile..toolSettings.definitions = value;
|
||||
case ToolSetting.autoIGC:
|
||||
return profile..toolSettings.autoIGC = value;
|
||||
case ToolSetting.enableTTS:
|
||||
return profile..toolSettings.enableTTS = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -67,6 +83,8 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
return toolSettings.definitions;
|
||||
case ToolSetting.autoIGC:
|
||||
return toolSettings.autoIGC;
|
||||
case ToolSetting.enableTTS:
|
||||
return toolSettings.enableTTS;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/language_tile.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_settings_switch_list_tile.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SettingsLearningView extends StatelessWidget {
|
||||
final SettingsLearningController controller;
|
||||
|
|
@ -14,89 +20,122 @@ class SettingsLearningView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dialogContent = Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
L10n.of(context).learningSettings,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: Column(
|
||||
children: [
|
||||
LanguageTile(controller),
|
||||
CountryPickerTile(controller),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).toggleToolSettingsDescription),
|
||||
return StreamBuilder(
|
||||
stream: Matrix.of(context).client.onSync.stream.where((update) {
|
||||
return update.accountData != null &&
|
||||
update.accountData!.any(
|
||||
(event) => event.type == ModelKey.userProfile,
|
||||
);
|
||||
}),
|
||||
builder: (context, _) {
|
||||
final dialogContent = Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
L10n.of(context).learningSettings,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: Column(
|
||||
children: [
|
||||
LanguageTile(controller),
|
||||
CountryPickerTile(controller),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).toggleToolSettingsDescription),
|
||||
),
|
||||
for (final toolSetting in ToolSetting.values
|
||||
.where((tool) => tool.isAvailableSetting))
|
||||
Column(
|
||||
children: [
|
||||
ProfileSettingsSwitchListTile.adaptive(
|
||||
defaultValue: controller.getToolSetting(toolSetting),
|
||||
title: toolSetting.toolName(context),
|
||||
subtitle: toolSetting == ToolSetting.enableTTS &&
|
||||
!controller.tts.isLanguageFullySupported
|
||||
? null
|
||||
: toolSetting.toolDescription(context),
|
||||
onChange: (bool value) =>
|
||||
controller.updateToolSetting(
|
||||
toolSetting,
|
||||
value,
|
||||
),
|
||||
enabled: toolSetting == ToolSetting.enableTTS
|
||||
? controller.tts.isLanguageFullySupported
|
||||
: true,
|
||||
),
|
||||
if (toolSetting == ToolSetting.enableTTS &&
|
||||
!controller.tts.isLanguageFullySupported)
|
||||
ListTile(
|
||||
trailing: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Icon(Icons.info_outlined),
|
||||
),
|
||||
subtitle: RichText(
|
||||
text: TextSpan(
|
||||
text: L10n.of(context).couldNotFindTTS,
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
if (PlatformInfos.isWindows ||
|
||||
PlatformInfos.isAndroid)
|
||||
TextSpan(
|
||||
text: L10n.of(context)
|
||||
.ttsInstructionsHyperlink,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(
|
||||
PlatformInfos.isWindows
|
||||
? AppConfig
|
||||
.windowsTTSDownloadInstructions
|
||||
: AppConfig
|
||||
.androidTTSDownloadInstructions,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
for (final toolSetting in ToolSetting.values
|
||||
.where((tool) => tool.isAvailableSetting))
|
||||
ProfileSettingsSwitchListTile.adaptive(
|
||||
defaultValue: controller.getToolSetting(toolSetting),
|
||||
title: toolSetting.toolName(context),
|
||||
subtitle: toolSetting.toolDescription(context),
|
||||
onChange: (bool value) => controller.updateToolSetting(
|
||||
toolSetting,
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return kIsWeb
|
||||
? Dialog(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600,
|
||||
maxHeight: 600,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: dialogContent,
|
||||
),
|
||||
),
|
||||
// ProfileSettingsSwitchListTile.adaptive(
|
||||
// defaultValue: controller.pangeaController.userController.profile
|
||||
// .userSettings.itAutoPlay,
|
||||
// title:
|
||||
// L10n.of(context).interactiveTranslatorAutoPlaySliderHeader,
|
||||
// subtitle: L10n.of(context).interactiveTranslatorAutoPlayDesc,
|
||||
// onChange: (bool value) => controller
|
||||
// .pangeaController.userController
|
||||
// .updateProfile((profile) {
|
||||
// profile.userSettings.itAutoPlay = value;
|
||||
// return profile;
|
||||
// }),
|
||||
// ),
|
||||
// ProfileSettingsSwitchListTile.adaptive(
|
||||
// defaultValue: controller.pangeaController.userController.profile
|
||||
// .userSettings.autoPlayMessages,
|
||||
// title: L10n.of(context).autoPlayTitle,
|
||||
// subtitle: L10n.of(context).autoPlayDesc,
|
||||
// onChange: (bool value) => controller
|
||||
// .pangeaController.userController
|
||||
// .updateProfile((profile) {
|
||||
// profile.userSettings.autoPlayMessages = value;
|
||||
// return profile;
|
||||
// }),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Dialog.fullscreen(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: dialogContent,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return kIsWeb
|
||||
? Dialog(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600,
|
||||
maxHeight: 600,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: dialogContent,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Dialog.fullscreen(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: dialogContent,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/user_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart';
|
||||
|
|
@ -18,17 +20,24 @@ class TtsController {
|
|||
List<String> _availableLangCodes = [];
|
||||
final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
|
||||
final TextToSpeech _alternativeTTS = TextToSpeech();
|
||||
StreamSubscription? _languageSubscription;
|
||||
|
||||
UserController get userController =>
|
||||
MatrixState.pangeaController.userController;
|
||||
|
||||
TtsController() {
|
||||
setupTTS();
|
||||
_languageSubscription =
|
||||
userController.stateStream.listen((_) => setupTTS());
|
||||
}
|
||||
|
||||
bool get _useAlternativeTTS {
|
||||
return PlatformInfos.getOperatingSystem() == 'Windows';
|
||||
return PlatformInfos.isWindows;
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _tts.stop();
|
||||
await _languageSubscription?.cancel();
|
||||
}
|
||||
|
||||
void _onError(dynamic message) => ErrorHandler.logError(
|
||||
|
|
@ -40,39 +49,46 @@ class TtsController {
|
|||
);
|
||||
|
||||
Future<void> setupTTS() async {
|
||||
if (_useAlternativeTTS) {
|
||||
await _setupAltTTS();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_tts.setErrorHandler(_onError);
|
||||
debugger(when: kDebugMode && targetLanguage == null);
|
||||
if (_useAlternativeTTS) {
|
||||
await _setupAltTTS();
|
||||
} else {
|
||||
_tts.setErrorHandler(_onError);
|
||||
debugger(when: kDebugMode && targetLanguage == null);
|
||||
|
||||
_tts.setLanguage(
|
||||
targetLanguage ?? "en",
|
||||
);
|
||||
_tts.setLanguage(
|
||||
targetLanguage ?? "en",
|
||||
);
|
||||
|
||||
await _tts.awaitSpeakCompletion(true);
|
||||
await _tts.awaitSpeakCompletion(true);
|
||||
|
||||
final voices = (await _tts.getVoices) as List?;
|
||||
_availableLangCodes = (voices ?? [])
|
||||
.map((v) {
|
||||
// on iOS / web, the codes are in 'locale', but on Android, they are in 'name'
|
||||
final nameCode = v['name']?.split("-").first;
|
||||
final localeCode = v['locale']?.split("-").first;
|
||||
return nameCode.length == 2 ? nameCode : localeCode;
|
||||
})
|
||||
.toSet()
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
debugPrint("availableLangCodes: $_availableLangCodes");
|
||||
|
||||
debugger(when: kDebugMode && !_isLanguageFullySupported);
|
||||
final voices = (await _tts.getVoices) as List?;
|
||||
_availableLangCodes = (voices ?? [])
|
||||
.map((v) {
|
||||
// on iOS / web, the codes are in 'locale', but on Android, they are in 'name'
|
||||
final nameCode = v['name']?.split("-").first;
|
||||
final localeCode = v['locale']?.split("-").first;
|
||||
return nameCode.length == 2 ? nameCode : localeCode;
|
||||
})
|
||||
.toSet()
|
||||
.cast<String>()
|
||||
.toList();
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
} finally {
|
||||
debugPrint("availableLangCodes: $_availableLangCodes");
|
||||
final enableTTSSetting = userController.profile.toolSettings.enableTTS;
|
||||
if (enableTTSSetting != isLanguageFullySupported) {
|
||||
await userController.updateProfile(
|
||||
(profile) {
|
||||
profile.toolSettings.enableTTS = isLanguageFullySupported;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +164,12 @@ class TtsController {
|
|||
// TODO - make non-nullable again
|
||||
String? eventID,
|
||||
) async {
|
||||
if (_isLanguageFullySupported) {
|
||||
if (!MatrixState
|
||||
.pangeaController.userController.profile.toolSettings.enableTTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLanguageFullySupported) {
|
||||
await _speak(text);
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -200,6 +221,6 @@ class TtsController {
|
|||
}
|
||||
}
|
||||
|
||||
bool get _isLanguageFullySupported =>
|
||||
bool get isLanguageFullySupported =>
|
||||
_availableLangCodes.contains(targetLanguage);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ class ProfileSettingsSwitchListTile extends StatefulWidget {
|
|||
final String title;
|
||||
final String? subtitle;
|
||||
final Function(bool) onChange;
|
||||
final bool enabled;
|
||||
|
||||
const ProfileSettingsSwitchListTile.adaptive({
|
||||
super.key,
|
||||
required this.defaultValue,
|
||||
required this.title,
|
||||
required this.onChange,
|
||||
this.enabled = true,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
|
|
@ -30,6 +32,14 @@ class PSettingsSwitchListTileState
|
|||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ProfileSettingsSwitchListTile oldWidget) {
|
||||
if (oldWidget.defaultValue != widget.defaultValue) {
|
||||
setState(() => currentValue = widget.defaultValue);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile.adaptive(
|
||||
|
|
@ -37,18 +47,20 @@ class PSettingsSwitchListTileState
|
|||
title: Text(widget.title),
|
||||
activeColor: AppConfig.activeToggleColor,
|
||||
subtitle: widget.subtitle != null ? Text(widget.subtitle!) : null,
|
||||
onChanged: (bool newValue) async {
|
||||
try {
|
||||
widget.onChange(newValue);
|
||||
setState(() => currentValue = newValue);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to updates user setting",
|
||||
s: s,
|
||||
);
|
||||
}
|
||||
},
|
||||
onChanged: widget.enabled
|
||||
? (bool newValue) async {
|
||||
try {
|
||||
widget.onChange(newValue);
|
||||
setState(() => currentValue = newValue);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to updates user setting",
|
||||
s: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import '../config/app_config.dart';
|
|||
abstract class PlatformInfos {
|
||||
static bool get isWeb => kIsWeb;
|
||||
static bool get isLinux => !kIsWeb && Platform.isLinux;
|
||||
static bool get isWindows => !kIsWeb && Platform.isWindows;
|
||||
// #Pangea
|
||||
// static bool get isWindows => !kIsWeb && Platform.isWindows;
|
||||
static bool get isWindows => getOperatingSystem() == 'Windows';
|
||||
// Pangea#
|
||||
static bool get isMacOS => !kIsWeb && Platform.isMacOS;
|
||||
static bool get isIOS => !kIsWeb && Platform.isIOS;
|
||||
static bool get isAndroid => !kIsWeb && Platform.isAndroid;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue