Skip to content

Commit

Permalink
Add account_view.dart
Browse files Browse the repository at this point in the history
  • Loading branch information
Ziedelth committed Apr 21, 2024
1 parent 2ae45d5 commit 916c59d
Show file tree
Hide file tree
Showing 18 changed files with 1,183 additions and 76 deletions.
Binary file added assets/avatar.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 43 additions & 6 deletions lib/components/anime_component.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:application/components/card_component.dart';
import 'package:application/components/followed_stream_builder.dart';
import 'package:application/components/image_component.dart';
import 'package:application/components/lang_type_component.dart';
import 'package:application/dtos/anime_dto.dart';
Expand Down Expand Up @@ -27,13 +28,49 @@ class AnimeComponent extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ImageComponent(
uuid: anime.uuid,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
SizedBox(
height: 280,
child: Stack(
children: [
ImageComponent(
uuid: anime.uuid,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
FollowedStreamBuilder(
anime: anime,
builder: (context, containsAnime, _) {
if (!containsAnime) {
return const SizedBox.shrink();
}

return Positioned(
top: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
bottomLeft: Radius.circular(16),
),
),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.bookmark,
color: containsAnime ? bookmarkColor : null,
size: 16,
),
),
),
);
},
),
],
),
),
const SizedBox(height: 4),
Padding(
Expand Down
2 changes: 1 addition & 1 deletion lib/components/episodes/episode_action_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class EpisodeActionBar extends StatelessWidget {
),
),
),
const WatchlistButton(),
WatchlistButton(episode: episode),
],
);
}
Expand Down
101 changes: 65 additions & 36 deletions lib/components/episodes/watchlist_button.dart
Original file line number Diff line number Diff line change
@@ -1,52 +1,81 @@
import 'package:application/components/followed_stream_builder.dart';
import 'package:application/controllers/member_controller.dart';
import 'package:application/dtos/anime_dto.dart';
import 'package:application/dtos/episode_mapping_dto.dart';
import 'package:flutter/material.dart';
import 'package:like_button/like_button.dart';
import 'package:vibration/vibration.dart';

const _bookmarkColor = Colors.yellow;

class WatchlistButton extends StatelessWidget {
const WatchlistButton({super.key});
final EpisodeMappingDto? episode;
final AnimeDto? anime;

const WatchlistButton({
super.key,
this.episode,
this.anime,
});

@override
Widget build(BuildContext context) {
return LikeButton(
likeBuilder: (isLiked) {
return Icon(
isLiked ? Icons.bookmark : Icons.bookmark_border,
color: isLiked ? _bookmarkColor : null,
);
},
circleColor: const CircleColor(
start: _bookmarkColor,
end: _bookmarkColor,
),
bubblesColor: const BubblesColor(
dotPrimaryColor: _bookmarkColor,
dotSecondaryColor: _bookmarkColor,
),
onTap: (isLiked) async {
if (!isLiked) {
// Vibration.vibrate(duration: 100);
Vibration.vibrate(pattern: [0, 50, 125, 50, 125, 50]);
return FollowedStreamBuilder(
builder: (context, containsAnime, containsEpisode) {
return LikeButton(
isLiked: containsAnime || containsEpisode,
likeBuilder: (isLiked) {
return Icon(
isLiked ? Icons.bookmark : Icons.bookmark_border,
color: isLiked ? _bookmarkColor : null,
);
},
circleColor: const CircleColor(
start: _bookmarkColor,
end: _bookmarkColor,
),
bubblesColor: const BubblesColor(
dotPrimaryColor: _bookmarkColor,
dotSecondaryColor: _bookmarkColor,
),
onTap: (isLiked) async {
if (!isLiked) {
if (anime != null) {
MemberController.instance.followAnime(anime!);
} else {
MemberController.instance.followEpisode(episode!);
}

// Create snackbar
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bookmark),
SizedBox(width: 8),
Text('Bookmarked'),
],
),
duration: Duration(seconds: 2),
),
);
}
Vibration.vibrate(pattern: [0, 50, 125, 50, 125, 50]);

return !isLiked;
// Create snackbar
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bookmark),
SizedBox(width: 8),
Text('Bookmarked'),
],
),
duration: Duration(seconds: 2),
),
);
} else {
if (anime != null) {
MemberController.instance.unfollowAnime(anime!);
} else {
MemberController.instance.unfollowEpisode(episode!);
}
}

return !isLiked;
},
);
},
anime: anime,
episode: episode,
);
}
}
35 changes: 35 additions & 0 deletions lib/components/followed_stream_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:application/controllers/member_controller.dart';
import 'package:application/dtos/anime_dto.dart';
import 'package:application/dtos/episode_mapping_dto.dart';
import 'package:flutter/material.dart';

class FollowedStreamBuilder extends StatelessWidget {
final Widget Function(BuildContext, bool, bool) builder;
final AnimeDto? anime;
final EpisodeMappingDto? episode;

const FollowedStreamBuilder({
super.key,
required this.builder,
this.anime,
this.episode,
});

@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: MemberController.instance.streamController.stream,
initialData: null,
builder: (context, snapshot) {
final memberDto = MemberController.instance.member;

final containsAnime =
anime != null && memberDto!.followedAnimes.contains(anime?.uuid);
final containsEpisode = episode != null &&
memberDto!.followedEpisodes.contains(episode?.uuid);

return builder(context, containsAnime, containsEpisode);
},
);
}
}
4 changes: 3 additions & 1 deletion lib/controllers/anime_details_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import 'package:application/utils/http_request.dart';
import 'package:flutter/material.dart';

