feat: Add i18n using slang
This commit is contained in:
parent
7530fe5b80
commit
13ed7144cb
56
assets/i18n/strings.i18n.json
Normal file
56
assets/i18n/strings.i18n.json
Normal 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
7
build.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
slang_build_runner:
|
||||
options:
|
||||
input_directory: assets/i18n
|
||||
output_directory: lib/i18n
|
24
flake.lock
24
flake.lock
@ -1,15 +1,12 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -39,21 +36,6 @@
|
||||
"flake-utils": "flake-utils",
|
||||
"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",
|
||||
|
@ -38,9 +38,8 @@
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
flutter pinnedJDK android.platform-tools dart scrcpy # Flutter/Android
|
||||
pythonEnv gnumake # Build scripts
|
||||
gitlint jq # Code hygiene
|
||||
ripgrep # General utilities
|
||||
gitlint jq # Code hygiene
|
||||
ripgrep # General utilities
|
||||
];
|
||||
|
||||
JAVA_HOME = pinnedJDK;
|
||||
|
322
lib/i18n/strings.g.dart
Normal file
322
lib/i18n/strings.g.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:anitrack/i18n/strings.g.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_search_bloc.dart';
|
||||
@ -37,6 +38,9 @@ void main() async {
|
||||
AnimesLoadedEvent(),
|
||||
);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
LocaleSettings.useDeviceLocale();
|
||||
|
||||
runApp(
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:anitrack/i18n/strings.g.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// The type of medium we are tracking. Useful for UI stuff.
|
||||
@ -74,25 +75,25 @@ extension MediumStateExtension on MediumTrackingState {
|
||||
case MediumTrackingState.ongoing:
|
||||
switch (type) {
|
||||
case TrackingMediumType.anime:
|
||||
return 'Watching';
|
||||
return t.data.ongoing.anime;
|
||||
case TrackingMediumType.manga:
|
||||
return 'Reading';
|
||||
return t.data.ongoing.manga;
|
||||
}
|
||||
case MediumTrackingState.completed:
|
||||
return 'Completed';
|
||||
return t.data.completed;
|
||||
case MediumTrackingState.planned:
|
||||
switch (type) {
|
||||
case TrackingMediumType.anime:
|
||||
return 'Plan to watch';
|
||||
return t.data.planned.anime;
|
||||
case TrackingMediumType.manga:
|
||||
return 'Plan to read';
|
||||
return t.data.planned.manga;
|
||||
}
|
||||
case MediumTrackingState.dropped:
|
||||
return 'Dropped';
|
||||
return t.data.dropped;
|
||||
case MediumTrackingState.paused:
|
||||
return 'Paused';
|
||||
return t.data.paused;
|
||||
case MediumTrackingState.all:
|
||||
return 'All';
|
||||
return t.data.all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:anitrack/i18n/strings.g.dart';
|
||||
import 'package:anitrack/licenses.g.dart';
|
||||
import 'package:anitrack/src/ui/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -19,7 +20,7 @@ class AboutPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('About'),
|
||||
title: Text(t.about.title),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: ossLicenses.length + 1,
|
||||
@ -41,7 +42,7 @@ class AboutPage extends StatelessWidget {
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
child: const Text('Source'),
|
||||
child: Text(t.about.source),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:anitrack/i18n/strings.g.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_search_bloc.dart';
|
||||
@ -58,9 +59,9 @@ class AnimeListPageState extends State<AnimeListPage> {
|
||||
String _getPageTitle(TrackingMediumType type) {
|
||||
switch (type) {
|
||||
case TrackingMediumType.anime:
|
||||
return 'Anime';
|
||||
return t.content.anime;
|
||||
case TrackingMediumType.manga:
|
||||
return 'Manga';
|
||||
return t.content.manga;
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,14 +157,14 @@ class AnimeListPageState extends State<AnimeListPage> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('Settings'),
|
||||
title: Text(t.settings.title),
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(settingsRoute);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
title: const Text('About'),
|
||||
title: Text(t.about.title),
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(aboutRoute);
|
||||
},
|
||||
@ -294,7 +295,7 @@ class AnimeListPageState extends State<AnimeListPage> {
|
||||
AnimeSearchRequestedEvent(state.trackingType),
|
||||
);
|
||||
},
|
||||
tooltip: 'Add new item',
|
||||
tooltip: t.tooltips.addNewItem,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
@ -314,15 +315,15 @@ class AnimeListPageState extends State<AnimeListPage> {
|
||||
|
||||
_controller.jumpToPage(index);
|
||||
},
|
||||
items: const [
|
||||
items: [
|
||||
BottomBarItem(
|
||||
icon: Icon(Icons.tv),
|
||||
title: Text('Anime'),
|
||||
icon: const Icon(Icons.tv),
|
||||
title: Text(t.content.anime),
|
||||
activeColor: Colors.blue,
|
||||
),
|
||||
BottomBarItem(
|
||||
icon: Icon(Icons.auto_stories),
|
||||
title: Text('Manga'),
|
||||
icon: const Icon(Icons.auto_stories),
|
||||
title: Text(t.content.manga),
|
||||
activeColor: Colors.red,
|
||||
),
|
||||
],
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:anitrack/i18n/strings.g.dart';
|
||||
import 'package:anitrack/src/data/type.dart';
|
||||
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
|
||||
import 'package:anitrack/src/ui/constants.dart';
|
||||
@ -25,8 +26,8 @@ class AnimeSearchPage extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
state.trackingType == TrackingMediumType.anime
|
||||
? 'Anime Search'
|
||||
: 'Manga Search',
|
||||
? t.search.anime
|
||||
: t.search.manga,
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
@ -34,9 +35,9 @@ class AnimeSearchPage extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Search query',
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: t.search.query,
|
||||
),
|
||||
onSubmitted: (_) {
|
||||
context.read<AnimeSearchBloc>().add(
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:anitrack/i18n/strings.g.dart';
|
||||
import 'package:anitrack/src/data/anime.dart';
|
||||
import 'package:anitrack/src/data/manga.dart';
|
||||
import 'package:anitrack/src/data/type.dart';
|
||||
@ -26,7 +27,7 @@ class DetailsPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Details'),
|
||||
title: Text(t.details.title),
|
||||
),
|
||||
body: BlocBuilder<DetailsBloc, DetailsState>(
|
||||
builder: (context, state) {
|
||||
@ -68,10 +69,10 @@ class DetailsPage extends StatelessWidget {
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Remove "${state.data!.title}"?',
|
||||
t.details.removeTitle(title: state.data!.title),
|
||||
),
|
||||
content: Text(
|
||||
'Are you sure you want to remove "${state.data!.title}" from the list?',
|
||||
t.details.removeBody(title: state.data!.title),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -82,14 +83,14 @@ class DetailsPage extends StatelessWidget {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Remove'),
|
||||
child: Text(t.details.removeButton),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.pop(false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
child: Text(t.details.cancelButton),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -120,8 +121,8 @@ class DetailsPage extends StatelessWidget {
|
||||
),
|
||||
child: DropdownSelector<MediumTrackingState>(
|
||||
title: state.trackingType == TrackingMediumType.anime
|
||||
? 'Watch state'
|
||||
: 'Read state',
|
||||
? t.details.watchState
|
||||
: t.details.readState,
|
||||
onChanged: (MediumTrackingState newState) {
|
||||
if (state.trackingType ==
|
||||
TrackingMediumType.anime) {
|
||||
@ -182,8 +183,8 @@ class DetailsPage extends StatelessWidget {
|
||||
child: IntegerInput(
|
||||
labelText:
|
||||
state.trackingType == TrackingMediumType.anime
|
||||
? 'Episodes'
|
||||
: 'Chapters',
|
||||
? t.details.episodes
|
||||
: t.details.chapters,
|
||||
onChanged: (value) {
|
||||
switch (state.trackingType) {
|
||||
case TrackingMediumType.anime:
|
||||
@ -221,7 +222,7 @@ class DetailsPage extends StatelessWidget {
|
||||
vertical: 8,
|
||||
),
|
||||
child: IntegerInput(
|
||||
labelText: 'Volumes owned',
|
||||
labelText: t.details.volumesOwned,
|
||||
onChanged: (value) {
|
||||
final data = state.data! as MangaTrackingData;
|
||||
context.read<DetailsBloc>().add(
|
||||
|
@ -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/constants.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@ -32,15 +33,13 @@ class SettingsPage extends StatelessWidget {
|
||||
bottom: 0,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
title: Text(t.settings.title),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Import anime list'),
|
||||
subtitle: const Text(
|
||||
'Import anime list exported from MyAnimeList.',
|
||||
),
|
||||
title: Text(t.settings.importAnime),
|
||||
subtitle: Text(t.settings.importAnimeDesc),
|
||||
onTap: () async {
|
||||
// Pick the file
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
@ -49,11 +48,9 @@ class SettingsPage extends StatelessWidget {
|
||||
if (!result.files.first.path!.endsWith('.xml.gz')) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const AlertDialog(
|
||||
title: Text('Invalid anime list'),
|
||||
content: Text(
|
||||
'The selected file is not a MAL anime list. It lacks the ".xml.gz" suffix.',
|
||||
),
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(t.settings.invalidAnimeListTitle),
|
||||
content: Text(t.settings.invalidAnimeListBody),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@ -68,10 +65,8 @@ class SettingsPage extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Import manga list'),
|
||||
subtitle: const Text(
|
||||
'Import manga list exported from MyAnimeList.',
|
||||
),
|
||||
title: Text(t.settings.importManga),
|
||||
subtitle: Text(t.settings.importMangaDesc),
|
||||
onTap: () async {
|
||||
// Pick the file
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
@ -80,11 +75,9 @@ class SettingsPage extends StatelessWidget {
|
||||
if (!result.files.first.path!.endsWith('.xml.gz')) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const AlertDialog(
|
||||
title: Text('Invalid manga list'),
|
||||
content: Text(
|
||||
'The selected file is not a MAL manga list. It lacks the ".xml.gz" suffix.',
|
||||
),
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(t.settings.invalidMangaListTitle),
|
||||
content: Text(t.settings.invalidMangaListBody),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@ -137,7 +130,10 @@ class SettingsPage extends StatelessWidget {
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
Text(
|
||||
'${state.importCurrent} of ${state.importTotal}',
|
||||
t.settings.importIndicator(
|
||||
current: state.importCurrent,
|
||||
total: state.importTotal,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
|
40
pubspec.lock
40
pubspec.lock
@ -217,6 +217,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
csv:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csv
|
||||
sha256: "016b31a51a913744a0a1655c74ff13c9379e1200e246a03d96c81c5d9ed297b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -456,6 +464,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.5"
|
||||
json2yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json2yaml
|
||||
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -709,6 +725,30 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -22,6 +22,8 @@ dependencies:
|
||||
get_it: ^7.2.0
|
||||
jikan_api: ^2.0.0
|
||||
json_annotation: 4.6.0
|
||||
slang: 3.19.0
|
||||
slang_flutter: 3.19.0
|
||||
sqflite: ^2.2.4+1
|
||||
swipeable_tile: ^2.0.0+3
|
||||
url_launcher: ^6.1.8
|
||||
@ -36,6 +38,7 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
freezed: ^2.1.0+1
|
||||
json_serializable: ^6.3.1
|
||||
slang_build_runner: 3.19.0
|
||||
very_good_analysis: ^3.0.1
|
||||
|
||||
flutter:
|
||||
|
Loading…
Reference in New Issue
Block a user