diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index e1f1cae..0c460f9 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -707,5 +707,8 @@ "donate": "Donate", "donateDescription": "Support us to continue developing the Solar Network and keep the server up and running.", "fileId": "File ID", - "fileIdHint": "The file ID is the ID you get after upload the file via the Solar Network Drive." + "fileIdHint": "The file ID is the ID you get after upload the file via the Solar Network Drive.", + "translate": "Translate", + "translating": "Translating", + "translated": "Translated" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 85714e9..27ad8a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; - import 'package:island/services/notify.dart'; import 'package:island/services/timezone.dart'; import 'package:island/widgets/alert.dart'; @@ -30,6 +29,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { @@ -51,6 +51,7 @@ void main() async { } try { + await langdetect.initLangDetect(); await EasyLocalization.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, diff --git a/lib/pods/translate.dart b/lib/pods/translate.dart new file mode 100644 index 0000000..cb6517e --- /dev/null +++ b/lib/pods/translate.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/network.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; + +part 'translate.freezed.dart'; +part 'translate.g.dart'; + +@freezed +sealed class TranslateQuery with _$TranslateQuery { + const factory TranslateQuery({required String text, required String lang}) = + _TranslateQuery; +} + +@riverpod +Future translateString(Ref ref, TranslateQuery query) async { + final client = ref.watch(apiClientProvider); + final response = await client.post( + '/sphere/translate', + queryParameters: {'to': query.lang}, + data: jsonEncode(query.text), + ); + return response.data as String; +} + +@riverpod +String? detectStringLanguage(Ref ref, String text) { + try { + return langdetect.detectLangs(text).firstOrNull?.lang; + } catch (err) { + log('[Language] Unable to detect text\'s language: $text'); + return null; + } +} diff --git a/lib/pods/translate.freezed.dart b/lib/pods/translate.freezed.dart new file mode 100644 index 0000000..a150851 --- /dev/null +++ b/lib/pods/translate.freezed.dart @@ -0,0 +1,268 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'translate.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$TranslateQuery { + + String get text; String get lang; +/// Create a copy of TranslateQuery +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TranslateQueryCopyWith get copyWith => _$TranslateQueryCopyWithImpl(this as TranslateQuery, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TranslateQuery&&(identical(other.text, text) || other.text == text)&&(identical(other.lang, lang) || other.lang == lang)); +} + + +@override +int get hashCode => Object.hash(runtimeType,text,lang); + +@override +String toString() { + return 'TranslateQuery(text: $text, lang: $lang)'; +} + + +} + +/// @nodoc +abstract mixin class $TranslateQueryCopyWith<$Res> { + factory $TranslateQueryCopyWith(TranslateQuery value, $Res Function(TranslateQuery) _then) = _$TranslateQueryCopyWithImpl; +@useResult +$Res call({ + String text, String lang +}); + + + + +} +/// @nodoc +class _$TranslateQueryCopyWithImpl<$Res> + implements $TranslateQueryCopyWith<$Res> { + _$TranslateQueryCopyWithImpl(this._self, this._then); + + final TranslateQuery _self; + final $Res Function(TranslateQuery) _then; + +/// Create a copy of TranslateQuery +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? lang = null,}) { + return _then(_self.copyWith( +text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String,lang: null == lang ? _self.lang : lang // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [TranslateQuery]. +extension TranslateQueryPatterns on TranslateQuery { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _TranslateQuery value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _TranslateQuery() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _TranslateQuery value) $default,){ +final _that = this; +switch (_that) { +case _TranslateQuery(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _TranslateQuery value)? $default,){ +final _that = this; +switch (_that) { +case _TranslateQuery() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String text, String lang)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _TranslateQuery() when $default != null: +return $default(_that.text,_that.lang);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String text, String lang) $default,) {final _that = this; +switch (_that) { +case _TranslateQuery(): +return $default(_that.text,_that.lang);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String text, String lang)? $default,) {final _that = this; +switch (_that) { +case _TranslateQuery() when $default != null: +return $default(_that.text,_that.lang);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _TranslateQuery implements TranslateQuery { + const _TranslateQuery({required this.text, required this.lang}); + + +@override final String text; +@override final String lang; + +/// Create a copy of TranslateQuery +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TranslateQueryCopyWith<_TranslateQuery> get copyWith => __$TranslateQueryCopyWithImpl<_TranslateQuery>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TranslateQuery&&(identical(other.text, text) || other.text == text)&&(identical(other.lang, lang) || other.lang == lang)); +} + + +@override +int get hashCode => Object.hash(runtimeType,text,lang); + +@override +String toString() { + return 'TranslateQuery(text: $text, lang: $lang)'; +} + + +} + +/// @nodoc +abstract mixin class _$TranslateQueryCopyWith<$Res> implements $TranslateQueryCopyWith<$Res> { + factory _$TranslateQueryCopyWith(_TranslateQuery value, $Res Function(_TranslateQuery) _then) = __$TranslateQueryCopyWithImpl; +@override @useResult +$Res call({ + String text, String lang +}); + + + + +} +/// @nodoc +class __$TranslateQueryCopyWithImpl<$Res> + implements _$TranslateQueryCopyWith<$Res> { + __$TranslateQueryCopyWithImpl(this._self, this._then); + + final _TranslateQuery _self; + final $Res Function(_TranslateQuery) _then; + +/// Create a copy of TranslateQuery +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? lang = null,}) { + return _then(_TranslateQuery( +text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String,lang: null == lang ? _self.lang : lang // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/lib/pods/translate.g.dart b/lib/pods/translate.g.dart new file mode 100644 index 0000000..7c8fc05 --- /dev/null +++ b/lib/pods/translate.g.dart @@ -0,0 +1,274 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'translate.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$translateStringHash() => r'bcbfb648347a52ddf0572794db4533e896d4149d'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [translateString]. +@ProviderFor(translateString) +const translateStringProvider = TranslateStringFamily(); + +/// See also [translateString]. +class TranslateStringFamily extends Family> { + /// See also [translateString]. + const TranslateStringFamily(); + + /// See also [translateString]. + TranslateStringProvider call(TranslateQuery query) { + return TranslateStringProvider(query); + } + + @override + TranslateStringProvider getProviderOverride( + covariant TranslateStringProvider provider, + ) { + return call(provider.query); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'translateStringProvider'; +} + +/// See also [translateString]. +class TranslateStringProvider extends AutoDisposeFutureProvider { + /// See also [translateString]. + TranslateStringProvider(TranslateQuery query) + : this._internal( + (ref) => translateString(ref as TranslateStringRef, query), + from: translateStringProvider, + name: r'translateStringProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$translateStringHash, + dependencies: TranslateStringFamily._dependencies, + allTransitiveDependencies: + TranslateStringFamily._allTransitiveDependencies, + query: query, + ); + + TranslateStringProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.query, + }) : super.internal(); + + final TranslateQuery query; + + @override + Override overrideWith( + FutureOr Function(TranslateStringRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TranslateStringProvider._internal( + (ref) => create(ref as TranslateStringRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + query: query, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _TranslateStringProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is TranslateStringProvider && other.query == query; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, query.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin TranslateStringRef on AutoDisposeFutureProviderRef { + /// The parameter `query` of this provider. + TranslateQuery get query; +} + +class _TranslateStringProviderElement + extends AutoDisposeFutureProviderElement + with TranslateStringRef { + _TranslateStringProviderElement(super.provider); + + @override + TranslateQuery get query => (origin as TranslateStringProvider).query; +} + +String _$detectStringLanguageHash() => + r'697b68464b3d00927cc43ccc1ba8ba93f2a470ed'; + +/// See also [detectStringLanguage]. +@ProviderFor(detectStringLanguage) +const detectStringLanguageProvider = DetectStringLanguageFamily(); + +/// See also [detectStringLanguage]. +class DetectStringLanguageFamily extends Family { + /// See also [detectStringLanguage]. + const DetectStringLanguageFamily(); + + /// See also [detectStringLanguage]. + DetectStringLanguageProvider call(String text) { + return DetectStringLanguageProvider(text); + } + + @override + DetectStringLanguageProvider getProviderOverride( + covariant DetectStringLanguageProvider provider, + ) { + return call(provider.text); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'detectStringLanguageProvider'; +} + +/// See also [detectStringLanguage]. +class DetectStringLanguageProvider extends AutoDisposeProvider { + /// See also [detectStringLanguage]. + DetectStringLanguageProvider(String text) + : this._internal( + (ref) => detectStringLanguage(ref as DetectStringLanguageRef, text), + from: detectStringLanguageProvider, + name: r'detectStringLanguageProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$detectStringLanguageHash, + dependencies: DetectStringLanguageFamily._dependencies, + allTransitiveDependencies: + DetectStringLanguageFamily._allTransitiveDependencies, + text: text, + ); + + DetectStringLanguageProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.text, + }) : super.internal(); + + final String text; + + @override + Override overrideWith( + String? Function(DetectStringLanguageRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: DetectStringLanguageProvider._internal( + (ref) => create(ref as DetectStringLanguageRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + text: text, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _DetectStringLanguageProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DetectStringLanguageProvider && other.text == text; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, text.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin DetectStringLanguageRef on AutoDisposeProviderRef { + /// The parameter `text` of this provider. + String get text; +} + +class _DetectStringLanguageProviderElement + extends AutoDisposeProviderElement + with DetectStringLanguageRef { + _DetectStringLanguageProviderElement(super.provider); + + @override + String get text => (origin as DetectStringLanguageProvider).text; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/account/leveling.g.dart b/lib/screens/account/leveling.g.dart index 810cc48..d457c50 100644 --- a/lib/screens/account/leveling.g.dart +++ b/lib/screens/account/leveling.g.dart @@ -7,7 +7,7 @@ part of 'leveling.dart'; // ************************************************************************** String _$accountStellarSubscriptionHash() => - r'37fb821460e3ac50b5cf777c933b6779f732daee'; + r'80abcdefb3868775fd8fe3c980215713efff5948'; /// See also [accountStellarSubscription]. @ProviderFor(accountStellarSubscription) diff --git a/lib/screens/creators/stickers/pack_detail.g.dart b/lib/screens/creators/stickers/pack_detail.g.dart index 57780fe..f4bdd66 100644 --- a/lib/screens/creators/stickers/pack_detail.g.dart +++ b/lib/screens/creators/stickers/pack_detail.g.dart @@ -7,7 +7,7 @@ part of 'pack_detail.dart'; // ************************************************************************** String _$stickerPackContentHash() => - r'78de848fba1f341f217f8ae4b9eef2d8afa67964'; + r'42d74f51022e67e35cb601c2f30f4f02e1f2be9d'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/posts/post_search.dart b/lib/screens/posts/post_search.dart index b414749..a7abbba 100644 --- a/lib/screens/posts/post_search.dart +++ b/lib/screens/posts/post_search.dart @@ -56,7 +56,7 @@ class PostSearchNotifier 'query': _currentQuery, 'offset': offset, 'take': _pageSize, - 'useVector': true, + 'useVector': false, }, ); diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 21ed6a8..d45060e 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -12,6 +12,7 @@ import 'package:island/models/embed.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/translate.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/screens/posts/compose.dart'; import 'package:island/services/responsive.dart'; @@ -247,6 +248,46 @@ class PostItem extends HookConsumerWidget { .map((e) => e.key) .first; + final postLanguage = + item.content != null + ? ref.watch(detectStringLanguageProvider(item.content!)) + : null; + + final currentLanguage = context.locale.toString(); + final translatableLanguage = + postLanguage != null + ? postLanguage.substring(0, 2) != currentLanguage.substring(0, 2) + : false; + + final translating = useState(false); + final translatedText = useState(null); + + Future translate() async { + if (translatedText.value != null) { + translatedText.value = null; + return; + } + + if (translating.value) return; + if (item.content == null) return; + translating.value = true; + try { + final text = await ref.watch( + translateStringProvider( + TranslateQuery( + text: item.content!, + lang: currentLanguage.substring(0, 2), + ), + ).future, + ); + translatedText.value = text; + } catch (err) { + showErrorAlert(err); + } finally { + translating.value = false; + } + } + return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -378,9 +419,53 @@ class PostItem extends HookConsumerWidget { left: renderingPadding.horizontal, right: renderingPadding.horizontal, ), - child: MarkdownTextContent( - content: item.isTruncated ? '${item.content!}...' : item.content!, - isSelectable: isTextSelectable, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MarkdownTextContent( + content: + item.isTruncated ? '${item.content!}...' : item.content!, + isSelectable: isTextSelectable, + ), + if (translatedText.value?.isNotEmpty ?? false) + ...([ + Row( + children: [ + Expanded(child: Divider()), + const Gap(8), + Text('translated').tr().fontSize(11).opacity(0.75), + ], + ), + MarkdownTextContent( + content: translatedText.value!, + isSelectable: isTextSelectable, + ), + ]), + if (translatableLanguage) + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: translating.value ? null : translate, + style: ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.zero), + visualDensity: const VisualDensity( + horizontal: 0, + vertical: -4, + ), + foregroundColor: WidgetStatePropertyAll( + translatedText.value == null ? null : Colors.grey, + ), + ), + icon: const Icon(Symbols.translate), + label: + translatedText.value != null + ? Text('translated').tr() + : translating.value + ? Text('translating').tr() + : Text('translate').tr(), + ), + ), + ], ), ), if (item.isTruncated && item.type != 1)