enum Sort {
oldest(value: 'sort=season,episodeType,number&desc=episodeType'),
oldest(
value: 'sort=releaseDateTime,season,episodeType,number&desc=episodeType',
),
newest(
value:
'sort=releaseDateTime,season,episodeType,number&desc=releaseDateTime,season,episodeType,number',
Expand Down
12 changes: 11 additions & 1 deletion lib/controllers/anime_weekly_controller.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:application/controllers/member_controller.dart';
import 'package:application/dtos/week_day_dto.dart';
import 'package:application/utils/http_request.dart';
import 'package:flutter/material.dart';
Expand All @@ -10,6 +11,7 @@ class AnimeWeeklyController {
final scrollController = ScrollController();
final streamController = StreamController<List<WeekDayDto>>.broadcast();
bool isLoading = false;
bool memberMode = false;

Future<void> init() async {
weekDays.clear();
Expand Down Expand Up @@ -37,7 +39,15 @@ class AnimeWeeklyController {
isLoading = true;

try {
final json = await HttpRequest.instance.get<List>('/v1/animes/weekly');
String endpoint = '/v1/animes/weekly';
String? token = '';

if (memberMode) {
endpoint = '/v1/animes/member-weekly';
token = MemberController.instance.member!.token;
}

final json = await HttpRequest.instance.get<List>(endpoint, token: token);

weekDays.addAll(
json.map((e) => WeekDayDto.fromJson(e as Map<String, dynamic>)),
Expand Down
128 changes: 128 additions & 0 deletions lib/controllers/member_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'dart:async';
import 'dart:convert';

import 'package:application/dtos/anime_dto.dart';
import 'package:application/dtos/episode_mapping_dto.dart';
import 'package:application/dtos/member_dto.dart';
import 'package:application/utils/http_request.dart';
import 'package:shared_preferences/shared_preferences.dart';

class MemberController {
static MemberController instance = MemberController();
late final SharedPreferences _sharedPreferences;
final streamController = StreamController.broadcast();
String? identifier;
MemberDto? member;

Future<void> init() async {
_sharedPreferences = await SharedPreferences.getInstance();
identifier =
_sharedPreferences.getString('identifier') ?? 'test-private-identifier';
await login();
}

Future<void> login() async {
final response =
await HttpRequest().post('/v1/members/private-login', identifier);

if (response.statusCode != 200) {
throw Exception('Failed to login');
}

final json = jsonDecode(utf8.decode(response.bodyBytes));
member = MemberDto.fromJson(json);
}

Future<void> followAnime(AnimeDto anime) async {
final response = await HttpRequest().put(
'/v1/members/animes',
jsonEncode({'uuid': anime.uuid}),
token: member!.token,
);

if (response.statusCode != 200) {
throw Exception('Failed to follow anime');
}

member!.followedAnimes.add(anime.uuid);
streamController.add(null);
}

Future<void> unfollowAnime(AnimeDto anime) async {
final response = await HttpRequest().delete(
'/v1/members/animes',
jsonEncode({'uuid': anime.uuid}),
token: member!.token,
);

if (response.statusCode != 200) {
throw Exception('Failed to unfollow anime');
}

member!.followedAnimes.remove(anime.uuid);
streamController.add(null);
}

Future<void> followEpisode(EpisodeMappingDto episode) async {
if (!member!.followedAnimes.contains(episode.anime.uuid)) {
await followAnime(episode.anime);
}

final response = await HttpRequest().put(
'/v1/members/episodes',
jsonEncode({'uuid': episode.uuid}),
token: member!.token,
);

if (response.statusCode != 200) {
throw Exception('Failed to follow episode');
}

member!.followedEpisodes.add(episode.uuid);
member = member!
.copyWith(totalDuration: member!.totalDuration + episode.duration);
streamController.add(null);
}

Future<void> unfollowEpisode(EpisodeMappingDto episode) async {
final response = await HttpRequest().delete(
'/v1/members/episodes',
jsonEncode({'uuid': episode.uuid}),
token: member!.token,
);

if (response.statusCode != 200) {
throw Exception('Failed to unfollow episode');
}

member!.followedEpisodes.remove(episode.uuid);
member = member!
.copyWith(totalDuration: member!.totalDuration - episode.duration);
streamController.add(null);
}

String buildTotalDuration() {
final duration = Duration(seconds: member!.totalDuration);
// Build string like '1d 2h 3m 4s'
// If a value is 0, it is not included
final parts = <String>[];

if (duration.inDays > 0) {
parts.add('${duration.inDays}j');
}

if (duration.inHours > 0) {
parts.add('${duration.inHours % 24}h');
}

if (duration.inMinutes > 0) {
parts.add('${duration.inMinutes % 60}m');
}

if (duration.inSeconds > 0) {
parts.add('${duration.inSeconds % 60}s');
}

return parts.join(' ');
}
}
Loading

0 comments on commit 916c59d

Please sign in to comment.