rewrite: Initial Setup

This commit is contained in:
Kevin Rodrigues Borges
2025-07-10 02:46:13 +01:00
parent ae91ed4be4
commit 227e560af9
229 changed files with 2765 additions and 18403 deletions

4
.fvmrc
View File

@@ -1,3 +1,3 @@
{
"flutter": "3.29.3"
}
"flutter": "3.32.6"
}

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
revision: "077b4a4ce10a07b82caa6897f0c626f9c0a3ac90"
channel: "stable"
project_type: app
@@ -13,11 +13,20 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
- platform: linux
create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
- platform: macos
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
- platform: web
create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
- platform: windows
create_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
base_revision: 077b4a4ce10a07b82caa6897f0c626f9c0a3ac90
# User provided section

Binary file not shown.

View File

@@ -1,425 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:easy_localization/easy_localization.dart';
import 'package:unyo/models/models.dart';
import 'package:http/http.dart' as http;
import 'package:unyo/util/constants.dart';
import 'package:url_launcher/url_launcher.dart';
const String anilistEndPointGetToken =
"https://anilist.co/api/v2/oauth/authorize?client_id=17550&response_type=token";
const int maxAttempts = 5;
Future<List<AnimeModel>> getAnimeModelListTrending(
int page, int n, int attempt) async {
Map<String, dynamic> query = {
"query":
"query(\$page:Int = 1 \$id:Int \$type:MediaType \$isAdult:Boolean = false \$search:String \$format:[MediaFormat] \$status:MediaStatus \$countryOfOrigin:CountryCode \$source:MediaSource \$season:MediaSeason \$seasonYear:Int \$year:String \$onList:Boolean \$yearLesser:FuzzyDateInt \$yearGreater:FuzzyDateInt \$episodeLesser:Int \$episodeGreater:Int \$durationLesser:Int \$durationGreater:Int \$chapterLesser:Int \$chapterGreater:Int \$volumeLesser:Int \$volumeGreater:Int \$licensedBy:[Int]\$isLicensed:Boolean \$genres:[String]\$excludedGenres:[String]\$tags:[String]\$excludedTags:[String]\$minimumTagRank:Int \$sort:[MediaSort]=[POPULARITY_DESC,SCORE_DESC]){Page(page:\$page,perPage:$n){pageInfo{total perPage currentPage lastPage hasNextPage}media(id:\$id type:\$type season:\$season format_in:\$format status:\$status countryOfOrigin:\$countryOfOrigin source:\$source search:\$search onList:\$onList seasonYear:\$seasonYear startDate_like:\$year startDate_lesser:\$yearLesser startDate_greater:\$yearGreater episodes_lesser:\$episodeLesser episodes_greater:\$episodeGreater duration_lesser:\$durationLesser duration_greater:\$durationGreater chapters_lesser:\$chapterLesser chapters_greater:\$chapterGreater volumes_lesser:\$volumeLesser volumes_greater:\$volumeGreater licensedById_in:\$licensedBy isLicensed:\$isLicensed genre_in:\$genres genre_not_in:\$excludedGenres tag_in:\$tags tag_not_in:\$excludedTags minimumTagRank:\$minimumTagRank sort:\$sort isAdult:\$isAdult){id idMal title{userPreferred english romaji}coverImage{extraLarge large color}startDate{year month day}endDate{year month day}bannerImage season seasonYear description type format status(version:2)episodes duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode}mediaListEntry{id status}studios(isMain:true){edges{isMain node{id name}}}}}}",
"variables": {
"page": page,
"type": "ANIME",
"sort": ["TRENDING_DESC", "POPULARITY_DESC"]
}
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("Trending list: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getAnimeModelListRecentlyReleased(page, n, newAttempt);
}
return [];
} else {
List<dynamic> media = jsonDecode(response.body)["data"]["Page"]["media"];
List<AnimeModel> list = [];
for (int i = 0; i < n; i++) {
Map<String, dynamic> json = media[i];
list.add(AnimeModel.fromJson(json));
}
return list;
}
}
Future<List<AnimeModel>> getAnimeModelListRecentlyReleased(
int page, int n, int attempt) async {
Map<String, dynamic> query = {
"query":
"query{ Page(page: $page, perPage: $n) { airingSchedules (sort: TIME_DESC, notYetAired: false) {episode media { id idMal title { userPreferred romaji english} coverImage { large } bannerImage format startDate { year month day } endDate { year month day } type description status averageScore episodes duration}}}}"
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("Recently released: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getAnimeModelListRecentlyReleased(page, n, newAttempt);
}
return [];
} else {
List<dynamic> media =
jsonDecode(response.body)["data"]["Page"]["airingSchedules"];
List<AnimeModel> list = [];
for (int i = 0; i < media.length; i++) {
var currentMedia = media[i]["media"];
list.add(AnimeModel.fromJson(currentMedia));
}
for (int i = 0; i < list.length; i++) {
for (int j = i + 1; j < list.length - 1; j++) {
if (list[i].id == list[j].id) {
list.removeAt(j);
}
}
}
return list;
}
}
Future<List<AnimeModel>> getAnimeModelListSeasonPopular(
int page, int n, int year, String season, int attempt) async {
Map<String, dynamic> query = {
"query":
"query(\$page:Int = 1 \$id:Int \$type:MediaType \$isAdult:Boolean = false \$search:String \$format:[MediaFormat]\$status:MediaStatus \$countryOfOrigin:CountryCode \$source:MediaSource \$season:MediaSeason \$seasonYear:Int \$year:String \$onList:Boolean \$yearLesser:FuzzyDateInt \$yearGreater:FuzzyDateInt \$episodeLesser:Int \$episodeGreater:Int \$durationLesser:Int \$durationGreater:Int \$chapterLesser:Int \$chapterGreater:Int \$volumeLesser:Int \$volumeGreater:Int \$licensedBy:[Int]\$isLicensed:Boolean \$genres:[String]\$excludedGenres:[String]\$tags:[String]\$excludedTags:[String]\$minimumTagRank:Int \$sort:[MediaSort]=[POPULARITY_DESC,SCORE_DESC]){Page(page:\$page,perPage:$n){pageInfo{total perPage currentPage lastPage hasNextPage}media(id:\$id type:\$type season:\$season format_in:\$format status:\$status countryOfOrigin:\$countryOfOrigin source:\$source search:\$search onList:\$onList seasonYear:\$seasonYear startDate_like:\$year startDate_lesser:\$yearLesser startDate_greater:\$yearGreater episodes_lesser:\$episodeLesser episodes_greater:\$episodeGreater duration_lesser:\$durationLesser duration_greater:\$durationGreater chapters_lesser:\$chapterLesser chapters_greater:\$chapterGreater volumes_lesser:\$volumeLesser volumes_greater:\$volumeGreater licensedById_in:\$licensedBy isLicensed:\$isLicensed genre_in:\$genres genre_not_in:\$excludedGenres tag_in:\$tags tag_not_in:\$excludedTags minimumTagRank:\$minimumTagRank sort:\$sort isAdult:\$isAdult){id idMal title{userPreferred english romaji}coverImage{extraLarge large color}startDate{year month day}endDate{year month day}bannerImage season seasonYear description type format status(version:2)episodes duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode}mediaListEntry{id status}studios(isMain:true){edges{isMain node{id name}}}}}}",
"variables": {
"page": page,
"type": "ANIME",
"seasonYear": year,
"season": season,
}
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("Season Popular: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getAnimeModelListRecentlyReleased(page, n, newAttempt);
}
return [];
} else {
List<dynamic> media = jsonDecode(response.body)["data"]["Page"]["media"];
List<AnimeModel> list = [];
for (int i = 0; i < n; i++) {
Map<String, dynamic> json = media[i];
list.add(AnimeModel.fromJson(json));
}
return list;
}
}
Future<List<AnimeModel>> getAnimeModelListSearch(
String search,
String genre,
String sort,
String season,
String status,
String format,
String year,
int n,
) async {
String finalSearch = search.isNotEmpty ? "search:\"$search\"" : "";
Map<String, dynamic> query = {
"query":
"query(\$page:Int = 1 \$id:Int \$type:MediaType \$isAdult:Boolean = false \$format:[MediaFormat] \$status:MediaStatus \$countryOfOrigin:CountryCode \$source:MediaSource \$season:MediaSeason \$seasonYear:Int \$year:String \$onList:Boolean \$yearLesser:FuzzyDateInt \$yearGreater:FuzzyDateInt \$episodeLesser:Int \$episodeGreater:Int \$durationLesser:Int \$durationGreater:Int \$chapterLesser:Int \$chapterGreater:Int \$volumeLesser:Int \$volumeGreater:Int \$licensedBy:[Int]\$isLicensed:Boolean \$genres:[String]\$excludedGenres:[String]\$tags:[String]\$excludedTags:[String]\$minimumTagRank:Int \$sort:[MediaSort]=[POPULARITY_DESC,SCORE_DESC]){Page(page:\$page,perPage:$n){pageInfo{total perPage currentPage lastPage hasNextPage}media(id:\$id type:\$type season:\$season format_in:\$format status:\$status countryOfOrigin:\$countryOfOrigin source:\$source $finalSearch onList:\$onList seasonYear:\$seasonYear startDate_like:\$year startDate_lesser:\$yearLesser startDate_greater:\$yearGreater episodes_lesser:\$episodeLesser episodes_greater:\$episodeGreater duration_lesser:\$durationLesser duration_greater:\$durationGreater chapters_lesser:\$chapterLesser chapters_greater:\$chapterGreater volumes_lesser:\$volumeLesser volumes_greater:\$volumeGreater licensedById_in:\$licensedBy isLicensed:\$isLicensed genre_in:\$genres genre_not_in:\$excludedGenres tag_in:\$tags tag_not_in:\$excludedTags minimumTagRank:\$minimumTagRank sort:\$sort isAdult:\$isAdult){id idMal title{userPreferred english romaji}coverImage{extraLarge large color}startDate{year month day}endDate{year month day}bannerImage season seasonYear description type format status(version:2)episodes duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode}mediaListEntry{id status}studios(isMain:true){edges{isMain node{id name}}}}}}",
"variables": {
"page": 1,
"type": "ANIME",
if (sort == "Select Sorting")
"sort": "SEARCH_MATCH"
else
"sort": "${sort.toUpperCase()}_DESC",
if (format != "Select Format") "format": [format.toUpperCase().replaceAll(' ', '_')],
if (season != "Select Season") "season": season.toUpperCase(),
if (year != "Select Year") "seasonYear": int.parse(year),
if (genre != "Select Genre") "genres": [genre],
if (status != "Select Status") "status": status.toUpperCase().replaceAll(' ', '_')
}
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
print("ERROR:\n${response.statusCode}\n${response.body}");
return [];
} else {
List<dynamic> media = jsonDecode(response.body)["data"]["Page"]["media"];
List<AnimeModel> list = [];
for (int i = 0; i < media.length; i++) {
Map<String, dynamic> json = media[i];
list.add(AnimeModel.fromJson(json));
}
print(list.length);
// if ((sort != "Select Sorting")) {
// return list.reversed.toList();
// }
return list;
}
}
Future<String> getRandomAnimeBanner(int attempt) async {
var url = Uri.parse(anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query(\$page:Int, \$perPage:Int){ Page(page: \$page, perPage: \$perPage) { media(type: ANIME) { bannerImage } } }",
"variables": {
"perPage": 50,
"page": Random().nextInt(395),
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
Map<String, dynamic> jsonResponse = json.decode(response.body);
if (attempt < maxAttempts) {
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("Random anime banner: $attempt - failure");
int newAttempt = attempt + 1;
return getRandomAnimeBanner(newAttempt);
}
for (int i = 0; i < 50; i++) {
if (jsonResponse["data"]["Page"]["media"][i]["bannerImage"] == null) {
continue;
} else {
return jsonResponse["data"]["Page"]["media"][i]["bannerImage"];
}
}
await Future.delayed(const Duration(milliseconds: 200));
print("Random anime banner: $attempt - failure");
int newAttempt = attempt + 1;
return getRandomAnimeBanner(newAttempt);
} else {
return "";
}
}
getUserToken() async {
var url = Uri.parse(anilistEndPointGetToken);
launchUrl(url, mode: LaunchMode.platformDefault);
}
String capitalize(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
}
Future<Map<String, Map<String, double>>> getUserStatsMaps({int? newAttempt}) async {
Map<String, Map<String, double>> userStatsMaps = {};
int attempt = newAttempt ?? 0;
var url = Uri.parse(anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query(\$name:String){User(name:\$name){statistics{anime{episodesWatched minutesWatched formats{format\n\tcount\n\tmeanScore\n\tminutesWatched\n\tchaptersRead\n\tmediaIds\n}statuses{status\n\tcount\n\tmeanScore\n\tminutesWatched\n\tchaptersRead\n\tmediaIds\n}releaseYears{releaseYear\n\tcount\n\tmeanScore\n\tminutesWatched\n\tchaptersRead\n\tmediaIds\n}}}}}",
"variables": {
"name": userName,
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("User stats: $attempt - failure");
print(response.body);
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getUserStatsMaps(newAttempt: newAttempt);
}
return {};
}
var animeStatistics =
json.decode(response.body)["data"]["User"]["statistics"]["anime"];
List<dynamic> formats = animeStatistics["formats"];
List<dynamic> statuses = animeStatistics["statuses"];
List<dynamic> releaseYears = animeStatistics["releaseYears"];
Map<String, double> formatsMap = {};
for (int i = 0; i < formats.length; i++) {
formatsMap.addAll(
{capitalize(formats[i]["format"]): formats[i]["count"].toDouble()});
}
userStatsMaps.addAll({"formats": formatsMap});
Map<String, double> statusesMap = {};
for (int i = 0; i < statuses.length; i++) {
statusesMap.addAll(
{capitalize(statuses[i]["status"]): statuses[i]["count"].toDouble()});
}
userStatsMaps.addAll({"statuses": statusesMap});
Map<String, double> releaseYearsMap = {};
for (int i = 0; i < releaseYears.length; i++) {
releaseYearsMap.addAll({
releaseYears[i]["releaseYear"].toString():
releaseYears[i]["count"].toDouble()
});
}
// userStatsMaps.addAll({"releaseYears": releaseYearsMap});
Map<String, double> watchedStatisticsMap = {};
watchedStatisticsMap.addAll(
{"episodesWatched": animeStatistics["episodesWatched"].toDouble()});
watchedStatisticsMap
.addAll({"minutesWatched": animeStatistics["minutesWatched"].toDouble()});
userStatsMaps.addAll({"watchedStatistics": watchedStatisticsMap});
return userStatsMaps;
}
Future<List<String>> getUserAccessToken(String code, int attempt) async {
var url = Uri.parse("https://anilist.co/api/v2/oauth/token");
Map<String, dynamic> query = {
"grant_type": "authorization_code",
"client_id": 17550,
"client_secret": "xI8KTZlKm2F3kHXLko1ArQ21bKap4MojgDTk6Ukx",
"redirect_uri": "http://localhost:9999/auth", // http://example.com/callback
"code": code,
};
var response = await http.post(
url,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("User access token : $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return getUserAccessToken(code, newAttempt);
}
}
print("Response: ${response.body}");
Map<String, dynamic> jsonResponse = json.decode(response.body);
return [jsonResponse["access_token"], jsonResponse["refresh_token"]];
}
Future<int> getAnimeCurrentEpisode(int mediaId, int attempt) async {
var url = Uri.parse(anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query{ AiringSchedule(mediaId: $mediaId, sort: TIME_DESC, notYetAired: false){ episode } }",
};
var response = await http.post(
url,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("anime current episode: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return getAnimeCurrentEpisode(mediaId, newAttempt);
}
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
return jsonResponse["data"]["AiringSchedule"]["episode"];
}
Future<Map<String, List<AnimeModel>>> getCalendar(
String localeTag,
Map<String, List<AnimeModel>> calendarListMap,
int page,
int airingAtGreater,
int airingAtLesser,
int attempt,
) async {
var url = Uri.parse(anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query{Page(page: $page, perPage: 50) { pageInfo { hasNextPage total } airingSchedules(airingAt_greater: $airingAtGreater, airingAt_lesser: $airingAtLesser, sort: TIME_DESC) { episode airingAt media { id idMal status chapters episodes nextAiringEpisode { episode } isAdult type meanScore isFavourite format bannerImage startDate {day month year} endDate {day month year} countryOfOrigin coverImage { large } title { english romaji userPreferred } mediaListEntry { progress private score(format: POINT_100) status } } } }}",
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("Calendar lists: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getCalendar(localeTag, calendarListMap, page,
airingAtGreater, airingAtLesser, newAttempt);
}
//NOTE empry Map
return {};
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
List<dynamic> mediaList = jsonResponse["data"]["Page"]["airingSchedules"];
List<AnimeModel> animeModelList = [];
for (int j = 0; j < mediaList.length; j++) {
Map<String, dynamic> json = mediaList[j]["media"];
animeModelList.add(AnimeModel.fromJson(json));
}
calendarListMap = formatCalendarListMap(
localeTag, calendarListMap, animeModelList, mediaList);
if (jsonResponse["data"]["Page"]["pageInfo"]["hasNextPage"]) {
int newPage = page + 1;
return await getCalendar(localeTag, calendarListMap, newPage,
airingAtGreater, airingAtLesser, attempt);
} else {
return Map.fromEntries(calendarListMap.entries.toList().reversed);
}
}
Map<String, List<AnimeModel>> formatCalendarListMap(
String locale,
Map<String, List<AnimeModel>> calendarListMap,
List<AnimeModel> animeModelList,
List<dynamic> mediaList) {
for (int i = 0; i < animeModelList.length; i++) {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(
mediaList[i]["airingAt"] * 1000,
isUtc: true);
DateFormat dateFormat = DateFormat('EEEE, MMMM d y', locale);
String listKey = dateFormat.format(dateTime);
String formattedListKey =
"${listKey[0].toUpperCase()}${listKey.substring(1)}";
if (!calendarListMap.containsKey(formattedListKey)) {
calendarListMap.addAll({
formattedListKey: [animeModelList[i]]
});
} else {
calendarListMap[formattedListKey]!.add(animeModelList[i]);
}
}
return calendarListMap;
}

View File

@@ -1,512 +0,0 @@
import 'dart:convert';
import 'package:unyo/models/models.dart';
import 'package:http/http.dart' as http;
import 'package:unyo/util/constants.dart';
const String anilistEndPointGetToken =
"https://anilist.co/api/v2/oauth/authorize?client_id=17550&response_type=token";
Future<List<MangaModel>> getMangaModelListTrending(
int page, int n, int attempt) async {
Map<String, dynamic> query = {
"query":
"query(\$page:Int \$id:Int \$type:MediaType \$isAdult:Boolean = false \$search:String \$format:[MediaFormat] \$status:MediaStatus \$countryOfOrigin:CountryCode \$source:MediaSource \$season:MediaSeason \$seasonYear:Int \$year:String \$onList:Boolean \$yearLesser:FuzzyDateInt \$yearGreater:FuzzyDateInt \$episodeLesser:Int \$episodeGreater:Int \$durationLesser:Int \$durationGreater:Int \$chapterLesser:Int \$chapterGreater:Int \$volumeLesser:Int \$volumeGreater:Int \$licensedBy:[Int]\$isLicensed:Boolean \$genres:[String]\$excludedGenres:[String]\$tags:[String]\$excludedTags:[String]\$minimumTagRank:Int \$sort:[MediaSort]=[POPULARITY_DESC,SCORE_DESC]){Page(page:\$page,perPage:$n){pageInfo{total perPage currentPage lastPage hasNextPage}media(id:\$id type:\$type season:\$season format_in:\$format status:\$status countryOfOrigin:\$countryOfOrigin source:\$source search:\$search onList:\$onList seasonYear:\$seasonYear startDate_like:\$year startDate_lesser:\$yearLesser startDate_greater:\$yearGreater episodes_lesser:\$episodeLesser episodes_greater:\$episodeGreater duration_lesser:\$durationLesser duration_greater:\$durationGreater chapters_lesser:\$chapterLesser chapters_greater:\$chapterGreater volumes_lesser:\$volumeLesser volumes_greater:\$volumeGreater licensedById_in:\$licensedBy isLicensed:\$isLicensed genre_in:\$genres genre_not_in:\$excludedGenres tag_in:\$tags tag_not_in:\$excludedTags minimumTagRank:\$minimumTagRank sort:\$sort isAdult:\$isAdult){id idMal title{userPreferred english romaji}coverImage{extraLarge large color}startDate{year month day}endDate{year month day}bannerImage season seasonYear description type format status(version:2)chapters duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode}mediaListEntry{id status}studios(isMain:true){edges{isMain node{id name}}}}}}",
"variables": {
"page": page,
"type": "MANGA",
"sort": ["TRENDING_DESC", "POPULARITY_DESC"]
}
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode == 500) {
if (attempt < 5) {
print("mangaModelListTrending : $attempt - failure");
await Future.delayed(const Duration(milliseconds: 200));
int newAttempt = attempt + 1;
return await getMangaModelListTrending(page, n, newAttempt);
}
return [];
} else {
List<dynamic> media = jsonDecode(response.body)["data"]["Page"]["media"];
List<MangaModel> list = [];
for (int i = 0; i < n; i++) {
Map<String, dynamic> json = media[i];
// list.add(MangaModel(
// id: json["id"],
// title: json["title"]["userPreferred"],
// coverImage: json["coverImage"]["large"],
// bannerImage: json["bannerImage"],
// startDate:
// "${json["startDate"]["day"]}/${json["startDate"]["month"]}/${json["startDate"]["year"]}",
// endDate:
// "${json["endDate"]["day"]}/${json["endDate"]["month"]}/${json["endDate"]["year"]}",
// type: json["type"],
// description: json["description"],
// status: json["status"],
// averageScore: json["averageScore"],
// chapters: json["chapters"],
// duration: json["duration"],
// format: json["format"],
// ));
list.add(MangaModel.fromJson(json));
}
// print(list);
return list;
}
}
Future<List<MangaModel>> getMangaModelListYearlyPopular(
int page, int year, int attempt, int n) async {
Map<String, dynamic> query = {
"query":
"query(\$page:Int \$id:Int \$type:MediaType \$isAdult:Boolean = false \$search:String \$format:[MediaFormat]\$status:MediaStatus \$countryOfOrigin:CountryCode \$source:MediaSource \$season:MediaSeason \$seasonYear:Int \$year:String \$onList:Boolean \$yearLesser:FuzzyDateInt \$yearGreater:FuzzyDateInt \$episodeLesser:Int \$episodeGreater:Int \$durationLesser:Int \$durationGreater:Int \$chapterLesser:Int \$chapterGreater:Int \$volumeLesser:Int \$volumeGreater:Int \$licensedBy:[Int]\$isLicensed:Boolean \$genres:[String]\$excludedGenres:[String]\$tags:[String]\$excludedTags:[String]\$minimumTagRank:Int \$sort:[MediaSort]=[POPULARITY_DESC,SCORE_DESC]){Page(page:\$page,perPage:$n){pageInfo{total perPage currentPage lastPage hasNextPage}media(id:\$id type:\$type season:\$season format_in:\$format status:\$status countryOfOrigin:\$countryOfOrigin source:\$source search:\$search onList:\$onList seasonYear:\$seasonYear startDate_like:\$year startDate_lesser:\$yearLesser startDate_greater:\$yearGreater episodes_lesser:\$episodeLesser episodes_greater:\$episodeGreater duration_lesser:\$durationLesser duration_greater:\$durationGreater chapters_lesser:\$chapterLesser chapters_greater:\$chapterGreater volumes_lesser:\$volumeLesser volumes_greater:\$volumeGreater licensedById_in:\$licensedBy isLicensed:\$isLicensed genre_in:\$genres genre_not_in:\$excludedGenres tag_in:\$tags tag_not_in:\$excludedTags minimumTagRank:\$minimumTagRank sort:\$sort isAdult:\$isAdult){id idMal title{userPreferred english romaji}coverImage{extraLarge large color}startDate{year month day}endDate{year month day}bannerImage season seasonYear description type format status(version:2)episodes duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode}mediaListEntry{id status}studios(isMain:true){edges{isMain node{id name}}}}}}",
"variables": {
"page": page,
"type": "MANGA",
"year": "${year.toString()}%",
}
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode == 500) {
if (attempt < 5) {
print("mangaModelListYearlyPopular : $attempt - failure");
await Future.delayed(const Duration(milliseconds: 200));
int newAttempt = attempt + 1;
return await getMangaModelListRecentlyReleased(page, year, newAttempt);
}
return [];
} else {
print(response.body);
List<dynamic> media = jsonDecode(response.body)["data"]["Page"]["media"];
List<MangaModel> list = [];
for (int i = 0; i < media.length; i++) {
Map<String, dynamic> json = media[i];
list.add(MangaModel.fromJson(json));
// list.add(MangaModel(
// id: json["id"],
// title: json["title"]["userPreferred"],
// coverImage: json["coverImage"]["large"],
// bannerImage: json["bannerImage"],
// startDate:
// "${json["startDate"]["day"]}/${json["startDate"]["month"]}/${json["startDate"]["year"]}",
// endDate:
// "${json["endDate"]["day"]}/${json["endDate"]["month"]}/${json["endDate"]["year"]}",
// type: json["type"],
// description: json["description"],
// status: json["status"],
// averageScore: json["averageScore"],
// chapters: json["chapters"],
// duration: json["duration"],
// format: json["format"],
// ));
}
return list;
}
}
//TODO fix for mangas
Future<List<MangaModel>> getMangaModelListRecentlyReleased(
int page, int n, int attempt) async {
Map<String, dynamic> query = {
"query":
"query{ Page(page: $page, perPage: $n) { airingSchedules (sort: TIME_DESC, notYetAired: false) {episode media { id idMal title { userPreferred english romaji} coverImage { large } bannerImage format startDate { year month day } endDate { year month day } type description status averageScore episodes duration}}}}"
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode == 500) {
if (attempt < 5) {
print("mangaModelListRecentlyReleased : $attempt - failure");
await Future.delayed(const Duration(milliseconds: 200));
int newAttempt = attempt + 1;
return await getMangaModelListRecentlyReleased(page, n, newAttempt);
}
return [];
} else {
List<dynamic> media =
jsonDecode(response.body)["data"]["Page"]["airingSchedules"];
List<MangaModel> list = [];
for (int i = 0; i < media.length; i++) {
var json = media[i]["media"];
list.add(MangaModel.fromJson(json));
// list.add(MangaModel(
// id: json["id"],
// title: json["title"]["userPreferred"],
// coverImage: json["coverImage"]["large"],
// bannerImage: json["bannerImage"],
// startDate:
// "${json["startDate"]["day"]}/${json["startDate"]["month"]}/${json["startDate"]["year"]}",
// endDate:
// "${json["endDate"]["day"]}/${json["endDate"]["month"]}/${json["endDate"]["year"]}",
// type: json["type"],
// status: json["status"],
// averageScore: json["averageScore"],
// chapters: json["chapters"],
// currentEpisode: media[i]["episode"],
// duration: json["duration"],
// description: json["description"],
// format: json["format"],
// ));
}
for (int i = 0; i < list.length; i++) {
for (int j = i + 1; j < list.length - 1; j++) {
if (list[i].id == list[j].id) {
list.removeAt(j);
}
}
}
return list;
}
}
// Future<List<MangaModel>> getUserMangaLists(
// int userId, String listName, int attempt) async {
// var url = Uri.parse(anilistEndpoint);
// Map<String, dynamic> query = {
// "query":
// "query(\$userId:Int,\$userName:String,\$type:MediaType){MediaListCollection(userId:\$userId,userName:\$userName,type:\$type,sort:UPDATED_TIME_DESC){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id title{userPreferred romaji english native}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore description popularity isAdult countryOfOrigin genres bannerImage startDate{year month day}}}",
// "variables": {
// "userId": userId,
// "type": "MANGA",
// }
// };
// var response = await http.post(
// url,
// headers: {"Content-Type": "application/json"},
// body: json.encode(query),
// );
// if (response.statusCode != 200) {
// print(response.body);
// if (attempt < 5) {
// print("userMangaLists : $attempt - failure");
// await Future.delayed(const Duration(milliseconds: 200));
// int newAttempt = attempt + 1;
// return await getUserMangaLists(userId, listName, newAttempt);
// }
// return [];
// }
// List<MangaModel> animeModelList = [];
// Map<String, dynamic> jsonResponse = json.decode(response.body);
// List<dynamic> animeLists =
// jsonResponse["data"]["MediaListCollection"]["lists"];
// for (int i = 0; i < animeLists.length; i++) {
// if (animeLists[i]["name"] == listName) {
// List<dynamic> wantedList = animeLists[i]["entries"];
// for (int i = 0; i < wantedList.length; i++) {
// animeModelList.add(
// MangaModel(
// id: wantedList[i]["media"]["id"],
// title: wantedList[i]["media"]["title"]["userPreferred"],
// coverImage: wantedList[i]["media"]["coverImage"]["large"],
// bannerImage: wantedList[i]["media"]["bannerImage"],
// startDate:
// "${wantedList[i]["media"]["startDate"]["day"]}/${wantedList[i]["media"]["startDate"]["month"]}/${wantedList[i]["media"]["startDate"]["year"]}",
// endDate: "",
// //"${wantedList[i]["media"]["endDate"]["day"]}/${wantedList[i]["media"]["endDate"]["month"]}/${wantedList[i]["media"]["endDate"]["year"]}",
// type: wantedList[i]["media"]["type"],
// description: wantedList[i]["media"]["description"],
// status: wantedList[i]["media"]["status"],
// averageScore: wantedList[i]["media"]["averageScore"],
// chapters: wantedList[i]["media"]["chapters"],
// duration: wantedList[i]["media"]["episodes"],
// format: wantedList[i]["media"]["format"],
// ),
// );
// }
// break;
// }
// }
// return animeModelList;
// }
// Future<Map<String, List<MangaModel>>> getAllUserMangaLists(
// int userId, int attempt) async {
// var url = Uri.parse(anilistEndpoint);
// Map<String, dynamic> query = {
// "query":
// "query(\$userId:Int,\$userName:String,\$type:MediaType){MediaListCollection(userId:\$userId,userName:\$userName,type:\$type){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id title{userPreferred romaji english native}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore description popularity isAdult countryOfOrigin genres bannerImage startDate{year month day}}}",
// "variables": {
// "userId": /*859862*/ userId,
// "type": "MANGA",
// }
// };
// var response = await http.post(
// url,
// headers: {"Content-Type": "application/json"},
// body: json.encode(query),
// );
// if (response.statusCode != 200) {
// print(response.body);
// if (attempt < 5) {
// print("allUserMangaLists : $attempt - failure");
// await Future.delayed(const Duration(milliseconds: 200));
// int newAttempt = attempt + 1;
// return await getAllUserMangaLists(userId, newAttempt);
// }
// //NOTE empry Map
// return {};
// }
// Map<String, List<MangaModel>> userMangaListsMap = {};
// Map<String, dynamic> jsonResponse = json.decode(response.body);
// List<dynamic> userMangaLists =
// jsonResponse["data"]["MediaListCollection"]["lists"];
// for (int i = 0; i < userMangaLists.length; i++) {
// List<dynamic> currentList = userMangaLists[i]["entries"];
//
// List<MangaModel> mangaModelList = [];
//
// for (int j = 0; j < currentList.length; j++) {
// mangaModelList.add(
// MangaModel(
// id: currentList[j]["media"]["id"],
// title: currentList[j]["media"]["title"]["userPreferred"],
// coverImage: currentList[j]["media"]["coverImage"]["large"],
// bannerImage: currentList[j]["media"]["bannerImage"],
// startDate:
// "${currentList[j]["media"]["startDate"]["day"]}/${currentList[j]["media"]["startDate"]["month"]}/${currentList[j]["media"]["startDate"]["year"]}",
// endDate: "",
// //"${wantedList[i]["media"]["endDate"]["day"]}/${wantedList[i]["media"]["endDate"]["month"]}/${wantedList[i]["media"]["endDate"]["year"]}",
// type: currentList[j]["media"]["type"],
// description: currentList[j]["media"]["description"],
// status: currentList[j]["media"]["status"],
// averageScore: currentList[j]["media"]["averageScore"],
// chapters: currentList[j]["media"]["chapters"],
// duration: currentList[j]["media"]["episodes"],
// format: currentList[j]["media"]["format"],
// ),
// );
// }
//
// userMangaListsMap.addAll({userMangaLists[i]["name"]: mangaModelList});
// }
// print(userMangaListsMap);
// return userMangaListsMap;
// }
// Future<UserMediaModel> getUserMangaInfo(int mediaId, int attempt) async {
// var url = Uri.parse(anilistEndpoint);
// Map<String, dynamic> query = {
// "query":
// "query{ Media(id: $mediaId){ mediaListEntry { score progress repeat priority status startedAt{day month year} completedAt{day month year} } } }",
// };
// var response = await http.post(
// url,
// headers: {
// "Authorization": "Bearer $accessToken",
// "Content-Type": "application/json",
// },
// body: json.encode(query),
// );
// if (response.statusCode != 200) {
// if (attempt < 5) {
// print("userMangaInfo : $attempt - failure");
// await Future.delayed(const Duration(milliseconds: 200));
// int newAttempt = attempt + 1;
// return getUserMangaInfo(mediaId, newAttempt);
// }
// }
// Map<String, dynamic> jsonResponse = json.decode(response.body);
// if (jsonResponse["data"]["Media"]["mediaListEntry"] == null) {
// return UserMediaModel(
// score: 0,
// progress: 0,
// repeat: 0,
// priority: 0,
// status: "",
// startDate: "~/~/~",
// endDate: "~/~/~",
// );
// }
// Map<String, dynamic> mediaListEntry =
// jsonResponse["data"]["Media"]["mediaListEntry"];
// return UserMediaModel(
// score: mediaListEntry["score"],
// progress: mediaListEntry["progress"],
// repeat: mediaListEntry["repeat"],
// priority: mediaListEntry["priority"],
// status: mediaListEntry["status"],
// startDate:
// "${mediaListEntry["startedAt"]["day"]}/${mediaListEntry["startedAt"]["month"]}/${mediaListEntry["startedAt"]["year"]}",
// endDate:
// "${mediaListEntry["completedAt"]["day"]}/${mediaListEntry["completedAt"]["month"]}/${mediaListEntry["completedAt"]["year"]}",
// );
// }
// void deleteUserManga(int mediaId) async {
// var url = Uri.parse(anilistEndpoint);
//
// Map<String, dynamic> query1 = {
// "query": "query(\$mediaId:Int){ MediaList(mediaId:\$mediaId){ id } }",
// "variables": {
// "mediaId": mediaId,
// },
// };
// var response1 = await http.post(
// url,
// headers: {
// "Authorization": "Bearer $accessToken",
// "Content-Type": "application/json",
// },
// body: json.encode(query1),
// );
//
// int entryId = jsonDecode(response1.body)["data"]["MediaList"]["id"];
//
// Map<String, dynamic> query = {
// "query":
// "mutation (\$entryId: Int) {DeleteMediaListEntry(id: \$entryId){ deleted }}",
// "variables": {
// "entryId": entryId,
// },
// };
// var response = await http.post(
// url,
// headers: {
// "Authorization": "Bearer $accessToken",
// "Content-Type": "application/json",
// },
// body: json.encode(query),
// );
// print(response.body);
// }
//
// void setUserMangaInfo(int mediaId, Map<String, String> receivedQuery) async {
// var url = Uri.parse(anilistEndpoint);
// Map<String, dynamic> query = {
// "query":
// "mutation (\$mediaId: Int, \$status: MediaListStatus, \$score: Float, \$progress: Int, \$startedAt: FuzzyDateInput, \$completedAt: FuzzyDateInput) { SaveMediaListEntry(mediaId: \$mediaId, status: \$status, score: \$score, progress: \$progress, startedAt: \$startedAt, completedAt: \$completedAt) { mediaId status score progress startedAt { year month day } completedAt { year month day } } } ",
// "variables": {
// "mediaId": mediaId,
// "status": receivedQuery["status"],
// "score": double.parse(receivedQuery["score"]!),
// "progress": int.parse(receivedQuery["progress"]!),
// "startedAt": {
// "day": receivedQuery["startDateDay"],
// "month": receivedQuery["startDateMonth"],
// "year": receivedQuery["startDateYear"]
// },
// "completedAt": {
// "day": receivedQuery["endDateDay"],
// "month": receivedQuery["endDateMonth"],
// "year": receivedQuery["endDateYear"]
// },
// },
// };
// var response = await http.post(
// url,
// headers: {
// "Authorization": "Bearer $accessToken",
// "Content-Type": "application/json",
// },
// body: json.encode(query),
// );
// print(response.body);
// }
Future<List<MangaModel>> getMangaModelListSearch(
String search,
String sort,
String format,
String status,
String country,
String genre,
int n)
async {
String finalSearch = search.isNotEmpty ? "search:\"$search\"" : "";
String? countryFilter;
if (country != "Select Country") {
switch (country) {
case 'Japan':
countryFilter = 'JP';
break;
case 'Taiwan':
countryFilter = 'TW';
break;
case 'South Korea':
countryFilter = 'KR';
break;
case 'China':
countryFilter = 'CN';
break;
default:
countryFilter = country.toUpperCase();
}
}
Map<String, dynamic> query = {
"query":
"query(\$page:Int = 1 \$id:Int \$type:MediaType \$isAdult:Boolean = false \$format:[MediaFormat] \$status:MediaStatus \$countryOfOrigin:CountryCode \$source:MediaSource \$season:MediaSeason \$seasonYear:Int \$year:String \$onList:Boolean \$yearLesser:FuzzyDateInt \$yearGreater:FuzzyDateInt \$episodeLesser:Int \$episodeGreater:Int \$durationLesser:Int \$durationGreater:Int \$chapterLesser:Int \$chapterGreater:Int \$volumeLesser:Int \$volumeGreater:Int \$licensedBy:[Int]\$isLicensed:Boolean \$genres:[String]\$excludedGenres:[String]\$tags:[String]\$excludedTags:[String]\$minimumTagRank:Int \$sort:[MediaSort]=[POPULARITY_DESC,SCORE_DESC]){Page(page:\$page,perPage:$n){pageInfo{total perPage currentPage lastPage hasNextPage}media(id:\$id type:\$type season:\$season format_in:\$format status:\$status countryOfOrigin:\$countryOfOrigin source:\$source $finalSearch onList:\$onList seasonYear:\$seasonYear startDate_like:\$year startDate_lesser:\$yearLesser startDate_greater:\$yearGreater episodes_lesser:\$episodeLesser episodes_greater:\$episodeGreater duration_lesser:\$durationLesser duration_greater:\$durationGreater chapters_lesser:\$chapterLesser chapters_greater:\$chapterGreater volumes_lesser:\$volumeLesser volumes_greater:\$volumeGreater licensedById_in:\$licensedBy isLicensed:\$isLicensed genre_in:\$genres genre_not_in:\$excludedGenres tag_in:\$tags tag_not_in:\$excludedTags minimumTagRank:\$minimumTagRank sort:\$sort isAdult:\$isAdult){id idMal title{userPreferred english romaji}coverImage{extraLarge large color}startDate{year month day}endDate{year month day}bannerImage season seasonYear description type format status(version:2)episodes duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode}mediaListEntry{id status}studios(isMain:true){edges{isMain node{id name}}}}}}",
"variables": {
"page": 1,
"type": "MANGA",
if (sort == "Select Sorting")
"sort": "SEARCH_MATCH"
else if (sort == "A-Z")
"sort": "TITLE_ENGLISH"
else if (sort == "Z-A")
"sort": "TITLE_ENGLISH_DESC"
else
"sort": sort.toUpperCase(),
if (format != "Select Format") "format": [format.toUpperCase().replaceAll(' ', '_')],
if (genre != "Select Genre") "genres": [genre],
if (status != "Select Status") "status": status.toUpperCase().replaceAll(' ', '_'),
if (countryFilter != null) "countryOfOrigin": countryFilter,
}
};
var url = Uri.parse(anilistEndpoint);
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
print("ERROR:\n${response.statusCode}\n${response.body}");
return [];
} else {
List<dynamic> media = jsonDecode(response.body)["data"]["Page"]["media"];
List<MangaModel> list = [];
for (int i = 0; i < media.length; i++) {
Map<String, dynamic> json = media[i];
list.add(MangaModel.fromJson(json));
// list.add(MangaModel(
// id: json["id"],
// title: json["title"]["userPreferred"],
// coverImage: json["coverImage"]["large"],
// bannerImage: json["bannerImage"],
// startDate:
// "${json["startDate"]["day"]}/${json["startDate"]["month"]}/${json["startDate"]["year"]}",
// endDate:
// "${json["endDate"]["day"]}/${json["endDate"]["month"]}/${json["endDate"]["year"]}",
// type: json["type"],
// description: json["description"],
// status: json["status"],
// averageScore: json["averageScore"],
// chapters: json["chapters"],
// duration: json["duration"],
// format: json["format"],
// ));
}
print(list.length);
if ((sort != "Select Sorting")) {
return list.reversed.toList();
}
return list;
}
}

View File

@@ -1,20 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
const aniskipEndpoint = "https://api.aniskip.com/v1/skip-times";
Future<Map<String, double>> getOpeningSkipTimeStamps(
String? malId, String episode) async {
var url = Uri.parse("$aniskipEndpoint/$malId/$episode?types=op");
var response = await http.get(url);
Map<String, dynamic> json = jsonDecode(response.body);
if (json["found"]!= null && json['found'] && malId != null && malId != "-1") {
return {
"start": json["results"][0]["interval"]["start_time"].toDouble(),
"end": json["results"][0]["interval"]["end_time"].toDouble(),
};
} else {
return {"start": -1, "end": -1};
}
}

View File

@@ -1,117 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
import 'package:http/http.dart' as http;
import 'package:html/parser.dart' as parser;
const searchEndPoint =
"https://www.opensubtitles.org/libs/suggest.php?format=json3&MovieName=";
const animeEpisodesEndpoint =
"https://www.opensubtitles.org/en/ssearch/idmovie-";
class OpenSubtitlesApi {
Future<Map<String, String>> getSubtitlesUrl(
String query, int season, int episode) async {
String animeId = await getAnimeId(query);
if (animeId == "-1") {
return {};
}
List<List<String>> seasons = await getAnimeEpisodes(animeId);
print(seasons);
if (seasons.isEmpty ||
seasons.length < season ||
seasons[season - 1].length < episode) {
return {};
}
String episodeSubtitlesUrl = seasons[season - 1][episode - 1];
Map<String, String> subtitles =
await getSubtitlesUrlAndLanguage(episodeSubtitlesUrl);
return subtitles;
}
Future<String> getAnimeId(String query) async {
var url =
Uri.parse("$searchEndPoint${query.replaceAll(RegExp(r'[:!.,]'), '')}");
var response = await http.get(url);
print(response.body);
List<dynamic> jsonResponse = [];
try {
jsonResponse = json.decode(response.body);
} catch (e) {
print("OpenSubtitles is Down");
return "-1";
}
if (jsonResponse.isEmpty) {
return "-1";
}
return jsonResponse[0]["id"].toString();
}
Future<List<List<String>>> getAnimeEpisodes(String animeId) async {
var url = Uri.parse("$animeEpisodesEndpoint$animeId");
var response = await http.get(url);
String html = response.body;
print(url);
var document = parser.parse(html);
List<Element> elements = document.querySelectorAll('tr');
List<String> idsAttribute = elements
.map((element) =>
element.querySelector('td span[id]')?.attributes['id'] ?? "-1")
.toList();
List<int> ids = idsAttribute
.asMap()
.entries
.where((element) => element.value.contains("season"))
.map((e) => e.key)
.toList();
List<String> urls = elements
.map((element) =>
element.querySelector('td a[href]')?.attributes['href'] ?? "-1")
.toList();
List<List<String>> seasons = urls
.asMap()
.entries
.splitBefore((element) => ids.contains(element.key))
.toList()
.map((list) => list.map((map) => map.value).toList())
.toList();
if (seasons.isNotEmpty) {
seasons.removeAt(0);
}
for (List<String> list in seasons) {
list.removeAt(0);
list.removeWhere((element) => !element.contains("imdbid"));
}
return seasons;
}
Future<Map<String, String>> getSubtitlesUrlAndLanguage(
String episodeUrl) async {
var url =
Uri.parse("https://www.opensubtitles.org$episodeUrl/subformat-srt");
var response = await http.get(url);
var document = parser.parse(response.body);
List<String> hrefsWithSrt = document
.querySelectorAll('tr td')
.where((tdElement) =>
tdElement.querySelector('span')?.text.contains("srt") ?? false)
.map((tdElement) => tdElement.querySelector('a')?.attributes['href'])
.where((href) => href != null)
.cast<String>()
.toList();
List<String> titlesWithSublanguageId = document
.querySelectorAll('tr td a[href*="sublanguageid"]')
.map((aElement) => aElement.attributes['title'])
.where((title) => title != null)
.cast<String>()
.toList();
titlesWithSublanguageId.removeLast();
print(titlesWithSublanguageId.length);
print(hrefsWithSrt.length);
if (hrefsWithSrt.length != titlesWithSublanguageId.length) {
return {};
}
return Map.fromIterables(titlesWithSublanguageId, hrefsWithSrt);
}
}

View File

@@ -0,0 +1,47 @@
// External dependencies
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logger/logger.dart';
// Internal dependencies
import 'package:unyo/application/effects/app_effects.dart';
mixin EffectMixin<State> on Cubit<State> {
State copyStateWithEffects(State state, List<AppEffect> effects);
Logger get logger;
void addEffect(AppEffect effect) {
logger.d("Adding AppEffect: $effect");
final current = state;
final newEffects = [..._currentEffects, effect];
emit(copyStateWithEffects(current, newEffects));
}
void clearEffects() {
emit(copyStateWithEffects(state, []));
}
List<AppEffect> get _currentEffects {
if (state is HasEffects) {
return (state as HasEffects).stateEffects;
}
throw StateError('Cubit/Bloc State must implement HasEffects');
}
void showSnackBarEffect({required String message}) {
logger.i("ShowSnackbar with message: $message");
addEffect(ShowSnackbarEffect(message));
}
void replaceRouteEffect({required String path}) {
logger.i("ReplaceRoute with path: $path");
addEffect(ReplaceRouteEffect(path));
}
void pushRouteEffect({required String path}) {
logger.i("PushRoute with path: $path");
addEffect(PushRouteEffect(path));
}
}
abstract class HasEffects {
List<AppEffect> get stateEffects;
}

View File

@@ -0,0 +1,54 @@
// Dart dependencies
import 'dart:async';
// External dependencies
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:logger/logger.dart';
// Internal dependencies
import 'package:unyo/application/cubits/effect_mixin.dart';
import 'package:unyo/application/states/home_state.dart';
import 'package:unyo/core/di/locator.dart';
import 'package:unyo/core/notifier/user_notifier.dart';
import 'package:unyo/data/models/models.dart';
import 'package:unyo/data/repositories/repositories.dart';
import 'package:unyo/application/effects/app_effects.dart';
class HomeCubit extends Cubit<HomeState> with EffectMixin<HomeState> {
//Repositories
final UserRepositoryHive _userRepository;
// Notifiers / Subscriptions
final UserNotifier _userNotifier;
late StreamSubscription<UserModel> _userSubscription;
final Logger _logger = sl<Logger>();
HomeCubit(
this._userRepository,
this._userNotifier,
) : super(HomeState(user: UserModel.empty())) {
_init();
}
@override
HomeState copyStateWithEffects(HomeState state, List<AppEffect> effects) {
return state.copyWith(effects: effects);
}
@override
Logger get logger => _logger;
@override
Future<void> close() {
_userSubscription.cancel();
return super.close();
}
void _init() {
_userSubscription = _userNotifier.userStream.listen((user) {
emit(state.copyWith(user: user)); // Update state on new data
});
}
}

View File

@@ -0,0 +1,48 @@
// External dependencies
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:logger/logger.dart';
// Internal dependencies
import 'package:unyo/application/cubits/effect_mixin.dart';
import 'package:unyo/application/states/login_state.dart';
import 'package:unyo/core/di/locator.dart';
import 'package:unyo/core/notifier/user_notifier.dart';
import 'package:unyo/data/models/models.dart';
import 'package:unyo/data/repositories/repositories.dart';
import 'package:unyo/application/effects/app_effects.dart';
import 'package:unyo/domain/entities/user.dart';
class LoginCubit extends Cubit<LoginState> with EffectMixin<LoginState> {
// Repositories
final UserRepositoryHive _userRepository;
final Logger _logger = sl<Logger>();
// Notifiers / Subscriptions
final UserNotifier _userNotifier;
LoginCubit(this._userRepository, this._userNotifier) : super(LoginState(user: UserModel.empty(), availableUsers: []));
@override
LoginState copyStateWithEffects(LoginState state, List<AppEffect> effects) {
return state.copyWith(effects: effects);
}
@override
Logger get logger => _logger;
// Future<void> initiateAccountCreation() {
// // Effect that creates dialog
// }
Future<void> fetchAllUsers() async{
List<UserModel> usersAvailable = (await _userRepository.fetchAllUsers()).cast<UserModel>();
updateAvailableUsers(usersAvailable);
}
void updateAvailableUsers(List<UserModel> users) {
emit(state.copyWith(availableUsers: users));
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
abstract class AppEffect {}
// Navigation Effects
class ReplaceRouteEffect extends AppEffect {
final String routeName;
final Object? arguments;
ReplaceRouteEffect(this.routeName, {this.arguments});
}
class PushRouteEffect extends AppEffect {
final String routeName;
final Object? arguments;
PushRouteEffect(this.routeName, {this.arguments});
}
// Feedback Effects
class ShowSnackbarEffect extends AppEffect {
final String message;
ShowSnackbarEffect(this.message);
}
class ShowDialogEffect extends AppEffect {
final Dialog dialog;
ShowDialogEffect(this.dialog);
}

View File

@@ -0,0 +1,22 @@
//External dependencies
import 'package:freezed_annotation/freezed_annotation.dart';
//Internal dependencies
import 'package:unyo/application/cubits/effect_mixin.dart';
import 'package:unyo/application/effects/app_effects.dart';
import 'package:unyo/data/models/models.dart';
part 'home_state.freezed.dart';
@freezed
abstract class HomeState with _$HomeState implements HasEffects{
const factory HomeState({
required UserModel user,
@Default(<AppEffect>[]) List<AppEffect> effects,
}) = _HomeState;
const HomeState._();
@override
List<AppEffect> get stateEffects => effects;
}

View File

@@ -0,0 +1,298 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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, unnecessary_question_mark
part of 'home_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$HomeState {
UserModel get user; List<AppEffect> get effects;
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$HomeStateCopyWith<HomeState> get copyWith => _$HomeStateCopyWithImpl<HomeState>(this as HomeState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeState&&(identical(other.user, user) || other.user == user)&&const DeepCollectionEquality().equals(other.effects, effects));
}
@override
int get hashCode => Object.hash(runtimeType,user,const DeepCollectionEquality().hash(effects));
@override
String toString() {
return 'HomeState(user: $user, effects: $effects)';
}
}
/// @nodoc
abstract mixin class $HomeStateCopyWith<$Res> {
factory $HomeStateCopyWith(HomeState value, $Res Function(HomeState) _then) = _$HomeStateCopyWithImpl;
@useResult
$Res call({
UserModel user, List<AppEffect> effects
});
$UserModelCopyWith<$Res> get user;
}
/// @nodoc
class _$HomeStateCopyWithImpl<$Res>
implements $HomeStateCopyWith<$Res> {
_$HomeStateCopyWithImpl(this._self, this._then);
final HomeState _self;
final $Res Function(HomeState) _then;
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? user = null,Object? effects = null,}) {
return _then(_self.copyWith(
user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as UserModel,effects: null == effects ? _self.effects : effects // ignore: cast_nullable_to_non_nullable
as List<AppEffect>,
));
}
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserModelCopyWith<$Res> get user {
return $UserModelCopyWith<$Res>(_self.user, (value) {
return _then(_self.copyWith(user: value));
});
}
}
/// Adds pattern-matching-related methods to [HomeState].
extension HomeStatePatterns on HomeState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _HomeState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _HomeState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _HomeState value) $default,){
final _that = this;
switch (_that) {
case _HomeState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _HomeState value)? $default,){
final _that = this;
switch (_that) {
case _HomeState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( UserModel user, List<AppEffect> effects)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _HomeState() when $default != null:
return $default(_that.user,_that.effects);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( UserModel user, List<AppEffect> effects) $default,) {final _that = this;
switch (_that) {
case _HomeState():
return $default(_that.user,_that.effects);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( UserModel user, List<AppEffect> effects)? $default,) {final _that = this;
switch (_that) {
case _HomeState() when $default != null:
return $default(_that.user,_that.effects);case _:
return null;
}
}
}
/// @nodoc
class _HomeState extends HomeState {
const _HomeState({required this.user, final List<AppEffect> effects = const <AppEffect>[]}): _effects = effects,super._();
@override final UserModel user;
final List<AppEffect> _effects;
@override@JsonKey() List<AppEffect> get effects {
if (_effects is EqualUnmodifiableListView) return _effects;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_effects);
}
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$HomeStateCopyWith<_HomeState> get copyWith => __$HomeStateCopyWithImpl<_HomeState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeState&&(identical(other.user, user) || other.user == user)&&const DeepCollectionEquality().equals(other._effects, _effects));
}
@override
int get hashCode => Object.hash(runtimeType,user,const DeepCollectionEquality().hash(_effects));
@override
String toString() {
return 'HomeState(user: $user, effects: $effects)';
}
}
/// @nodoc
abstract mixin class _$HomeStateCopyWith<$Res> implements $HomeStateCopyWith<$Res> {
factory _$HomeStateCopyWith(_HomeState value, $Res Function(_HomeState) _then) = __$HomeStateCopyWithImpl;
@override @useResult
$Res call({
UserModel user, List<AppEffect> effects
});
@override $UserModelCopyWith<$Res> get user;
}
/// @nodoc
class __$HomeStateCopyWithImpl<$Res>
implements _$HomeStateCopyWith<$Res> {
__$HomeStateCopyWithImpl(this._self, this._then);
final _HomeState _self;
final $Res Function(_HomeState) _then;
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? user = null,Object? effects = null,}) {
return _then(_HomeState(
user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as UserModel,effects: null == effects ? _self._effects : effects // ignore: cast_nullable_to_non_nullable
as List<AppEffect>,
));
}
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserModelCopyWith<$Res> get user {
return $UserModelCopyWith<$Res>(_self.user, (value) {
return _then(_self.copyWith(user: value));
});
}
}
// dart format on

