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": {
|
"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",
|
||||||
|
@ -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
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/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: [
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
40
pubspec.lock
40
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user