Skip to content

Commit 716f764

Browse files
committed
feat: enable asynchronous loading of individual translations
1 parent 3a72f42 commit 716f764

File tree

14 files changed

+413
-173
lines changed

14 files changed

+413
-173
lines changed

lib/app/view/app.dart

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,7 @@ class AppView extends StatefulWidget {
7272
}
7373

7474
class _AppViewState extends State<AppView> {
75-
@override
76-
void initState() {
77-
context.read<SettingsBloc>().add(const SettingsFetch());
78-
super.initState();
79-
}
75+
8076

8177
@override
8278
Widget build(BuildContext context) {

lib/bootstrap.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,12 @@ Future<void> bootstrap(
6363
}
6464

6565
// Initialize settings bloc
66+
final settingsRepository = SettingsRepository(storage: secureStorage);
6667
final settingsBloc = SettingsBloc(
67-
settingsRepository: SettingsRepository(storage: secureStorage),
68-
);
68+
settingsRepository: settingsRepository,
69+
)..add(
70+
SettingsFetch(await settingsRepository.getAPIKey()),
71+
);
6972

7073
// Initialize HTTP repository
7174
GetIt.instance.registerLazySingleton<Http>(Http.new);

lib/presentation/home/bloc/home_bloc.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import 'package:bloc/bloc.dart';
44
import 'package:bloc_concurrency/bloc_concurrency.dart';
55
import 'package:equatable/equatable.dart';
66
import 'package:flutter/material.dart';
7-
import 'package:meta/meta.dart';
87
import 'package:qack/presentation/home/models/base_translation_details.dart';
98
import 'package:qack/presentation/home/repositories/repositories.dart';
109
import 'package:qack/presentation/settings/bloc/settings_bloc.dart';
10+
import 'package:qack/presentation/settings/models/models.dart';
1111
import 'package:rxdart/rxdart.dart';
1212

1313
part 'home_event.dart';
@@ -17,7 +17,13 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
1717
HomeBloc({
1818
required this.homeRepository,
1919
required this.settingsBloc,
20-
}) : super(const HomeInitial()) {
20+
}) : super(
21+
HomeState(
22+
_fillEmptyTranslationDetails(
23+
settingsBloc.state.translatorSettings!.enabledTranslators,
24+
),
25+
),
26+
) {
2127
on<HomeTextChanged>(
2228
_onHomeTextChanged,
2329
transformer: restartableDebounce(const Duration(milliseconds: 500)),
@@ -32,12 +38,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
3238
Emitter<HomeState> emit,
3339
) async {
3440
try {
35-
emit(const HomeTextTranslateLoading());
41+
emit(state.loading());
3642

3743
final sourceText = event.sourceText.trim();
3844

3945
if (sourceText.isEmpty) {
40-
emit(const HomeTextStateEmpty());
46+
emit(state.empty());
4147
return;
4248
}
4349

@@ -59,16 +65,16 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
5965
translationDetails[baseTranslationDetail.translatorName] =
6066
baseTranslationDetail;
6167

62-
return HomeTextTranslateSuccess(translationDetails);
68+
return state.success(translationDetails);
6369
},
6470
onError: (e, stackTrace) {
6571
debugPrint('error: $e');
66-
return HomeTextTranslateError(exception: Exception(e));
72+
return state.error(Exception(e));
6773
},
6874
);
6975
} on Exception catch (e) {
7076
debugPrint(e.toString());
71-
emit(HomeTextTranslateError(exception: e));
77+
emit(state.error(e));
7278
}
7379
}
7480
}
@@ -77,3 +83,12 @@ EventTransformer<T> restartableDebounce<T>(Duration duration) {
7783
return (events, mapper) =>
7884
restartable<T>().call(events.debounceTime(duration), mapper);
7985
}
86+
87+
Map<Translator, BaseTranslationDetails> _fillEmptyTranslationDetails(
88+
List<Translator> enabledTranslators,
89+
) {
90+
return {
91+
for (final translator in enabledTranslators)
92+
translator: const EmptyTranslationDetails(),
93+
};
94+
}
Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
11
part of 'home_bloc.dart';
22

