feat: Add i18n using slang

This commit is contained in:
PapaTutuWawa 2023-06-21 22:07:12 +02:00
parent 7530fe5b80
commit 13ed7144cb
14 changed files with 493 additions and 79 deletions

View File

@ -0,0 +1,56 @@
{
"settings": {
"title": "Settings",
"importAnime": "Import anime list",
"importAnimeDesc": "Import anime list exported from MyAnimeList.",
"invalidAnimeListTitle": "Invalid anime list",
"invalidAnimeListBody": "The selected file is not a MAL anime list. It lacks the \".xml.gz\" suffix.",
"importManga": "Import manga list",
"importMangaDesc": "Import manga list exported from MyAnimeList.",
"invalidMangaListTitle": "Invalid manga list",
"invalidMangaListBody": "The selected file is not a MAL manga list. It lacks the \".xml.gz\" suffix.",
"importIndicator": "$current of $total"
},
"about": {
"title": "About",
"source": "Source code"
},
"tooltips": {
"addNewItem": "Add new item"
},
"content": {
"anime": "Anime",
"manga": "Manga"
},
"search": {
"anime": "Anime search",
"manga": "Manga search",
"query": "Search query"
},
"details": {
"title": "Details",
"removeTitle": "Remove $title?",
"removeBody": "Are you sure you want to remove \"$title\" from the list?",
"removeButton": "Remove",
"cancelButton": "Cancel",
"watchState": "Watch state",
"readState": "Read state",
"episodes": "Episodes",
"chapters": "Chapters",
"volumesOwned": "Volumes owned"
},
"data": {
"ongoing": {
"anime": "Watching",
"manga": "Reading"
},
"completed": "Completed",
"planned": {
"anime": "Plan to watch",
"manga": "Plan to read"
},
"dropped": "Dropped",
"paused": "Paused",
"all": "All"
}
}

7
build.yaml Normal file
View File

@ -0,0 +1,7 @@
targets:
$default:
builders:
slang_build_runner:
options:
input_directory: assets/i18n
output_directory: lib/i18n

View File

@ -1,15 +1,12 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1681202837, "lastModified": 1667395993,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401", "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,21 +36,6 @@
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -38,7 +38,6 @@
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart scrcpy # Flutter/Android flutter pinnedJDK android.platform-tools dart scrcpy # Flutter/Android
pythonEnv gnumake # Build scripts
gitlint jq # Code hygiene gitlint jq # Code hygiene
ripgrep # General utilities ripgrep # General utilities
]; ];

322
lib/i18n/strings.g.dart Normal file
View File

