Skip to content

Commit 554bfa8

Browse files
committed
fix: locale progress
1 parent c28cedc commit 554bfa8

4 files changed

Lines changed: 152 additions & 112 deletions

File tree

lib/api/exptech.dart

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -352,26 +352,23 @@ class ExpTech {
352352
return json.map((e) => e as String).toList();
353353
}
354354

355-
Future<List<CrowdinLocalizationProgress>> getLocalizationProgress() async {
355+
Future<Result<List<CrowdinLocalizationProgress>, String>>
356+
getLocalizationProgress() async {
356357
final requestUrl = Routes.locale();
357358

358-
final res = await _sharedClient.get(requestUrl);
359+
final response = await _sharedClient.get(requestUrl);
359360

360-
if (res.statusCode != 200) {
361-
throw HttpException(
362-
'The server returned a status of ${res.statusCode}',
363-
uri: requestUrl,
364-
);
361+
if (response.statusCode != 200) {
362+
return Err('無法從 Crowdin 取得翻譯狀態');
365363
}
366364

367-
final json = jsonDecode(res.body) as List;
368-
369-
return json
370-
.map(
371-
(e) =>
372-
CrowdinLocalizationProgress.fromJson(e as Map<String, dynamic>),
373-
)
365+
final data = response.json()['data'] as List;
366+
final progress = data
367+
.cast<Map<String, dynamic>>()
368+
.map((e) => CrowdinLocalizationProgress.fromJson(e))
374369
.toList();
370+
371+
return Ok(progress);
375372
}
376373

377374
Future<List<String>> getRadarList() async {

lib/api/route.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class Routes {
6161

6262
static Uri station() => Uri.parse('$api/v1/trem/station');
6363

64-
static Uri locale() => Uri.parse('https://exptech.dev/api/dpip/locale');
64+
static Uri locale() => Uri.parse('https://exptech.dev/api/v1/dpip/locale');
6565

6666
static Uri radarList() => Uri.parse('$onlyapi/v1/tiles/radar/list');
6767

Lines changed: 138 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,176 @@
1+
import 'dart:math';
2+
13
import 'package:collection/collection.dart';
24
import 'package:dpip/api/model/crowdin/localization_progress.dart';
35
import 'package:dpip/core/i18n.dart';
46
import 'package:dpip/global.dart';
57
import 'package:dpip/models/settings/ui.dart';
68
import 'package:dpip/utils/extensions/build_context.dart';
9+
import 'package:dpip/utils/extensions/color_scheme.dart';
710
import 'package:dpip/utils/extensions/locale.dart';
811
import 'package:dpip/widgets/list/segmented_list.dart';
912
import 'package:flutter/material.dart';
1013
import 'package:go_router/go_router.dart';
1114
import 'package:i18n_extension/i18n_extension.dart';
1215
import 'package:intl/intl.dart';
16+
import 'package:m3e_collection/m3e_collection.dart';
1317
import 'package:material_symbols_icons/material_symbols_icons.dart';
14-
import 'package:provider/provider.dart';
18+
import 'package:option_result/result.dart';
1519

1620
class SettingsLocaleSelectPage extends StatefulWidget {
1721
const SettingsLocaleSelectPage({super.key});
1822

19-
static const route = '/settings/locale/select';
20-
2123
@override
2224
State<StatefulWidget> createState() => _SettingsLocaleSelectPageState();
2325
}
2426

25-
class _SettingsLocaleSelectPageState extends State<SettingsLocaleSelectPage> {
26-
List<CrowdinLocalizationProgress> progress = [];
27+
class _SettingsLocaleSelectPageState extends State<SettingsLocaleSelectPage>
28+
with TickerProviderStateMixin {
29+
late final _animationController =
30+
AnimationController(
31+
vsync: this,
32+
duration: const Duration(milliseconds: 1200),
33+
)
34+
..addListener(() {
35+
if (mounted) setState(() {});
36+
})
37+
..repeat();
38+
39+
Result<List<CrowdinLocalizationProgress>, String>? progress;
2740
List<Locale> localeList = I18n.supportedLocales
2841
.where((e) => !['zh'].contains(e.toLanguageTag()))
2942
.toList();
3043

44+
Future<void> _refresh() async {
45+
final result = await Global.api.getLocalizationProgress();
46+
setState(() => progress = result);
47+
}
48+
3149
@override
3250
void initState() {
3351
super.initState();
34-
Global.api.getLocalizationProgress().then((value) {
35-
setState(() => progress = value);
36-
});
52+
_refresh();
3753
}
3854

3955
@override
4056
Widget build(BuildContext context) {
41-
return ListView(
42-
padding: EdgeInsets.only(top: 8, bottom: 16 + context.padding.bottom),
43-
children: [
44-
SegmentedList(
45-
label: Text('選擇語言'.i18n),
46-
children: [
47-
for (final (index, item) in localeList.indexed)
48-
Selector<SettingsUserInterfaceModel, Locale?>(
49-
selector: (_, model) => model.locale,
50-
builder: (context, locale, child) {
51-
final p = progress.firstWhereOrNull(
52-
(e) => e.id == item.toLanguageTag(),
53-
);
54-
55-
final translated = p != null
56-
? NumberFormat('#.#%').format(p.translation / 100)
57-
: '...';
58-
final approved = p != null
59-
? NumberFormat('#.#%').format(p.approval / 100)
60-
: '...';
61-
62-
final isSelected =
63-
item.toLanguageTag() == locale?.toLanguageTag();
64-
65-
return SegmentedListTile(
66-
isFirst: index == 0,
67-
isLast: index == localeList.length - 1,
68-
title: Text(item.nativeName),
69-
subtitle: (item.toLanguageTag() != 'zh-Hant')
70-
? Column(
71-
crossAxisAlignment: CrossAxisAlignment.start,
72-
children: [
73-
Text(
74-
'已翻譯 {translated}・已校對 {approved}'.i18n.args({
75-
'translated': translated,
76-
'approved': approved,
77-
}),
78-
),
79-
Padding(
80-
padding: const EdgeInsets.symmetric(
81-
vertical: 4,
82-
),
83-
child: p != null
84-
? Stack(
85-
children: [
86-
LinearProgressIndicator(
87-
value: p.translation / 100,
88-
color: Colors.blue,
89-
),
90-
LinearProgressIndicator(
91-
value: p.approval / 100,
92-
color: Colors.lightGreen,
93-
backgroundColor: Colors.transparent,
94-
),
95-
],
96-
)
97-
: const LinearProgressIndicator(),
98-
),
99-
],
100-
)
101-
: Text('來源語言'.i18n),
102-
leading: Container(
103-
height: 28,
104-
width: 40,
105-
decoration: BoxDecoration(
106-
color: context.colors.secondaryContainer,
107-
borderRadius: BorderRadius.circular(6),
108-
),
109-
child: Center(
110-
child: Text(
111-
item.iconLabel,
112-
style: context.texts.labelLarge?.copyWith(
113-
color: context.colors.onSecondaryContainer,
114-
height: 1,
115-
),
116-
),
117-
),
57+
final locale = context.useUserInterface.locale;
58+
final data = switch (progress) {
59+
Ok(:final value)? => value,
60+
_ => null,
61+
};
62+
63+
final phaseValue = _animationController.value * 2 * pi;
64+
65+
final children = localeList.mapIndexed((index, item) {
66+
final p = data?.firstWhereOrNull(
67+
(e) => e.id == item.toLanguageTag(),
68+
);
69+
70+
final translated = p != null
71+
? NumberFormat('#.#%').format(p.translation / 100)
72+
: '...';
73+
final approved = p != null
74+
? NumberFormat('#.#%').format(p.approval / 100)
75+
: '...';
76+
77+
final isSelected = item.toLanguageTag() == locale?.toLanguageTag();
78+
79+
final progressBar = IgnorePointer(
80+
child: SizedBox(
81+
height: 10,
82+
child: switch (p) {
83+
null => LinearProgressIndicatorM3E(
84+
size: .s,
85+
phase: phaseValue,
86+
),
87+
_ => SliderTheme(
88+
data: SliderThemeData(
89+
thumbSize: .all(.zero),
90+
trackGap: 1,
91+
trackHeight: 5,
92+
padding: .zero,
93+
year2023: false,
94+
),
95+
child: Slider(
96+
activeColor: context.theme.extendedColors.green,
97+
secondaryActiveColor: context.theme.extendedColors.blue,
98+
value: p.approval / 100,
99+
secondaryTrackValue: p.translation / 100,
100+
onChanged: (_) {},
101+
),
102+
),
103+
},
104+
),
105+
);
106+
107+
return SegmentedListTile(
108+
isFirst: index == 0,
109+
isLast: index == localeList.length - 1,
110+
title: Text(item.nativeName),
111+
subtitle: (item.toLanguageTag() != 'zh-Hant')
112+
? Column(
113+
crossAxisAlignment: .start,
114+
children: [
115+
Text(
116+
'已翻譯 {translated}・已校對 {approved}'.i18n.args({
117+
'translated': translated,
118+
'approved': approved,
119+
}),
120+
),
121+
RepaintBoundary(
122+
child: Padding(
123+
padding: const .symmetric(vertical: 4),
124+
child: progressBar,
118125
),
119-
trailing: Icon(isSelected ? Symbols.check_rounded : null),
120-
onTap: () {
121-
context.locale = item;
122-
context.read<SettingsUserInterfaceModel>().setLocale(
123-
item,
124-
);
125-
context.pop();
126-
},
127-
);
128-
},
126+
),
127+
],
128+
)
129+
: Text('來源語言'.i18n),
130+
leading: Container(
131+
height: 28,
132+
width: 40,
133+
decoration: BoxDecoration(
134+
color: context.colors.secondaryContainer,
135+
borderRadius: .circular(6),
136+
),
137+
child: Center(
138+
child: Text(
139+
item.iconLabel,
140+
style: context.texts.labelLarge?.copyWith(
141+
color: context.colors.onSecondaryContainer,
142+
height: 1,
129143
),
130-
],
144+
),
145+
),
131146
),
132-
],
147+
trailing: Icon(isSelected ? Symbols.check_rounded : null),
148+
onTap: () {
149+
context.locale = item;
150+
context.userInterface.setLocale(item);
151+
context.pop();
152+
},
153+
);
154+
}).toList();
155+
156+
return ExpressiveRefreshIndicator.contained(
157+
onRefresh: _refresh,
158+
backgroundColor: context.colors.primaryContainer,
159+
child: ListView(
160+
padding: EdgeInsets.only(top: 8, bottom: 16 + context.padding.bottom),
161+
children: [
162+
SegmentedList(
163+
label: Text('選擇語言'.i18n),
164+
children: children,
165+
),
166+
],
167+
),
133168
);
134169
}
170+
171+
@override
172+
void dispose() {
173+
_animationController.dispose();
174+
super.dispose();
175+
}
135176
}

lib/app/settings/theme/page.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class SettingsThemePage extends StatelessWidget {
4040
),
4141
title: Text('主題模式'.i18n),
4242
subtitle: Text(themeMode.label.i18n),
43+
trailing: const Icon(Symbols.chevron_right_rounded),
4344
onTap: () => const SettingsThemeModeRoute().push(context),
4445
);
4546
},
@@ -58,6 +59,7 @@ class SettingsThemePage extends StatelessWidget {
5859
null => '使用系統配色'.i18n,
5960
final v => ColorTools.nameThatColor(v),
6061
}),
62+
trailing: const Icon(Symbols.chevron_right_rounded),
6163
onTap: () =>
6264
const SettingsThemeColorRoute().push(context),
6365
);

0 commit comments

Comments
 (0)