View File

@@ -0,0 +1,23 @@
//External dependencies
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:unyo/application/cubits/effect_mixin.dart';
import 'package:unyo/application/effects/app_effects.dart';
//Internal dependencies
import 'package:unyo/data/models/models.dart';
part 'login_state.freezed.dart';
@freezed
abstract class LoginState with _$LoginState implements HasEffects{
const factory LoginState({
required UserModel user,
required List<UserModel> availableUsers,
@Default(<AppEffect>[]) List<AppEffect> effects,
}) = _LoginState;
const LoginState._();
@override
List<AppEffect> get stateEffects => effects;
}

View File

@@ -0,0 +1,307 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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, unnecessary_question_mark
part of 'login_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LoginState {
UserModel get user; List<UserModel> get availableUsers; List<AppEffect> get effects;
/// Create a copy of LoginState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LoginStateCopyWith<LoginState> get copyWith => _$LoginStateCopyWithImpl<LoginState>(this as LoginState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoginState&&(identical(other.user, user) || other.user == user)&&const DeepCollectionEquality().equals(other.availableUsers, availableUsers)&&const DeepCollectionEquality().equals(other.effects, effects));
}
@override
int get hashCode => Object.hash(runtimeType,user,const DeepCollectionEquality().hash(availableUsers),const DeepCollectionEquality().hash(effects));
@override
String toString() {
return 'LoginState(user: $user, availableUsers: $availableUsers, effects: $effects)';
}
}
/// @nodoc
abstract mixin class $LoginStateCopyWith<$Res> {
factory $LoginStateCopyWith(LoginState value, $Res Function(LoginState) _then) = _$LoginStateCopyWithImpl;
@useResult
$Res call({
UserModel user, List<UserModel> availableUsers, List<AppEffect> effects
});
$UserModelCopyWith<$Res> get user;
}
/// @nodoc
class _$LoginStateCopyWithImpl<$Res>
implements $LoginStateCopyWith<$Res> {
_$LoginStateCopyWithImpl(this._self, this._then);
final LoginState _self;
final $Res Function(LoginState) _then;
/// Create a copy of LoginState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? user = null,Object? availableUsers = null,Object? effects = null,}) {
return _then(_self.copyWith(
user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as UserModel,availableUsers: null == availableUsers ? _self.availableUsers : availableUsers // ignore: cast_nullable_to_non_nullable
as List<UserModel>,effects: null == effects ? _self.effects : effects // ignore: cast_nullable_to_non_nullable
as List<AppEffect>,
));
}
/// Create a copy of LoginState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserModelCopyWith<$Res> get user {
return $UserModelCopyWith<$Res>(_self.user, (value) {
return _then(_self.copyWith(user: value));
});
}
}
/// Adds pattern-matching-related methods to [LoginState].
extension LoginStatePatterns on LoginState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LoginState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LoginState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LoginState value) $default,){
final _that = this;
switch (_that) {
case _LoginState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LoginState value)? $default,){
final _that = this;
switch (_that) {
case _LoginState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( UserModel user, List<UserModel> availableUsers, List<AppEffect> effects)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LoginState() when $default != null:
return $default(_that.user,_that.availableUsers,_that.effects);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( UserModel user, List<UserModel> availableUsers, List<AppEffect> effects) $default,) {final _that = this;
switch (_that) {
case _LoginState():
return $default(_that.user,_that.availableUsers,_that.effects);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( UserModel user, List<UserModel> availableUsers, List<AppEffect> effects)? $default,) {final _that = this;
switch (_that) {
case _LoginState() when $default != null:
return $default(_that.user,_that.availableUsers,_that.effects);case _:
return null;
}
}
}
/// @nodoc
class _LoginState extends LoginState {
const _LoginState({required this.user, required final List<UserModel> availableUsers, final List<AppEffect> effects = const <AppEffect>[]}): _availableUsers = availableUsers,_effects = effects,super._();
@override final UserModel user;
final List<UserModel> _availableUsers;
@override List<UserModel> get availableUsers {
if (_availableUsers is EqualUnmodifiableListView) return _availableUsers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_availableUsers);
}
final List<AppEffect> _effects;
@override@JsonKey() List<AppEffect> get effects {
if (_effects is EqualUnmodifiableListView) return _effects;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_effects);
}
/// Create a copy of LoginState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LoginStateCopyWith<_LoginState> get copyWith => __$LoginStateCopyWithImpl<_LoginState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoginState&&(identical(other.user, user) || other.user == user)&&const DeepCollectionEquality().equals(other._availableUsers, _availableUsers)&&const DeepCollectionEquality().equals(other._effects, _effects));
}
@override
int get hashCode => Object.hash(runtimeType,user,const DeepCollectionEquality().hash(_availableUsers),const DeepCollectionEquality().hash(_effects));
@override
String toString() {
return 'LoginState(user: $user, availableUsers: $availableUsers, effects: $effects)';
}
}
/// @nodoc
abstract mixin class _$LoginStateCopyWith<$Res> implements $LoginStateCopyWith<$Res> {
factory _$LoginStateCopyWith(_LoginState value, $Res Function(_LoginState) _then) = __$LoginStateCopyWithImpl;
@override @useResult
$Res call({
UserModel user, List<UserModel> availableUsers, List<AppEffect> effects
});
@override $UserModelCopyWith<$Res> get user;
}
/// @nodoc
class __$LoginStateCopyWithImpl<$Res>
implements _$LoginStateCopyWith<$Res> {
__$LoginStateCopyWithImpl(this._self, this._then);
final _LoginState _self;
final $Res Function(_LoginState) _then;
/// Create a copy of LoginState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? user = null,Object? availableUsers = null,Object? effects = null,}) {
return _then(_LoginState(
user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
as UserModel,availableUsers: null == availableUsers ? _self._availableUsers : availableUsers // ignore: cast_nullable_to_non_nullable
as List<UserModel>,effects: null == effects ? _self._effects : effects // ignore: cast_nullable_to_non_nullable
as List<AppEffect>,
));
}
/// Create a copy of LoginState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserModelCopyWith<$Res> get user {
return $UserModelCopyWith<$Res>(_self.user, (value) {
return _then(_self.copyWith(user: value));
});
}
}
// dart format on