@ -0,0 +1,322 @@
/// Generated file. Do not edit.
///
/// Locales: 1
/// Strings: 36
///
/// Built on 2023-06-21 at 20:03 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
import 'package:flutter/widgets.dart';
import 'package:slang/builder/model/node.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
const AppLocale _baseLocale = AppLocale.en;
/// Supported locales, see extension methods below.
///
/// Usage:
/// - LocaleSettings.setLocale(AppLocale.en) // set locale
/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
enum AppLocale with BaseAppLocale<AppLocale, _StringsEn> {
en(languageCode: 'en', build: _StringsEn.build);
const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element
@override final String languageCode;
@override final String? scriptCode;
@override final String? countryCode;
@override final TranslationBuilder<AppLocale, _StringsEn> build;
/// Gets current instance managed by [LocaleSettings].
_StringsEn get translations => LocaleSettings.instance.translationMap[this]!;
}
/// Method A: Simple
///
/// No rebuild after locale change.
/// Translation happens during initialization of the widget (call of t).
/// Configurable via 'translate_var'.
///
/// Usage:
/// String a = t.someKey.anotherKey;
/// String b = t['someKey.anotherKey']; // Only for edge cases!
_StringsEn get t => LocaleSettings.instance.currentTranslations;
/// Method B: Advanced
///
/// All widgets using this method will trigger a rebuild when locale changes.
/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
///
/// Step 1:
/// wrap your App with
/// TranslationProvider(
/// child: MyApp()
/// );
///
/// Step 2:
/// final t = Translations.of(context); // Get t variable.
/// String a = t.someKey.anotherKey; // Use t variable.
/// String b = t['someKey.anotherKey']; // Only for edge cases!
class Translations {
Translations._(); // no constructor
static _StringsEn of(BuildContext context) => InheritedLocaleData.of<AppLocale, _StringsEn>(context).translations;
}
/// The provider for method B
class TranslationProvider extends BaseTranslationProvider<AppLocale, _StringsEn> {
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
static InheritedLocaleData<AppLocale, _StringsEn> of(BuildContext context) => InheritedLocaleData.of<AppLocale, _StringsEn>(context);
}
/// Method B shorthand via [BuildContext] extension method.
/// Configurable via 'translate_var'.
///
/// Usage (e.g. in a widget's build method):
/// context.t.someKey.anotherKey
extension BuildContextTranslationsExtension on BuildContext {
_StringsEn get t => TranslationProvider.of(this).translations;
}
/// Manages all translation instances and the current locale
class LocaleSettings extends BaseFlutterLocaleSettings<AppLocale, _StringsEn> {
LocaleSettings._() : super(utils: AppLocaleUtils.instance);
static final instance = LocaleSettings._();
// static aliases (checkout base methods for documentation)
static AppLocale get currentLocale => instance.currentLocale;
static Stream<AppLocale> getLocaleStream() => instance.getLocaleStream();
static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale useDeviceLocale() => instance.useDeviceLocale();
@Deprecated('Use [AppLocaleUtils.supportedLocales]') static List<Locale> get supportedLocales => instance.supportedLocales;
@Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
/// Provides utility functions without any side effects.
class AppLocaleUtils extends BaseAppLocaleUtils<AppLocale, _StringsEn> {
AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values);
static final instance = AppLocaleUtils._();
// static aliases (checkout base methods for documentation)
static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
static AppLocale findDeviceLocale() => instance.findDeviceLocale();
static List<Locale> get supportedLocales => instance.supportedLocales;
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
}
// translations
// Path: <root>
class _StringsEn implements BaseTranslations<AppLocale, _StringsEn> {
/// You can call this constructor and build your own translation instance of this locale.
/// Constructing via the enum [AppLocale.build] is preferred.
_StringsEn.build({Map<String, Node>? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})
: assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'),
$meta = TranslationMetadata(
locale: AppLocale.en,
overrides: overrides ?? {},
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
) {
$meta.setFlatMapFunction(_flatMapFunction);
}
/// Metadata for the translations of <en>.
@override final TranslationMetadata<AppLocale, _StringsEn> $meta;
/// Access flat map
dynamic operator[](String key) => $meta.getTranslation(key);
late final _StringsEn _root = this; // ignore: unused_field
// Translations
late final _StringsSettingsEn settings = _StringsSettingsEn._(_root);
late final _StringsAboutEn about = _StringsAboutEn._(_root);
late final _StringsTooltipsEn tooltips = _StringsTooltipsEn._(_root);
late final _StringsContentEn content = _StringsContentEn._(_root);
late final _StringsSearchEn search = _StringsSearchEn._(_root);
late final _StringsDetailsEn details = _StringsDetailsEn._(_root);
late final _StringsDataEn data = _StringsDataEn._(_root);
}
// Path: settings
class _StringsSettingsEn {
_StringsSettingsEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get title => 'Settings';
String get importAnime => 'Import anime list';
String get importAnimeDesc => 'Import anime list exported from MyAnimeList.';
String get invalidAnimeListTitle => 'Invalid anime list';
String get invalidAnimeListBody => 'The selected file is not a MAL anime list. It lacks the ".xml.gz" suffix.';
String get importManga => 'Import manga list';
String get importMangaDesc => 'Import manga list exported from MyAnimeList.';
String get invalidMangaListTitle => 'Invalid manga list';
String get invalidMangaListBody => 'The selected file is not a MAL manga list. It lacks the ".xml.gz" suffix.';
String importIndicator({required Object current, required Object total}) => '${current} of ${total}';
}
// Path: about
class _StringsAboutEn {
_StringsAboutEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get title => 'About';
String get source => 'Source code';
}
// Path: tooltips
class _StringsTooltipsEn {
_StringsTooltipsEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get addNewItem => 'Add new item';
}
// Path: content
class _StringsContentEn {
_StringsContentEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get anime => 'Anime';
String get manga => 'Manga';
}
// Path: search
class _StringsSearchEn {
_StringsSearchEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get anime => 'Anime search';
String get manga => 'Manga search';
String get query => 'Search query';
}
// Path: details
class _StringsDetailsEn {
_StringsDetailsEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get title => 'Details';
String removeTitle({required Object title}) => 'Remove ${title}?';
String removeBody({required Object title}) => 'Are you sure you want to remove "${title}" from the list?';
String get removeButton => 'Remove';
String get cancelButton => 'Cancel';
String get watchState => 'Watch state';
String get readState => 'Read state';
String get episodes => 'Episodes';
String get chapters => 'Chapters';
String get volumesOwned => 'Volumes owned';
}
// Path: data
class _StringsDataEn {
_StringsDataEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
late final _StringsDataOngoingEn ongoing = _StringsDataOngoingEn._(_root);
String get completed => 'Completed';
late final _StringsDataPlannedEn planned = _StringsDataPlannedEn._(_root);
String get dropped => 'Dropped';
String get paused => 'Paused';
String get all => 'All';
}
// Path: data.ongoing
class _StringsDataOngoingEn {
_StringsDataOngoingEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get anime => 'Watching';
String get manga => 'Reading';
}
// Path: data.planned
class _StringsDataPlannedEn {
_StringsDataPlannedEn._(this._root);
final _StringsEn _root; // ignore: unused_field
// Translations
String get anime => 'Plan to watch';
String get manga => 'Plan to read';
}
/// Flat map(s) containing all translations.
/// Only for edge cases! For simple maps, use the map function of this library.
extension on _StringsEn {
dynamic _flatMapFunction(String path) {
switch (path) {
case 'settings.title': return 'Settings';
case 'settings.importAnime': return 'Import anime list';
case 'settings.importAnimeDesc': return 'Import anime list exported from MyAnimeList.';
case 'settings.invalidAnimeListTitle': return 'Invalid anime list';
case 'settings.invalidAnimeListBody': return 'The selected file is not a MAL anime list. It lacks the ".xml.gz" suffix.';
case 'settings.importManga': return 'Import manga list';
case 'settings.importMangaDesc': return 'Import manga list exported from MyAnimeList.';
case 'settings.invalidMangaListTitle': return 'Invalid manga list';
case 'settings.invalidMangaListBody': return 'The selected file is not a MAL manga list. It lacks the ".xml.gz" suffix.';
case 'settings.importIndicator': return ({required Object current, required Object total}) => '${current} of ${total}';
case 'about.title': return 'About';
case 'about.source': return 'Source code';
case 'tooltips.addNewItem': return 'Add new item';
case 'content.anime': return 'Anime';
case 'content.manga': return 'Manga';
case 'search.anime': return 'Anime search';
case 'search.manga': return 'Manga search';
case 'search.query': return 'Search query';
case 'details.title': return 'Details';
case 'details.removeTitle': return ({required Object title}) => 'Remove ${title}?';
case 'details.removeBody': return ({required Object title}) => 'Are you sure you want to remove "${title}" from the list?';
case 'details.removeButton': return 'Remove';
case 'details.cancelButton': return 'Cancel';
case 'details.watchState': return 'Watch state';
case 'details.readState': return 'Read state';
case 'details.episodes': return 'Episodes';
case 'details.chapters': return 'Chapters';
case 'details.volumesOwned': return 'Volumes owned';
case 'data.ongoing.anime': return 'Watching';
case 'data.ongoing.manga': return 'Reading';
case 'data.completed': return 'Completed';
case 'data.planned.anime': return 'Plan to watch';
case 'data.planned.manga': return 'Plan to read';
case 'data.dropped': return 'Dropped';
case 'data.paused': return 'Paused';
case 'data.all': return 'All';
default: return null;
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/service/database.dart'; import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
@ -37,6 +38,9 @@ void main() async {
AnimesLoadedEvent(), AnimesLoadedEvent(),
); );
WidgetsFlutterBinding.ensureInitialized();
LocaleSettings.useDeviceLocale();
runApp( runApp(
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
/// The type of medium we are tracking. Useful for UI stuff. /// The type of medium we are tracking. Useful for UI stuff.
@ -74,25 +75,25 @@ extension MediumStateExtension on MediumTrackingState {
case MediumTrackingState.ongoing: case MediumTrackingState.ongoing:
switch (type) { switch (type) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
return 'Watching'; return t.data.ongoing.anime;
case TrackingMediumType.manga: case TrackingMediumType.manga:
return 'Reading'; return t.data.ongoing.manga;
} }
case MediumTrackingState.completed: case MediumTrackingState.completed:
return 'Completed'; return t.data.completed;
case MediumTrackingState.planned: case MediumTrackingState.planned:
switch (type) { switch (type) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
return 'Plan to watch'; return t.data.planned.anime;
case TrackingMediumType.manga: case TrackingMediumType.manga:
return 'Plan to read'; return t.data.planned.manga;
} }
case MediumTrackingState.dropped: case MediumTrackingState.dropped:
return 'Dropped'; return t.data.dropped;
case MediumTrackingState.paused: case MediumTrackingState.paused:
return 'Paused'; return t.data.paused;
case MediumTrackingState.all: case MediumTrackingState.all:
return 'All'; return t.data.all;
} }
} }
} }

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/licenses.g.dart'; import 'package:anitrack/licenses.g.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -19,7 +20,7 @@ class AboutPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('About'), title: Text(t.about.title),
), ),
body: ListView.builder( body: ListView.builder(
itemCount: ossLicenses.length + 1, itemCount: ossLicenses.length + 1,
@ -41,7 +42,7 @@ class AboutPage extends StatelessWidget {
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
}, },
child: const Text('Source'), child: Text(t.about.source),
), ),
], ],
), ),

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
@ -58,9 +59,9 @@ class AnimeListPageState extends State<AnimeListPage> {
String _getPageTitle(TrackingMediumType type) { String _getPageTitle(TrackingMediumType type) {
switch (type) { switch (type) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
return 'Anime'; return t.content.anime;
case TrackingMediumType.manga: case TrackingMediumType.manga:
return 'Manga'; return t.content.manga;
} }
} }
@ -156,14 +157,14 @@ class AnimeListPageState extends State<AnimeListPage> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
title: const Text('Settings'), title: Text(t.settings.title),
onTap: () { onTap: () {
Navigator.of(context).pushNamed(settingsRoute); Navigator.of(context).pushNamed(settingsRoute);
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.info), leading: const Icon(Icons.info),
title: const Text('About'), title: Text(t.about.title),
onTap: () { onTap: () {
Navigator.of(context).pushNamed(aboutRoute); Navigator.of(context).pushNamed(aboutRoute);
}, },
@ -294,7 +295,7 @@ class AnimeListPageState extends State<AnimeListPage> {
AnimeSearchRequestedEvent(state.trackingType), AnimeSearchRequestedEvent(state.trackingType),
); );
}, },
tooltip: 'Add new item', tooltip: t.tooltips.addNewItem,
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
); );
@ -314,15 +315,15 @@ class AnimeListPageState extends State<AnimeListPage> {
_controller.jumpToPage(index); _controller.jumpToPage(index);
}, },
items: const [ items: [
BottomBarItem( BottomBarItem(
icon: Icon(Icons.tv), icon: const Icon(Icons.tv),
title: Text('Anime'), title: Text(t.content.anime),
activeColor: Colors.blue, activeColor: Colors.blue,
), ),
BottomBarItem( BottomBarItem(
icon: Icon(Icons.auto_stories), icon: const Icon(Icons.auto_stories),
title: Text('Manga'), title: Text(t.content.manga),
activeColor: Colors.red, activeColor: Colors.red,
), ),
], ],

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
@ -25,8 +26,8 @@ class AnimeSearchPage extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
state.trackingType == TrackingMediumType.anime state.trackingType == TrackingMediumType.anime
? 'Anime Search' ? t.search.anime
: 'Manga Search', : t.search.manga,
), ),
), ),
body: Column( body: Column(
@ -34,9 +35,9 @@ class AnimeSearchPage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: TextField( child: TextField(
decoration: const InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: 'Search query', labelText: t.search.query,
), ),
onSubmitted: (_) { onSubmitted: (_) {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
@ -26,7 +27,7 @@ class DetailsPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Details'), title: Text(t.details.title),
), ),
body: BlocBuilder<DetailsBloc, DetailsState>( body: BlocBuilder<DetailsBloc, DetailsState>(
builder: (context, state) { builder: (context, state) {
@ -68,10 +69,10 @@ class DetailsPage extends StatelessWidget {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text( title: Text(
'Remove "${state.data!.title}"?', t.details.removeTitle(title: state.data!.title),
), ),
content: Text( content: Text(
'Are you sure you want to remove "${state.data!.title}" from the list?', t.details.removeBody(title: state.data!.title),
), ),
actions: [ actions: [
TextButton( TextButton(
@ -82,14 +83,14 @@ class DetailsPage extends StatelessWidget {
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.red, foregroundColor: Colors.red,
), ),
child: const Text('Remove'), child: Text(t.details.removeButton),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context) Navigator.of(context)
.pop(false); .pop(false);
}, },
child: const Text('Cancel'), child: Text(t.details.cancelButton),
), ),
], ],
); );
@ -120,8 +121,8 @@ class DetailsPage extends StatelessWidget {
), ),
child: DropdownSelector<MediumTrackingState>( child: DropdownSelector<MediumTrackingState>(
title: state.trackingType == TrackingMediumType.anime title: state.trackingType == TrackingMediumType.anime
? 'Watch state' ? t.details.watchState
: 'Read state', : t.details.readState,
onChanged: (MediumTrackingState newState) { onChanged: (MediumTrackingState newState) {
if (state.trackingType == if (state.trackingType ==
TrackingMediumType.anime) { TrackingMediumType.anime) {
@ -182,8 +183,8 @@ class DetailsPage extends StatelessWidget {
child: IntegerInput( child: IntegerInput(
labelText: labelText:
state.trackingType == TrackingMediumType.anime state.trackingType == TrackingMediumType.anime
? 'Episodes' ? t.details.episodes
: 'Chapters', : t.details.chapters,
onChanged: (value) { onChanged: (value) {
switch (state.trackingType) { switch (state.trackingType) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
@ -221,7 +222,7 @@ class DetailsPage extends StatelessWidget {
vertical: 8, vertical: 8,
), ),
child: IntegerInput( child: IntegerInput(
labelText: 'Volumes owned', labelText: t.details.volumesOwned,
onChanged: (value) { onChanged: (value) {
final data = state.data! as MangaTrackingData; final data = state.data! as MangaTrackingData;
context.read<DetailsBloc>().add( context.read<DetailsBloc>().add(

View File

@ -1,3 +1,4 @@
import 'package:anitrack/i18n/strings.g.dart';
import 'package:anitrack/src/ui/bloc/settings_bloc.dart'; import 'package:anitrack/src/ui/bloc/settings_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@ -32,15 +33,13 @@ class SettingsPage extends StatelessWidget {
bottom: 0, bottom: 0,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Settings'), title: Text(t.settings.title),
), ),
body: ListView( body: ListView(
children: [ children: [
ListTile( ListTile(
title: const Text('Import anime list'), title: Text(t.settings.importAnime),
subtitle: const Text( subtitle: Text(t.settings.importAnimeDesc),
'Import anime list exported from MyAnimeList.',
),
onTap: () async { onTap: () async {
// Pick the file // Pick the file
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.platform.pickFiles();
@ -49,11 +48,9 @@ class SettingsPage extends StatelessWidget {
if (!result.files.first.path!.endsWith('.xml.gz')) { if (!result.files.first.path!.endsWith('.xml.gz')) {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (_) => const AlertDialog( builder: (_) => AlertDialog(
title: Text('Invalid anime list'), title: Text(t.settings.invalidAnimeListTitle),
content: Text( content: Text(t.settings.invalidAnimeListBody),
'The selected file is not a MAL anime list. It lacks the ".xml.gz" suffix.',
),
), ),
); );
return; return;
@ -68,10 +65,8 @@ class SettingsPage extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('Import manga list'), title: Text(t.settings.importManga),
subtitle: const Text( subtitle: Text(t.settings.importMangaDesc),
'Import manga list exported from MyAnimeList.',
),
onTap: () async { onTap: () async {
// Pick the file // Pick the file
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.platform.pickFiles();
@ -80,11 +75,9 @@ class SettingsPage extends StatelessWidget {
if (!result.files.first.path!.endsWith('.xml.gz')) { if (!result.files.first.path!.endsWith('.xml.gz')) {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (_) => const AlertDialog( builder: (_) => AlertDialog(
title: Text('Invalid manga list'), title: Text(t.settings.invalidMangaListTitle),
content: Text( content: Text(t.settings.invalidMangaListBody),
'The selected file is not a MAL manga list. It lacks the ".xml.gz" suffix.',
),
), ),
); );
return; return;
@ -137,7 +130,10 @@ class SettingsPage extends StatelessWidget {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
Text( Text(
'${state.importCurrent} of ${state.importTotal}', t.settings.importIndicator(
current: state.importCurrent,
total: state.importTotal,
),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
], ],

View File

@ -217,6 +217,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
csv:
dependency: transitive
description:
name: csv
sha256: "016b31a51a913744a0a1655c74ff13c9379e1200e246a03d96c81c5d9ed297b5"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -456,6 +464,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.5" version: "0.6.5"
json2yaml:
dependency: transitive
description:
name: json2yaml
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
url: "https://pub.dev"
source: hosted
version: "3.0.1"
json_annotation: json_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -709,6 +725,30 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
slang:
dependency: "direct main"
description:
name: slang
sha256: "187e35d220765ff22be030b30506d1b8e0e94842a2c1801a6a2941c95db5a9eb"
url: "https://pub.dev"
source: hosted
version: "3.19.0"
slang_build_runner:
dependency: "direct dev"
description:
name: slang_build_runner
sha256: "3c48c91736704879b767552bf9e7ba38f1974dd06f44b5e15981cadfde06d760"
url: "https://pub.dev"
source: hosted
version: "3.19.0"
slang_flutter:
dependency: "direct main"
description:
name: slang_flutter
sha256: c6c58162ef66fe88be0313d8062a39e98ae9b539dde7b35f59fa206eb4db2030
url: "https://pub.dev"
source: hosted
version: "3.19.0"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:

View File

@ -22,6 +22,8 @@ dependencies:
get_it: ^7.2.0 get_it: ^7.2.0
jikan_api: ^2.0.0 jikan_api: ^2.0.0
json_annotation: 4.6.0 json_annotation: 4.6.0
slang: 3.19.0
slang_flutter: 3.19.0
sqflite: ^2.2.4+1 sqflite: ^2.2.4+1
swipeable_tile: ^2.0.0+3 swipeable_tile: ^2.0.0+3
url_launcher: ^6.1.8 url_launcher: ^6.1.8
@ -36,6 +38,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
freezed: ^2.1.0+1 freezed: ^2.1.0+1
json_serializable: ^6.3.1 json_serializable: ^6.3.1
slang_build_runner: 3.19.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1
flutter: flutter: