feat: Implement a basic anime calendar
This commit is contained in:
parent
fbe72d1232
commit
9fed2116b1
@ -19,6 +19,7 @@
|
|||||||
"addNewItem": "Add new item"
|
"addNewItem": "Add new item"
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
|
"list": "List",
|
||||||
"anime": "Anime",
|
"anime": "Anime",
|
||||||
"manga": "Manga"
|
"manga": "Manga"
|
||||||
},
|
},
|
||||||
@ -39,6 +40,19 @@
|
|||||||
"chapters": "Chapters",
|
"chapters": "Chapters",
|
||||||
"volumesOwned": "Volumes owned"
|
"volumesOwned": "Volumes owned"
|
||||||
},
|
},
|
||||||
|
"calendar": {
|
||||||
|
"calendar": "Calendar",
|
||||||
|
"days": {
|
||||||
|
"monday": "Monday",
|
||||||
|
"tuesday": "Tuesday",
|
||||||
|
"wednesday": "Wednesday",
|
||||||
|
"thursday": "Thursday",
|
||||||
|
"friday": "Friday",
|
||||||
|
"saturday": "Saturday",
|
||||||
|
"sunday": "Sunday",
|
||||||
|
"unknown": "Unknown"
|
||||||
|
}
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"ongoing": {
|
"ongoing": {
|
||||||
"anime": "Watching",
|
"anime": "Watching",
|
||||||
|
@ -4,4 +4,6 @@ targets:
|
|||||||
slang_build_runner:
|
slang_build_runner:
|
||||||
options:
|
options:
|
||||||
input_directory: assets/i18n
|
input_directory: assets/i18n
|
||||||
output_directory: lib/i18n
|
output_directory: lib/i18n
|
||||||
|
fallback_strategy: base_locale
|
||||||
|
base_locale: en
|
||||||
|
@ -2,6 +2,7 @@ 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';
|
||||||
|
import 'package:anitrack/src/ui/bloc/calendar_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/bloc/settings_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/settings_bloc.dart';
|
||||||
@ -9,6 +10,7 @@ import 'package:anitrack/src/ui/constants.dart';
|
|||||||
import 'package:anitrack/src/ui/pages/about.dart';
|
import 'package:anitrack/src/ui/pages/about.dart';
|
||||||
import 'package:anitrack/src/ui/pages/anime_list.dart';
|
import 'package:anitrack/src/ui/pages/anime_list.dart';
|
||||||
import 'package:anitrack/src/ui/pages/anime_search.dart';
|
import 'package:anitrack/src/ui/pages/anime_search.dart';
|
||||||
|
import 'package:anitrack/src/ui/pages/calendar.dart';
|
||||||
import 'package:anitrack/src/ui/pages/details.dart';
|
import 'package:anitrack/src/ui/pages/details.dart';
|
||||||
import 'package:anitrack/src/ui/pages/settings.dart';
|
import 'package:anitrack/src/ui/pages/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -32,6 +34,7 @@ void main() async {
|
|||||||
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
|
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
|
||||||
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
|
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
|
||||||
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
|
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
|
||||||
|
GetIt.I.registerSingleton<CalendarBloc>(CalendarBloc());
|
||||||
|
|
||||||
// Load animes
|
// Load animes
|
||||||
GetIt.I.get<AnimeListBloc>().add(
|
GetIt.I.get<AnimeListBloc>().add(
|
||||||
@ -59,6 +62,9 @@ void main() async {
|
|||||||
BlocProvider<SettingsBloc>(
|
BlocProvider<SettingsBloc>(
|
||||||
create: (_) => GetIt.I.get<SettingsBloc>(),
|
create: (_) => GetIt.I.get<SettingsBloc>(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<CalendarBloc>(
|
||||||
|
create: (_) => GetIt.I.get<CalendarBloc>(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: MyApp(navKey),
|
child: MyApp(navKey),
|
||||||
),
|
),
|
||||||
@ -95,6 +101,8 @@ class MyApp extends StatelessWidget {
|
|||||||
return AnimeListPage.route;
|
return AnimeListPage.route;
|
||||||
case animeSearchRoute:
|
case animeSearchRoute:
|
||||||
return AnimeSearchPage.route;
|
return AnimeSearchPage.route;
|
||||||
|
case calendarRoute:
|
||||||
|
return CalendarPage.route;
|
||||||
case detailsRoute:
|
case detailsRoute:
|
||||||
return DetailsPage.route;
|
return DetailsPage.route;
|
||||||
case aboutRoute:
|
case aboutRoute:
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
import 'package:anitrack/src/data/type.dart';
|
import 'package:anitrack/src/data/type.dart';
|
||||||
|
import 'package:anitrack/src/service/database.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'anime.freezed.dart';
|
part 'anime.freezed.dart';
|
||||||
part 'anime.g.dart';
|
part 'anime.g.dart';
|
||||||
|
|
||||||
|
class BoolConverter implements JsonConverter<bool, int> {
|
||||||
|
const BoolConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool fromJson(int json) => json.toBool();
|
||||||
|
|
||||||
|
@override
|
||||||
|
int toJson(bool object) => object.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
/// Data about a tracked anime
|
/// Data about a tracked anime
|
||||||
@freezed
|
@freezed
|
||||||
class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
|
class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
|
||||||
@ -25,6 +36,12 @@ class AnimeTrackingData with _$AnimeTrackingData, TrackingMedium {
|
|||||||
|
|
||||||
/// URL to the thumbnail/cover art for the anime.
|
/// URL to the thumbnail/cover art for the anime.
|
||||||
String thumbnailUrl,
|
String thumbnailUrl,
|
||||||
|
|
||||||
|
/// Flag whether the anime is airing
|
||||||
|
@BoolConverter() bool airing,
|
||||||
|
|
||||||
|
/// The day of the week the anime is airing
|
||||||
|
String? broadcastDay,
|
||||||
) = _AnimeTrackingData;
|
) = _AnimeTrackingData;
|
||||||
|
|
||||||
/// JSON
|
/// JSON
|
||||||
|
@ -39,6 +39,13 @@ mixin _$AnimeTrackingData {
|
|||||||
/// URL to the thumbnail/cover art for the anime.
|
/// URL to the thumbnail/cover art for the anime.
|
||||||
String get thumbnailUrl => throw _privateConstructorUsedError;
|
String get thumbnailUrl => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Flag whether the anime is airing
|
||||||
|
@BoolConverter()
|
||||||
|
bool get airing => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// The day of the week the anime is airing
|
||||||
|
String? get broadcastDay => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$AnimeTrackingDataCopyWith<AnimeTrackingData> get copyWith =>
|
$AnimeTrackingDataCopyWith<AnimeTrackingData> get copyWith =>
|
||||||
@ -56,7 +63,9 @@ abstract class $AnimeTrackingDataCopyWith<$Res> {
|
|||||||
String title,
|
String title,
|
||||||
int episodesWatched,
|
int episodesWatched,
|
||||||
int? episodesTotal,
|
int? episodesTotal,
|
||||||
String thumbnailUrl});
|
String thumbnailUrl,
|
||||||
|
@BoolConverter() bool airing,
|
||||||
|
String? broadcastDay});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -76,6 +85,8 @@ class _$AnimeTrackingDataCopyWithImpl<$Res>
|
|||||||
Object? episodesWatched = freezed,
|
Object? episodesWatched = freezed,
|
||||||
Object? episodesTotal = freezed,
|
Object? episodesTotal = freezed,
|
||||||
Object? thumbnailUrl = freezed,
|
Object? thumbnailUrl = freezed,
|
||||||
|
Object? airing = freezed,
|
||||||
|
Object? broadcastDay = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
id: id == freezed
|
id: id == freezed
|
||||||
@ -102,6 +113,14 @@ class _$AnimeTrackingDataCopyWithImpl<$Res>
|
|||||||
? _value.thumbnailUrl
|
? _value.thumbnailUrl
|
||||||
: thumbnailUrl // ignore: cast_nullable_to_non_nullable
|
: thumbnailUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
airing: airing == freezed
|
||||||
|
? _value.airing
|
||||||
|
: airing // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
broadcastDay: broadcastDay == freezed
|
||||||
|
? _value.broadcastDay
|
||||||
|
: broadcastDay // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,7 +138,9 @@ abstract class _$$_AnimeTrackingDataCopyWith<$Res>
|
|||||||
String title,
|
String title,
|
||||||
int episodesWatched,
|
int episodesWatched,
|
||||||
int? episodesTotal,
|
int? episodesTotal,
|
||||||
String thumbnailUrl});
|
String thumbnailUrl,
|
||||||
|
@BoolConverter() bool airing,
|
||||||
|
String? broadcastDay});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -141,6 +162,8 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res>
|
|||||||
Object? episodesWatched = freezed,
|
Object? episodesWatched = freezed,
|
||||||
Object? episodesTotal = freezed,
|
Object? episodesTotal = freezed,
|
||||||
Object? thumbnailUrl = freezed,
|
Object? thumbnailUrl = freezed,
|
||||||
|
Object? airing = freezed,
|
||||||
|
Object? broadcastDay = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$_AnimeTrackingData(
|
return _then(_$_AnimeTrackingData(
|
||||||
id == freezed
|
id == freezed
|
||||||
@ -167,6 +190,14 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res>
|
|||||||
? _value.thumbnailUrl
|
? _value.thumbnailUrl
|
||||||
: thumbnailUrl // ignore: cast_nullable_to_non_nullable
|
: thumbnailUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
airing == freezed
|
||||||
|
? _value.airing
|
||||||
|
: airing // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
broadcastDay == freezed
|
||||||
|
? _value.broadcastDay
|
||||||
|
: broadcastDay // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,8 +205,15 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$_AnimeTrackingData implements _AnimeTrackingData {
|
class _$_AnimeTrackingData implements _AnimeTrackingData {
|
||||||
_$_AnimeTrackingData(this.id, @MediumTrackingStateConverter() this.state,
|
_$_AnimeTrackingData(
|
||||||
this.title, this.episodesWatched, this.episodesTotal, this.thumbnailUrl);
|
this.id,
|
||||||
|
@MediumTrackingStateConverter() this.state,
|
||||||
|
this.title,
|
||||||
|
this.episodesWatched,
|
||||||
|
this.episodesTotal,
|
||||||
|
this.thumbnailUrl,
|
||||||
|
@BoolConverter() this.airing,
|
||||||
|
this.broadcastDay);
|
||||||
|
|
||||||
factory _$_AnimeTrackingData.fromJson(Map<String, dynamic> json) =>
|
factory _$_AnimeTrackingData.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$_AnimeTrackingDataFromJson(json);
|
_$$_AnimeTrackingDataFromJson(json);
|
||||||
@ -205,9 +243,18 @@ class _$_AnimeTrackingData implements _AnimeTrackingData {
|
|||||||
@override
|
@override
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
|
|
||||||
|
/// Flag whether the anime is airing
|
||||||
|
@override
|
||||||
|
@BoolConverter()
|
||||||
|
final bool airing;
|
||||||
|
|
||||||
|
/// The day of the week the anime is airing
|
||||||
|
@override
|
||||||
|
final String? broadcastDay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AnimeTrackingData(id: $id, state: $state, title: $title, episodesWatched: $episodesWatched, episodesTotal: $episodesTotal, thumbnailUrl: $thumbnailUrl)';
|
return 'AnimeTrackingData(id: $id, state: $state, title: $title, episodesWatched: $episodesWatched, episodesTotal: $episodesTotal, thumbnailUrl: $thumbnailUrl, airing: $airing, broadcastDay: $broadcastDay)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -223,7 +270,10 @@ class _$_AnimeTrackingData implements _AnimeTrackingData {
|
|||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.episodesTotal, episodesTotal) &&
|
.equals(other.episodesTotal, episodesTotal) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.thumbnailUrl, thumbnailUrl));
|
.equals(other.thumbnailUrl, thumbnailUrl) &&
|
||||||
|
const DeepCollectionEquality().equals(other.airing, airing) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.broadcastDay, broadcastDay));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@ -235,7 +285,9 @@ class _$_AnimeTrackingData implements _AnimeTrackingData {
|
|||||||
const DeepCollectionEquality().hash(title),
|
const DeepCollectionEquality().hash(title),
|
||||||
const DeepCollectionEquality().hash(episodesWatched),
|
const DeepCollectionEquality().hash(episodesWatched),
|
||||||
const DeepCollectionEquality().hash(episodesTotal),
|
const DeepCollectionEquality().hash(episodesTotal),
|
||||||
const DeepCollectionEquality().hash(thumbnailUrl));
|
const DeepCollectionEquality().hash(thumbnailUrl),
|
||||||
|
const DeepCollectionEquality().hash(airing),
|
||||||
|
const DeepCollectionEquality().hash(broadcastDay));
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
@ -258,7 +310,9 @@ abstract class _AnimeTrackingData implements AnimeTrackingData {
|
|||||||
final String title,
|
final String title,
|
||||||
final int episodesWatched,
|
final int episodesWatched,
|
||||||
final int? episodesTotal,
|
final int? episodesTotal,
|
||||||
final String thumbnailUrl) = _$_AnimeTrackingData;
|
final String thumbnailUrl,
|
||||||
|
@BoolConverter() final bool airing,
|
||||||
|
final String? broadcastDay) = _$_AnimeTrackingData;
|
||||||
|
|
||||||
factory _AnimeTrackingData.fromJson(Map<String, dynamic> json) =
|
factory _AnimeTrackingData.fromJson(Map<String, dynamic> json) =
|
||||||
_$_AnimeTrackingData.fromJson;
|
_$_AnimeTrackingData.fromJson;
|
||||||
@ -289,6 +343,15 @@ abstract class _AnimeTrackingData implements AnimeTrackingData {
|
|||||||
/// URL to the thumbnail/cover art for the anime.
|
/// URL to the thumbnail/cover art for the anime.
|
||||||
String get thumbnailUrl;
|
String get thumbnailUrl;
|
||||||
@override
|
@override
|
||||||
|
|
||||||
|
/// Flag whether the anime is airing
|
||||||
|
@BoolConverter()
|
||||||
|
bool get airing;
|
||||||
|
@override
|
||||||
|
|
||||||
|
/// The day of the week the anime is airing
|
||||||
|
String? get broadcastDay;
|
||||||
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$_AnimeTrackingDataCopyWith<_$_AnimeTrackingData> get copyWith =>
|
_$$_AnimeTrackingDataCopyWith<_$_AnimeTrackingData> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
@ -14,6 +14,8 @@ _$_AnimeTrackingData _$$_AnimeTrackingDataFromJson(Map<String, dynamic> json) =>
|
|||||||
json['episodesWatched'] as int,
|
json['episodesWatched'] as int,
|
||||||
json['episodesTotal'] as int?,
|
json['episodesTotal'] as int?,
|
||||||
json['thumbnailUrl'] as String,
|
json['thumbnailUrl'] as String,
|
||||||
|
const BoolConverter().fromJson(json['airing'] as int),
|
||||||
|
json['broadcastDay'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$_AnimeTrackingDataToJson(
|
Map<String, dynamic> _$$_AnimeTrackingDataToJson(
|
||||||
@ -25,4 +27,6 @@ Map<String, dynamic> _$$_AnimeTrackingDataToJson(
|
|||||||
'episodesWatched': instance.episodesWatched,
|
'episodesWatched': instance.episodesWatched,
|
||||||
'episodesTotal': instance.episodesTotal,
|
'episodesTotal': instance.episodesTotal,
|
||||||
'thumbnailUrl': instance.thumbnailUrl,
|
'thumbnailUrl': instance.thumbnailUrl,
|
||||||
|
'airing': const BoolConverter().toJson(instance.airing),
|
||||||
|
'broadcastDay': instance.broadcastDay,
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,8 @@ class SearchResult {
|
|||||||
this.total,
|
this.total,
|
||||||
this.thumbnailUrl,
|
this.thumbnailUrl,
|
||||||
this.description,
|
this.description,
|
||||||
|
this.isAiring,
|
||||||
|
this.broadcastDay,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The title of the anime.
|
/// The title of the anime.
|
||||||
@ -22,4 +24,10 @@ class SearchResult {
|
|||||||
|
|
||||||
/// The description of the anime
|
/// The description of the anime
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
|
/// Flag whether the anime is airing.
|
||||||
|
final bool isAiring;
|
||||||
|
|
||||||
|
/// The day of the week the anime is airing.
|
||||||
|
final String? broadcastDay;
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,37 @@
|
|||||||
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/service/migrations/0000_airing.dart';
|
||||||
import 'package:anitrack/src/service/migrations/0000_score.dart';
|
import 'package:anitrack/src/service/migrations/0000_score.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
const animeTable = 'Anime';
|
const animeTable = 'Anime';
|
||||||
const mangaTable = 'Manga';
|
const mangaTable = 'Manga';
|
||||||
|
|
||||||
|
extension BoolToInt on bool {
|
||||||
|
int toInt() {
|
||||||
|
return this ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IntToBool on int {
|
||||||
|
bool toBool() {
|
||||||
|
return this == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _createDatabase(Database db, int version) async {
|
Future<void> _createDatabase(Database db, int version) async {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $animeTable(
|
CREATE TABLE $animeTable(
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
state INTEGER NOT NULL,
|
state INTEGER NOT NULL,
|
||||||
episodesTotal INTEGER,
|
episodesTotal INTEGER,
|
||||||
episodesWatched INTEGER NOT NULL,
|
episodesWatched INTEGER NOT NULL,
|
||||||
thumbnailUrl TEXT NOT NULL,
|
thumbnailUrl TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
score INTEGER
|
score INTEGER,
|
||||||
|
airing INTEGER NOT NULL,
|
||||||
|
broadcastDay TEXT
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@ -40,7 +55,7 @@ class DatabaseService {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
_db = await openDatabase(
|
_db = await openDatabase(
|
||||||
'anitrack.db',
|
'anitrack.db',
|
||||||
version: 2,
|
version: 3,
|
||||||
onConfigure: (db) async {
|
onConfigure: (db) async {
|
||||||
// In order to do schema changes during database upgrades, we disable foreign
|
// In order to do schema changes during database upgrades, we disable foreign
|
||||||
// keys in the onConfigure phase, but re-enable them here.
|
// keys in the onConfigure phase, but re-enable them here.
|
||||||
@ -56,6 +71,9 @@ class DatabaseService {
|
|||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
await migrateFromV1ToV2(db);
|
await migrateFromV1ToV2(db);
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 3) {
|
||||||
|
await migrateFromV2ToV3(db);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
11
lib/src/service/migrations/0000_airing.dart
Normal file
11
lib/src/service/migrations/0000_airing.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:anitrack/src/service/database.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> migrateFromV2ToV3(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $animeTable ADD COLUMN airing INTEGER NOT NULL DEFAULT ${true.toInt()};',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $animeTable ADD COLUMN broadcastDay TEXT;',
|
||||||
|
);
|
||||||
|
}
|
@ -270,6 +270,8 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
animes: _getFilteredAnime(),
|
animes: _getFilteredAnime(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await GetIt.I.get<DatabaseService>().updateAnime(event.anime);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onMangaUpdated(
|
Future<void> _onMangaUpdated(
|
||||||
|
@ -43,9 +43,12 @@ class AnimeTrackingTypeChanged extends AnimeListEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AnimeUpdatedEvent extends AnimeListEvent {
|
class AnimeUpdatedEvent extends AnimeListEvent {
|
||||||
AnimeUpdatedEvent(this.anime);
|
AnimeUpdatedEvent(this.anime, {this.commit = false});
|
||||||
|
|
||||||
final AnimeTrackingData anime;
|
final AnimeTrackingData anime;
|
||||||
|
|
||||||
|
/// Commit the new anime data to the database.
|
||||||
|
final bool commit;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnimeRemovedEvent extends AnimeListEvent {
|
class AnimeRemovedEvent extends AnimeListEvent {
|
||||||
|
@ -82,6 +82,8 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
|
|||||||
anime.episodes,
|
anime.episodes,
|
||||||
anime.imageUrl,
|
anime.imageUrl,
|
||||||
anime.synopsis ?? '',
|
anime.synopsis ?? '',
|
||||||
|
anime.airing,
|
||||||
|
anime.broadcast?.split(' ').first,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -104,6 +106,9 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
|
|||||||
manga.chapters,
|
manga.chapters,
|
||||||
manga.imageUrl,
|
manga.imageUrl,
|
||||||
manga.synopsis ?? '',
|
manga.synopsis ?? '',
|
||||||
|
// TODO(Unknown): Implement for Manga
|
||||||
|
false,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -126,6 +131,8 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
|
|||||||
0,
|
0,
|
||||||
event.result.total,
|
event.result.total,
|
||||||
event.result.thumbnailUrl,
|
event.result.thumbnailUrl,
|
||||||
|
event.result.isAiring,
|
||||||
|
event.result.broadcastDay,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: list.MangaAddedEvent(
|
: list.MangaAddedEvent(
|
||||||
|
59
lib/src/ui/bloc/calendar_bloc.dart
Normal file
59
lib/src/ui/bloc/calendar_bloc.dart
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:jikan_api/jikan_api.dart';
|
||||||
|
|
||||||
|
part 'calendar_state.dart';
|
||||||
|
part 'calendar_bloc.freezed.dart';
|
||||||
|
part 'calendar_event.dart';
|
||||||
|
|
||||||
|
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||||
|
CalendarBloc() : super(CalendarState(false, 0, 0)) {
|
||||||
|
on<RefreshPerformedEvent>(_onRefreshPerformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRefreshPerformed(
|
||||||
|
RefreshPerformedEvent event,
|
||||||
|
Emitter<CalendarState> emit,
|
||||||
|
) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
refreshing: true,
|
||||||
|
refreshingCount: 0,
|
||||||
|
refreshingTotal: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final al = GetIt.I.get<AnimeListBloc>();
|
||||||
|
final animes = al.state.animes.where((anime) => anime.airing);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
refreshing: true,
|
||||||
|
refreshingCount: 0,
|
||||||
|
refreshingTotal: animes.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final anime in animes) {
|
||||||
|
emit(state.copyWith(refreshingCount: state.refreshingCount + 1));
|
||||||
|
|
||||||
|
final apiData = await Jikan().getAnime(int.parse(anime.id));
|
||||||
|
if (!apiData.airing) {
|
||||||
|
al.add(
|
||||||
|
AnimeUpdatedEvent(
|
||||||
|
anime.copyWith(airing: false, broadcastDay: null),
|
||||||
|
commit: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent hammering Jikan
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(refreshing: false));
|
||||||
|
}
|
||||||
|
}
|
169
lib/src/ui/bloc/calendar_bloc.freezed.dart
Normal file
169
lib/src/ui/bloc/calendar_bloc.freezed.dart
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||||
|
|
||||||
|
part of 'calendar_bloc.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$CalendarState {
|
||||||
|
bool get refreshing => throw _privateConstructorUsedError;
|
||||||
|
int get refreshingCount => throw _privateConstructorUsedError;
|
||||||
|
int get refreshingTotal => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$CalendarStateCopyWith<CalendarState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $CalendarStateCopyWith<$Res> {
|
||||||
|
factory $CalendarStateCopyWith(
|
||||||
|
CalendarState value, $Res Function(CalendarState) then) =
|
||||||
|
_$CalendarStateCopyWithImpl<$Res>;
|
||||||
|
$Res call({bool refreshing, int refreshingCount, int refreshingTotal});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$CalendarStateCopyWithImpl<$Res>
|
||||||
|
implements $CalendarStateCopyWith<$Res> {
|
||||||
|
_$CalendarStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
final CalendarState _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function(CalendarState) _then;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? refreshing = freezed,
|
||||||
|
Object? refreshingCount = freezed,
|
||||||
|
Object? refreshingTotal = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
refreshing: refreshing == freezed
|
||||||
|
? _value.refreshing
|
||||||
|
: refreshing // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
refreshingCount: refreshingCount == freezed
|
||||||
|
? _value.refreshingCount
|
||||||
|
: refreshingCount // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
refreshingTotal: refreshingTotal == freezed
|
||||||
|
? _value.refreshingTotal
|
||||||
|
: refreshingTotal // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_CalendarStateCopyWith<$Res>
|
||||||
|
implements $CalendarStateCopyWith<$Res> {
|
||||||
|
factory _$$_CalendarStateCopyWith(
|
||||||
|
_$_CalendarState value, $Res Function(_$_CalendarState) then) =
|
||||||
|
__$$_CalendarStateCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
$Res call({bool refreshing, int refreshingCount, int refreshingTotal});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_CalendarStateCopyWithImpl<$Res>
|
||||||
|
extends _$CalendarStateCopyWithImpl<$Res>
|
||||||
|
implements _$$_CalendarStateCopyWith<$Res> {
|
||||||
|
__$$_CalendarStateCopyWithImpl(
|
||||||
|
_$_CalendarState _value, $Res Function(_$_CalendarState) _then)
|
||||||
|
: super(_value, (v) => _then(v as _$_CalendarState));
|
||||||
|
|
||||||
|
@override
|
||||||
|
_$_CalendarState get _value => super._value as _$_CalendarState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? refreshing = freezed,
|
||||||
|
Object? refreshingCount = freezed,
|
||||||
|
Object? refreshingTotal = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$_CalendarState(
|
||||||
|
refreshing == freezed
|
||||||
|
? _value.refreshing
|
||||||
|
: refreshing // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
refreshingCount == freezed
|
||||||
|
? _value.refreshingCount
|
||||||
|
: refreshingCount // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
refreshingTotal == freezed
|
||||||
|
? _value.refreshingTotal
|
||||||
|
: refreshingTotal // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_CalendarState implements _CalendarState {
|
||||||
|
_$_CalendarState(this.refreshing, this.refreshingCount, this.refreshingTotal);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool refreshing;
|
||||||
|
@override
|
||||||
|
final int refreshingCount;
|
||||||
|
@override
|
||||||
|
final int refreshingTotal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CalendarState(refreshing: $refreshing, refreshingCount: $refreshingCount, refreshingTotal: $refreshingTotal)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_CalendarState &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.refreshing, refreshing) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.refreshingCount, refreshingCount) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.refreshingTotal, refreshingTotal));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(refreshing),
|
||||||
|
const DeepCollectionEquality().hash(refreshingCount),
|
||||||
|
const DeepCollectionEquality().hash(refreshingTotal));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
_$$_CalendarStateCopyWith<_$_CalendarState> get copyWith =>
|
||||||
|
__$$_CalendarStateCopyWithImpl<_$_CalendarState>(this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _CalendarState implements CalendarState {
|
||||||
|
factory _CalendarState(final bool refreshing, final int refreshingCount,
|
||||||
|
final int refreshingTotal) = _$_CalendarState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get refreshing;
|
||||||
|
@override
|
||||||
|
int get refreshingCount;
|
||||||
|
@override
|
||||||
|
int get refreshingTotal;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_CalendarStateCopyWith<_$_CalendarState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
6
lib/src/ui/bloc/calendar_event.dart
Normal file
6
lib/src/ui/bloc/calendar_event.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
part of 'calendar_bloc.dart';
|
||||||
|
|
||||||
|
abstract class CalendarEvent {}
|
||||||
|
|
||||||
|
/// Triggered by the UI when the user wants to refresh the airing anime list.
|
||||||
|
class RefreshPerformedEvent extends CalendarEvent {}
|
10
lib/src/ui/bloc/calendar_state.dart
Normal file
10
lib/src/ui/bloc/calendar_state.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
part of 'calendar_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class CalendarState with _$CalendarState {
|
||||||
|
factory CalendarState(
|
||||||
|
bool refreshing,
|
||||||
|
int refreshingCount,
|
||||||
|
int refreshingTotal,
|
||||||
|
) = _CalendarState;
|
||||||
|
}
|
@ -28,6 +28,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
trackingType: TrackingMediumType.anime,
|
trackingType: TrackingMediumType.anime,
|
||||||
|
heroImagePrefix: event.heroImagePrefix,
|
||||||
data: event.anime,
|
data: event.anime,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,7 @@ final _privateConstructorUsedError = UnsupportedError(
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$DetailsState {
|
mixin _$DetailsState {
|
||||||
TrackingMedium? get data => throw _privateConstructorUsedError;
|
TrackingMedium? get data => throw _privateConstructorUsedError;
|
||||||
|
String? get heroImagePrefix => throw _privateConstructorUsedError;
|
||||||
TrackingMediumType get trackingType => throw _privateConstructorUsedError;
|
TrackingMediumType get trackingType => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@ -29,7 +30,10 @@ abstract class $DetailsStateCopyWith<$Res> {
|
|||||||
factory $DetailsStateCopyWith(
|
factory $DetailsStateCopyWith(
|
||||||
DetailsState value, $Res Function(DetailsState) then) =
|
DetailsState value, $Res Function(DetailsState) then) =
|
||||||
_$DetailsStateCopyWithImpl<$Res>;
|
_$DetailsStateCopyWithImpl<$Res>;
|
||||||
$Res call({TrackingMedium? data, TrackingMediumType trackingType});
|
$Res call(
|
||||||
|
{TrackingMedium? data,
|
||||||
|
String? heroImagePrefix,
|
||||||
|
TrackingMediumType trackingType});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -43,6 +47,7 @@ class _$DetailsStateCopyWithImpl<$Res> implements $DetailsStateCopyWith<$Res> {
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? data = freezed,
|
Object? data = freezed,
|
||||||
|
Object? heroImagePrefix = freezed,
|
||||||
Object? trackingType = freezed,
|
Object? trackingType = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
@ -50,6 +55,10 @@ class _$DetailsStateCopyWithImpl<$Res> implements $DetailsStateCopyWith<$Res> {
|
|||||||
? _value.data
|
? _value.data
|
||||||
: data // ignore: cast_nullable_to_non_nullable
|
: data // ignore: cast_nullable_to_non_nullable
|
||||||
as TrackingMedium?,
|
as TrackingMedium?,
|
||||||
|
heroImagePrefix: heroImagePrefix == freezed
|
||||||
|
? _value.heroImagePrefix
|
||||||
|
: heroImagePrefix // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
trackingType: trackingType == freezed
|
trackingType: trackingType == freezed
|
||||||
? _value.trackingType
|
? _value.trackingType
|
||||||
: trackingType // ignore: cast_nullable_to_non_nullable
|
: trackingType // ignore: cast_nullable_to_non_nullable
|
||||||
@ -65,7 +74,10 @@ abstract class _$$_DetailsStateCopyWith<$Res>
|
|||||||
_$_DetailsState value, $Res Function(_$_DetailsState) then) =
|
_$_DetailsState value, $Res Function(_$_DetailsState) then) =
|
||||||
__$$_DetailsStateCopyWithImpl<$Res>;
|
__$$_DetailsStateCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
$Res call({TrackingMedium? data, TrackingMediumType trackingType});
|
$Res call(
|
||||||
|
{TrackingMedium? data,
|
||||||
|
String? heroImagePrefix,
|
||||||
|
TrackingMediumType trackingType});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -82,6 +94,7 @@ class __$$_DetailsStateCopyWithImpl<$Res>
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? data = freezed,
|
Object? data = freezed,
|
||||||
|
Object? heroImagePrefix = freezed,
|
||||||
Object? trackingType = freezed,
|
Object? trackingType = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$_DetailsState(
|
return _then(_$_DetailsState(
|
||||||
@ -89,6 +102,10 @@ class __$$_DetailsStateCopyWithImpl<$Res>
|
|||||||
? _value.data
|
? _value.data
|
||||||
: data // ignore: cast_nullable_to_non_nullable
|
: data // ignore: cast_nullable_to_non_nullable
|
||||||
as TrackingMedium?,
|
as TrackingMedium?,
|
||||||
|
heroImagePrefix: heroImagePrefix == freezed
|
||||||
|
? _value.heroImagePrefix
|
||||||
|
: heroImagePrefix // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
trackingType: trackingType == freezed
|
trackingType: trackingType == freezed
|
||||||
? _value.trackingType
|
? _value.trackingType
|
||||||
: trackingType // ignore: cast_nullable_to_non_nullable
|
: trackingType // ignore: cast_nullable_to_non_nullable
|
||||||
@ -100,17 +117,22 @@ class __$$_DetailsStateCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
||||||
class _$_DetailsState implements _DetailsState {
|
class _$_DetailsState implements _DetailsState {
|
||||||
_$_DetailsState({this.data, this.trackingType = TrackingMediumType.anime});
|
_$_DetailsState(
|
||||||
|
{this.data,
|
||||||
|
this.heroImagePrefix,
|
||||||
|
this.trackingType = TrackingMediumType.anime});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final TrackingMedium? data;
|
final TrackingMedium? data;
|
||||||
@override
|
@override
|
||||||
|
final String? heroImagePrefix;
|
||||||
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final TrackingMediumType trackingType;
|
final TrackingMediumType trackingType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'DetailsState(data: $data, trackingType: $trackingType)';
|
return 'DetailsState(data: $data, heroImagePrefix: $heroImagePrefix, trackingType: $trackingType)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -119,6 +141,8 @@ class _$_DetailsState implements _DetailsState {
|
|||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _$_DetailsState &&
|
other is _$_DetailsState &&
|
||||||
const DeepCollectionEquality().equals(other.data, data) &&
|
const DeepCollectionEquality().equals(other.data, data) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.heroImagePrefix, heroImagePrefix) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.trackingType, trackingType));
|
.equals(other.trackingType, trackingType));
|
||||||
}
|
}
|
||||||
@ -127,6 +151,7 @@ class _$_DetailsState implements _DetailsState {
|
|||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
runtimeType,
|
runtimeType,
|
||||||
const DeepCollectionEquality().hash(data),
|
const DeepCollectionEquality().hash(data),
|
||||||
|
const DeepCollectionEquality().hash(heroImagePrefix),
|
||||||
const DeepCollectionEquality().hash(trackingType));
|
const DeepCollectionEquality().hash(trackingType));
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@ -138,11 +163,14 @@ class _$_DetailsState implements _DetailsState {
|
|||||||
abstract class _DetailsState implements DetailsState {
|
abstract class _DetailsState implements DetailsState {
|
||||||
factory _DetailsState(
|
factory _DetailsState(
|
||||||
{final TrackingMedium? data,
|
{final TrackingMedium? data,
|
||||||
|
final String? heroImagePrefix,
|
||||||
final TrackingMediumType trackingType}) = _$_DetailsState;
|
final TrackingMediumType trackingType}) = _$_DetailsState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TrackingMedium? get data;
|
TrackingMedium? get data;
|
||||||
@override
|
@override
|
||||||
|
String? get heroImagePrefix;
|
||||||
|
@override
|
||||||
TrackingMediumType get trackingType;
|
TrackingMediumType get trackingType;
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
@ -3,10 +3,15 @@ part of 'details_bloc.dart';
|
|||||||
abstract class DetailsEvent {}
|
abstract class DetailsEvent {}
|
||||||
|
|
||||||
class AnimeDetailsRequestedEvent extends DetailsEvent {
|
class AnimeDetailsRequestedEvent extends DetailsEvent {
|
||||||
AnimeDetailsRequestedEvent(this.anime);
|
AnimeDetailsRequestedEvent(
|
||||||
|
this.anime, {
|
||||||
|
this.heroImagePrefix,
|
||||||
|
});
|
||||||
|
|
||||||
/// The anime to show details about
|
/// The anime to show details about
|
||||||
final AnimeTrackingData anime;
|
final AnimeTrackingData anime;
|
||||||
|
|
||||||
|
final String? heroImagePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaDetailsRequestedEvent extends DetailsEvent {
|
class MangaDetailsRequestedEvent extends DetailsEvent {
|
||||||
|
@ -4,6 +4,7 @@ part of 'details_bloc.dart';
|
|||||||
class DetailsState with _$DetailsState {
|
class DetailsState with _$DetailsState {
|
||||||
factory DetailsState({
|
factory DetailsState({
|
||||||
TrackingMedium? data,
|
TrackingMedium? data,
|
||||||
|
String? heroImagePrefix,
|
||||||
@Default(TrackingMediumType.anime) TrackingMediumType trackingType,
|
@Default(TrackingMediumType.anime) TrackingMediumType trackingType,
|
||||||
}) = _DetailsState;
|
}) = _DetailsState;
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,9 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
|||||||
// 0 means that MAL does not know
|
// 0 means that MAL does not know
|
||||||
totalEpisodes == 0 ? null : totalEpisodes,
|
totalEpisodes == 0 ? null : totalEpisodes,
|
||||||
data.imageUrl,
|
data.imageUrl,
|
||||||
|
// NOTE: When the calendar gets refreshed, this should also get cleared
|
||||||
|
true,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const animeListRoute = '/anime/list';
|
const animeListRoute = '/anime/list';
|
||||||
const animeSearchRoute = '/anime/search';
|
const animeSearchRoute = '/anime/search';
|
||||||
const detailsRoute = '/anime/details';
|
const detailsRoute = '/anime/details';
|
||||||
|
const calendarRoute = '/calendar';
|
||||||
const aboutRoute = '/about';
|
const aboutRoute = '/about';
|
||||||
const settingsRoute = '/settings';
|
const settingsRoute = '/settings';
|
||||||
|
58
lib/src/ui/helpers.dart
Normal file
58
lib/src/ui/helpers.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import 'package:anitrack/i18n/strings.g.dart';
|
||||||
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Widget getDrawer(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
const DrawerHeader(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xffcf4aff),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'AniTrack',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xff232323),
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.list),
|
||||||
|
title: Text(t.content.list),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
animeListRoute,
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.calendar_today),
|
||||||
|
title: Text(t.calendar.calendar),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
calendarRoute,
|
||||||
|
(_) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: Text(t.settings.title),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(settingsRoute);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.info),
|
||||||
|
title: Text(t.about.title),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(aboutRoute);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ 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';
|
||||||
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
||||||
import 'package:anitrack/src/ui/constants.dart';
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
|
import 'package:anitrack/src/ui/helpers.dart';
|
||||||
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
||||||
import 'package:anitrack/src/ui/widgets/image.dart';
|
import 'package:anitrack/src/ui/widgets/image.dart';
|
||||||
import 'package:bottom_bar/bottom_bar.dart';
|
import 'package:bottom_bar/bottom_bar.dart';
|
||||||
@ -140,38 +141,7 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
_getPopupButton(context, state),
|
_getPopupButton(context, state),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: Drawer(
|
drawer: getDrawer(context),
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
const DrawerHeader(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xffcf4aff),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'AniTrack',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color(0xff232323),
|
|
||||||
fontSize: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.settings),
|
|
||||||
title: Text(t.settings.title),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pushNamed(settingsRoute);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.info),
|
|
||||||
title: Text(t.about.title),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pushNamed(aboutRoute);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: PageView(
|
body: PageView(
|
||||||
// Prevent swiping between pages
|
// Prevent swiping between pages
|
||||||
// (https://github.com/flutter/flutter/issues/37510#issuecomment-612663656)
|
// (https://github.com/flutter/flutter/issues/37510#issuecomment-612663656)
|
||||||
|
304
lib/src/ui/pages/calendar.dart
Normal file
304
lib/src/ui/pages/calendar.dart
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import 'package:anitrack/i18n/strings.g.dart';
|
||||||
|
import 'package:anitrack/src/data/anime.dart';
|
||||||
|
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
|
||||||
|
import 'package:anitrack/src/ui/bloc/calendar_bloc.dart';
|
||||||
|
import 'package:anitrack/src/ui/bloc/details_bloc.dart';
|
||||||
|
import 'package:anitrack/src/ui/constants.dart';
|
||||||
|
import 'package:anitrack/src/ui/helpers.dart';
|
||||||
|
import 'package:anitrack/src/ui/widgets/grid_item.dart';
|
||||||
|
import 'package:anitrack/src/ui/widgets/image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
enum Weekday {
|
||||||
|
monday,
|
||||||
|
tuesday,
|
||||||
|
wednesday,
|
||||||
|
thursday,
|
||||||
|
friday,
|
||||||
|
saturday,
|
||||||
|
sunday,
|
||||||
|
unknown;
|
||||||
|
|
||||||
|
String toName() {
|
||||||
|
switch (this) {
|
||||||
|
case Weekday.monday:
|
||||||
|
return t.calendar.days.monday;
|
||||||
|
case Weekday.tuesday:
|
||||||
|
return t.calendar.days.tuesday;
|
||||||
|
case Weekday.wednesday:
|
||||||
|
return t.calendar.days.wednesday;
|
||||||
|
case Weekday.thursday:
|
||||||
|
return t.calendar.days.thursday;
|
||||||
|
case Weekday.friday:
|
||||||
|
return t.calendar.days.friday;
|
||||||
|
case Weekday.saturday:
|
||||||
|
return t.calendar.days.saturday;
|
||||||
|
case Weekday.sunday:
|
||||||
|
return t.calendar.days.sunday;
|
||||||
|
case Weekday.unknown:
|
||||||
|
return t.calendar.days.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AddIfExists<K, V> on Map<K, List<V>> {
|
||||||
|
void addOrSet(K key, V value) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
this[key]!.add(value);
|
||||||
|
} else {
|
||||||
|
this[key] = List<V>.from([value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CalendarPage extends StatelessWidget {
|
||||||
|
const CalendarPage({super.key});
|
||||||
|
|
||||||
|
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
|
||||||
|
builder: (_) => const CalendarPage(),
|
||||||
|
settings: const RouteSettings(
|
||||||
|
name: calendarRoute,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _renderWeekdayList(
|
||||||
|
BuildContext context,
|
||||||
|
Weekday day,
|
||||||
|
Map<Weekday, List<AnimeTrackingData>> data,
|
||||||
|
) {
|
||||||
|
if (!data.containsKey(day)) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data[day]!.isNotEmpty, 'There should be at least one anime');
|
||||||
|
return [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
day.toName(),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: GridView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
childAspectRatio: 120 / (100 * (16 / 9)),
|
||||||
|
),
|
||||||
|
itemCount: data[day]!.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final anime = data[day]![index];
|
||||||
|
return GridItem(
|
||||||
|
child: AnimeCoverImage(
|
||||||
|
url: anime.thumbnailUrl,
|
||||||
|
hero: 'calendar_${anime.id}',
|
||||||
|
onTap: () {
|
||||||
|
context.read<DetailsBloc>().add(
|
||||||
|
AnimeDetailsRequestedEvent(
|
||||||
|
anime,
|
||||||
|
heroImagePrefix: 'calendar_',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<CalendarBloc, CalendarState>(
|
||||||
|
builder: (context, calendarState) =>
|
||||||
|
BlocBuilder<AnimeListBloc, AnimeListState>(
|
||||||
|
buildWhen: (previous, current) => !calendarState.refreshing,
|
||||||
|
builder: (context, state) {
|
||||||
|
final airingAnimeMap = <Weekday, List<AnimeTrackingData>>{};
|
||||||
|
for (final anime in state.animes) {
|
||||||
|
if (!anime.airing) continue;
|
||||||
|
|
||||||
|
final Weekday day;
|
||||||
|
switch (anime.broadcastDay) {
|
||||||
|
case 'Mondays':
|
||||||
|
day = Weekday.monday;
|
||||||
|
break;
|
||||||
|
case 'Tuesdays':
|
||||||
|
day = Weekday.tuesday;
|
||||||
|
break;
|
||||||
|
case 'Wednesdays':
|
||||||
|
day = Weekday.wednesday;
|
||||||
|
break;
|
||||||
|
case 'Thursdays':
|
||||||
|
day = Weekday.thursday;
|
||||||
|
break;
|
||||||
|
case 'Fridays':
|
||||||
|
day = Weekday.friday;
|
||||||
|
break;
|
||||||
|
case 'Saturdays':
|
||||||
|
day = Weekday.saturday;
|
||||||
|
break;
|
||||||
|
case 'Sundays':
|
||||||
|
day = Weekday.sunday;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
day = Weekday.unknown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
airingAnimeMap.addOrSet(day, anime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(t.calendar.calendar),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<CalendarBloc>().add(RefreshPerformedEvent());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawer: getDrawer(context),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
// Render all available weekdays
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.unknown,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.monday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.tuesday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.wednesday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.thursday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.friday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.saturday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
..._renderWeekdayList(
|
||||||
|
context,
|
||||||
|
Weekday.sunday,
|
||||||
|
airingAnimeMap,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Provide a nice bottom padding, while keeping the elastic effect attached
|
||||||
|
// to the bottom-most edge.
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: BlocBuilder<CalendarBloc, CalendarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (!state.refreshing) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return const ModalBarrier(
|
||||||
|
dismissible: false,
|
||||||
|
color: Colors.black54,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: BlocBuilder<CalendarBloc, CalendarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (!state.refreshing) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(25),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.settings.importIndicator(
|
||||||
|
current: state.refreshingCount,
|
||||||
|
total: state.refreshingTotal,
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,7 @@ class DetailsPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
AnimeCoverImage(
|
AnimeCoverImage(
|
||||||
url: state.data!.thumbnailUrl,
|
url: state.data!.thumbnailUrl,
|
||||||
hero: state.data!.id,
|
hero: '${state.heroImagePrefix}${state.data!.id}',
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -69,10 +69,14 @@ class DetailsPage extends StatelessWidget {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
t.details.removeTitle(title: state.data!.title),
|
t.details.removeTitle(
|
||||||
|
title: state.data!.title,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
t.details.removeBody(title: state.data!.title),
|
t.details.removeBody(
|
||||||
|
title: state.data!.title,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -83,14 +87,18 @@ class DetailsPage extends StatelessWidget {
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: Colors.red,
|
foregroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
child: Text(t.details.removeButton),
|
child: Text(
|
||||||
|
t.details.removeButton,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pop(false);
|
.pop(false);
|
||||||
},
|
},
|
||||||
child: Text(t.details.cancelButton),
|
child: Text(
|
||||||
|
t.details.cancelButton,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -4,15 +4,18 @@ import 'package:flutter/material.dart';
|
|||||||
class GridItem extends StatefulWidget {
|
class GridItem extends StatefulWidget {
|
||||||
const GridItem({
|
const GridItem({
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.plusCallback,
|
this.plusCallback,
|
||||||
required this.minusCallback,
|
this.minusCallback,
|
||||||
|
this.enableDrag = true,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
final void Function() plusCallback;
|
final bool enableDrag;
|
||||||
final void Function() minusCallback;
|
|
||||||
|
final void Function()? plusCallback;
|
||||||
|
final void Function()? minusCallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GridItemState createState() => GridItemState();
|
GridItemState createState() => GridItemState();
|
||||||
@ -26,16 +29,20 @@ class GridItemState extends State<GridItem> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onHorizontalDragUpdate: (details) {
|
onHorizontalDragUpdate: (details) {
|
||||||
|
if (!widget.enableDrag) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_offset += details.delta.dx;
|
_offset += details.delta.dx;
|
||||||
_translationX = 160 / (1 + exp(-1 * (1 / 30) * _offset)) - 80;
|
_translationX = 160 / (1 + exp(-1 * (1 / 30) * _offset)) - 80;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onHorizontalDragEnd: (_) {
|
onHorizontalDragEnd: (_) {
|
||||||
|
if (!widget.enableDrag) return;
|
||||||
|
|
||||||
if (_translationX <= -60) {
|
if (_translationX <= -60) {
|
||||||
widget.plusCallback();
|
widget.plusCallback!();
|
||||||
} else if (_translationX >= 60) {
|
} else if (_translationX >= 60) {
|
||||||
widget.minusCallback();
|
widget.minusCallback!();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the view
|
// Reset the view
|
||||||
|
Loading…
Reference in New Issue
Block a user