4
lib/config/config.dart Normal file
View File

@@ -0,0 +1,4 @@
const String baseUrl = 'https://api.example.com';
const String version = 'v1.0.0';
const plusImageUrl = "https://i.ibb.co/Kj8CQZH/cross.png";

33
lib/core/di/locator.dart Normal file
View File

@@ -0,0 +1,33 @@
// External dependencies
import 'package:get_it/get_it.dart';
import 'package:logger/logger.dart';
// Internal dependencies
import 'package:unyo/application/cubits/login_cubit.dart';
import 'package:unyo/core/log/logger.dart';
import 'package:unyo/core/notifier/user_notifier.dart';
import 'package:unyo/core/services/api/http/http_service.dart';
import 'package:unyo/core/services/effects/app_effect_handler.dart';
import 'package:unyo/data/repositories/repositories.dart';
import 'package:unyo/application/cubits/home_cubit.dart';
final sl = GetIt.instance;
void setupLocator() {
// Singletons
sl.registerLazySingleton<Logger>(() => getLogger());
// Services
sl.registerLazySingleton<HttpService>(() => HttpService());
sl.registerLazySingleton<AppEffectHandler>(() => AppEffectHandler());
// Notifiers
sl.registerLazySingleton<UserNotifier>(() => UserNotifier());
// Repositories
sl.registerLazySingleton<UserRepositoryHive>(() => UserRepositoryHive());
// Cubits / Blocs
sl.registerFactory<LoginCubit>(() => LoginCubit(sl(), sl()));
sl.registerFactory<HomeCubit>(() => HomeCubit(sl(), sl()));
}

8
lib/core/log/logger.dart Normal file
View File

@@ -0,0 +1,8 @@
import 'package:logger/logger.dart';
Logger getLogger() {
return Logger(
printer: PrettyPrinter(),
filter: ProductionFilter(),
);
}

View File

@@ -0,0 +1,25 @@
// External dependencies
import 'package:logger/logger.dart';
import 'package:rxdart/rxdart.dart';
// Internal dependencies
import 'package:unyo/core/di/locator.dart';
import 'package:unyo/data/models/user_model.dart';
class UserNotifier {
final BehaviorSubject<UserModel> _userSubject;
final _logger = sl<Logger>();
UserNotifier() : _userSubject = BehaviorSubject.seeded(UserModel.empty());
// Public stream for Cubits to subscribe
Stream<UserModel> get userStream => _userSubject.stream;
void updateUser(UserModel user) {
_logger.d("User notifier updated with: ${user.name}");
_userSubject.add(user);
}
UserModel get currentUser => _userSubject.value;
void dispose() => _userSubject.close();
}

View File

