Skip to content

Commit 5a7da54

Browse files
committed
fix: disabling a translator doesn't reflect on UI
1 parent 8420e3d commit 5a7da54

File tree

10 files changed

+151
-41
lines changed

10 files changed

+151
-41
lines changed

lib/bootstrap.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,14 @@ Future<void> bootstrap(
105105
// Initialize word of the day cubit
106106
final wordOfTheDayCubit = WordOfTheDayCubit(
107107
const WordOfTheDayRepository(),
108-
deepSeekApiKey: translatorSettings.apiKeys[KeyNameConstants.deepSeek],
109108
// ignore: unawaited_futures
110-
)..fetchWordOfTheDay();
109+
);
110+
111+
// Fetch the word of the day if DeepSeek API key is available
112+
final deepSeekApiKey = translatorSettings.apiKeys[KeyNameConstants.deepSeek];
113+
if (deepSeekApiKey != null && deepSeekApiKey.isNotEmpty) {
114+
unawaited(wordOfTheDayCubit.fetch(deepSeekApiKey));
115+
}
111116

112117
Bloc.observer = const AppBlocObserver();
113118

lib/presentation/home/bloc/home_bloc.dart

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
3333
_onHomeTextChanged,
3434
transformer: restartableDebounce(const Duration(milliseconds: 500)),
3535
);
36+
on<HomeTranslatorRemoved>(_onHomeTranslatorRemoved);
3637
}
3738

3839
final HomeRepository homeRepository;
@@ -45,28 +46,28 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
4546
Emitter<HomeState> emit,
4647
) {
4748
event.textController.clear();
48-
emit(state.empty());
49+
emit(state.empty(method: HomeMethod.textCleared));
4950
}
5051

5152
Future<void> _onHomeTextChanged(
5253
HomeTextChanged event,
5354
Emitter<HomeState> emit,
5455
) async {
5556
try {
56-
emit(state.loading());
57+
emit(state.loading(method: HomeMethod.textChanged));
5758

5859
final sourceText = event.sourceText.trim();
5960

6061
if (sourceText.isEmpty) {
61-
emit(state.empty());
62-
return;
63-
}
64-
65-
// TODO: Tell the user that the translation won't work if there is no
66-
// translator enabled
67-
if (settingsBloc.state.translatorSettings == null ||
62+
emit(state.empty(method: HomeMethod.textChanged));
63+
} else if (settingsBloc.state.translatorSettings == null ||
6864
settingsBloc.state.translatorSettings!.enabledTranslators.isEmpty) {
69-
emit(state.failure(const NoTranslatorEnabledException()));
65+
emit(
66+
state.failure(
67+
const NoTranslatorEnabledException(),
68+
method: HomeMethod.textChanged,
69+
),
70+
);
7071
return;
7172
}
7273

@@ -85,20 +86,56 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
8586
// For now, we register all the translated text even if they
8687
// are one letter apart
8788

88-
return state.success(translationDetails);
89+
return state.success(
90+
translationDetails,
91+
method: HomeMethod.textChanged,
92+
);
8993
},
9094
onError: (e, stackTrace) {
9195
log('error: $e, stackTrace: $stackTrace');
9296
// Log error to Sentry
9397
Sentry.captureException(e, stackTrace: stackTrace);
94-
return state.failure(Exception(e));
98+
return state.failure(
99+
Exception('An error has occurred.'),
100+
method: HomeMethod.textChanged,
101+
);
95102
},
96103
);
97104
} on Exception catch (e) {
98105
log(e.toString());
99106
unawaited(Sentry.captureException(e));
100-
emit(state.failure(e));
107+
emit(
108+
state.failure(
109+
Exception('An error has occurred.'),
110+
method: HomeMethod.textChanged,
111+
),
112+
);
113+
}
114+
}
115+
116+
Future<void> _onHomeTranslatorRemoved(
117+
HomeTranslatorRemoved event,
118+
Emitter<HomeState> emit,
119+
) async {
120+
// Remove existing translation details for the removed translators
121+
final updatedTranslationDetails = Map.of(state.translationDetails);
122+
123+
if (event.removedTranslators.isEmpty) {
124+
return;
101125
}
126+
127+
for (final translator in event.removedTranslators) {
128+
updatedTranslationDetails.remove(translator);
129+
}
130+
131+
emit(
132+
state.copyWith(
133+
translationDetails: updatedTranslationDetails,
134+
method: HomeMethod.settingsApiKeyRemoved,
135+
// Deliberately not changing the status so that the UI depends on the
136+
// status of the last translation
137+
),
138+
);
102139
}
103140
}
104141

