diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b2a0c6819..833aa5836 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -211,6 +211,15 @@ "openNoteInBrowsers": "ブラウザでノートを開く", "changeFullScreen": "フルスクリーンに切り替え", "shareNotes": "ノートを共有", + "translateNote": "ノートを翻訳", + "translatedFrom": "{lang}から翻訳", + "@translatedFrom": { + "placeholders": { + "lang": { + "type": "String" + } + } + }, "deleteFavorite": "お気に入り解除", "notesAfterRenote": "リノート直後のノート", "deletedRecreate": "削除してなおす", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 44ba48a1c..7dbe709c7 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -921,6 +921,18 @@ abstract class S { /// **'ノートを共有'** String get shareNotes; + /// No description provided for @translateNote. + /// + /// In ja, this message translates to: + /// **'ノートを翻訳'** + String get translateNote; + + /// No description provided for @translatedFrom. + /// + /// In ja, this message translates to: + /// **'{lang}から翻訳'** + String translatedFrom(String lang); + /// No description provided for @deleteFavorite. /// /// In ja, this message translates to: diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 272513d18..88283ebce 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -457,6 +457,14 @@ class SJa extends S { @override String get shareNotes => 'ノートを共有'; + @override + String get translateNote => 'ノートを翻訳'; + + @override + String translatedFrom(String lang) { + return '$langから翻訳'; + } + @override String get deleteFavorite => 'お気に入り解除'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ada733c8e..80b8b848d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -456,6 +456,14 @@ class SZh extends S { @override String get shareNotes => '分享帖子'; + @override + String get translateNote => 'ノートを翻訳'; + + @override + String translatedFrom(String lang) { + return '$langから翻訳'; + } + @override String get deleteFavorite => '取消收藏'; diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index d1176cabb..e0bf78d69 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -30,6 +30,7 @@ import "package:miria/view/common/color_picker_dialog.dart"; import "package:miria/view/common/misskey_notes/reaction_user_dialog.dart"; import "package:miria/view/common/misskey_notes/renote_modal_sheet.dart"; import "package:miria/view/common/misskey_notes/renote_user_dialog.dart"; +import "package:miria/view/common/misskey_notes/translate_note_modal_sheet.dart"; import "package:miria/view/explore_page/explore_page.dart"; import "package:miria/view/explore_page/explore_role_users_page.dart"; import "package:miria/view/favorited_note_page/favorited_note_page.dart"; @@ -179,6 +180,7 @@ class AppRouter extends RootStackRouter { AutoModalRouteSheet(page: AntennaModalRoute.page), AutoModalRouteSheet(page: ClipModalRoute.page), AutoModalRouteSheet(page: UsersListModalRoute.page), + AutoModalRouteSheet(page: TranslateNoteModalRoute.page), AutoModalRouteSheet(page: DriveModalRoute.page), ]; } diff --git a/lib/router/app_router.gr.dart b/lib/router/app_router.gr.dart index a16e1b443..497e8d4dd 100644 --- a/lib/router/app_router.gr.dart +++ b/lib/router/app_router.gr.dart @@ -3305,6 +3305,73 @@ class TimelinePresetRoute extends PageRouteInfo { ); } +/// generated route for +/// [TranslateNoteModalSheet] +class TranslateNoteModalRoute + extends PageRouteInfo { + TranslateNoteModalRoute({ + required AccountContext accountContext, + required Note note, + Key? key, + List? children, + }) : super( + TranslateNoteModalRoute.name, + args: TranslateNoteModalRouteArgs( + accountContext: accountContext, + note: note, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'TranslateNoteModalRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return WrappedRoute( + child: TranslateNoteModalSheet( + accountContext: args.accountContext, + note: args.note, + key: args.key, + ), + ); + }, + ); +} + +class TranslateNoteModalRouteArgs { + const TranslateNoteModalRouteArgs({ + required this.accountContext, + required this.note, + this.key, + }); + + final AccountContext accountContext; + + final Note note; + + final Key? key; + + @override + String toString() { + return 'TranslateNoteModalRouteArgs{accountContext: $accountContext, note: $note, key: $key}'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! TranslateNoteModalRouteArgs) return false; + return accountContext == other.accountContext && + note == other.note && + key == other.key; + } + + @override + int get hashCode => accountContext.hashCode ^ note.hashCode ^ key.hashCode; +} + /// generated route for /// [UpdateMemoDialog] class UpdateMemoRoute extends PageRouteInfo { diff --git a/lib/view/common/misskey_notes/translate_note_modal_sheet.dart b/lib/view/common/misskey_notes/translate_note_modal_sheet.dart new file mode 100644 index 000000000..e75b77de9 --- /dev/null +++ b/lib/view/common/misskey_notes/translate_note_modal_sheet.dart @@ -0,0 +1,109 @@ +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/l10n/app_localizations.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "translate_note_modal_sheet.g.dart"; + +@Riverpod(dependencies: [misskeyPostContext]) +FutureOr _notesTranslate( + Ref ref, { + required String noteId, + required String targetLang, +}) { + return ref + .read(misskeyPostContextProvider) + .notes + .translate(NotesTranslateRequest(noteId: noteId, targetLang: targetLang)); +} + +@RoutePage() +class TranslateNoteModalSheet extends ConsumerWidget + implements AutoRouteWrapper { + const TranslateNoteModalSheet({ + required this.accountContext, + required this.note, + super.key, + }); + + final AccountContext accountContext; + final Note note; + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final translatedNote = ref.watch( + _notesTranslateProvider( + noteId: note.id, + targetLang: Localizations.localeOf(context).toLanguageTag(), + ), + ); + + return Scaffold( + body: ListView( + children: [ + ...switch (translatedNote) { + AsyncValue(value: final translatedNote?) => [ + ListTile( + title: Text( + S.of(context).translatedFrom(translatedNote.sourceLang), + ), + trailing: IconButton( + tooltip: S.of(context).copyContents, + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: translatedNote.text), + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + }, + icon: const Icon(Icons.copy), + ), + ), + const Divider(height: 0.0), + SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: MfmText( + mfmText: translatedNote.text, + host: note.user.host, + emoji: note.emojis, + isEnableAnimatedMFM: ref + .watch(generalSettingsRepositoryProvider) + .settings + .enableAnimatedMFM, + ), + ), + ), + ], + AsyncValue(:final error?, :final stackTrace) => [ + ErrorDetail(error: error, stackTrace: stackTrace), + ], + _ => [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Center(child: CircularProgressIndicator.adaptive()), + ), + ], + }, + ], + ), + ); + } +} diff --git a/lib/view/common/misskey_notes/translate_note_modal_sheet.g.dart b/lib/view/common/misskey_notes/translate_note_modal_sheet.g.dart new file mode 100644 index 000000000..1680eb8b5 --- /dev/null +++ b/lib/view/common/misskey_notes/translate_note_modal_sheet.g.dart @@ -0,0 +1,107 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'translate_note_modal_sheet.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +@ProviderFor(_notesTranslate) +const _notesTranslateProvider = _NotesTranslateFamily._(); + +final class _NotesTranslateProvider + extends + $FunctionalProvider< + AsyncValue, + NotesTranslateResponse, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + const _NotesTranslateProvider._({ + required _NotesTranslateFamily super.from, + required ({String noteId, String targetLang}) super.argument, + }) : super( + retry: null, + name: r'_notesTranslateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + static const $allTransitiveDependencies0 = misskeyPostContextProvider; + static const $allTransitiveDependencies1 = + MisskeyPostContextProvider.$allTransitiveDependencies0; + + @override + String debugGetCreateSourceHash() => _$notesTranslateHash(); + + @override + String toString() { + return r'_notesTranslateProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as ({String noteId, String targetLang}); + return _notesTranslate( + ref, + noteId: argument.noteId, + targetLang: argument.targetLang, + ); + } + + @override + bool operator ==(Object other) { + return other is _NotesTranslateProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$notesTranslateHash() => r'e1ed9adf98f2c71aebeee8531c25fa9675dee658'; + +final class _NotesTranslateFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + ({String noteId, String targetLang}) + > { + const _NotesTranslateFamily._() + : super( + retry: null, + name: r'_notesTranslateProvider', + dependencies: const [misskeyPostContextProvider], + $allTransitiveDependencies: const [ + _NotesTranslateProvider.$allTransitiveDependencies0, + _NotesTranslateProvider.$allTransitiveDependencies1, + ], + isAutoDispose: true, + ); + + _NotesTranslateProvider call({ + required String noteId, + required String targetLang, + }) => _NotesTranslateProvider._( + argument: (noteId: noteId, targetLang: targetLang), + from: this, + ); + + @override + String toString() => r'_notesTranslateProvider'; +} + +// 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/view/note_modal_sheet/note_modal_sheet.dart b/lib/view/note_modal_sheet/note_modal_sheet.dart index c711e748e..8a9915ade 100644 --- a/lib/view/note_modal_sheet/note_modal_sheet.dart +++ b/lib/view/note_modal_sheet/note_modal_sheet.dart @@ -405,6 +405,18 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { }); }, ), + if (accountContext.postAccount.i.policies.canUseTranslator && + (accountContext.postAccount.meta?.translatorAvailable ?? false)) + ListTile( + leading: const Icon(Icons.translate), + title: Text(S.of(context).translateNote), + onTap: () async => await context.pushRoute( + TranslateNoteModalRoute( + accountContext: accountContext, + note: targetNote, + ), + ), + ), if (accountContext.isSame) switch (noteStatus) { null => const SizedBox.shrink(),