@@ -0,0 +1,25 @@
import 'package:auto_route/auto_route.dart';
import 'package:unyo/core/router/app_router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
class AppRouter extends RootStackRouter {
// @override
// RouteType get defaultRouteType => RouteType.custom();
@override
List<AutoRoute> get routes => [
AutoRoute(
page: RootScaffoldRoute.page,
path: '/',
children: [
AutoRoute(page: LoginRoute.page, path: 'login', initial: true),
AutoRoute(page: HomeRoute.page, path: 'home')
],
),
];
@override
List<AutoRouteGuard> get guards => [
// guards can be added here
];
}

View File

@@ -0,0 +1,63 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouterGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i4;
import 'package:unyo/presentation/screens/home_screen.dart' as _i1;
import 'package:unyo/presentation/screens/login_screen.dart' as _i2;
import 'package:unyo/presentation/screens/root_scaffold_screen.dart' as _i3;
/// generated route for
/// [_i1.HomeScreen]
class HomeRoute extends _i4.PageRouteInfo<void> {
const HomeRoute({List<_i4.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i1.HomeScreen();
},
);
}
/// generated route for
/// [_i2.LoginScreen]
class LoginRoute extends _i4.PageRouteInfo<void> {
const LoginRoute({List<_i4.PageRouteInfo>? children})
: super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i2.LoginScreen();
},
);
}
/// generated route for
/// [_i3.RootScaffoldScreen]
class RootScaffoldRoute extends _i4.PageRouteInfo<void> {
const RootScaffoldRoute({List<_i4.PageRouteInfo>? children})
: super(RootScaffoldRoute.name, initialChildren: children);
static const String name = 'RootScaffoldRoute';
static _i4.PageInfo page = _i4.PageInfo(
name,
builder: (data) {
return const _i3.RootScaffoldScreen();
},
);
}

View File

@@ -0,0 +1,17 @@
# Http
A composable, Future-based library for making HTTP requests that contains a set of high-level functions and classes that make it easy to consume HTTP resources.
## Folders
- http_helpers (Contains all the information about base urls, api calls and exception handling)
### http_helpers
- [http_client.dart](api_sdk/http/http_helpers/http_client.dart) - HttpClient class is responsible for handling all the network call methods.
- [http_exception.dart](api_sdk/http/http_helpers/http_exception.dart) - HttpExceptions handles all the exceptions for different error codes.
- [http_interceptor.dart](api_sdk/http/http_helpers/http_interceptor.dart) - HttpInterceptor lets you intercept the different requests and responses.
### http_api_sdk
Contains two classes HttpApi and HttpService that will eventually call the network methods. **HttpApi** class will create a client instance which will have interceptors added to it. **HttpService** will eventually call the network methods present in [http_service.dart](api_sdk/http/http_service.dart)

View File

@@ -0,0 +1,98 @@
// Dart dependencies
import 'dart:convert';
import 'dart:io';
// External dependencies
import 'package:http/http.dart' as http;
import 'http_exception.dart';
class HttpClient {
final http.Client client;
HttpClient({required this.client});
Future<dynamic> get(
String url, {
Map<String, String>? headers,
bool isTokenRequired = false,
}) async {
http.Response? response;
dynamic responseJson;
try {
response =
await http.get(Uri.parse(url), headers: headers ?? {});
responseJson = _returnResponse(response);
} on SocketException {
throw FetchDataException('No Internet connection');
}
return responseJson;
}
Future<dynamic> post(String url,
{Map<String, String>? headers, Object? body}) async {
http.Response? response;
dynamic responseJson;
try {
response = await http.post(Uri.parse(url),
headers: headers ?? {}, body: body);
responseJson = _returnResponse(response);
} on SocketException {
throw FetchDataException('No Internet connection');
}
return responseJson;
}
Future<dynamic> put(String url,
{Map<String, String>? headers, Object? body}) async {
http.Response? response;
dynamic responseJson;
try {
response = await http.put(Uri.parse(url),
headers: headers ?? {}, body: body);
responseJson = _returnResponse(response);
} on SocketException {
throw FetchDataException('No Internet connection');
}
return responseJson;
}
Future<dynamic> delete(String url, {Map<String, String>? headers}) async {
http.Response? response;
dynamic responseJson;
try {
response =
await http.delete(Uri.parse(url), headers: headers ?? {});
responseJson = _returnResponse(response);
} on SocketException {
throw FetchDataException('No Internet connection');
}
return responseJson;
}
}
dynamic _returnResponse(http.Response response) {
switch (response.statusCode) {
case 200:
var responseJson = json.decode(response.body.toString());
return responseJson;
case 400:
throw BadRequestException(response.body.toString());
case 401:
throw UnauthorisedException(response.body.toString());
case 403:
throw UnauthorisedException(response.body.toString());
case 404:
throw FileNotFoundException(response.body.toString());
case 500:
throw InternalServerException(response.body.toString());
case 502:
throw BadGateWayException(response.body.toString());
case 503:
throw BadGateWayException(response.body.toString());
default:
return FetchDataException(
'Error occured while Communication with Server with StatusCode : ${response.statusCode}');
}
}

View File

@@ -0,0 +1,42 @@
class HttpException implements Exception {
final dynamic _message;
final dynamic _prefix;
HttpException([this._message, this._prefix]);
@override
String toString() {
return "$_prefix$_message";
}
}
class FetchDataException extends HttpException {
FetchDataException([String? message])
: super(message, "Error During Communication: ");
}
class BadRequestException extends HttpException {
BadRequestException([message]) : super(message, "Invalid Request: ");
}
class UnauthorisedException extends HttpException {
UnauthorisedException([message]) : super(message, "Unauthorised: ");
}
class InvalidInputException extends HttpException {
InvalidInputException([String? message]) : super(message, "Invalid Input: ");
}
class FileNotFoundException extends HttpException {
FileNotFoundException([String? message]) : super(message, "File not found: ");
}
class InternalServerException extends HttpException {
InternalServerException([String? message])
: super(message, "Internal Server Exception: ");
}
class BadGateWayException extends HttpException {
BadGateWayException([String? message])
: super(message, "Bad Gateway Exception: ");
}

View File

@@ -0,0 +1,33 @@
//External dependencies
import 'package:http/http.dart';
//Internal dependencies
import 'package:unyo/core/services/api/http/helpers/http_client.dart';
class HttpService {
//Empty constructor
const HttpService();
Client createHttp() {
return Client();
}
getData(String url) async {
final client = HttpClient(client: createHttp());
final response = await client.get(url);
return response;
}
postData(String url, dynamic body) async {
final client = HttpClient(client: createHttp());
final response = await client.post(url, body: body);
return response;
}
putData(String url, dynamic body) async {
final client = HttpClient(client: createHttp());
final response = await client.put(url, body: body);
return response;
}
}

View File

@@ -0,0 +1,61 @@
// External dependencies
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
// Internal dependencies
import 'package:unyo/application/effects/app_effects.dart';
import 'package:unyo/core/di/locator.dart';
class AppEffectHandler {
final _logger = sl<Logger>();
AppEffectHandler();
void handleEffects(
BuildContext context,
List<AppEffect> effects,
void Function() clearAppEffects,
) {
for (var effect in effects) {
handle(effect, context);
}
clearAppEffects();
}
void handle(AppEffect effect, BuildContext context) {
switch (effect) {
case ShowSnackbarEffect showSnackBarEffect:
_handleShowSnackbarEffect(showSnackBarEffect);
case ReplaceRouteEffect replaceRouteEffect:
_handleReplaceRouteEffect(replaceRouteEffect, context);
case PushRouteEffect pushRouteEffect:
_handlePushRouteEffect(pushRouteEffect, context);
default:
_handleUnkownEffect(effect);
}
}
void _handleShowSnackbarEffect(ShowSnackbarEffect effect) {
_logger.d("Handling ShowSnackbarEffect");
}
void _handleUnkownEffect(AppEffect effect) {
_logger.e("Unimplemented Effect Handler for effect: $effect");
}
void _handleReplaceRouteEffect(ReplaceRouteEffect effect, BuildContext context,) {
_logger.d("Handling ReplaceRouteEffect");
context.router.root.replacePath(effect.routeName.replaceFirst("/", ""), onFailure: _handleRouteFailure);
}
void _handlePushRouteEffect(PushRouteEffect effect, BuildContext context) {
_logger.d("Handling PushRouteEffect");
context.router.root.pushPath(effect.routeName.replaceFirst("/", ""), onFailure: _handleRouteFailure);
}
void _handleRouteFailure(NavigationFailure failure) {
_logger.e("Navigation failure: ${failure.toString()}");
}
}

View File

@@ -0,0 +1 @@
export 'user_model.dart';

View File

@@ -0,0 +1,34 @@
//External dependencies
import 'package:freezed_annotation/freezed_annotation.dart';
//Internal dependencies
import 'package:unyo/domain/entities/user.dart';
part 'user_model.freezed.dart';
part 'user_model.g.dart'; // For JSON serialization
@freezed
abstract class UserModel with _$UserModel implements User {
const UserModel._();
const factory UserModel({required String name, required String avatarImage}) = _UserModel;
factory UserModel.empty() => const UserModel(name: '', avatarImage: 'https://i.imgur.com/EKtChtm.png');
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
@override
Map<String, dynamic> toJson() => _$UserModelToJson(this as _UserModel);
}
class UserConverter implements JsonConverter<User, Map<String, dynamic>> {
const UserConverter();
@override
User fromJson(Map<String, dynamic> json) => UserModel.fromJson(json);
@override
Map<String, dynamic> toJson(User object) => (object as UserModel).toJson();
}

View File

@@ -0,0 +1,280 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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, unnecessary_question_mark
part of 'user_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$UserModel {
String get name; String get avatarImage;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UserModelCopyWith<UserModel> get copyWith => _$UserModelCopyWithImpl<UserModel>(this as UserModel, _$identity);
/// Serializes this UserModel to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UserModel&&(identical(other.name, name) || other.name == name)&&(identical(other.avatarImage, avatarImage) || other.avatarImage == avatarImage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,avatarImage);
@override
String toString() {
return 'UserModel(name: $name, avatarImage: $avatarImage)';
}
}
/// @nodoc
abstract mixin class $UserModelCopyWith<$Res> {
factory $UserModelCopyWith(UserModel value, $Res Function(UserModel) _then) = _$UserModelCopyWithImpl;
@useResult
$Res call({
String name, String avatarImage
});
}
/// @nodoc
class _$UserModelCopyWithImpl<$Res>
implements $UserModelCopyWith<$Res> {
_$UserModelCopyWithImpl(this._self, this._then);
final UserModel _self;
final $Res Function(UserModel) _then;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? avatarImage = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,avatarImage: null == avatarImage ? _self.avatarImage : avatarImage // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [UserModel].
extension UserModelPatterns on UserModel {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UserModel value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UserModel value) $default,){
final _that = this;
switch (_that) {
case _UserModel():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UserModel value)? $default,){
final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String avatarImage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that.name,_that.avatarImage);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String avatarImage) $default,) {final _that = this;
switch (_that) {
case _UserModel():
return $default(_that.name,_that.avatarImage);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String avatarImage)? $default,) {final _that = this;
switch (_that) {
case _UserModel() when $default != null:
return $default(_that.name,_that.avatarImage);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _UserModel extends UserModel {
const _UserModel({required this.name, required this.avatarImage}): super._();
factory _UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
@override final String name;
@override final String avatarImage;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UserModelCopyWith<_UserModel> get copyWith => __$UserModelCopyWithImpl<_UserModel>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$UserModelToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UserModel&&(identical(other.name, name) || other.name == name)&&(identical(other.avatarImage, avatarImage) || other.avatarImage == avatarImage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,avatarImage);
@override
String toString() {
return 'UserModel(name: $name, avatarImage: $avatarImage)';
}
}
/// @nodoc
abstract mixin class _$UserModelCopyWith<$Res> implements $UserModelCopyWith<$Res> {
factory _$UserModelCopyWith(_UserModel value, $Res Function(_UserModel) _then) = __$UserModelCopyWithImpl;
@override @useResult
$Res call({
String name, String avatarImage
});
}
/// @nodoc
class __$UserModelCopyWithImpl<$Res>
implements _$UserModelCopyWith<$Res> {
__$UserModelCopyWithImpl(this._self, this._then);
final _UserModel _self;
final $Res Function(_UserModel) _then;
/// Create a copy of UserModel
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? avatarImage = null,}) {
return _then(_UserModel(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,avatarImage: null == avatarImage ? _self.avatarImage : avatarImage // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_UserModel _$UserModelFromJson(Map<String, dynamic> json) => _UserModel(
name: json['name'] as String,
avatarImage: json['avatarImage'] as String,
);
Map<String, dynamic> _$UserModelToJson(_UserModel instance) =>
<String, dynamic>{
'name': instance.name,
'avatarImage': instance.avatarImage,
};

View File

@@ -0,0 +1 @@
export 'user_repository_hive.dart';

View File

@@ -0,0 +1,24 @@
// External Dependencies
import 'package:logger/logger.dart';
// Internal Dependencies
import 'package:unyo/core/di/locator.dart';
import 'package:unyo/data/models/models.dart';
import 'package:unyo/domain/entities/entities.dart';
import 'package:unyo/domain/repositories/user_repository.dart';
class UserRepositoryHive implements UserRepository {
final Logger _logger = sl<Logger>();
@override
Future<List<User>> fetchAllUsers() async {
return [UserModel.empty()];
}
@override
Future<void> registerUser(User user) {
throw UnimplementedError();
}
}

View File

@@ -1,79 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
import 'package:unyo/widgets/widgets.dart';
import 'package:logger/logger.dart';
void showChangeRepoDialog(
BuildContext context, TextEditingController controller) {
logger.i("Opened changeRepo dialog");
showDialog(
context: context,
builder: (context) {
return ChangeRepoDialog(
controller: controller,
);
});
}
class ChangeRepoDialog extends StatelessWidget {
const ChangeRepoDialog({super.key, required this.controller});
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(context.tr("change_repo"),
style: const TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(context.tr("change_repo_message"),
style: const TextStyle(color: Colors.white)),
StyledTextField(
width: 500,
controller: controller,
color: Colors.white,
hintColor: Colors.grey,
hint: prefs.getString("extensions_json_url") ??
"https://raw.githubusercontent.com/K3vinb5/Unyo-Extensions/main/index.json",
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
StyledButton(
onPressed: () {
logger.i("Restored extensions repository to default");
prefs.setString("extensions_json_url",
"https://raw.githubusercontent.com/K3vinb5/Unyo-Extensions/main/index.json");
Navigator.of(context).pop();
},
text: context.tr("restore_default"),
),
const SizedBox(
width: 5,
),
StyledButton(
onPressed: () {
logger.i("Changed extensions repository");
if (controller.text.trim() != "") {
prefs.setString(
"extensions_json_url", controller.text.trim());
}
Navigator.of(context).pop();
},
text: context.tr("confirm"),
),
],
),
],
),
),
);
}
}

View File

@@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
void showConnectionSuccessfulDialog(BuildContext context) {
logger.i("Opened connection successful dialog");
showDialog(
context: context,
builder: (context) {
return const ConnectionSuccessfulDialog();
},
);
}
class ConnectionSuccessfulDialog extends StatelessWidget {
const ConnectionSuccessfulDialog({super.key});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Connection Successful",
style: TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
children: [
ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Ok", style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@@ -1,63 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
import 'package:unyo/widgets/widgets.dart';
void showCreateLocalAccoutDialog(BuildContext context,
void Function(int) setUserInfo, void Function() goToMainMenu) {
logger.i("Opened create local account dialog");
showDialog(
context: context,
builder: (_) => CreateLocalAccountDialog(setUserInfo: setUserInfo, goToMainMenu: goToMainMenu));
}
class CreateLocalAccountDialog extends StatelessWidget {
const CreateLocalAccountDialog({super.key, required this.setUserInfo, required this.goToMainMenu});
final void Function(int)
setUserInfo;
final void Function() goToMainMenu;
@override
Widget build(BuildContext context) {
TextEditingController controller = TextEditingController();
return AlertDialog(
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
title: Text(
context.tr("create_local_account_title"),
style: const TextStyle(color: Colors.white),
),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.4,
height: MediaQuery.of(context).size.height * 0.25,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
StyledTextField(
width: 300,
controller: controller,
color: Colors.white,
hintColor: Colors.grey,
hint: context.tr("insert_new_name"),
),
StyledButton(
onPressed: () async{
if (controller.text.trim() != "") {
await prefs.loginUser(controller.text.trim());
userName = controller.text.trim();
prefs.setString("userName", userName!);
setUserInfo(1);
goToMainMenu();
logger.i("Created local account with name: ${controller.text.trim()}");
if(!context.mounted) return;
Navigator.of(context).pop();
}
},
text: context.tr("confirm"),
),
],
),
),
);
}
}

View File

@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
class DeleteUserMediaDialog extends StatelessWidget {
const DeleteUserMediaDialog(
{super.key,
required this.totalHeight,
required this.totalWidth,
required this.currentMediaId,
required this.deleteUserAnime,
});
final double totalHeight;
final double totalWidth;
final int currentMediaId;
final void Function(int) deleteUserAnime;
@override
Widget build(BuildContext context) {
return SizedBox(
height: totalHeight * 0.2,
width: totalWidth * 0.1,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [
ElevatedButton(
onPressed: () {
logger.i("Deleting user media with id: $currentMediaId");
deleteUserAnime(currentMediaId);
Navigator.of(context).pop();
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
child: const Text("Confirm"),
),
const SizedBox(
width: 20,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
child: const Text("Cancel"),
),
],
),
);
}
}

View File

@@ -1,14 +0,0 @@
export 'update_dialog.dart';
export 'video_quality_dialog.dart';
export 'wrong_title_dialog.dart';
export 'media_info_dialog.dart';
export 'delete_user_media_dialog.dart';
export 'login_manually_dialog.dart';
export 'connection_successful_dialog.dart';
export 'error_dialog.dart';
export 'simple_dialog.dart';
export 'change_repo_dialog.dart';
export 'no_extensions_dialog.dart';
export 'log_out_dialog.dart';
export 'create_local_account_dialog.dart';
export 'sign_in_dialog.dart';

View File

@@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
void showErrorDialog(BuildContext context,
{String? exception, void Function()? onPressedAfterPop}) {
logger.i("Opened error dialog");
logger.e("Unknown error", error: exception);
showDialog(
context: context,
builder: (context) {
return ErrorDialog(exception: exception);
});
}
class ErrorDialog extends StatelessWidget {
const ErrorDialog({super.key, this.exception, this.onPressedAfterPop});
final String? exception;
final void Function()? onPressedAfterPop;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("An error occured D:",
style: TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(exception ?? "", style: const TextStyle(color: Colors.white)),
ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
if (onPressedAfterPop != null) {
logger.i("Executing provided error resulting callback");
onPressedAfterPop!();
}
},
child: const Text("Ok", style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@@ -1,93 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
import 'package:unyo/widgets/widgets.dart';
void showLogOutDialog(
BuildContext context,
void Function(void Function()) setState,
void Function() attemptLogin,
double adjustedHeight,
double adjustedWidth) {
logger.i("Opened log out dialog");
showDialog(
context: context,
builder: (context) => LogOutDialog(
attemptLogin: attemptLogin,
adjustedWidth: adjustedWidth,
adjustedHeight: adjustedHeight,
setState: setState,
));
}
class LogOutDialog extends StatelessWidget {
const LogOutDialog(
{super.key,
required this.attemptLogin,
required this.adjustedWidth,
required this.adjustedHeight,
required this.setState});
final void Function() attemptLogin;
final double adjustedWidth;
final double adjustedHeight;
final void Function(void Function()) setState;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(context.tr("logout_title"),
style: const TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: SizedBox(
width: adjustedWidth * 0.15,
height: adjustedHeight * 0.15,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.tr("logout_text"),
style: const TextStyle(color: Colors.white),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
StyledButton(
onPressed: () {
Navigator.of(context).pop();
},
text: context.tr("cancel"),
),
const SizedBox(
width: 20,
),
StyledButton(
onPressed: () {
logger.i("Logging out...");
prefs.logOut();
if (prefs.getBool("remote_endpoint") ?? false) {
processManager.stopProcess();
}
setState(() {
bannerImageUrl = null;
avatarImageUrl = null;
watchingList = null;
readingList = null;
userName = null;
userId = null;
accessToken = null;
});
attemptLogin();
Navigator.of(context).pop();
},
text: context.tr("confirm"),
),
],
),
],
),
),
);
}
}

View File

