Skip to content

Commit

Permalink
feat(l10n): add full localization support
Browse files Browse the repository at this point in the history
  • Loading branch information
ZhuJHua committed Jan 18, 2025
1 parent 4f12267 commit 5af6d90
Show file tree
Hide file tree
Showing 56 changed files with 1,795 additions and 932 deletions.
5 changes: 3 additions & 2 deletions lib/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:mood_diary/common/models/hitokoto.dart';
import 'package:mood_diary/common/models/hunyuan.dart';
import 'package:mood_diary/common/models/image.dart';
import 'package:mood_diary/common/models/weather.dart';
import 'package:mood_diary/main.dart';
import 'package:mood_diary/utils/http_util.dart';
import 'package:mood_diary/utils/notice_util.dart';
import 'package:mood_diary/utils/signature_util.dart';
Expand Down Expand Up @@ -58,11 +59,11 @@ class Api {
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
NoticeUtil.showToast('请开启定位权限');
NoticeUtil.showToast(l10n.noticeEnableLocation);
return null;
}
if (permission == LocationPermission.deniedForever) {
NoticeUtil.showToast('请前往设置开启定位权限');
NoticeUtil.showToast(l10n.noticeEnableLocation2);
return null;
}
}
Expand Down
24 changes: 24 additions & 0 deletions lib/common/values/language.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:mood_diary/main.dart';

enum Language {
system('system'),
chinese('zh'),
english('en');

final String languageCode;

const Language(this.languageCode);
}

extension LanguageExtension on Language {
String get l10nText {
switch (this) {
case Language.system:
return l10n.settingLanguageSystem;
case Language.chinese:
return l10n.settingLanguageSimpleChinese;
case Language.english:
return l10n.settingLanguageEnglish;
}
}
}
19 changes: 19 additions & 0 deletions lib/components/base/button.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:mood_diary/main.dart';
import 'package:refreshed/refreshed.dart';

class FrostedGlassButton extends StatelessWidget {
final Widget child;
Expand Down Expand Up @@ -96,3 +98,20 @@ class MultiFabLayoutDelegate extends MultiChildLayoutDelegate {
oldDelegate.layoutIds != layoutIds;
}
}

class PageBackButton extends StatelessWidget {
const PageBackButton({super.key});

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: IconButton(
onPressed: Get.back,
icon: const Icon(Icons.arrow_back_rounded),
color: colorScheme.onSurface,
tooltip: l10n.back,
),
);
}
}
297 changes: 297 additions & 0 deletions lib/components/base/marquee.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import 'dart:async';

import 'package:flutter/material.dart';

class IntegralCurve extends Curve {
static double delta = 0.01;

const IntegralCurve._(this.original, this.integral, this._values);

final Curve original;
final double integral;
final Map<double, double> _values;

factory IntegralCurve(Curve original) {
double integral = 0.0;
final values = <double, double>{};

for (double t = 0.0; t <= 1.0; t += delta) {
integral += original.transform(t) * delta;
values[t] = integral;
}
values[1.0] = integral;

// Normalize.
for (final double t in values.keys) {
values[t] = values[t]! / integral;
}

return IntegralCurve._(original, integral, values);
}

@override
double transform(double t) {
if (t < 0) return 0.0;
for (final key in _values.keys) {
if (key > t) return _values[key]!;
}
return 1.0;
}
}

class Marquee extends StatefulWidget {
Marquee({
super.key,
required this.text,
this.style,
this.textScaleFactor,
this.textDirection = TextDirection.ltr,
this.scrollAxis = Axis.horizontal,
this.crossAxisAlignment = CrossAxisAlignment.center,
this.blankSpace = 0.0,
this.velocity = 50.0,
this.startAfter = Duration.zero,
this.pauseAfterRound = Duration.zero,
this.fadingEdgeStartFraction = 0.0,
this.fadingEdgeEndFraction = 0.0,
this.numberOfRounds,
this.startPadding = 0.0,
this.accelerationDuration = Duration.zero,
Curve accelerationCurve = Curves.decelerate,
this.decelerationDuration = Duration.zero,
Curve decelerationCurve = Curves.decelerate,
this.onDone,
}) : assert(!blankSpace.isNaN),
assert(blankSpace >= 0, "The blankSpace needs to be positive or zero."),
assert(blankSpace.isFinite),
assert(!velocity.isNaN),
assert(velocity != 0.0, "The velocity cannot be zero."),
assert(velocity.isFinite),
assert(
pauseAfterRound >= Duration.zero,
"The pauseAfterRound cannot be negative as time travel isn't "
"invented yet.",
),
assert(
fadingEdgeStartFraction >= 0 && fadingEdgeStartFraction <= 1,
"The fadingEdgeGradientFractionOnStart value should be between 0 and "
"1, inclusive",
),
assert(
fadingEdgeEndFraction >= 0 && fadingEdgeEndFraction <= 1,
"The fadingEdgeGradientFractionOnEnd value should be between 0 and "
"1, inclusive",
),
assert(numberOfRounds == null || numberOfRounds > 0),
assert(
accelerationDuration >= Duration.zero,
"The accelerationDuration cannot be negative as time travel isn't "
"invented yet.",
),
assert(
decelerationDuration >= Duration.zero,
"The decelerationDuration must be positive or zero as time travel "
"isn't invented yet.",
),
accelerationCurve = IntegralCurve(accelerationCurve),
decelerationCurve = IntegralCurve(decelerationCurve);
final String text;
final TextStyle? style;
final double? textScaleFactor;
final TextDirection textDirection;
final Axis scrollAxis;
final CrossAxisAlignment crossAxisAlignment;
final double blankSpace;
final double velocity;
final Duration startAfter;
final Duration pauseAfterRound;
final int? numberOfRounds;
final double fadingEdgeStartFraction;
final double fadingEdgeEndFraction;
final double startPadding;
final Duration accelerationDuration;
final IntegralCurve accelerationCurve;
final Duration decelerationDuration;
final IntegralCurve decelerationCurve;
final VoidCallback? onDone;

@override
State<StatefulWidget> createState() => _MarqueeState();
}