lib/presentation/home/bloc/home_event.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,13 @@ final class HomeTextChanged extends HomeEvent {
2424
@override
2525
List<Object?> get props => [sourceText];
2626
}
27+
28+
final class HomeTranslatorRemoved extends HomeEvent {
29+
const HomeTranslatorRemoved(this.removedTranslators);
30+
31+
/// List of translators whose API keys were removed from the settings.
32+
final List<Translator> removedTranslators;
33+
34+
@override
35+
List<Object?> get props => [removedTranslators];
36+
}

lib/presentation/home/bloc/home_state.dart

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
part of 'home_bloc.dart';
22

3+
enum HomeMethod {
4+
initial,
5+
textCleared,
6+
textChanged,
7+
settingsApiKeyRemoved,
8+
}
9+
310
enum HomeStatus {
411
initial,
512
loading,
@@ -15,38 +22,59 @@ final class HomeState extends Equatable {
1522

1623
const HomeState._({
1724
required this.translationDetails,
25+
this.method = HomeMethod.initial,
1826
this.status = HomeStatus.initial,
1927
this.exception,
2028
});
2129
final TranslationDetails translationDetails;
2230

31+
final HomeMethod method;
2332
final HomeStatus status;
2433
final Exception? exception;
2534

26-
HomeState loading() => HomeState._(
35+
HomeState loading({required HomeMethod method}) => HomeState._(
2736
translationDetails: translationDetails,
37+
method: method,
2838
status: HomeStatus.loading,
2939
);
3040

31-
HomeState empty() => HomeState._(
41+
HomeState empty({required HomeMethod method}) => HomeState._(
3242
translationDetails: translationDetails,
43+
method: method,
3344
status: HomeStatus.empty,
3445
);
3546

36-
HomeState success(TranslationDetails details) => HomeState._(
47+
HomeState success(TranslationDetails details, {required HomeMethod method}) =>
48+
HomeState._(
3749
translationDetails: details,
50+
method: method,
3851
status: HomeStatus.success,
3952
);
4053

4154
/// This error is a general error in [HomeRepository]'s translateText method.
4255
/// It is not a translation error.
4356
/// See [BaseTranslationError] for translation errors.
44-
HomeState failure(Exception e) => HomeState._(
57+
HomeState failure(Exception e, {required HomeMethod method}) => HomeState._(
4558
translationDetails: translationDetails,
59+
method: method,
4660
status: HomeStatus.error,
4761
exception: e,
4862
);
4963

64+
HomeState copyWith({
65+
TranslationDetails? translationDetails,
66+
HomeMethod? method,
67+
HomeStatus? status,
68+
Exception? exception,
69+
}) {
70+
return HomeState._(
71+
translationDetails: translationDetails ?? this.translationDetails,
72+
method: method ?? this.method,
73+
status: status ?? this.status,
74+
exception: exception ?? this.exception,
75+
);
76+
}
77+
5078
@override
51-
List<Object?> get props => [translationDetails, status, exception];
79+
List<Object?> get props => [translationDetails, method, status, exception];
5280
}

lib/presentation/home/cubit/word_of_the_day_cubit.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,16 @@ import 'package:qack/presentation/home/repositories/repositories.dart';
77
part 'word_of_the_day_state.dart';
88

99
class WordOfTheDayCubit extends Cubit<WordOfTheDayState> {
10-
WordOfTheDayCubit(this.wordOfTheDayRepository, {required this.deepSeekApiKey})
10+
WordOfTheDayCubit(this.wordOfTheDayRepository)
1111
: super(const WordOfTheDayInitial());
1212
final WordOfTheDayRepository wordOfTheDayRepository;
13-
final String? deepSeekApiKey;
1413

15-
Future<void> fetchWordOfTheDay() async {
14+
Future<void> fetch(String deepSeekApiKey) async {
1615
try {
17-
if (deepSeekApiKey != null && deepSeekApiKey!.isNotEmpty) {
18-
final wordOfTheDay =
19-
await wordOfTheDayRepository.fetchWordOfTheDay(deepSeekApiKey!);
16+
final wordOfTheDay =
17+
await wordOfTheDayRepository.fetchWordOfTheDay(deepSeekApiKey);
2018

21-
emit(WordOfTheDaySuccess(wordOfTheDay));
22-
}
19+
emit(WordOfTheDaySuccess(wordOfTheDay));
2320
} on Exception catch (e) {
2421
emit(WordOfTheDayFailure(e));
2522
}

lib/presentation/home/view/home_page.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:qack/presentation/home/components/components.dart';
1212
import 'package:qack/presentation/home/components/translation_input_text.dart';
1313
import 'package:qack/presentation/home/cubit/translation_input_text_cubit.dart';
1414
import 'package:qack/presentation/home/cubit/word_of_the_day_cubit.dart';
15+
import 'package:qack/presentation/home/models/models.dart';
1516
import 'package:qack/presentation/home/models/word_of_the_day/word_of_the_day.dart';
1617
import 'package:qack/presentation/settings/bloc/settings_bloc.dart';
1718
import 'package:qack/theme/theme.dart';
@@ -98,7 +99,9 @@ class HomeView extends StatelessWidget {
9899
sliver: BlocBuilder<HomeBloc, HomeState>(
99100
builder: (context, state) {
100101
if (state.status == HomeStatus.initial ||
101-
state.status == HomeStatus.empty) {
102+
state.status == HomeStatus.empty ||
103+
(state.method == HomeMethod.textCleared &&
104+
state.translationDetails.isEmpty)) {
102105
final translatorSettings =
103106
context.read<SettingsBloc>().state.translatorSettings;
104107
if (translatorSettings != null &&
@@ -119,7 +122,7 @@ class HomeView extends StatelessWidget {
119122
),
120123
);
121124
}
122-
125+
123126
if (state.history.isEmpty) {
124127
return SliverToBoxAdapter(
125128
child: Center(
@@ -132,7 +135,7 @@ class HomeView extends StatelessWidget {
132135
),
133136
);
134137
}
135-
138+
136139
return SliverList(
137140
delegate: SliverChildBuilderDelegate(
138141
(context, index) {
@@ -186,6 +189,19 @@ class HomeView extends StatelessWidget {
186189
),
187190
);
188191
} else if (state.status == HomeStatus.error) {
192+
if (state.exception is NoTranslatorEnabledException) {
193+
return SliverToBoxAdapter(
194+
child: Center(
195+
child: Text(
196+
'Go to the settings page to enable a translator '
197+
'service.',
198+
style: AppTextStyle.displayXS.medium.copyWith(
199+
color: theme.errorColor,
200+
),
201+
),
202+
),
203+
);
204+
}
189205
return SliverList(
190206
delegate: SliverChildBuilderDelegate(
191207
(context, index) {

lib/presentation/settings/bloc/settings_bloc.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
2828
SettingsFetchSuccess(event.translatorSettings),
2929
);
3030
} else {
31-
emit(const SettingsFetchLoading());
31+
emit(SettingsFetchLoading(state.translatorSettings));
3232

3333
final translatorSettings = await settingsRepository.getAPIKey();
3434

@@ -54,7 +54,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
5454
return;
5555
}
5656

57-
emit(const SettingsEditTranslatorLoading());
57+
emit(SettingsEditTranslatorLoading(state.translatorSettings));
5858

5959
// Load all the keys in to the apiKeys map
6060
final apiKeys = <String, String>{
@@ -80,6 +80,16 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
8080
translatorSettings: translatorSettings,
8181
);
8282

83+
// Check which translators are removed
84+
final removedTranslators = state.translatorSettings?.enabledTranslators
85+
.where(
86+
(translator) => !event.enabledTranslators.contains(translator),
87+
)
88+
.toList() ??
89+
[];
90+
91+
event.onTranslatorSettingsChanged?.call(removedTranslators);
92+
8393
emit(
8494
SettingsEditTranslatorSuccess(translatorSettings),
8595
);

lib/presentation/settings/bloc/settings_event.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ final class SettingsEditTranslator extends SettingsEvent {
2525
required this.deepSeekApiKey,
2626
required this.youDaoAppID,
2727
required this.youDaoSecretKey,
28+
this.onTranslatorSettingsChanged,
2829
});
2930

3031
final GlobalKey<FormState> formKey;
3132

3233
final List<Translator> enabledTranslators;
3334

35+
/// Callback to notify when the translator settings are changed.
36+
/// This is used to update the UI in other pages that rely on
37+
/// the translator settings.
38+
final void Function(List<Translator>)? onTranslatorSettingsChanged;
39+
3440
// Google api keys
3541
final String? googleApiKey;
3642

lib/presentation/settings/bloc/settings_state.dart

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ final class SettingsInitial extends SettingsState {
1616
}
1717

1818
final class SettingsFetchLoading extends SettingsState {
19-
const SettingsFetchLoading();
19+
const SettingsFetchLoading(super.translatorSettings);
2020
}
2121

2222
final class SettingsFetchSuccess extends SettingsState {
2323
const SettingsFetchSuccess(super.translatorSettings);
24-
25-
@override
26-
List<Object?> get props => [translatorSettings];
2724
}
2825

2926
/// The existing api key is unchanged if a failure occurs.
@@ -36,14 +33,11 @@ final class SettingsFetchFailure extends SettingsState {
3633
}
3734

3835
final class SettingsEditTranslatorLoading extends SettingsState {
39-
const SettingsEditTranslatorLoading();
36+
const SettingsEditTranslatorLoading(super.translatorSettings);
4037
}
4138

4239
final class SettingsEditTranslatorSuccess extends SettingsState {
4340
const SettingsEditTranslatorSuccess(super.translatorSettings);
44-
45-
@override
46-
List<Object?> get props => [translatorSettings];
4741
}
4842

4943
/// Failed to save translator settings.

0 commit comments

Comments
 (0)