@@ -1,112 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/widgets/widgets.dart';
class LoginManuallyDialog extends StatelessWidget {
const LoginManuallyDialog(
{super.key,
required this.manualLoginController,
required this.getCodeFunction,
required this.loginFunction
});
final TextEditingController manualLoginController;
final void Function() getCodeFunction;
final void Function() loginFunction;
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
title: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Paste the Authentication Code",
style: TextStyle(color: Colors.white)),
],
),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.3,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 20,
),
Column(
children: [
StyledTextField(
width: 350,
controller: manualLoginController,
color: Colors.white,
hintColor: Colors.grey,
hint: "Paste your code here",
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: getCodeFunction,
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.black12),
),
child: const Text(
"Get your Code!",
style: TextStyle(
color: Colors.white,
),
),
),
],
),
const SizedBox(
height: 20,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.black12),
),
child: Text(
"cancel".tr(),
style: const TextStyle(color: Colors.white),
),
),
const SizedBox(
width: 20,
),
ElevatedButton(
onPressed: loginFunction,
style: const ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.black12),
),
child: Text(
"confirm".tr(),
style: const TextStyle(color: Colors.white),
),
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -1,452 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/models/models.dart';
import 'package:unyo/util/utils.dart';
import 'package:unyo/widgets/widgets.dart';
class MediaInfoDialogManager {
static final MediaInfoDialogManager _instance =
MediaInfoDialogManager._internal();
MediaInfoDialogManager._internal();
factory MediaInfoDialogManager() => _instance;
void openAnimeInfoDialog(BuildContext context, totalWidth, totalHeight) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
const Text(
"List Editor",
style: TextStyle(color: Colors.white),
),
IconButton(
onPressed: () {
askForDeleteUserMedia();
},
icon: const Icon(Icons.delete, color: Colors.white),
)
],
),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: MediaInfoDialog(
id: widget.currentAnime.id,
episodes: widget.currentAnime.episodes,
totalWidth: totalWidth,
totalHeight: totalHeight,
statuses: statuses,
query: query,
progress: progress,
currentEpisode: latestReleasedEpisode,
score: score,
setUserMediaModel: setUserAnimeModel,
startDate: startDate,
endDate: endDate,
animeModel: widget.currentAnime),
);
},
);
}
void askForDeleteUserMedia() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Are you sure you wish to delete this media entry",
style: TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: DeleteUserMediaDialog(
totalHeight: totalHeight,
totalWidth: totalWidth,
currentMediaId: widget.currentAnime.id,
deleteUserAnime: loggedUserModel.deleteUserAnime,
),
);
},
);
}
}
class MediaInfoDialog extends StatefulWidget {
const MediaInfoDialog({
super.key,
required this.totalWidth,
required this.totalHeight,
required this.statuses,
required this.query,
required this.episodes,
required this.progress,
required this.currentEpisode,
required this.score,
required this.setUserMediaModel,
required this.startDate,
required this.endDate,
required this.id,
this.animeModel,
this.mangaModel,
});
final double totalWidth;
final double totalHeight;
final List<String> statuses;
final Map<String, String> query;
final int? episodes;
final int id;
final double progress;
final int currentEpisode;
final double score;
final String startDate;
final String endDate;
final void Function() setUserMediaModel;
final AnimeModel? animeModel;
final MangaModel? mangaModel;
@override
State<MediaInfoDialog> createState() => _MediaInfoDialogState();
}
class _MediaInfoDialogState extends State<MediaInfoDialog> {
late Map<String, String> query;
late double progress;
late int currentEpisode;
late String startDate;
late String endDate;
late double score;
@override
void initState() {
super.initState();
query = widget.query;
progress = widget.progress;
currentEpisode = widget.currentEpisode;
startDate = widget.startDate;
endDate = widget.endDate;
score = widget.score;
}
@override
void didUpdateWidget(covariant MediaInfoDialog oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.query != widget.query) {
query = widget.query;
} else if (oldWidget.progress != widget.progress) {
progress = widget.progress;
} else if (oldWidget.currentEpisode != widget.currentEpisode) {
currentEpisode = widget.currentEpisode;
} else if (oldWidget.score != widget.score) {
score = widget.score;
} else if (oldWidget.startDate != widget.startDate) {
startDate = widget.startDate;
} else if (oldWidget.endDate != widget.endDate) {
endDate = widget.endDate;
}
}
@override
Widget build(BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return SizedBox(
width: widget.totalWidth * 0.5,
height: widget.totalHeight * 0.6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.max,
children: [
Text(
"status".tr(),
style: const TextStyle(color: Colors.white),
),
const SizedBox(
height: 10,
),
StyledDropDown(
items: [
...widget.statuses.map((status) => Text(status)),
],
horizontalPadding: 10,
onTap: (index) {
String newCurrentStatus = widget.statuses[index];
logger.i("Selected status: $newCurrentStatus");
widget.statuses.removeAt(index);
widget.statuses.insert(0, newCurrentStatus);
query.remove("status");
query.addAll({"status": newCurrentStatus});
},
color: Colors.white,
width: widget.totalWidth * 0.4,
),
const SizedBox(
height: 15,
),
Text(
"progress".tr(),
style: const TextStyle(color: Colors.white),
),
Row(
children: [
Text(
progress.toInt().toString(),
style: const TextStyle(color: Colors.white),
),
Expanded(
child: Slider(
activeColor: Colors.grey,
min: 0,
max: widget.episodes?.toDouble() ??
currentEpisode.toDouble(),
value: progress,
label: progress.round().toString(),
divisions: widget.episodes ??
(currentEpisode > 0 ? currentEpisode : 1),
onChanged: (value) {
setState(() {
progress =
value; // Update the progress variable when slider value changes
});
},
onChangeEnd: (value) {
logger.i("Progress slider value updated: $value");
query.remove("progress");
query.addAll({"progress": progress.toInt().toString()});
},
),
),
],
),
const SizedBox(
height: 15,
),
Text(
context.tr("score"),
style: const TextStyle(color: Colors.white),
),
Row(
children: [
Text(
score.toInt().toString(),
style: const TextStyle(color: Colors.white),
),
Expanded(
child: Slider(
activeColor: Colors.grey,
min: 0,
max: 10,
value: score,
divisions: 10,
label: score.round().toString(),
onChanged: (value) {
setState(() {
score =
value; // Update the progress variable when slider value changes
});
},
onChangeEnd: (value) {
logger.i("Score slider value updated: $value");
query.remove("score");
query.addAll({"score": score.toString()});
},
),
),
],
),
const SizedBox(
height: 15,
),
Text(
"start_end_data".tr(),
style: const TextStyle(color: Colors.white),
),
Row(
children: [
IconButton(
onPressed: () async {
logger.i("Opening date picker for start date");
DateTime? chosenDateTime = await showDatePicker(
context: context,
firstDate: DateTime(1970, 1, 1),
lastDate: DateTime.now(),
builder: (BuildContext context, Widget? child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.dark(
// Use ColorScheme.dark to reflect a dark theme
primary: Color.fromARGB(
255, 44, 44, 44), // Header background color
onPrimary: Colors.white, // Header text color
surface: Color.fromARGB(
255, 55, 44, 55), // Dialog background color
onSurface: Colors.white, // Body text color
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 37,
37, 37), // Button background color
foregroundColor:
Colors.white, // Button text color
),
),
dialogBackgroundColor: const Color.fromARGB(255,
44, 44, 44), // Entire dialog background color
),
child: child!,
);
},
);
if (chosenDateTime != null) {
setState(() {
startDate =
"${chosenDateTime.day}/${chosenDateTime.month}/${chosenDateTime.year}";
});
query.remove("startDateDay");
query.addAll(
{"startDateDay": chosenDateTime.day.toString()});
query.remove("startDateMonth");
query.addAll({
"startDateMonth": chosenDateTime.month.toString()
});
query.remove("startDateYear");
query.addAll(
{"startDateYear": chosenDateTime.year.toString()});
}
},
icon: const Icon(Icons.calendar_month, color: Colors.grey),
),
Text(
startDate,
style: const TextStyle(color: Colors.white),
),
const SizedBox(
width: 20,
),
Text(
endDate,
style: const TextStyle(color: Colors.white),
),
IconButton(
onPressed: () async {
logger.i("Opening date picker for end date");
DateTime? chosenDateTime = await showDatePicker(
context: context,
firstDate: DateTime(1970, 1, 1),
lastDate: DateTime.now(),
builder: (BuildContext context, Widget? child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
// Use ColorScheme.dark to reflect a dark theme
primary: Color.fromARGB(
255, 44, 44, 44), // Header background color
onPrimary: Colors.white, // Header text color
surface: Color.fromARGB(
255, 55, 44, 55), // Dialog background color
onSurface: Colors.white, // Body text color
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 37,
37, 37), // Button background color
foregroundColor:
Colors.white, // Button text color
),
),
dialogBackgroundColor: const Color.fromARGB(255,
44, 44, 44), // Entire dialog background color
),
child: child!,
);
},
);
if (chosenDateTime != null) {
setState(() {
endDate =
"${chosenDateTime.day}/${chosenDateTime.month}/${chosenDateTime.year}";
});
query.remove("endDateDay");
query.addAll(
{"endDateDay": chosenDateTime.day.toString()});
query.remove("endDateMonth");
query.addAll(
{"endDateMonth": chosenDateTime.month.toString()});
query.remove("endDateYear");
query.addAll(
{"endDateYear": chosenDateTime.year.toString()});
}
},
icon: const Icon(
Icons.calendar_month,
color: Colors.grey,
),
),
],
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () {
logger.i("Updated user media info");
if (widget.animeModel != null) {
loggedUserModel.setUserAnimeInfo(widget.id, query,
animeModel: widget.animeModel);
} else {
loggedUserModel.setUserMangaInfo(widget.id, query,
mangaModel: widget.mangaModel);
}
Timer(
const Duration(milliseconds: 1500),
() {
widget.setUserMediaModel();
},
);
Navigator.of(context).pop();
},
child: Text("confirm".tr()),
),
const SizedBox(
width: 20,
),
ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
child: Text("cancel".tr()),
),
],
),
],
),
),
],
),
);
},
);
}
}

View File

@@ -1,56 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
void showNoExtensionsDialog(
BuildContext context) {
logger.i("No extensions dialog opened");
showDialog(
context: context,
builder: (context) {
return NoExtensionsDialog(
title: context.tr("no_extensions_title"),
message: context.tr("no_extensions_message"),
);
});
}
class NoExtensionsDialog extends StatelessWidget {
const NoExtensionsDialog(
{super.key, required this.message, required this.title});
final String message;
final String title;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(title, style: const TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(message, style: const TextStyle(color: Colors.white)),
ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Ok", style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@@ -1,161 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/icons/anilist_icons.dart';
import 'package:unyo/dialogs/dialogs.dart';
import 'package:unyo/util/constants.dart';
import 'package:unyo/widgets/widgets.dart';
void showSignInDialog(
{required BuildContext context,
required double totalHeight,
required double totalWidth,
required void Function() login,
required void Function() getCodeFunction,
required void Function() goToMainScreen,
required void Function(int) setUserInfo,
required void Function(String) manualLogin}) {
logger.i("Opened sign in dialog");
showDialog(
context: context,
builder: (_) => SignInDialog(
totalHeight: totalHeight,
login: login,
getCodeFunction: getCodeFunction,
manualLogin: manualLogin,
goToMainScreen: goToMainScreen,
setUserInfo: setUserInfo,
totalWidth: totalWidth,
));
}
class SignInDialog extends StatelessWidget {
const SignInDialog({
super.key,
required this.totalHeight,
required this.login,
required this.getCodeFunction,
required this.manualLogin,
required this.goToMainScreen,
required this.setUserInfo,
required this.totalWidth,
});
final double totalHeight;
final double totalWidth;
final void Function() login;
final void Function() getCodeFunction;
final void Function() goToMainScreen;
final void Function(int) setUserInfo;
final void Function(String) manualLogin;
@override
Widget build(BuildContext context) {
TextEditingController controller = TextEditingController();
return AlertDialog(
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
title: Text(context.tr("sign_int_title"),
style: const TextStyle(color: Colors.white)),
content: SizedBox(
width: totalWidth * 0.5,
height: totalHeight * 0.5,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StyledButton(
onPressed: () {
login();
logger.i("Logged in to Anilist");
Navigator.of(context).pop();
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 50.0),
child: SizedBox(
width: 240,
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Login to Anilist ",
style: TextStyle(color: Colors.white),
),
Icon(Anilist.anilist),
],
),
),
),
),
const SizedBox(
height: 20,
),
StyledButton(
onPressed: () {
Navigator.of(context).pop();
showDialog(
context: context,
builder: (context) {
return LoginManuallyDialog(
manualLoginController: controller,
getCodeFunction: getCodeFunction,
loginFunction: () async {
manualLogin(controller.text.trim());
Navigator.of(context).pop();
},
);
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 50.0),
child: SizedBox(
width: 240,
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Login to Anilist (Copying Code) ",
style: TextStyle(color: Colors.white),
),
Icon(Anilist.anilist),
],
),
),
),
),
const SizedBox(
height: 20,
),
StyledButton(
onPressed: () {
Navigator.of(context).pop();
showCreateLocalAccoutDialog(
context,
setUserInfo,
goToMainScreen,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: SizedBox(
width: 240,
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${context.tr("create_local_account")} ",
style: const TextStyle(color: Colors.white),
),
const Icon(Icons.computer_rounded),
],
),
),
),
),
],
),
),
);
}
}

View File

@@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:unyo/util/constants.dart';
void showSimpleDialog(BuildContext context, String title, String message) {
logger.i("Opened simple dialog: $title - $message");
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: title,
message: message,
);
});
}
class SimpleDialog extends StatelessWidget {
const SimpleDialog({super.key, required this.message, required this.title});
final String message;
final String title;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(title, style: const TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(message, style: const TextStyle(color: Colors.white)),
ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Ok", style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@@ -1,156 +0,0 @@
import 'dart:convert';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:smooth_list_view/smooth_list_view.dart';
import 'package:unyo/util/utils.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
const String apiEndpoint =
"https://api.github.com/repos/K3vinb5/Unyo/releases/latest";
const String latestVersionEndpoint =
"https://github.com/K3vinb5/Unyo/releases/latest";
class UpdateDialog extends StatelessWidget {
const UpdateDialog({super.key, required this.markdown});
final String markdown;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.5,
width: MediaQuery.of(context).size.width * 0.5,
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
opacity: 0.1,
image: NetworkImage("https://i.imgur.com/JEGaQWx.png"),
),
),
child: Column(
children: [
const SizedBox(
height: 40,
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
flex: 8,
child: SingleChildScrollView(
child: Theme(
data: Theme.of(context).copyWith(
textTheme: Theme.of(context)
.textTheme
.apply(bodyColor: Colors.white)),
child: MarkdownBody(data: markdown),
),
),
),
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
style: const ButtonStyle(
foregroundColor: MaterialStatePropertyAll(Colors.white),
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37)),
),
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Later"),
),
const SizedBox(
width: 20,
),
ElevatedButton(
style: const ButtonStyle(
foregroundColor: MaterialStatePropertyAll(Colors.white),
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37)),
),
onPressed: () {
logger.i("Opening newest release on your browser");
goToLatestRelease();
Navigator.of(context).pop();
},
child: const Text("Download Update"),
),
],
),
),
],
),
)
],
),
);
}
}
/// Returns true if [newVersion] is strictly greater than [currentVersion].
bool _isVersionGreater(String newVersion, String currentVersion) {
List<String> newParts = newVersion.split('.');
List<String> currParts = currentVersion.split('.');
int maxLength = newParts.length > currParts.length ? newParts.length : currParts.length;
for (int i = 0; i < maxLength; i++) {
int n = i < newParts.length ? int.tryParse(newParts[i]) ?? 0 : 0;
int c = i < currParts.length ? int.tryParse(currParts[i]) ?? 0 : 0;
if (n > c) return true;
if (n < c) return false;
}
return false;
}
void showUpdateDialog(BuildContext context) async {
var url = Uri.parse(apiEndpoint);
var response = await http.get(url);
if (!context.mounted) return;
if (response.statusCode != 200) {
showDialog(
context: context,
builder: (context) {
return const AlertDialog(
backgroundColor: Color.fromARGB(255, 34, 33, 34),
title: Text(
"Error when looking for updates",
style: TextStyle(color: Colors.white, fontSize: 22),
),
);
});
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
final String markdown = jsonResponse["body"] as String;
final String newVersion = jsonResponse["tag_name"] as String;
// Don't show dialog if version is not newer or tagged to ignore
if (!_isVersionGreater(newVersion, currentVersion)) return;
if (newVersion.contains("ignore")) return;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: const Color.fromARGB(255, 34, 33, 34),
title: Text(
"New version available, update to version $newVersion",
style: const TextStyle(color: Colors.white, fontSize: 22),
),
content: UpdateDialog(
markdown: markdown,
),
);
},
);
}
void goToLatestRelease() async {
if (await canLaunchUrl(Uri.parse(latestVersionEndpoint))) {
await launchUrl(Uri.parse(latestVersionEndpoint));
}
}

View File

@@ -1,167 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:smooth_list_view/smooth_list_view.dart';
import 'package:unyo/api/aniskip_api.dart';
import 'package:unyo/models/models.dart';
import 'package:unyo/router/custom_page_route.dart';
import 'package:unyo/screens/screens.dart';
import 'package:unyo/sources/sources.dart';
import 'package:unyo/util/utils.dart';
class VideoQualityDialog extends StatefulWidget {
const VideoQualityDialog({
super.key,
required this.adjustedWidth,
required this.adjustedHeight,
required this.updateEntry,
required this.animeEpisode,
required this.animeModel,
required this.currentAnimeSource,
required this.id,
required this.idMal,
});
final double adjustedWidth;
final double adjustedHeight;
final int animeEpisode;
final AnimeModel animeModel;
final void Function(int) updateEntry;
final AnimeSource currentAnimeSource;
final String id;
final String idMal;
@override
State<VideoQualityDialog> createState() => _VideoQualityDialogState();
}
class _VideoQualityDialogState extends State<VideoQualityDialog> {
StreamData? streamData;
int source = 0;
VideoScreen? videoScreen;
@override
void initState() {
super.initState();
getStreamInfo();
}
void getStreamInfo() async {
logger.i("Getting stream info for ${widget.animeModel.getDefaultTitle()} episode ${widget.animeEpisode}");
streamData = await widget.currentAnimeSource.getAnimeStreamAndCaptions(
widget.id,
widget.animeModel.englishTitle ?? "",
widget.animeEpisode,
context);
setState(() {});
}
void onStreamSelected(int selected, Map<String, double> timestamps) {
logger.i("Selected stream $selected for ${widget.animeModel.getDefaultTitle()} episode ${widget.animeEpisode}");
source = selected;
Navigator.of(context).pop();
videoScreen = VideoScreen(
source: source,
streamData: streamData!,
updateEntry: () {
widget.updateEntry(widget.animeEpisode);
},
title:
"${widget.animeModel.getDefaultTitle()}, ${"episode".tr()} ${widget.animeEpisode}",
mqqtKey:
"${widget.animeModel.userPreferedTitle}-ep${widget.animeEpisode}",
episode: widget.animeEpisode,
timestamps: timestamps,
);
logger.i("Opening video screen for ${widget.animeModel.getDefaultTitle()} episode ${widget.animeEpisode}");
if (!context.mounted) return;
Navigator.push(
context,
customPageRouter(videoScreen!),
);
}
String getUtf8Text(String text) {
List<int> bytes = text.codeUnits;
try {
return utf8.decode(bytes);
} catch (e) {
logger.e("Error decoding text: $text", error: e.toString());
}
return text;
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.adjustedWidth * 0.4,
height: widget.adjustedHeight * 0.7,
child: streamData != null
? streamData!.qualities.isNotEmpty
? SmoothListView(
duration: const Duration(milliseconds: 200),
children: [
...streamData!.qualities.mapIndexed(
(index, text) =>
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 8.0),
child: SizedBox(
// height: 60,
child: ElevatedButton(
style: const ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 37, 37, 37),
),
foregroundColor: MaterialStatePropertyAll(
Colors.white,
),
),
onPressed: () async {
Map<String, double> timestamps =
await getOpeningSkipTimeStamps(widget.idMal,
widget.animeEpisode.toString());
onStreamSelected(index, timestamps);
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 20.0, horizontal: 3.0),
child: Text(getUtf8Text(text)),
),
),
),
),
),
],
)
: Center(
child: Text(
"quality_no_results".tr(),
style: const TextStyle(
color: Colors.white,
),
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LoadingAnimationWidget.inkDrop(
color: Colors.white,
size: 30,
),
const SizedBox(
height: 15,
),
Text(
"please_wait_text".tr(),
style: const TextStyle(
color: Colors.white,
),
)
],
),
);
}}

View File