3-
@immutable
4-
sealed class HomeState extends Equatable {
5-
const HomeState([this.translationDetails = const {}]);
6-
final TranslationDetails translationDetails;
7-
8-
@override
9-
List<Object?> get props => [translationDetails];
3+
enum HomeStatus {
4+
initial,
5+
loading,
6+
empty,
7+
success,
8+
error,
109
}
1110

12-
final class HomeInitial extends HomeState {
13-
const HomeInitial();
14-
}
15-
16-
final class HomeTextTranslateLoading extends HomeState {
17-
const HomeTextTranslateLoading();
18-
}
19-
20-
final class HomeTextStateEmpty extends HomeState {
21-
const HomeTextStateEmpty();
22-
}
23-
24-
final class HomeTextTranslateSuccess extends HomeState {
25-
const HomeTextTranslateSuccess(super.translationDetails);
26-
}
11+
@immutable
12+
final class HomeState extends Equatable {
13+
const HomeState(TranslationDetails details)
14+
: this._(translationDetails: details);
15+
16+
const HomeState._({
17+
required this.translationDetails,
18+
this.status = HomeStatus.initial,
19+
this.exception,
20+
});
21+
final TranslationDetails translationDetails;
2722

28-
final class HomeTextTranslateError extends HomeState {
29-
const HomeTextTranslateError({required this.exception});
30-
// TODO: Add which translator got the error
31-
final Exception exception;
23+
final HomeStatus status;
24+
final Exception? exception;
25+
26+
HomeState loading() => HomeState._(
27+
translationDetails: translationDetails,
28+
status: HomeStatus.loading,
29+
);
30+
31+
HomeState empty() => HomeState._(
32+
translationDetails: translationDetails,
33+
status: HomeStatus.empty,
34+
);
35+
36+
HomeState success(TranslationDetails details) => HomeState._(
37+
translationDetails: details,
38+
status: HomeStatus.success,
39+
);
40+
41+
/// This error is a general error in [HomeRepository]'s translateText method.
42+
/// It is not a translation error.
43+
/// See [BaseTranslationError] for translation errors.
44+
HomeState error(Exception e) => HomeState._(
45+
translationDetails: translationDetails,
46+
status: HomeStatus.error,
47+
exception: e,
48+
);
3249

3350
@override
34-
List<Object?> get props => [exception];
51+
List<Object?> get props => [translationDetails, status, exception];
3552
}

lib/presentation/home/components/translation_card.dart

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,39 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_svg/svg.dart';
33
import 'package:gap/gap.dart';
44
import 'package:qack/layout/layout_handler.dart';
5+
import 'package:qack/presentation/home/bloc/home_bloc.dart';
56
import 'package:qack/presentation/home/models/base_translation_details.dart';
7+
import 'package:qack/presentation/home/repositories/repositories.dart';
8+
import 'package:qack/presentation/settings/models/models.dart';
69
import 'package:qack/theme/themes/light_theme.dart';
710
import 'package:url_launcher/url_launcher.dart';
811
import 'package:vector_graphics/vector_graphics.dart';
912

1013
class TranslationCard extends StatelessWidget {
11-
const TranslationCard({required this.translationDetails, super.key});
12-
final BaseTranslationDetails translationDetails;
14+
const TranslationCard({
15+
required this.status,
16+
required this.translationMapEntry,
17+
required this.exception,
18+
super.key,
19+
});
20+
final HomeStatus status;
21+
final MapEntry<Translator, BaseTranslationDetails> translationMapEntry;
22+
23+
/// This error is a general error in [HomeRepository]'s translateText method.
24+
/// It is not a translation error.
25+
/// See [BaseTranslationError] or [BaseTranslationDetails] .empty methods for
26+
/// translation errors.
27+
final Exception? exception;
1328

1429
@override
1530
Widget build(BuildContext context) {
1631
const theme = LightTheme();
1732
return LayoutHandler(
1833
mobile: TranslationCardView(
19-
translationDetails: translationDetails,
34+
status: status,
35+
translator: translationMapEntry.key,
36+
translationDetails: translationMapEntry.value,
37+
exception: exception,
2038
cardMargin: const EdgeInsets.only(bottom: 16),
2139
cardPadding:
2240
const EdgeInsets.only(top: 12, left: 12, right: 12, bottom: 4),
@@ -32,7 +50,10 @@ class TranslationCard extends StatelessWidget {
3250
),
3351
),
3452
tablet: TranslationCardView(
35-
translationDetails: translationDetails,
53+
status: status,
54+
translator: translationMapEntry.key,
55+
translationDetails: translationMapEntry.value,
56+
exception: exception,
3657
cardMargin: const EdgeInsets.only(bottom: 16),
3758
cardPadding:
3859
const EdgeInsets.only(top: 12, left: 12, right: 12, bottom: 4),
@@ -53,6 +74,8 @@ class TranslationCard extends StatelessWidget {
5374

5475
class TranslationCardView extends StatelessWidget {
5576
const TranslationCardView({
77+
required this.status,
78+
required this.translator,
5679
required this.translationDetails,
5780
required this.cardMargin,
5881
required this.cardPadding,
@@ -61,8 +84,13 @@ class TranslationCardView extends StatelessWidget {
6184
required this.translationPadding,
6285
required this.titleStyle,
6386
required this.translatedTextStyle,
87+
required this.exception,
6488
super.key,
6589
});
90+
final HomeStatus status;
91+
final Exception? exception;
92+
93+
final Translator translator;
6694
final BaseTranslationDetails translationDetails;
6795

6896
final EdgeInsets cardMargin;
@@ -79,6 +107,40 @@ class TranslationCardView extends StatelessWidget {
79107
@override
80108
Widget build(BuildContext context) {
81109
const theme = LightTheme();
110+
var translationStyle = translatedTextStyle;
111+
late final String translationText;
112+
113+
switch (status) {
114+
case HomeStatus.initial:
115+
case HomeStatus.empty:
116+
translationText = 'Empty translation.';
117+
case HomeStatus.loading:
118+
translationText = 'Loading...';
119+
case HomeStatus.success:
120+
// Check if the translation is empty
121+
if (translationDetails is EmptyTranslationDetails) {
122+
translationText = 'Loading...';
123+
break;
124+
}
125+
126+
if (translationDetails.status == TranslationStatus.loading) {
127+
translationText = 'Loading...';
128+
} else if (translationDetails.status == TranslationStatus.success) {
129+
translationText = translationDetails.translatedText!.outputText;
130+
} else if (translationDetails.status == TranslationStatus.error ||
131+
translationDetails.exception != null) {
132+
translationText =
133+
'Translation error: ${translationDetails.exception}';
134+
} else {
135+
translationText = 'Unknown error';
136+
}
137+
case HomeStatus.error:
138+
translationStyle = translatedTextStyle.copyWith(
139+
color: theme.errorColor,
140+
);
141+
translationText = 'Error: $exception';
142+
}
143+
82144
return Padding(
83145
padding: cardMargin,
84146
child: DecoratedBox(
@@ -99,13 +161,13 @@ class TranslationCardView extends StatelessWidget {
99161
width: 28,
100162
child: SvgPicture(
101163
AssetBytesLoader(
102-
translationDetails.svgPath,
164+
translator.svgPath,
103165
),
104166
),
105167
),
106168
Gap(titleGap),
107169
Text(
108-
translationDetails.translatorName,
170+
translator.name,
109171
style: titleStyle,
110172
),
111173
const Spacer(),
@@ -115,22 +177,25 @@ class TranslationCardView extends StatelessWidget {
115177
padding: translationMargin,
116178
child: InkWell(
117179
onTap: () {
118-
launchUrl(
119-
Uri(
120-
scheme: 'plecoapi',
121-
host: 'x-callback-url',
122-
path: 's',
123-
queryParameters: {
124-
'q': translationDetails.translatedText.outputText,
125-
},
126-
),
127-
);
180+
if (status == HomeStatus.success &&
181+
translationDetails.translatedText != null) {
182+
launchUrl(
183+
Uri(
184+
scheme: 'plecoapi',
185+
host: 'x-callback-url',
186+
path: 's',
187+
queryParameters: {
188+
'q': translationText,
189+
},
190+
),
191+
);
192+
}
128193
},
129194
child: Padding(
130195
padding: translationPadding,
131196
child: Text(
132-
translationDetails.translatedText.outputText,
133-
style: translatedTextStyle,
197+
translationText,
198+
style: translationStyle,
134199
),
135200
),
136201
),

lib/presentation/home/models/baidu_translation.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,36 @@ final class BaiduTranslation extends BaseTranslationDetails {
1616
required this.baiduSrcLanguage,
1717
required this.baidutargetLanguage,
1818
required this.baiduTranslatedText,
19+
TranslationStatus status = TranslationStatus.success,
20+
Exception? exception,
1921
}) : super(
2022
srcLanguage: baiduSrcLanguage,
2123
targetLanguage: baidutargetLanguage,
2224
translatedText: baiduTranslatedText,
25+
status: status,
26+
exception: exception,
27+
);
28+
29+
const BaiduTranslation.loading()
30+
: this(
31+
baiduSrcLanguage: 'loading',
32+
baidutargetLanguage: 'loading',
33+
baiduTranslatedText: const TranslatedText(
34+
inputText: 'Loading',
35+
outputText: 'Loading',
36+
),
37+
status: TranslationStatus.loading,
38+
);
39+
40+
const BaiduTranslation.error(Exception e)
41+
: this(
42+
baiduSrcLanguage: 'err',
43+
baidutargetLanguage: 'err',
44+
baiduTranslatedText: const TranslatedText(
45+
inputText: 'Error',
46+
outputText: 'Error',
47+
),
48+
exception: e,
2349
);
2450

2551
factory BaiduTranslation.fromJson(Map<String, dynamic> json) =>

0 commit comments

Comments
 (0)