Merge pull request #17 from gskinnerTeam/feature/swappable-locale
Swapping locales feature
This commit is contained in:
commit
08a2c215f5
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"appName": "Wonderous",
|
"appName": "Wonderous",
|
||||||
|
"localeSwapButton": "简体中文",
|
||||||
"animatedArrowSemanticSwipe": "Explore details about {title}.",
|
"animatedArrowSemanticSwipe": "Explore details about {title}.",
|
||||||
"appBarTitleFactsHistory": "Facts and History",
|
"appBarTitleFactsHistory": "Facts and History",
|
||||||
"appBarTitleConstruction": "Construction",
|
"appBarTitleConstruction": "Construction",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"appName": "Wonderous",
|
"appName": "Wonderous",
|
||||||
|
"localeSwapButton": "English",
|
||||||
"animatedArrowSemanticSwipe": "查看关于{title}的详细信息。",
|
"animatedArrowSemanticSwipe": "查看关于{title}的详细信息。",
|
||||||
"appBarTitleFactsHistory": "历史与细节",
|
"appBarTitleFactsHistory": "历史与细节",
|
||||||
"appBarTitleConstruction": "建造",
|
"appBarTitleConstruction": "建造",
|
||||||
|
@ -30,8 +30,11 @@ class AppLogic {
|
|||||||
// Localizations
|
// Localizations
|
||||||
await localeLogic.load();
|
await localeLogic.load();
|
||||||
|
|
||||||
|
// Data load
|
||||||
|
wondersLogic.init();
|
||||||
|
|
||||||
// Timeline
|
// Timeline
|
||||||
await timelineLogic.init();
|
timelineLogic.init();
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
await settingsLogic.load();
|
await settingsLogic.load();
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:intl/intl_standalone.dart';
|
import 'package:intl/intl_standalone.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
|
|
||||||
class LocaleLogic {
|
class LocaleLogic {
|
||||||
AppLocalizations? _strings;
|
AppLocalizations? _strings;
|
||||||
@ -10,6 +11,8 @@ class LocaleLogic {
|
|||||||
|
|
||||||
bool get isLoaded => _strings != null;
|
bool get isLoaded => _strings != null;
|
||||||
|
|
||||||
|
bool get isEnglish => strings.localeName == 'en';
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final localeCode = await findSystemLocale();
|
final localeCode = await findSystemLocale();
|
||||||
Locale locale = Locale(localeCode.split('_')[0]);
|
Locale locale = Locale(localeCode.split('_')[0]);
|
||||||
@ -20,6 +23,13 @@ class LocaleLogic {
|
|||||||
if (AppLocalizations.supportedLocales.contains(locale) == false) {
|
if (AppLocalizations.supportedLocales.contains(locale) == false) {
|
||||||
locale = Locale('en');
|
locale = Locale('en');
|
||||||
}
|
}
|
||||||
|
settingsLogic.currentLocale.value = locale.languageCode;
|
||||||
|
_strings = await AppLocalizations.delegate.load(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshIfChanged(Locale locale) async {
|
||||||
|
if (_strings?.localeName != locale.languageCode && AppLocalizations.supportedLocales.contains(locale)) {
|
||||||
_strings = await AppLocalizations.delegate.load(locale);
|
_strings = await AppLocalizations.delegate.load(locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/logic/common/save_load_mixin.dart';
|
import 'package:wonders/logic/common/save_load_mixin.dart';
|
||||||
|
|
||||||
class SettingsLogic with ThrottledSaveLoadMixin {
|
class SettingsLogic with ThrottledSaveLoadMixin {
|
||||||
@ -7,6 +8,7 @@ class SettingsLogic with ThrottledSaveLoadMixin {
|
|||||||
|
|
||||||
late final hasCompletedOnboarding = ValueNotifier<bool>(false)..addListener(scheduleSave);
|
late final hasCompletedOnboarding = ValueNotifier<bool>(false)..addListener(scheduleSave);
|
||||||
late final hasDismissedSearchMessage = ValueNotifier<bool>(false)..addListener(scheduleSave);
|
late final hasDismissedSearchMessage = ValueNotifier<bool>(false)..addListener(scheduleSave);
|
||||||
|
late final currentLocale = ValueNotifier<String>('en')..addListener(scheduleSave);
|
||||||
|
|
||||||
final bool useBlurs = defaultTargetPlatform != TargetPlatform.android;
|
final bool useBlurs = defaultTargetPlatform != TargetPlatform.android;
|
||||||
|
|
||||||
@ -23,4 +25,11 @@ class SettingsLogic with ThrottledSaveLoadMixin {
|
|||||||
'hasDismissedSearchMessage': hasDismissedSearchMessage.value,
|
'hasDismissedSearchMessage': hasDismissedSearchMessage.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setLocale(Locale value) async {
|
||||||
|
currentLocale.value = value.languageCode;
|
||||||
|
await localeLogic.refreshIfChanged(value);
|
||||||
|
wondersLogic.init();
|
||||||
|
timelineLogic.init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,17 @@ import 'package:wonders/logic/common/string_utils.dart';
|
|||||||
import 'package:wonders/logic/data/timeline_data.dart';
|
import 'package:wonders/logic/data/timeline_data.dart';
|
||||||
|
|
||||||
class TimelineLogic {
|
class TimelineLogic {
|
||||||
final List<TimelineEvent> events = [];
|
List<TimelineEvent> events = [];
|
||||||
|
|
||||||
Future<void> init() async {
|
void init() {
|
||||||
events.addAll(GlobalEventsData().globalEvents);
|
events = [
|
||||||
|
...GlobalEventsData().globalEvents,
|
||||||
for (var w in wondersLogic.all) {
|
...wondersLogic.all.map(
|
||||||
events.add(
|
(w) => TimelineEvent(
|
||||||
TimelineEvent(
|
|
||||||
w.startYr,
|
w.startYr,
|
||||||
StringUtils.supplant($strings.timelineLabelConstruction, {'{title}': w.title}),
|
StringUtils.supplant($strings.timelineLabelConstruction, {'{title}': w.title}),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,16 +10,7 @@ import 'package:wonders/logic/data/wonders_data/pyramids_giza_data.dart';
|
|||||||
import 'package:wonders/logic/data/wonders_data/taj_mahal_data.dart';
|
import 'package:wonders/logic/data/wonders_data/taj_mahal_data.dart';
|
||||||
|
|
||||||
class WondersLogic {
|
class WondersLogic {
|
||||||
late List<WonderData> all = [
|
List<WonderData> all = [];
|
||||||
GreatWallData(),
|
|
||||||
PetraData(),
|
|
||||||
ColosseumData(),
|
|
||||||
ChichenItzaData(),
|
|
||||||
MachuPicchuData(),
|
|
||||||
TajMahalData(),
|
|
||||||
ChristRedeemerData(),
|
|
||||||
PyramidsGizaData(),
|
|
||||||
];
|
|
||||||
|
|
||||||
final int timelineStartYear = -3000;
|
final int timelineStartYear = -3000;
|
||||||
final int timelineEndYear = 2200;
|
final int timelineEndYear = 2200;
|
||||||
@ -29,4 +20,17 @@ class WondersLogic {
|
|||||||
if (result == null) throw ('Could not find data for wonder type $value');
|
if (result == null) throw ('Could not find data for wonder type $value');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
all = [
|
||||||
|
GreatWallData(),
|
||||||
|
PetraData(),
|
||||||
|
ColosseumData(),
|
||||||
|
ChichenItzaData(),
|
||||||
|
MachuPicchuData(),
|
||||||
|
TajMahalData(),
|
||||||
|
ChristRedeemerData(),
|
||||||
|
PyramidsGizaData(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,11 @@ class WondersApp extends StatelessWidget {
|
|||||||
const WondersApp({Key? key}) : super(key: key);
|
const WondersApp({Key? key}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<String>(
|
||||||
|
valueListenable: settingsLogic.currentLocale,
|
||||||
|
builder: (_, localeCode, __) {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
|
locale: Locale(localeCode),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
routerDelegate: appRouter.routerDelegate,
|
routerDelegate: appRouter.routerDelegate,
|
||||||
routeInformationProvider: appRouter.routeInformationProvider,
|
routeInformationProvider: appRouter.routeInformationProvider,
|
||||||
@ -45,6 +49,8 @@ class WondersApp extends StatelessWidget {
|
|||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create singletons (controllers and services) that can be shared across the app.
|
/// Create singletons (controllers and services) that can be shared across the app.
|
||||||
|
19
lib/ui/common/controls/locale_switcher.dart
Normal file
19
lib/ui/common/controls/locale_switcher.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
|
|
||||||
|
class LocaleSwitcher extends StatelessWidget with GetItMixin {
|
||||||
|
LocaleSwitcher({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final locale = watchX((SettingsLogic s) => s.currentLocale);
|
||||||
|
Future<void> handleSwapLocale() async {
|
||||||
|
final newLocale = Locale(locale == 'en' ? 'zh' : 'en');
|
||||||
|
await settingsLogic.setLocale(newLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppBtn.from(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: $styles.insets.sm, horizontal: $styles.insets.sm),
|
||||||
|
text: $strings.localeSwapButton,
|
||||||
|
onPressed: handleSwapLocale);
|
||||||
|
}
|
||||||
|
}
|
@ -31,11 +31,10 @@ class _ResultsGrid extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLanguageMessage(BuildContext context) {
|
Widget _buildLanguageMessage(BuildContext context) {
|
||||||
bool isEnglish = localeLogic.strings.localeName == 'en';
|
|
||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
valueListenable: settingsLogic.hasDismissedSearchMessage,
|
valueListenable: settingsLogic.hasDismissedSearchMessage,
|
||||||
builder: (_, value, __) {
|
builder: (_, value, __) {
|
||||||
if (isEnglish || value) return SizedBox();
|
if (localeLogic.isEnglish || value) return SizedBox();
|
||||||
return AppBtn.basic(
|
return AppBtn.basic(
|
||||||
onPressed: () => settingsLogic.hasDismissedSearchMessage.value = true,
|
onPressed: () => settingsLogic.hasDismissedSearchMessage.value = true,
|
||||||
semanticLabel: $strings.resultsSemanticDismiss,
|
semanticLabel: $strings.resultsSemanticDismiss,
|
||||||
|
@ -28,8 +28,7 @@ class _ScrollingContent extends StatelessWidget {
|
|||||||
final String dropChar = value.substring(0, 1);
|
final String dropChar = value.substring(0, 1);
|
||||||
final textScale = MediaQuery.of(context).textScaleFactor;
|
final textScale = MediaQuery.of(context).textScaleFactor;
|
||||||
final double dropCapWidth = StringUtils.measure(dropChar, dropStyle).width * textScale;
|
final double dropCapWidth = StringUtils.measure(dropChar, dropStyle).width * textScale;
|
||||||
final bool isEnglish = localeLogic.strings.localeName == 'en'; //TODO EC: Helper method for localLogic.isEnglish?
|
final bool skipCaps = !localeLogic.isEnglish || MediaQuery.of(context).accessibleNavigation;
|
||||||
final bool skipCaps = !isEnglish || MediaQuery.of(context).accessibleNavigation;
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: value,
|
label: value,
|
||||||
child: !skipCaps
|
child: !skipCaps
|
||||||
|
@ -4,6 +4,7 @@ import 'package:wonders/common_libs.dart';
|
|||||||
import 'package:wonders/logic/data/wonder_data.dart';
|
import 'package:wonders/logic/data/wonder_data.dart';
|
||||||
import 'package:wonders/ui/common/app_backdrop.dart';
|
import 'package:wonders/ui/common/app_backdrop.dart';
|
||||||
import 'package:wonders/ui/common/app_icons.dart';
|
import 'package:wonders/ui/common/app_icons.dart';
|
||||||
|
import 'package:wonders/ui/common/controls/locale_switcher.dart';
|
||||||
import 'package:wonders/ui/screens/home_menu/about_dialog_content.dart';
|
import 'package:wonders/ui/screens/home_menu/about_dialog_content.dart';
|
||||||
|
|
||||||
class HomeMenu extends StatelessWidget {
|
class HomeMenu extends StatelessWidget {
|
||||||
@ -49,11 +50,23 @@ class HomeMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
SafeArea(
|
||||||
|
child: PaddedRow(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: $styles.insets.md,
|
||||||
|
vertical: $styles.insets.sm,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
/// Back btn
|
/// Back btn
|
||||||
BackBtn.close(
|
BackBtn.close(
|
||||||
bgColor: Colors.transparent,
|
bgColor: Colors.transparent,
|
||||||
iconColor: $styles.colors.offWhite,
|
iconColor: $styles.colors.offWhite,
|
||||||
).safe(),
|
),
|
||||||
|
Spacer(),
|
||||||
|
LocaleSwitcher()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
/// Content
|
/// Content
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@ -75,7 +88,7 @@ class HomeMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -106,6 +119,9 @@ class HomeMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomBtns(BuildContext context) {
|
Widget _buildBottomBtns(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: settingsLogic.currentLocale,
|
||||||
|
builder: (_, __, ___) {
|
||||||
return SeparatedColumn(
|
return SeparatedColumn(
|
||||||
separatorBuilder: () => Divider(thickness: 1.5, height: 1).animate().scale(
|
separatorBuilder: () => Divider(thickness: 1.5, height: 1).animate().scale(
|
||||||
duration: $styles.times.slow,
|
duration: $styles.times.slow,
|
||||||
@ -131,6 +147,7 @@ class HomeMenu extends StatelessWidget {
|
|||||||
.fade(delay: $styles.times.pageTransition + 50.ms)
|
.fade(delay: $styles.times.pageTransition + 50.ms)
|
||||||
.slide(begin: Offset(0, .1), curve: Curves.easeOut),
|
.slide(begin: Offset(0, .1), curve: Curves.easeOut),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGridBtn(BuildContext context, WonderData btnData) {
|
Widget _buildGridBtn(BuildContext context, WonderData btnData) {
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/ui/common/app_icons.dart';
|
import 'package:wonders/ui/common/app_icons.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
||||||
|
import 'package:wonders/ui/common/controls/locale_switcher.dart';
|
||||||
import 'package:wonders/ui/common/static_text_scale.dart';
|
import 'package:wonders/ui/common/static_text_scale.dart';
|
||||||
import 'package:wonders/ui/common/themed_text.dart';
|
import 'package:wonders/ui/common/themed_text.dart';
|
||||||
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
||||||
@ -19,11 +20,7 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
static const double _textHeight = 155;
|
static const double _textHeight = 155;
|
||||||
static const double _pageIndicatorHeight = 55;
|
static const double _pageIndicatorHeight = 55;
|
||||||
|
|
||||||
static List<_PageData> pageData = [
|
static List<_PageData> pageData = [];
|
||||||
_PageData($strings.introTitleJourney, $strings.introDescriptionNavigate, 'camel', '1'),
|
|
||||||
_PageData($strings.introTitleExplore, $strings.introDescriptionUncover, 'petra', '2'),
|
|
||||||
_PageData($strings.introTitleDiscover, $strings.introDescriptionLearn, 'statue', '3'),
|
|
||||||
];
|
|
||||||
|
|
||||||
late final PageController _pageController = PageController()..addListener(_handlePageChanged);
|
late final PageController _pageController = PageController()..addListener(_handlePageChanged);
|
||||||
final ValueNotifier<int> _currentPage = ValueNotifier(0);
|
final ValueNotifier<int> _currentPage = ValueNotifier(0);
|
||||||
@ -51,6 +48,13 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Set the page data, as strings may have changed based on locale
|
||||||
|
pageData = [
|
||||||
|
_PageData($strings.introTitleJourney, $strings.introDescriptionNavigate, 'camel', '1'),
|
||||||
|
_PageData($strings.introTitleExplore, $strings.introDescriptionUncover, 'petra', '2'),
|
||||||
|
_PageData($strings.introTitleDiscover, $strings.introDescriptionLearn, 'statue', '3'),
|
||||||
|
];
|
||||||
|
|
||||||
// This view uses a full screen PageView to enable swipe navigation.
|
// This view uses a full screen PageView to enable swipe navigation.
|
||||||
// However, we only want the title / description to actually swipe,
|
// However, we only want the title / description to actually swipe,
|
||||||
// so we stack a PageView with that content over top of all the other
|
// so we stack a PageView with that content over top of all the other
|
||||||
|
Loading…
x
Reference in New Issue
Block a user