@@ -1,216 +0,0 @@
import 'dart:async';
import 'package:animated_snack_bar/animated_snack_bar.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unyo/notification/notification_manager.dart';
import 'package:unyo/sources/sources.dart';
import 'package:unyo/widgets/widgets.dart';
class WrongTitleDialogManager {
static final WrongTitleDialogManager _instance =
WrongTitleDialogManager._internal();
WrongTitleDialogManager._internal();
factory WrongTitleDialogManager() => _instance;
bool manualTitleSelection = false;
List<DropdownMenuEntry> wrongTitleEntries = [];
String oldWrongTitleSearch = "";
String? currentSearchString;
int? currentSearchIndex;
Timer wrongTitleSearchTimer = Timer(const Duration(milliseconds: 500), () {});
void Function() wrongTitleSearchFunction = () {};
TextEditingController wrongTitleSearchController = TextEditingController();
List<String> results = [];
void openWrongTitleDialog(BuildContext context, double width, double height,
{AnimeSource? currentAnimeSource, MangaSource? currentMangaSource}) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
setWrongTitleSearch(setState,
currentAnimeSource: currentAnimeSource,
currentMangaSource: currentMangaSource);
return AlertDialog(
title: Text(context.tr("select_title"),
style: const TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 44, 44, 44),
content: WrongTitleDialog(
width: width,
height: height,
wrongTitleSearchController: wrongTitleSearchController,
wrongTitleEntries: wrongTitleEntries,
currentSearchString: manualTitleSelection
? currentSearchString!
: results.isNotEmpty
? results[0]
: "",
onPressed: () async {
wrongTitleSearchTimer.cancel();
//NOTE dirty fix for a bug
if (!context.mounted) return;
NotificationManager().showWarningNotification(
context,
"Updating Title, don't close...",
DesktopSnackBarPosition.topCenter);
await Future.delayed(const Duration(seconds: 1));
if (!context.mounted) return;
NotificationManager().showSuccessNotification(context,
"Title Updated", DesktopSnackBarPosition.topCenter);
if (!context.mounted) return;
Navigator.of(context).pop();
},
onSelected: (value) {
manualTitleSelection = true;
currentSearchString = results[value];
currentSearchIndex = value!;
},
),
);
},
);
},
);
}
void setWrongTitleSearch(void Function(void Function()) setDialogState,
{AnimeSource? currentAnimeSource, MangaSource? currentMangaSource}) {
oldWrongTitleSearch = "";
//reset listener
wrongTitleSearchController.removeListener(wrongTitleSearchFunction);
wrongTitleSearchFunction = () {
wrongTitleSearchTimer.cancel();
wrongTitleSearchTimer =
Timer(const Duration(milliseconds: 500), () async {
if (wrongTitleSearchController.text != oldWrongTitleSearch &&
wrongTitleSearchController.text != "") {
if (currentMangaSource == null) {
results = await currentAnimeSource!
.getAnimeTitles(wrongTitleSearchController.text);
} else {
results = [];
// searches = await currentMangaSource!.get(title);
}
setDialogState(() {
wrongTitleEntries = [
...results.mapIndexed(
(index, title) {
return DropdownMenuEntry(
style: const ButtonStyle(
foregroundColor: MaterialStatePropertyAll(Colors.white),
),
value: index,
label: title,
);
},
),
];
});
}
oldWrongTitleSearch = wrongTitleSearchController.text;
});
};
wrongTitleSearchController.addListener(wrongTitleSearchFunction);
}
void clearProperties() {
manualTitleSelection = false;
currentSearchIndex = null;
currentSearchString = null;
}
}
class WrongTitleDialog extends StatelessWidget {
const WrongTitleDialog({
super.key,
required this.width,
required this.height,
required this.wrongTitleSearchController,
required this.onSelected,
required this.onPressed,
required this.wrongTitleEntries,
required this.currentSearchString,
});
final double width;
final double height;
final TextEditingController wrongTitleSearchController;
final void Function(dynamic)? onSelected;
final void Function() onPressed;
final List<DropdownMenuEntry<dynamic>> wrongTitleEntries;
final String currentSearchString;
@override
Widget build(BuildContext context) {
return Container(
width: width * 0.5,
height: height * 0.5,
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
alignment: Alignment.bottomCenter,
opacity: 0.1,
image: NetworkImage("https://i.imgur.com/fUX8AXq.png"),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
SizedBox(
height: height * 0.05,
),
Text("select_new_title_text".tr(),
style: const TextStyle(color: Colors.white, fontSize: 22)),
const SizedBox(
height: 30,
),
// TODO Review DropdownMenu manualSelection field
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownMenu(
// hintText: context.tr("search_from_website"),
width: width * 0.4,
textStyle: const TextStyle(color: Colors.white),
menuStyle: const MenuStyle(
backgroundColor: MaterialStatePropertyAll(
Color.fromARGB(255, 44, 44, 44),
),
),
controller: wrongTitleSearchController,
onSelected: onSelected,
initialSelection: /*manualSelection ?? 0*/ null,
dropdownMenuEntries: wrongTitleEntries,
menuHeight: height * 0.3,
),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
"${context.tr("current_selection")}: $currentSearchString",
style: const TextStyle(color: Colors.grey, fontSize: 18),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StyledButton(
text: "confirm".tr(),
onPressed: onPressed,
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1 @@
export 'user.dart';

View File

@@ -0,0 +1,12 @@
/// Represents a user entity.
///
/// The [User] type defines the basic properties of a user, including:
/// - [id]: The unique identifier for the user.
/// - [name]: The user's display name.
/// - [email]: The user's email address.
abstract class User {
final String name;
final String avatarImage;
User({required this.name, required this.avatarImage});
}

View File

@@ -0,0 +1 @@
export 'user_repository.dart';

View File

@@ -0,0 +1,6 @@
import 'package:unyo/domain/entities/user.dart';
abstract class UserRepository {
Future<List<User>> fetchAllUsers();
Future<void> registerUser(User user);
}

View File

@@ -1,84 +0,0 @@
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: unused_field
// ignore_for_file: unused_element
// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
// ignore_for_file: type=lint
import 'dart:ffi' as ffi;
/// Bindings to `lib/ffi/libmtorrentserver.h`.
class TorrentLibrary {
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
TorrentLibrary(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
/// The symbols are looked up with [lookup].
TorrentLibrary.fromLookup(
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
lookup)
: _lookup = lookup;
Start_return Start(
ffi.Pointer<ffi.Char> mcfg,
) {
return _Start(
mcfg,
);
}
late final _StartPtr =
_lookup<ffi.NativeFunction<Start_return Function(ffi.Pointer<ffi.Char>)>>(
'Start');
late final _Start =
_StartPtr.asFunction<Start_return Function(ffi.Pointer<ffi.Char>)>();
}
final class max_align_t extends ffi.Opaque {}
final class _GoString_ extends ffi.Struct {
external ffi.Pointer<ffi.Char> p;
@ptrdiff_t()
external int n;
}
typedef ptrdiff_t = ffi.Long;
typedef Dartptrdiff_t = int;
final class GoInterface extends ffi.Struct {
external ffi.Pointer<ffi.Void> t;
external ffi.Pointer<ffi.Void> v;
}
final class GoSlice extends ffi.Struct {
external ffi.Pointer<ffi.Void> data;
@GoInt()
external int len;
@GoInt()
external int cap;
}
typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int;
/// Return type for Start
final class Start_return extends ffi.Struct {
@GoInt()
external int r0;
external ffi.Pointer<ffi.Char> r1;
}
const int NULL = 0;

View File

@@ -1,39 +0,0 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'generated_bindings.dart';
Future<int> start(String mcfg) async {
var completer = Completer<int>();
var res = _bindings.Start(mcfg.toNativeUtf8().cast());
if (res.r1 != nullptr) {
completer.completeError(Exception(res.r1.cast<Utf8>().toDartString()));
} else {
completer.complete(res.r0);
}
return completer.future;
}
const String _libName = 'libmtorrentserver';
final DynamicLibrary _dylib = () {
if (kDebugMode){
return DynamicLibrary.open('./$_libName.so');
}
if (Platform.isMacOS) {
return DynamicLibrary.open('$_libName.dylib');
}
if (Platform.isLinux) {
return DynamicLibrary.open('$_libName.so');
}
if (Platform.isWindows) {
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
final TorrentLibrary _bindings = TorrentLibrary(_dylib);

View File

@@ -1,25 +0,0 @@
/// Flutter icons Anilist
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
/// following in your pubspec.yaml
///
/// flutter:
/// fonts:
/// - family: Anilist
/// fonts:
/// - asset: fonts/Anilist.ttf
///
///
///
import 'package:flutter/widgets.dart';
class Anilist {
Anilist._();
static const _kFontFam = 'Anilist';
static const String? _kFontPkg = null;
static const IconData anilist = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@@ -1,119 +1,41 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
//Flutter dependencies
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_acrylic/window.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:unyo/models/adapters/anilist_user_model_adapter.dart';
import 'package:unyo/models/adapters/anime_model_adapter.dart';
import 'package:unyo/models/adapters/local_user_model_adapter.dart';
import 'package:unyo/models/adapters/manga_model_adapter.dart';
import 'package:unyo/models/adapters/user_media_model_adapter.dart';
import 'package:unyo/router/router.dart';
import 'package:fvp/fvp.dart' as fvp;
import 'package:path/path.dart' as p;
import 'package:unyo/util/utils.dart';
import 'package:flutter_window_close/flutter_window_close.dart';
import 'package:unyo/util/constants.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
Future<void> shutdownCleanup() async {
await discord.cleanup();
processManager.stopProcess();
}
//Internal dependencies
import 'package:unyo/core/di/locator.dart';
import 'package:unyo/core/router/app_router.dart';
Future<void> main() async {
logger.i("Initializing dependencies");
final _appRouter = AppRouter();
void main() async {
// Initialize Flutter bindings
WidgetsFlutterBinding.ensureInitialized();
await Window.initialize();
await EasyLocalization.ensureInitialized();
final dir = await getApplicationSupportDirectory();
Hive.init(p.join(dir.path, "data"));
Hive.registerAdapter(AnilistUserModelAdapter());
Hive.registerAdapter(LocalUserModelAdapter());
Hive.registerAdapter(UserMediaModelAdapter());
Hive.registerAdapter(MangaModelAdapter());
Hive.registerAdapter(AnimeModelAdapter());
if (Platform.isWindows) {
fvp.registerWith(options: {
'platforms': ['windows'],
'video.decoders': ['DXVA', 'FFmpeg'],
'player': {"avformat.extension_picky": "0"}
});
} else {
fvp.registerWith(options: {
'platforms': ['linux', 'macos'],
});
}
// Handle forced shutdown (Ctrl+C, SIGTERM)
ProcessSignal.sigint.watch().listen((_) async {
await shutdownCleanup();
exit(0);
});
ProcessSignal.sigterm.watch().listen((_) async {
await shutdownCleanup();
exit(0);
});
logger.i("Initializing Unyo");
// Inject dependencies before running the app
setupLocator();
runApp(
EasyLocalization(
supportedLocales: const [
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('de'),
Locale('it'),
Locale('pt'),
Locale('ru'),
Locale('ja'),
Locale('bn'),
Locale('hi'),
],
supportedLocales: [Locale('en')],
path: 'assets/translations',
fallbackLocale: Locale('en'),
useOnlyLangCode: true,
path: 'assets/languages',
fallbackLocale: const Locale('en'),
child: const MyApp(),
child: ScreenUtilInit(
designSize: const Size(1280, 720),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
return const MyApp();
},
),
),
);
doWhenWindowReady(() {
appWindow.position = const Offset(200, 200);
appWindow.minSize = const Size(1280, 720);
appWindow.title = "Unyo";
appWindow.size = const Size(1280, 720);
appWindow.show();
});
}
class MyApp extends StatefulWidget {
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
FlutterWindowClose.setWindowShouldCloseHandler(() async {
logger.i("Unyo is exiting...");
await shutdownCleanup();
logger.i("Cleanup done; exiting now.");
return true;
});
}
@override
void dispose() {
Future.microtask(() async {
await shutdownCleanup();
});
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
@@ -121,9 +43,93 @@ class _MyAppState extends State<MyApp> {
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
theme: ThemeData(),
title: 'Unyo',
routerConfig: router,
title: "Unyo",
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color.fromARGB(255, 44, 44, 44),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
// colorScheme: ColorScheme.dark(
// primary: Colors.grey[200]!,
// secondary: Colors.grey[300]!,
// surface: Colors.white,
// onSurface: Colors.grey[300]!,
// ),
textTheme: TextTheme(
// Display styles (largest) - white
displayLarge: TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
displayMedium: TextStyle(
color: Colors.white,
fontSize: 35,
fontWeight: FontWeight.bold,
),
displaySmall: TextStyle(
color: Colors.white,
fontSize: 30,
fontWeight: FontWeight.bold,
),
// Headline styles - white to light gray
headlineLarge: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w600,
),
headlineMedium: TextStyle(
color: Colors.white,
fontSize: 22,
// fontWeight: FontWeight.w600,
),
headlineSmall: TextStyle(
color: Colors.white,
fontSize: 20,
// fontWeight: FontWeight.w600,
),
// Title styles - light gray to mid gray
titleLarge: TextStyle(
color: Colors.white,
fontSize: 18,
// fontWeight: FontWeight.w500,
),
titleMedium: TextStyle(
color: Colors.white,
fontSize: 16,
// fontWeight: FontWeight.w500,
),
titleSmall: TextStyle(
color: Colors.white,
fontSize: 14,
// fontWeight: FontWeight.w500,
),
// Body styles - dark gray to black
bodyLarge: TextStyle(color: Colors.grey[900], fontSize: 14),
bodyMedium: TextStyle(color: Colors.grey[900], fontSize: 12),
bodySmall: TextStyle(color: Colors.grey[900], fontSize: 10),
// Label styles - black
labelLarge: TextStyle(
color: Colors.grey[900],
fontSize: 12,
fontWeight: FontWeight.w500,
),
labelMedium: TextStyle(
color: Colors.grey[900],
fontSize: 11,
fontWeight: FontWeight.w500,
),
labelSmall: TextStyle(
color: Colors.grey[900],
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
routerConfig: _appRouter.config(),
);
}
}

View File

@@ -1,34 +0,0 @@
import 'package:hive/hive.dart';
import 'package:unyo/models/models.dart';
class AnilistUserModelAdapter extends TypeAdapter<AnilistUserModel> {
@override
AnilistUserModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AnilistUserModel(
avatarImage: fields[0] as String?,
bannerImage: fields[1] as String?,
userName: fields[2] as String?,
userId: fields[3] as int?
);
}
@override
int get typeId => 0;
@override
void write(BinaryWriter writer, AnilistUserModel obj) {
writer.writeByte(4);
writer.writeByte(0);
writer.write(obj.avatarImage);
writer.writeByte(1);
writer.write(obj.bannerImage);
writer.writeByte(2);
writer.write(obj.userName);
writer.writeByte(3);
writer.write(obj.userId);
}
}

View File

@@ -1,73 +0,0 @@
import 'package:hive/hive.dart';
import 'package:unyo/models/models.dart';
class AnimeModelAdapter extends TypeAdapter<AnimeModel> {
@override
AnimeModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AnimeModel(
id: fields[0],
idMal: fields[1],
userPreferedTitle: fields[2],
japaneseTitle: fields[3],
englishTitle: fields[4],
coverImage: fields[5],
bannerImage: fields[6],
startDate: fields[7],
endDate: fields[8],
type: fields[9],
status: fields[10],
description: fields[11],
format: fields[12],
averageScore: fields[13],
episodes: fields[14],
currentEpisode: fields[15],
duration: fields[16],
);
}
@override
int get typeId => 3;
@override
void write(BinaryWriter writer, AnimeModel obj) {
writer.writeByte(17);
writer.writeByte(0);
writer.write(obj.id);
writer.writeByte(1);
writer.write(obj.idMal);
writer.writeByte(2);
writer.write(obj.userPreferedTitle);
writer.writeByte(3);
writer.write(obj.japaneseTitle);
writer.writeByte(4);
writer.write(obj.englishTitle);
writer.writeByte(5);
writer.write(obj.coverImage);
writer.writeByte(6);
writer.write(obj.bannerImage);
writer.writeByte(7);
writer.write(obj.startDate);
writer.writeByte(8);
writer.write(obj.endDate);
writer.writeByte(9);
writer.write(obj.type);
writer.writeByte(10);
writer.write(obj.status);
writer.writeByte(11);
writer.write(obj.description);
writer.writeByte(12);
writer.write(obj.format);
writer.writeByte(13);
writer.write(obj.averageScore);
writer.writeByte(14);
writer.write(obj.episodes);
writer.writeByte(15);
writer.write(obj.currentEpisode);
writer.writeByte(16);
writer.write(obj.duration);
}
}

View File

@@ -1,34 +0,0 @@
import 'package:hive/hive.dart';
import 'package:unyo/models/models.dart';
class LocalUserModelAdapter extends TypeAdapter<LocalUserModel> {
@override
LocalUserModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return LocalUserModel(
avatarImage: fields[0] as String?,
bannerImage: fields[1] as String?,
userName: fields[2] as String?,
userId: fields[3] as int?
);
}
@override
int get typeId => 1;
@override
void write(BinaryWriter writer, LocalUserModel obj) {
writer.writeByte(4);
writer.writeByte(0);
writer.write(obj.avatarImage);
writer.writeByte(1);
writer.write(obj.bannerImage);
writer.writeByte(2);
writer.write(obj.userName);
writer.writeByte(3);
writer.write(obj.userId);
}
}

View File

@@ -1,73 +0,0 @@
import 'package:hive/hive.dart';
import 'package:unyo/models/models.dart';
class MangaModelAdapter extends TypeAdapter<MangaModel> {
@override
MangaModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MangaModel(
id: fields[0],
idMal: fields[1],
userPreferedTitle: fields[2],
japaneseTitle: fields[3],
englishTitle: fields[4],
coverImage: fields[5],
bannerImage: fields[6],
startDate: fields[7],
endDate: fields[8],
type: fields[9],
status: fields[10],
description: fields[11],
format: fields[12],
averageScore: fields[13],
chapters: fields[14],
currentEpisode: fields[15],
duration: fields[16],
);
}
@override
int get typeId => 4;
@override
void write(BinaryWriter writer, MangaModel obj) {
writer.writeByte(17);
writer.writeByte(0);
writer.write(obj.id);
writer.writeByte(1);
writer.write(obj.idMal);
writer.writeByte(2);
writer.write(obj.userPreferedTitle);
writer.writeByte(3);
writer.write(obj.japaneseTitle);
writer.writeByte(4);
writer.write(obj.englishTitle);
writer.writeByte(5);
writer.write(obj.coverImage);
writer.writeByte(6);
writer.write(obj.bannerImage);
writer.writeByte(7);
writer.write(obj.startDate);
writer.writeByte(8);
writer.write(obj.endDate);
writer.writeByte(9);
writer.write(obj.type);
writer.writeByte(10);
writer.write(obj.status);
writer.writeByte(11);
writer.write(obj.description);
writer.writeByte(12);
writer.write(obj.format);
writer.writeByte(13);
writer.write(obj.averageScore);
writer.writeByte(14);
writer.write(obj.chapters);
writer.writeByte(15);
writer.write(obj.currentEpisode);
writer.writeByte(16);
writer.write(obj.duration);
}
}

View File

@@ -1,43 +0,0 @@
import 'package:hive/hive.dart';
import 'package:unyo/models/models.dart';
class UserMediaModelAdapter extends TypeAdapter<UserMediaModel> {
@override
UserMediaModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserMediaModel(
score: fields[0] as num?,
progress: fields[1] as num?,
repeat: fields[2] as int?,
priority: fields[3] as int?,
status: fields[4] as String?,
startDate: fields[5] as String?,
endDate: fields[6] as String?,
);
}
@override
int get typeId => 2;
@override
void write(BinaryWriter writer, UserMediaModel obj) {
writer.writeByte(7);
writer.writeByte(0);
writer.write(obj.score);
writer.writeByte(1);
writer.write(obj.progress);
writer.writeByte(2);
writer.write(obj.repeat);
writer.writeByte(3);
writer.write(obj.priority);
writer.writeByte(4);
writer.write(obj.status);
writer.writeByte(5);
writer.write(obj.startDate);
writer.writeByte(6);
writer.write(obj.endDate);
}
}

View File

@@ -1,609 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:unyo/models/models.dart';
import 'package:unyo/util/constants.dart' as constants;
class AnilistUserModel implements UserModel {
final maxAttempts = 5;
@override
String? avatarImage;
@override
String? bannerImage;
@override
String? userName;
@override
int? userId;
AnilistUserModel(
{this.avatarImage, this.bannerImage, this.userName, this.userId});
@override
Future<List<String>> getUserNameAndId({int? newAttempt}) async {
if (userName != null && userId != null) {
return [userName!, userId!.toString()];
}
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query": "query {Viewer{name id}}",
};
var response = await http.post(
url,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer ${constants.accessToken}"
},
body: json.encode(query),
);
if (response.statusCode != 200) {
print(response.body);
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return getUserNameAndId(newAttempt: newAttempt);
}
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
userName = jsonResponse["data"]["Viewer"]["name"] as String?;
userId = jsonResponse["data"]["Viewer"]["id"] as int?;
return [
jsonResponse["data"]["Viewer"]["name"],
jsonResponse["data"]["Viewer"]["id"].toString()
];
}
@override
Future<String> getUserbannerImageUrl({int? newAttempt}) async {
if (bannerImage != null) {
return bannerImage!;
}
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query (\$id: Int \$name: String){User(id: \$id, name: \$name){bannerImage}}",
"variables": {
"name": constants.userName,
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("User banner image: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
String returnString =
await getUserbannerImageUrl(newAttempt: newAttempt);
return returnString;
}
return "https://i.imgur.com/x6TGK1x.png";
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
bannerImage = jsonResponse["data"]["User"]["bannerImage"] as String?;
return jsonResponse["data"]["User"]["bannerImage"];
}
@override
Future<String> getUserAvatarImageUrl({int? newAttempt}) async {
if (avatarImage != null) {
return avatarImage!;
}
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query (\$id: Int \$name: String){User(id: \$id, name: \$name){avatar {medium}}}",
"variables": {
"name": constants.userName,
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("User avatar image: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
String returnString =
await getUserAvatarImageUrl(newAttempt: newAttempt);
return returnString;
}
return "https://i.imgur.com/EKtChtm.png";
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
try {
String returnString = jsonResponse["data"]["User"]["avatar"]["medium"];
avatarImage = returnString as String?;
return returnString;
} catch (e) {
return "https://i.imgur.com/EKtChtm.png";
}
}
@override
Future<List<AnimeModel>> getUserAnimeLists(String listName,
{int? newAttempt}) async {
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query(\$userId:Int,\$userName:String,\$type:MediaType){MediaListCollection(userId:\$userId,userName:\$userName,type:\$type,sort:UPDATED_TIME_DESC){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id idMal title{userPreferred romaji english}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore description popularity isAdult countryOfOrigin genres bannerImage startDate{year month day} endDate{year month day}}}",
"variables": {
"userId": constants.userId,
"type": "ANIME",
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("User anime list $listName : $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getUserAnimeLists(listName, newAttempt: newAttempt);
}
return [];
}
List<AnimeModel> animeModelList = [];
Map<String, dynamic> jsonResponse = json.decode(response.body);
List<dynamic> animeLists =
jsonResponse["data"]["MediaListCollection"]["lists"];
for (int i = 0; i < animeLists.length; i++) {
if (animeLists[i]["name"] == listName) {
List<dynamic> wantedList = animeLists[i]["entries"];
for (int i = 0; i < wantedList.length; i++) {
Map<String, dynamic> json = wantedList[i]["media"];
animeModelList.add(AnimeModel.fromJson(json));
}
break;
}
}
return animeModelList;
}
@override
Future<Map<String, List<AnimeModel>>> getAllUserAnimeLists(
{int? newAttempt}) async {
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query(\$userId:Int,\$userName:String,\$type:MediaType){MediaListCollection(userId:\$userId,userName:\$userName,type:\$type){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id idMal title{userPreferred romaji english }coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore description popularity isAdult countryOfOrigin genres bannerImage startDate{year month day} endDate{day month year}}}",
"variables": {
"userId": constants.userId,
"type": "ANIME",
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("All user anime lists: $attempt - failure");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return await getAllUserAnimeLists(newAttempt: newAttempt);
}
//NOTE empry Map
return {};
}
Map<String, List<AnimeModel>> userAnimeListsMap = {};
Map<String, dynamic> jsonResponse = json.decode(response.body);
List<dynamic> userAnimeLists =
jsonResponse["data"]["MediaListCollection"]["lists"];
for (int i = 0; i < userAnimeLists.length; i++) {
List<dynamic> currentList = userAnimeLists[i]["entries"];
List<AnimeModel> animeModelList = [];
for (int j = 0; j < currentList.length; j++) {
Map<String, dynamic> json = currentList[j]["media"];
animeModelList.add(AnimeModel.fromJson(json));
}
userAnimeListsMap.addAll({userAnimeLists[i]["name"]: animeModelList});
}
return userAnimeListsMap;
}
@override
Future<UserMediaModel?> getUserAnimeInfo(int mediaId,
{int? newAttempt}) async {
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query{ Media(id: $mediaId){ mediaListEntry { score progress repeat priority status startedAt{day month year} completedAt{day month year} } } }",
};
var response = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query),
);
if (response.statusCode != 200) {
await Future.delayed(const Duration(milliseconds: 200));
print("User anime info: $attempt - failure {$response.body}");
if (attempt < maxAttempts) {
int newAttempt = attempt + 1;
return getUserAnimeInfo(mediaId, newAttempt: newAttempt);
}
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
if (jsonResponse["data"]["Media"]["mediaListEntry"] == null) {
return UserMediaModel(
score: 0,
progress: 0,
repeat: 0,
priority: 0,
status: "",
startDate: "~/~/~",
endDate: "~/~/~",
);
}
Map<String, dynamic> mediaListEntry =
jsonResponse["data"]["Media"]["mediaListEntry"];
return UserMediaModel(
score: mediaListEntry["score"],
progress: mediaListEntry["progress"],
repeat: mediaListEntry["repeat"],
priority: mediaListEntry["priority"],
status: mediaListEntry["status"],
startDate:
"${mediaListEntry["startedAt"]["day"]}/${mediaListEntry["startedAt"]["month"]}/${mediaListEntry["startedAt"]["year"]}",
endDate:
"${mediaListEntry["completedAt"]["day"]}/${mediaListEntry["completedAt"]["month"]}/${mediaListEntry["completedAt"]["year"]}",
);
}
@override
Future<List<MangaModel>> getUserMangaLists(String listName,
{int? newAttempt}) async {
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query(\$userId:Int,\$userName:String,\$type:MediaType){MediaListCollection(userId:\$userId,userName:\$userName,type:\$type,sort:UPDATED_TIME_DESC){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id idMal title{userPreferred romaji english}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore description popularity isAdult countryOfOrigin genres bannerImage startDate{year month day} endDate{year month day}}}",
"variables": {
"userId": constants.userId,
"type": "MANGA",
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
print(response.body);
if (attempt < 5) {
print("userMangaLists : $attempt - failure");
await Future.delayed(const Duration(milliseconds: 200));
int newAttempt = attempt + 1;
return await getUserMangaLists(listName, newAttempt: newAttempt);
}
return [];
}
List<MangaModel> animeModelList = [];
Map<String, dynamic> jsonResponse = json.decode(response.body);
List<dynamic> animeLists =
jsonResponse["data"]["MediaListCollection"]["lists"];
for (int i = 0; i < animeLists.length; i++) {
if (animeLists[i]["name"] == listName) {
List<dynamic> wantedList = animeLists[i]["entries"];
for (int i = 0; i < wantedList.length; i++) {
var json = wantedList[i]["media"];
animeModelList.add(MangaModel.fromJson(json));
// animeModelList.add(
// MangaModel(
// id: wantedList[i]["media"]["id"],
// title: wantedList[i]["media"]["title"]["userPreferred"],
// coverImage: wantedList[i]["media"]["coverImage"]["large"],
// bannerImage: wantedList[i]["media"]["bannerImage"],
// startDate:
// "${wantedList[i]["media"]["startDate"]["day"]}/${wantedList[i]["media"]["startDate"]["month"]}/${wantedList[i]["media"]["startDate"]["year"]}",
// endDate: "",
// //"${wantedList[i]["media"]["endDate"]["day"]}/${wantedList[i]["media"]["endDate"]["month"]}/${wantedList[i]["media"]["endDate"]["year"]}",
// type: wantedList[i]["media"]["type"],
// description: wantedList[i]["media"]["description"],
// status: wantedList[i]["media"]["status"],
// averageScore: wantedList[i]["media"]["averageScore"],
// chapters: wantedList[i]["media"]["chapters"],
// duration: wantedList[i]["media"]["episodes"],
// format: wantedList[i]["media"]["format"],
// ),
// );
}
break;
}
}
return animeModelList;
}
@override
Future<Map<String, List<MangaModel>>> getAllUserMangaLists(
{int? newAttempt}) async {
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query(\$userId:Int,\$userName:String,\$type:MediaType){MediaListCollection(userId:\$userId,userName:\$userName,type:\$type){lists{name isCustomList isCompletedList:isSplitCompletedList entries{...mediaListEntry}}user{id name avatar{large}mediaListOptions{scoreFormat rowOrder animeList{sectionOrder customLists splitCompletedSectionByFormat theme}mangaList{sectionOrder customLists splitCompletedSectionByFormat theme}}}}}fragment mediaListEntry on MediaList{id mediaId status score progress progressVolumes repeat priority private hiddenFromStatusLists customLists advancedScores notes updatedAt startedAt{year month day}completedAt{year month day}media{id idMal title{userPreferred romaji english}coverImage{extraLarge large}type format status(version:2)episodes volumes chapters averageScore description popularity isAdult countryOfOrigin genres bannerImage startDate{year month day} endDate{year month day}}}",
"variables": {
"userId": constants.userId,
"type": "MANGA",
}
};
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(query),
);
if (response.statusCode != 200) {
print(response.body);
if (attempt < 5) {
print("allUserMangaLists : $attempt - failure");
await Future.delayed(const Duration(milliseconds: 200));
int newAttempt = attempt + 1;
return await getAllUserMangaLists(newAttempt: newAttempt);
}
//NOTE empry Map
return {};
}
Map<String, List<MangaModel>> userMangaListsMap = {};
Map<String, dynamic> jsonResponse = json.decode(response.body);
List<dynamic> userMangaLists =
jsonResponse["data"]["MediaListCollection"]["lists"];
for (int i = 0; i < userMangaLists.length; i++) {
List<dynamic> currentList = userMangaLists[i]["entries"];
List<MangaModel> mangaModelList = [];
for (int j = 0; j < currentList.length; j++) {
var json = currentList[j]["media"];
mangaModelList.add(MangaModel.fromJson(json));
// mangaModelList.add(
// MangaModel(
// id: currentList[j]["media"]["id"],
// title: currentList[j]["media"]["title"]["userPreferred"],
// coverImage: currentList[j]["media"]["coverImage"]["large"],
// bannerImage: currentList[j]["media"]["bannerImage"],
// startDate:
// "${currentList[j]["media"]["startDate"]["day"]}/${currentList[j]["media"]["startDate"]["month"]}/${currentList[j]["media"]["startDate"]["year"]}",
// endDate: "",
// //"${wantedList[i]["media"]["endDate"]["day"]}/${wantedList[i]["media"]["endDate"]["month"]}/${wantedList[i]["media"]["endDate"]["year"]}",
// type: currentList[j]["media"]["type"],
// description: currentList[j]["media"]["description"],
// status: currentList[j]["media"]["status"],
// averageScore: currentList[j]["media"]["averageScore"],
// chapters: currentList[j]["media"]["chapters"],
// duration: currentList[j]["media"]["episodes"],
// format: currentList[j]["media"]["format"],
// ),
// );
}
userMangaListsMap.addAll({userMangaLists[i]["name"]: mangaModelList});
}
print(userMangaListsMap);
return userMangaListsMap;
}
@override
Future<UserMediaModel?> getUserMangaInfo(int mediaId,
{int? newAttempt}) async {
int attempt = newAttempt ?? 0;
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"query{ Media(id: $mediaId){ mediaListEntry { score progress repeat priority status startedAt{day month year} completedAt{day month year} } } }",
};
var response = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query),
);
if (response.statusCode != 200) {
if (attempt < 5) {
print("userMangaInfo : $attempt - failure");
await Future.delayed(const Duration(milliseconds: 200));
int newAttempt = attempt + 1;
return getUserMangaInfo(mediaId, newAttempt: newAttempt);
}
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
if (jsonResponse["data"]["Media"]["mediaListEntry"] == null) {
return UserMediaModel(
score: 0,
progress: 0,
repeat: 0,
priority: 0,
status: "",
startDate: "~/~/~",
endDate: "~/~/~",
);
}
Map<String, dynamic> mediaListEntry =
jsonResponse["data"]["Media"]["mediaListEntry"];
return UserMediaModel(
score: mediaListEntry["score"],
progress: mediaListEntry["progress"],
repeat: mediaListEntry["repeat"],
priority: mediaListEntry["priority"],
status: mediaListEntry["status"],
startDate:
"${mediaListEntry["startedAt"]["day"]}/${mediaListEntry["startedAt"]["month"]}/${mediaListEntry["startedAt"]["year"]}",
endDate:
"${mediaListEntry["completedAt"]["day"]}/${mediaListEntry["completedAt"]["month"]}/${mediaListEntry["completedAt"]["year"]}",
);
}
@override
void setUserAnimeInfo(int mediaId, Map<String, String> receivedQuery,
{AnimeModel? animeModel}) async {
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"mutation (\$mediaId: Int, \$status: MediaListStatus, \$score: Float, \$progress: Int, \$startedAt: FuzzyDateInput, \$completedAt: FuzzyDateInput) { SaveMediaListEntry(mediaId: \$mediaId, status: \$status, score: \$score, progress: \$progress, startedAt: \$startedAt, completedAt: \$completedAt) { mediaId status score progress startedAt { year month day } completedAt { year month day } } } ",
"variables": {
"mediaId": mediaId,
"status": receivedQuery["status"],
"score": double.parse(receivedQuery["score"]!),
"progress": int.parse(receivedQuery["progress"]!),
"startedAt": {
"day": receivedQuery["startDateDay"],
"month": receivedQuery["startDateMonth"],
"year": receivedQuery["startDateYear"]
},
"completedAt": {
"day": receivedQuery["endDateDay"],
"month": receivedQuery["endDateMonth"],
"year": receivedQuery["endDateYear"]
},
},
};
var response = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query),
);
print(response.body);
}
@override
void deleteUserAnime(int mediaId) async {
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query1 = {
"query": "query(\$mediaId:Int){ MediaList(mediaId:\$mediaId){ id } }",
"variables": {
"mediaId": mediaId,
},
};
var response1 = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query1),
);
int entryId = jsonDecode(response1.body)["data"]["MediaList"]["id"];
Map<String, dynamic> query = {
"query":
"mutation (\$entryId: Int) {DeleteMediaListEntry(id: \$entryId){ deleted }}",
"variables": {
"entryId": entryId,
},
};
var response = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query),
);
print(response.body);
}
@override
void deleteUserManga(int mediaId) async {
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query1 = {
"query": "query(\$mediaId:Int){ MediaList(mediaId:\$mediaId){ id } }",
"variables": {
"mediaId": mediaId,
},
};
var response1 = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query1),
);
int entryId = jsonDecode(response1.body)["data"]["MediaList"]["id"];
Map<String, dynamic> query = {
"query":
"mutation (\$entryId: Int) {DeleteMediaListEntry(id: \$entryId){ deleted }}",
"variables": {
"entryId": entryId,
},
};
var response = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query),
);
print(response.body);
}
@override
void setUserMangaInfo(int mediaId, Map<String, String> receivedQuery,
{MangaModel? mangaModel}) async {
var url = Uri.parse(constants.anilistEndpoint);
Map<String, dynamic> query = {
"query":
"mutation (\$mediaId: Int, \$status: MediaListStatus, \$score: Float, \$progress: Int, \$startedAt: FuzzyDateInput, \$completedAt: FuzzyDateInput) { SaveMediaListEntry(mediaId: \$mediaId, status: \$status, score: \$score, progress: \$progress, startedAt: \$startedAt, completedAt: \$completedAt) { mediaId status score progress startedAt { year month day } completedAt { year month day } } } ",
"variables": {
"mediaId": mediaId,
"status": receivedQuery["status"],
"score": double.parse(receivedQuery["score"]!),
"progress": int.parse(receivedQuery["progress"]!),
"startedAt": {
"day": receivedQuery["startDateDay"],
"month": receivedQuery["startDateMonth"],
"year": receivedQuery["startDateYear"]
},
"completedAt": {
"day": receivedQuery["endDateDay"],
"month": receivedQuery["endDateMonth"],
"year": receivedQuery["endDateYear"]
},
},
};
var response = await http.post(
url,
headers: {
"Authorization": "Bearer ${constants.accessToken}",
"Content-Type": "application/json",
},
body: json.encode(query),
);
print(response.body);
}
}