class _MarqueeState extends State<Marquee> with SingleTickerProviderStateMixin {
final ScrollController _controller = ScrollController();

late double _startPosition;
late double _accelerationTarget;
late double _linearTarget;
late double _decelerationTarget;

late Duration _totalDuration;

Duration get _accelerationDuration => widget.accelerationDuration;
Duration? _linearDuration;

Duration get _decelerationDuration => widget.decelerationDuration;

bool _running = false;
int _roundCounter = 0;

bool get isDone => widget.numberOfRounds == null
? false
: widget.numberOfRounds == _roundCounter;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!_running) {
_running = true;
if (_controller.hasClients) {
_controller.jumpTo(_startPosition);
await Future<void>.delayed(widget.startAfter);
Future.doWhile(_scroll);
}
}
});
}

Future<bool> _scroll() async {
await _makeRoundTrip();
if (isDone && widget.onDone != null) {
widget.onDone!();
}
return _running && !isDone && _controller.hasClients;
}

@override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget as Marquee);
}

@override
void dispose() {
_running = false;
_controller.dispose();
super.dispose();
}

void _initialize(BuildContext context) {
final totalLength = _getTextWidth(context) + widget.blankSpace;
final accelerationLength = widget.accelerationCurve.integral *
widget.velocity *
_accelerationDuration.inMilliseconds /
1000.0;
final decelerationLength = widget.decelerationCurve.integral *
widget.velocity *
_decelerationDuration.inMilliseconds /
1000.0;
final linearLength =
(totalLength - accelerationLength.abs() - decelerationLength.abs()) *
(widget.velocity > 0 ? 1 : -1);

_startPosition = 2 * totalLength - widget.startPadding;
_accelerationTarget = _startPosition + accelerationLength;
_linearTarget = _accelerationTarget + linearLength;
_decelerationTarget = _linearTarget + decelerationLength;

_totalDuration = _accelerationDuration +
_decelerationDuration +
Duration(milliseconds: (linearLength / widget.velocity * 1000).toInt());
_linearDuration =
_totalDuration - _accelerationDuration - _decelerationDuration;
}

Future<void> _makeRoundTrip() async {
if (!_controller.hasClients) return;
_controller.jumpTo(_startPosition);
if (!_running) return;

await _accelerate();
if (!_running) return;

await _moveLinearly();
if (!_running) return;

await _decelerate();

_roundCounter++;

if (!_running || !mounted) return;
}

Future<void> _accelerate() async {
await _animateTo(
_accelerationTarget,
_accelerationDuration,
widget.accelerationCurve,
);
}

Future<void> _moveLinearly() async {
await _animateTo(_linearTarget, _linearDuration, Curves.linear);
}

Future<void> _decelerate() async {
await _animateTo(
_decelerationTarget,
_decelerationDuration,
widget.decelerationCurve.flipped,
);
}

Future<void> _animateTo(
double? target,
Duration? duration,
Curve curve,
) async {
if (!_controller.hasClients) return;
if (duration! > Duration.zero) {
await _controller.animateTo(target!, duration: duration, curve: curve);
} else {
_controller.jumpTo(target!);
}
}

double _getTextWidth(BuildContext context) {
final span = TextSpan(text: widget.text, style: widget.style);
const constraints = BoxConstraints(maxWidth: double.infinity);
final richTextWidget = Text.rich(span).build(context) as RichText;
final renderObject = richTextWidget.createRenderObject(context);
renderObject.layout(constraints);
final boxes = renderObject.getBoxesForSelection(TextSelection(
baseOffset: 0,
extentOffset: TextSpan(text: widget.text).toPlainText().length,
));
return boxes.last.right;
}

@override
Widget build(BuildContext context) {
_initialize(context);
return ListView.builder(
controller: _controller,
scrollDirection: widget.scrollAxis,
reverse: widget.textDirection == TextDirection.rtl,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, i) {
return i.isEven
? Text(
widget.text,
style: widget.style,
textScaler: widget.textScaleFactor != null
? TextScaler.linear(widget.textScaleFactor!)
: null,
)
: SizedBox(
width: widget.scrollAxis == Axis.horizontal
? widget.blankSpace
: null,
height: widget.scrollAxis == Axis.vertical
? widget.blankSpace
: null,
);
},
);
}
}
Loading

0 comments on commit 5af6d90

Please sign in to comment.