View File

@@ -1,112 +0,0 @@
import 'package:unyo/util/constants.dart';
class AnimeModel {
AnimeModel({
required this.id,
this.idMal,
required this.userPreferedTitle,
this.englishTitle,
this.japaneseTitle,
required this.coverImage,
required this.bannerImage,
required this.startDate,
required this.endDate,
required this.type,
required this.status,
required this.averageScore,
required this.episodes,
required this.duration,
required this.description,
required this.format,
this.currentEpisode,
});
int id;
int? idMal;
String? userPreferedTitle;
String? englishTitle;
String? japaneseTitle;
String? coverImage;
String? bannerImage;
String? startDate;
String? endDate;
String? type;
String? status;
String? description;
String? format;
int? averageScore;
int? episodes;
int? currentEpisode;
int? duration;
factory AnimeModel.fromJson(Map<String, dynamic> json) {
return AnimeModel(
id: json['id'],
idMal: json['idMal'],
userPreferedTitle: json["title"]["userPreferred"],
japaneseTitle: json['title']['romaji'],
englishTitle: json['title']['english'],
coverImage: json['coverImage']['large'],
bannerImage: json['bannerImage'],
startDate:
"${json["startDate"]["day"]}/${json["startDate"]["month"]}/${json["startDate"]["year"]}",
endDate:
"${json["endDate"]["day"]}/${json["endDate"]["month"]}/${json["endDate"]["year"]}",
type: json['type'],
status: json['status'],
description: json['description'],
format: json['format'],
averageScore: json['averageScore'],
episodes: json['episodes'],
currentEpisode: json['currentEpisode'],
duration: json['duration'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'malId': idMal,
'userPreferedTitle': userPreferedTitle,
'englishTitle': englishTitle,
'japaneseTitle': japaneseTitle,
'coverImage': coverImage,
'bannerImage': bannerImage,
'startDate': startDate,
'endDate': endDate,
'type': type,
'status': status,
'description': description,
'format': format,
'averageScore': averageScore,
'episodes': episodes,
'currentEpisode': currentEpisode,
'duration': duration,
};
}
/// Returns the first non-empty string in [candidates], or `''` if none.
String _firstNonEmpty(List<String?> candidates) {
for (final s in candidates) {
if (s?.isNotEmpty ?? false) return s ?? "";
}
return '';
}
String getDefaultTitle() {
final defaultTitleType = prefs.getInt("default_title_type") ?? 0;
final orders = <List<String?>>[
[userPreferedTitle, englishTitle, japaneseTitle], // 0
[englishTitle, userPreferedTitle, japaneseTitle], // 1
[japaneseTitle, userPreferedTitle, englishTitle], // 2
];
return _firstNonEmpty(orders[defaultTitleType]);
}
@override
String toString() {
return "$id $userPreferedTitle";
}
}

View File

@@ -1,221 +0,0 @@
import 'package:unyo/models/models.dart';
import 'package:unyo/util/constants.dart';
class LocalUserModel implements UserModel {
@override
String? avatarImage;
@override
String? bannerImage;
@override
int? userId;
@override
String? userName;
LocalUserModel(
{this.avatarImage, this.bannerImage, this.userName, this.userId});
@override
void deleteUserAnime(int mediaId) {
prefs.box.put(mediaId, null);
}
@override
void deleteUserManga(int mediaId) {
prefs.box.put(mediaId, null);
}
@override
Future<Map<String, List<AnimeModel>>> getAllUserAnimeLists() async {
return (prefs.box.get("allUserAnimeLists") as Map<dynamic, dynamic>?)?.map(
(key, value) =>
MapEntry(key as String, (value as List).cast<AnimeModel>())) ??
{};
}
@override
Future<Map<String, List<MangaModel>>> getAllUserMangaLists() async {
return (prefs.box.get("allUserMangaLists") as Map<dynamic, dynamic>?)?.map(
(key, value) =>
MapEntry(key as String, (value as List).cast<MangaModel>())) ??
{};
}
@override
Future<UserMediaModel?> getUserAnimeInfo(int mediaId) async {
return prefs.box.get("userAnimeInfo-$mediaId") as UserMediaModel? ??
UserMediaModel(
score: 0,
progress: 0,
repeat: null,
priority: null,
status: "NOT SET",
startDate: null,
endDate: null);
}
@override
Future<List<AnimeModel>> getUserAnimeLists(String listName) async {
return (prefs.box.get("allUserAnimeLists")?[listName] as List<dynamic>?)
?.map((e) => e as AnimeModel)
.toList() ??
[];
}
@override
Future<String> getUserAvatarImageUrl() async {
// return prefs.box.get("avatarImageUrl") as String;
avatarImage ??= "https://i.imgur.com/EKtChtm.png";
return "https://i.imgur.com/EKtChtm.png";
}
@override
Future<UserMediaModel?> getUserMangaInfo(int mediaId) async {
return prefs.box.get("userMangaInfo-$mediaId") as UserMediaModel? ??
UserMediaModel(
score: 0,
progress: 0,
repeat: null,
priority: null,
status: "NOT SET",
startDate: null,
endDate: null);
}
@override
Future<List<MangaModel>> getUserMangaLists(String listName) async {
return (prefs.box.get("allUserMangaLists")?[listName] as List<dynamic>?)
?.map((e) => e as MangaModel)
.toList() ??
[];
}
@override
Future<List<String>> getUserNameAndId() async {
if (userName != null && userId != null) {
return [userName!, userId!.toString()];
}
userName = prefs.box.get("userName") as String?;
userId = prefs.box.get("userId") as int?;
return [prefs.box.get("userName") as String, ""];
}
@override
Future<String> getUserbannerImageUrl() async {
// return prefs.box.get("userBannerImage") as String;
bannerImage ??= "https://i.imgur.com/x6TGK1x.png";
return "https://i.imgur.com/x6TGK1x.png";
}
@override
void setUserAnimeInfo(int mediaId, Map<String, String> receivedQuery,
{AnimeModel? animeModel}) {
UserMediaModel userMediaModel = UserMediaModel(
score: double.parse(receivedQuery["score"]!),
progress: int.parse(receivedQuery["progress"]!),
repeat: null,
priority: null,
status: receivedQuery["status"],
startDate:
"${receivedQuery["startDateDay"]}/${receivedQuery["startDateMonth"]}/${receivedQuery["startDateYear"]}",
endDate:
"${receivedQuery["endDateDay"]}/${receivedQuery["endDateMonth"]}/${receivedQuery["endDateYear"]}",
);
prefs.box.put("userAnimeInfo-$mediaId", userMediaModel);
Map<String, List<AnimeModel>>? userAnimeLists = (prefs.box
.get("allUserAnimeLists") as Map<dynamic, dynamic>?)
?.map((key, value) =>
MapEntry(key as String, (value as List).cast<AnimeModel>())) ??
{};
for (var animeModelList in userAnimeLists.values) {
animeModelList.remove(animeModel);
}
switch (receivedQuery["status"]) {
case "CURRENT":
// if (userAnimeLists["Watching"] == null){
// userAnimeLists["Watching"] = [];
// }
userAnimeLists["Watching"] ??= [];
userAnimeLists["Watching"]!.add(animeModel!);
break;
case "COMPLETED":
userAnimeLists["Completed"] ??= [];
userAnimeLists["Completed"]!.add(animeModel!);
break;
case "PLANNING":
userAnimeLists["Planning"] ??= [];
userAnimeLists["Planning"]!.add(animeModel!);
break;
case "PAUSED":
userAnimeLists["Paused"] ??= [];
userAnimeLists["Paused"]!.add(animeModel!);
break;
case "DROPPED":
userAnimeLists["Dropped"] ??= [];
userAnimeLists["Dropped"]!.add(animeModel!);
break;
default:
}
prefs.box.put("allUserAnimeLists", userAnimeLists);
}
@override
void setUserMangaInfo(int mediaId, Map<String, String> receivedQuery,
{MangaModel? mangaModel}) {
UserMediaModel userMediaModel = UserMediaModel(
score: double.parse(receivedQuery["score"]!),
progress: int.parse(receivedQuery["progress"]!),
repeat: null,
priority: null,
status: receivedQuery["status"],
startDate:
"${receivedQuery["startDateDay"]}/${receivedQuery["startDateMonth"]}/${receivedQuery["startDateYear"]}",
endDate:
"${receivedQuery["endDateDay"]}/${receivedQuery["endDateMonth"]}/${receivedQuery["endDateYear"]}",
);
prefs.box.put("userMangaInfo-$mediaId", userMediaModel);
Map<String, List<MangaModel>>? userMangaLists = (prefs.box
.get("allUserMangaLists") as Map<dynamic, dynamic>?)
?.map((key, value) =>
MapEntry(key as String, (value as List).cast<MangaModel>())) ??
{};
for (var mangaModelList in userMangaLists.values) {
mangaModelList.remove(mangaModel!);
}
switch (receivedQuery["status"]) {
case "CURRENT":
// if (userMangaLists["Watching"] == null){
// userMangaLists["Watching"] = [];
// }
print("added");
userMangaLists["Reading"] ??= [];
userMangaLists["Reading"]!.add(mangaModel!);
print(userMangaLists["Reading"]);
break;
case "COMPLETED":
userMangaLists["Completed"] ??= [];
userMangaLists["Completed"]!.add(mangaModel!);
break;
case "PLANNING":
userMangaLists["Planning"] ??= [];
userMangaLists["Planning"]!.add(mangaModel!);
break;
case "PAUSED":
userMangaLists["Paused"] ??= [];
userMangaLists["Paused"]!.add(mangaModel!);
break;
case "DROPPED":
userMangaLists["Dropped"] ??= [];
userMangaLists["Dropped"]!.add(mangaModel!);
break;
default:
}
prefs.box.put("allUserMangaLists", userMangaLists);
}
}

View File

@@ -1,111 +0,0 @@
import 'package:unyo/util/constants.dart';
class MangaModel {
MangaModel({
required this.id,
required this.idMal,
required this.userPreferedTitle,
required this.englishTitle,
required this.japaneseTitle,
required this.coverImage,
required this.bannerImage,
required this.startDate,
required this.endDate,
required this.type,
required this.status,
required this.averageScore,
required this.chapters,
required this.duration,
required this.description,
required this.format,
this.currentEpisode,
});
int id;
int? idMal;
String? userPreferedTitle;
String? englishTitle;
String? japaneseTitle;
String? coverImage;
String? bannerImage;
String? startDate;
String? endDate;
String? type;
String? status;
String? description;
String? format;
int? averageScore;
int? chapters;
int? currentEpisode;
int? duration;
factory MangaModel.fromJson(Map<String, dynamic> json) {
return MangaModel(
id: json['id'],
idMal: json['idMal'],
userPreferedTitle: json["title"]["userPreferred"],
japaneseTitle: json['title']['romaji'],
englishTitle: json['title']['english'],
coverImage: json['coverImage']['large'],
bannerImage: json['bannerImage'],
startDate:
"${json["startDate"]["day"]}/${json["startDate"]["month"]}/${json["startDate"]["year"]}",
endDate:
"${json["endDate"]["day"]}/${json["endDate"]["month"]}/${json["endDate"]["year"]}",
type: json['type'],
status: json['status'],
description: json['description'],
format: json['format'],
averageScore: json['averageScore'],
chapters: json['chapters'],
currentEpisode: json['currentEpisode'],
duration: json['duration'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'malId': idMal,
'userPreferedTitle': userPreferedTitle,
'englishTitle': englishTitle,
'japaneseTitle': japaneseTitle,
'coverImage': coverImage,
'bannerImage': bannerImage,
'startDate': startDate,
'endDate': endDate,
'type': type,
'status': status,
'description': description,
'format': format,
'averageScore': averageScore,
'chapters': chapters,
'currentEpisode': currentEpisode,
'duration': duration,
};
}
/// Returns the first non-empty string in [candidates], or `''` if none.
String _firstNonEmpty(List<String?> candidates) {
for (final s in candidates) {
if (s?.isNotEmpty ?? false) return s ?? "";
}
return '';
}
String getDefaultTitle() {
final defaultTitleType = prefs.getInt("default_title_type") ?? 0;
// Define the three titleorderings
final orders = <List<String?>>[
[userPreferedTitle, englishTitle, japaneseTitle], // 0
[englishTitle, userPreferedTitle, japaneseTitle], // 1
[japaneseTitle, userPreferedTitle, englishTitle], // 2
];
return _firstNonEmpty(orders[defaultTitleType]);
}
@override
String toString() {
return "$id $userPreferedTitle";
}
}

View File

@@ -1,37 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
const String endpoint = "https://api.ani.zip/mappings?anilist_id=";
class MediaContentModel {
MediaContentModel({required this.anilistId});
void init() async{
var url = Uri.parse("$endpoint$anilistId");
var response = await http.get(url);
if (response.statusCode != 200){
if (attempt < 5){
}
}
Map<String, dynamic> jsonResponse = json.decode(response.body);
Map<String, dynamic> episodes = jsonResponse["episodes"];
imageUrls = episodes.values.map((e) => e["image"] as String?).toList();
titles = episodes.values.map((e) => e["title"]["en"] as String?).toList();
fanart = jsonResponse["images"]?[2]["url"];
banner = jsonResponse["images"]?[0]["url"];
// if (titles != null){
// titles!.addAll(List.filled(10, null));
// }
// if (imageUrls != null){
// imageUrls!.addAll(List.filled(10, null));
// }
}
final int anilistId;
String? fanart;
String? banner;
int attempt = 0;
List<String?>? titles;
List<String?>? imageUrls;
}

View File

@@ -1,8 +0,0 @@
export 'anime_model.dart';
export 'manga_model.dart';
export 'user_media_model.dart';
export 'media_content_model.dart';
export 'user_model.dart';
export 'anilist_user_model.dart';
export 'preferences_model.dart';
export 'local_user_model.dart';

View File

@@ -1,122 +0,0 @@
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:unyo/models/models.dart';
import 'package:unyo/util/utils.dart';
class PreferencesModel {
late SharedPreferences sharedPreferences;
late Box box;
String? userName;
Future<void> init() async {
sharedPreferences = await SharedPreferences.getInstance();
userName = sharedPreferences.getString("user_logged");
if (userName != null && userName != "null") {
box = await Hive.openBox(userName!);
var userBox = await Hive.openBox("users");
List<dynamic> anilistSavedUsers = userBox.get("anilistUsers") ?? [];
List<dynamic> localSavedUsers = userBox.get("localUsers") ?? [];
List<UserModel> savedUsers =
List.from(anilistSavedUsers.map((e) => e as AnilistUserModel));
savedUsers.addAll(localSavedUsers.map((e) => e as LocalUserModel));
users = savedUsers;
}
String? version = sharedPreferences.getString("version");
if(version == null || version != currentVersion){
print("New version, updating api");
processManager.downloadNewCore();
}
sharedPreferences.setString("version", currentVersion);
}
void getUsers(void Function(void Function()) setState) async {
var userBox = await Hive.openBox("users");
List<dynamic> anilistSavedUsers = userBox.get("anilistUsers") ?? [];
List<dynamic> localSavedUsers = userBox.get("localUsers") ?? [];
List<UserModel> savedUsers =
List.from(anilistSavedUsers.map((e) => e as AnilistUserModel));
savedUsers.addAll(localSavedUsers.map((e) => e as LocalUserModel));
print("saved users number: ${savedUsers.length}");
setState(() {
users = savedUsers;
});
}
void saveUser(UserModel user) async {
print("Saving user: ${user.userName}");
var userBox = await Hive.openBox("users");
List<dynamic> anilistSavedUsers = userBox.get("anilistUsers") ?? [];
List<dynamic> localSavedUsers = userBox.get("localUsers") ?? [];
List<UserModel> savedUsers =
List.from(anilistSavedUsers.map((e) => e as AnilistUserModel));
savedUsers.addAll(localSavedUsers.map((e) => e as LocalUserModel));
if (savedUsers
.where((listUser) => listUser.userName == user.userName)
.isEmpty) {
if (user is AnilistUserModel) {
anilistSavedUsers.add(user);
} else if (user is LocalUserModel) {
localSavedUsers.add(user);
}
}else{
UserModel oldUser = savedUsers.where((listUser) => listUser.userName == user.userName).toList()[0];
if (user.avatarImage == oldUser.avatarImage) return;
if (user is AnilistUserModel){
anilistSavedUsers.remove(oldUser);
anilistSavedUsers.add(user);
}else if(user is LocalUserModel){
localSavedUsers.remove(oldUser);
localSavedUsers.add(user);
}
}
userBox.put("anilistUsers", anilistSavedUsers);
userBox.put("localUsers", localSavedUsers);
users = savedUsers;
}
Future<void> loginUser(String user) async {
print("Logging user: $user");
sharedPreferences.setString("user_logged", user);
userName = user;
box = await Hive.openBox(user);
}
bool isUserLogged() {
return userName != null && userName != "null";
}
void logOut() {
userName = null;
sharedPreferences.setString("user_logged", "null");
}
String? getString(String key) {
return box.get(key) as String?;
}
void setString(String key, String value) {
box.put(key, value);
}
int? getInt(String key) {
return box.get(key) as int?;
}
void setInt(String key, int value) {
box.put(key, value);
}
bool? getBool(String key) {
return box.get(key) as bool?;
}
void setBool(String key, bool value) {
box.put(key, value);
}
}

View File

@@ -1,19 +0,0 @@
class UserMediaModel{
UserMediaModel({
required this.score,
required this.progress,
required this.repeat,
required this.priority,
required this.status,
required this.startDate,
required this.endDate,
});
num? score;
num? progress;
int? repeat;
int? priority;
String? status;
String? startDate;
String? endDate;
}

View File

@@ -1,37 +0,0 @@
import 'package:unyo/models/models.dart';
abstract class UserModel {
String? avatarImage;
String? bannerImage;
String? userName;
int? userId;
//user info
Future<List<String>> getUserNameAndId();
Future<String> getUserbannerImageUrl();
Future<String> getUserAvatarImageUrl();
//anime info
Future<List<AnimeModel>> getUserAnimeLists(String listName);
Future<Map<String, List<AnimeModel>>> getAllUserAnimeLists();
Future<UserMediaModel?> getUserAnimeInfo(int mediaId);
//manga info
Future<List<MangaModel>> getUserMangaLists(String listName);
Future<Map<String, List<MangaModel>>> getAllUserMangaLists();
Future<UserMediaModel?> getUserMangaInfo(int mediaId);
//anime setters
void setUserAnimeInfo(int mediaId, Map<String, String> receivedQuery,
{AnimeModel? animeModel});
void deleteUserAnime(int mediaId);
//manga setters
void setUserMangaInfo(int mediaId, Map<String, String> receivedQuery,
{MangaModel? mangaModel});
void deleteUserManga(int mediaId);
}

View File

@@ -1,55 +0,0 @@
import 'package:animated_snack_bar/animated_snack_bar.dart';
import 'package:flutter/cupertino.dart';
class NotificationManager {
// Singleton instance
static final NotificationManager _instance = NotificationManager._internal();
// Private constructor
NotificationManager._internal();
// Factory constructor to return the singleton instance
factory NotificationManager() {
return _instance;
}
/// Shows a success notification with the given message.
void showSuccessNotification(
BuildContext context, String message, DesktopSnackBarPosition position) {
AnimatedSnackBar.material(
message,
type: AnimatedSnackBarType.success,
desktopSnackBarPosition: position,
).show(context);
}
/// Shows an information notification with the given message.
void showInfoNotification(
BuildContext context, String message, DesktopSnackBarPosition position) {
AnimatedSnackBar.material(
message,
type: AnimatedSnackBarType.info,
desktopSnackBarPosition: position,
).show(context);
}
/// Shows a warning notification with the given message.
void showWarningNotification(
BuildContext context, String message, DesktopSnackBarPosition position) {
AnimatedSnackBar.material(
message,
type: AnimatedSnackBarType.warning,
desktopSnackBarPosition: position,
).show(context);
}
/// Shows an error notification with the given message.
void showErrorNotification(
BuildContext context, String message, DesktopSnackBarPosition position) {
AnimatedSnackBar.material(
message,
type: AnimatedSnackBarType.error,
desktopSnackBarPosition: position,
).show(context);
}
}

View File

@@ -0,0 +1,57 @@
// Flutter dependencies
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Internal dependencies
import 'package:unyo/application/cubits/home_cubit.dart';
import 'package:unyo/application/states/home_state.dart';
import 'package:unyo/core/di/locator.dart';
import 'package:unyo/core/services/effects/app_effect_handler.dart';
import 'package:unyo/presentation/widgets/text/texts.dart';
@RoutePage()
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<HomeCubit>(),
child: _HomeListener(),
);
}
}
class _HomeListener extends StatelessWidget {
const _HomeListener({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<HomeCubit, HomeState>(
listener: (context, state) {
if (state.effects.isNotEmpty) {
sl<AppEffectHandler>().handleEffects(
context,
state.effects,
context.read<HomeCubit>().clearEffects,
);
}
},
child: _HomeView(),
);
}
}
class _HomeView extends StatelessWidget {
const _HomeView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
return Column(children: []);
},
);
}
}

View File

@@ -0,0 +1,106 @@
// Flutter dependencies
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
//External dependencies
import 'package:auto_route/auto_route.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
//Internal dependencies
import 'package:unyo/application/cubits/login_cubit.dart';
import 'package:unyo/application/states/login_state.dart';
import 'package:unyo/core/services/effects/app_effect_handler.dart';
import 'package:unyo/presentation/widgets/styled/styled.dart';
import 'package:unyo/presentation/widgets/text/texts.dart';
import 'package:unyo/core/di/locator.dart';
@RoutePage()
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<LoginCubit>(),
child: _LoginListener(),
);
}
}
class _LoginListener extends StatelessWidget {
const _LoginListener({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state.effects.isNotEmpty) {
sl<AppEffectHandler>().handleEffects(
context,
state.effects,
context.read<LoginCubit>().clearEffects,
);
}
},
child: _LoginView(),
);
}
}
class _LoginView extends StatefulWidget {
const _LoginView({super.key});
@override
State<_LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<_LoginView> {
late final TextEditingController loginTextFieldController;
@override
void initState() {
super.initState();
loginTextFieldController = TextEditingController();
context.read<LoginCubit>().fetchAllUsers();
}
@override
void dispose() {
super.dispose();
loginTextFieldController.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextDisplayLarge(text: context.tr("select_user")),
SizedBox(height: 0.07.sh),
SizedBox(
height: 0.35.sh,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
...state.availableUsers.map(
(user) => UserAvatar(user: user, onPressed: (){}),
),
AddUserAvatar(onPressed: () {}),
// Display existing users
],
),
),
),
],
);
},
);
}
}

View File

@@ -0,0 +1,18 @@
//Flutter dependencies
import 'package:flutter/material.dart';
//External dependencies
import 'package:auto_route/auto_route.dart';
@RoutePage()
class RootScaffoldScreen extends StatelessWidget {
const RootScaffoldScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// Back
body: AutoRouter(),
);
}
}

View File

@@ -0,0 +1,54 @@
// External dependencies
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// Internal dependencies
import 'package:unyo/config/config.dart' as config;
import 'package:unyo/presentation/widgets/styled/hover_animated_container.dart';
import 'package:unyo/presentation/widgets/text/text_headline_large.dart';
class AddUserAvatar extends StatelessWidget {
final void Function() onPressed;
const AddUserAvatar({super.key, required this.onPressed});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 25.0.w),
child: Column(
children: [
HoverAnimatedContainer(
width: 0.25.sh,
height: 0.25.sh,
hoverWidth: 0.27.sh,
hoverHeight: 0.27.sh,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(140.r),
border: Border.all(color: Colors.white, width: 3.w),
),
hoverDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(150.r),
border: Border.all(color: Colors.white, width: 6.w),
),
child: InkWell(
borderRadius: BorderRadius.circular(140.r),
onTap: onPressed,
child: CircleAvatar(
radius: 0.125.sh,
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(config.plusImageUrl),
),
),
),
SizedBox(height: 10.h),
Align(
alignment: Alignment.centerLeft,
child: TextHeadlineLarge(text: context.tr("add_account")),
),
],
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class HoverAnimatedContainer extends StatefulWidget {
final double width;
final double hoverWidth;
final double height;
final double hoverHeight;
final Curve curve;
final Decoration decoration;
final Decoration hoverDecoration;
final SystemMouseCursor cursor;
final Widget child;
final EdgeInsetsGeometry margin;
final EdgeInsetsGeometry hoverMargin;
final Duration duration;
final Alignment alignment;
const HoverAnimatedContainer({
super.key,
required this.width,
required this.hoverWidth,
required this.height,
required this.hoverHeight,
required this.decoration,
required this.hoverDecoration,
required this.child,
this.curve = Curves.linear,
this.cursor = SystemMouseCursors.basic,
this.margin = EdgeInsets.zero,
this.hoverMargin = EdgeInsets.zero,
this.duration = const Duration(milliseconds: 200),
this.alignment = Alignment.center,
});
@override
State<HoverAnimatedContainer> createState() => _HoverAnimatedContainerState();
}
class _HoverAnimatedContainerState extends State<HoverAnimatedContainer> {
bool hovering = false;
void _onHover(bool value) => setState(() => hovering = value);
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => _onHover(true),
onExit: (_) => _onHover(false),
cursor: widget.cursor,
child: AnimatedContainer(
duration: widget.duration,
curve: widget.curve,
width: hovering ? widget.hoverWidth : widget.width,
height: hovering ? widget.hoverHeight : widget.height,
decoration: hovering ? widget.hoverDecoration : widget.decoration,
margin: hovering ? widget.hoverMargin : widget.margin,
alignment: widget.alignment,
child: widget.child,
),
);
}
}

View File

@@ -0,0 +1,3 @@
export 'hover_animated_container.dart';
export 'user_avatar.dart';
export 'add_user_avatar.dart';

View File

@@ -0,0 +1,53 @@
// External dependencies
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// Internal dependencies
import 'package:unyo/domain/entities/user.dart';
import 'package:unyo/presentation/widgets/styled/hover_animated_container.dart';
import 'package:unyo/presentation/widgets/text/text_headline_large.dart';
class UserAvatar extends StatelessWidget {
final User user;
final void Function() onPressed;
const UserAvatar({super.key, required this.user, required this.onPressed});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 25.0.w),
child: Column(
children: [
HoverAnimatedContainer(
width: 0.25.sh,
height: 0.25.sh,
hoverWidth: 0.27.sh,
hoverHeight: 0.27.sh,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(140.r),
border: Border.all(color: Colors.white, width: 3.w),
),
hoverDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(250.r),
border: Border.all(color: Colors.white, width: 6.w),
),
child: InkWell(
borderRadius: BorderRadius.circular(140.r),
onTap: onPressed,
child: CircleAvatar(
radius: 0.125.sh,
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(user.avatarImage),
),
),
),
SizedBox(height: 10.h),
Align(
alignment: Alignment.centerLeft,
child: TextHeadlineLarge(text: user.name),
),
],
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextBodyLarge extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextBodyLarge({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodyLarge;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextBodyMedium extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextBodyMedium({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodyMedium;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextBodySmall extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextBodySmall({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodySmall;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextDisplayLarge extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextDisplayLarge({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.displayLarge;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextDisplayMedium extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextDisplayMedium({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.displayMedium;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextDisplaySmall extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextDisplaySmall({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.displaySmall;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextHeadlineLarge extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextHeadlineLarge({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.headlineLarge;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextHeadlineMedium extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextHeadlineMedium({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.headlineMedium;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextHeadlineSmall extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextHeadlineSmall({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.headlineSmall;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextLabelLarge extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextLabelLarge({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.labelLarge;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextLabelMedium extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextLabelMedium({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.labelMedium;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class TextLabelSmall extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
final TextStyle? style;
const TextLabelSmall({
super.key,
required this.text,
this.textAlign,
this.maxLines,
this.overflow,
this.style,
});
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.labelSmall;
return Text(
text,
style: style?.merge(baseStyle) ?? baseStyle,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}

Some files were not shown because too many files have changed in this diff Show More