Skip to content

Commit

Permalink
Rework album screen (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
JAicewizard authored Nov 29, 2022
1 parent cb794f9 commit 28a6de6
Show file tree
Hide file tree
Showing 16 changed files with 818 additions and 209 deletions.
139 changes: 139 additions & 0 deletions integration_test/album.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:mockito/mockito.dart';
import 'package:reaxit/api/exceptions.dart';
import 'package:reaxit/blocs.dart';
import 'package:reaxit/main.dart' as app;
import 'package:reaxit/models.dart';

import '../test/mocks.mocks.dart';

const imagelink1 =
'https://raw.githubusercontent.com/svthalia/Reaxit/3e3a74364f10cd8de14ac1f74de8a05aa6d00b28/assets/img/album_placeholder.png';

const imagelink2 =
'https://raw.githubusercontent.com/svthalia/Reaxit/3e3a74364f10cd8de14ac1f74de8a05aa6d00b28/assets/img/default-avatar.jpg';

const coverphoto1 = CoverPhoto(
0,
0,
true,
Photo(
imagelink1,
imagelink1,
imagelink1,
imagelink1,
),
);

const albumphoto1 = AlbumPhoto(
0,
0,
false,
Photo(
imagelink1,
imagelink1,
imagelink1,
imagelink1,
),
false,
0,
);

const albumphoto2 = AlbumPhoto(
0,
0,
false,
Photo(
imagelink2,
imagelink2,
imagelink2,
imagelink2,
),
false,
0,
);

WidgetTesterCallback getTestMethod(Album album) {
return (tester) async {
// Setup mock.
final api = MockApiRepository();
when(api.getAlbum(slug: album.slug)).thenAnswer(
(realInvocation) async => album,
);
final authCubit = MockAuthCubit();

throwOnMissingStub(
api,
exceptionBuilder: (_) {
throw ApiException.unknownError;
},
);

final streamController = StreamController<AuthState>.broadcast()
..stream.listen((state) {
when(authCubit.state).thenReturn(state);
})
..add(LoadingAuthState())
..add(LoggedInAuthState(apiRepository: api));

when(authCubit.load()).thenAnswer((_) => Future.value(null));
when(authCubit.stream).thenAnswer((_) => streamController.stream);

// Start app
app.testingMain(authCubit, '/albums/${album.slug}');
await tester.pumpAndSettle();
await Future.delayed(const Duration(seconds: 5));
await tester.pumpAndSettle();

for (AlbumPhoto photo in album.photos) {
//TODO: wait for https://github.com/flutter/flutter/issues/115479 to be fixed
expect(
find.image(
NetworkImage(photo.small),
),
findsWidgets,
);
}
expect(find.text(album.title.toUpperCase()), findsOneWidget);
};
}

void testAlbum() async {
final _ = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group(
'Album',
() {
testWidgets(
'able to load an album',
getTestMethod(
const Album.fromlist(
'1',
'mock',
false,
false,
coverphoto1,
[albumphoto1],
),
),
);
testWidgets(
'able to load an album2',
getTestMethod(
const Album.fromlist(
'1',
'MOcK2',
false,
false,
coverphoto1,
[albumphoto1, albumphoto2],
),
),
);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'package:reaxit/main.dart' as app;

import '../test/mocks.mocks.dart';

void main() {
void testLogin() {
// ignore: unused_local_variable
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

Expand All @@ -31,7 +31,7 @@ void main() {
when(authCubit.stream).thenAnswer((_) => streamController.stream);

// Start app.
app.testingMain(authCubit);
app.testingMain(authCubit, null);
await tester.pumpAndSettle();
await Future.delayed(const Duration(seconds: 5));
await tester.pumpAndSettle();
Expand Down Expand Up @@ -85,7 +85,7 @@ void main() {
when(authCubit.stream).thenAnswer((_) => streamController.stream);

// Start app.
app.testingMain(authCubit);
app.testingMain(authCubit, null);
await tester.pumpAndSettle();
await Future.delayed(const Duration(seconds: 5));
await tester.pumpAndSettle();
Expand Down Expand Up @@ -120,7 +120,7 @@ void main() {
when(authCubit.logOut()).thenAnswer((_) => Future.value(null));

// Start app.
app.testingMain(authCubit);
app.testingMain(authCubit, null);
await tester.pumpAndSettle();
await Future.delayed(const Duration(seconds: 5));
await tester.pumpAndSettle();
Expand Down
7 changes: 7 additions & 0 deletions integration_test/main_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'album.dart';
import 'login.dart';

void main() {
testLogin();
testAlbum();
}
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ PODS:
- GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30910.0, >= 2.30908.0)
- Flutter (1.0.0)
- flutter_secure_storage (3.3.1):
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_web_auth_2 (1.1.1):
- Flutter
Expand Down Expand Up @@ -177,7 +177,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 004915af170935e3a583faefd5f8bc851afc220f
FirebaseMessaging: cc9f40f5b7494680f3844d08e517e92aa4e8d9f7
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_web_auth_2: a1bc00762c408a8f80b72a538cd7ff5b601c3e71
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gallery_saver: 9fc173c9f4fcc48af53b2a9ebea1b643255be542
Expand Down
3 changes: 3 additions & 0 deletions lib/api/api_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ abstract class ApiRepository {
/// Get the [Album] with the `slug`.
Future<Album> getAlbum({required String slug});

/// Create or delete a like on the photo with the `id`.
Future<void> updateLiked(int id, bool liked);

/// Get a list of [ListAlbum]s.
///
/// Use `limit` and `offset` for pagination. [ListResponse.count] is the
Expand Down
16 changes: 14 additions & 2 deletions lib/api/concrexit_api_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ConcrexitApiRepository implements ApiRepository {
///
/// Translates exceptions that can be thrown by [oauth2.Client.send()],
/// and throws exceptions based on status codes. By default, all status codes
/// other than 200, 201 and 204 result in an [ApiException], but this can be
/// other than 200, 201, 203, and 204 result in an [ApiException], but this can be
/// overridden with `allowedStatusCodes`.
///
/// Can be called for example as:
Expand All @@ -81,7 +81,7 @@ class ConcrexitApiRepository implements ApiRepository {
/// ```
Future<Response> _handleExceptions(
Future<Response> Function() request, {
List<int> allowedStatusCodes = const [200, 201, 204],
List<int> allowedStatusCodes = const [200, 201, 202, 204],
}) async {
try {
final response = await request();
Expand Down Expand Up @@ -923,6 +923,18 @@ class ConcrexitApiRepository implements ApiRepository {
});
}

@override
Future<void> updateLiked(int pk, bool liked) async {
return sandbox(() async {
final uri = _uri(path: '/photos/photos/$pk/like/');
await _handleExceptions(
() => liked
? _client.post(uri, headers: _jsonHeader)
: _client.delete(uri, headers: _jsonHeader),
);
});
}

@override
Future<ListResponse<ListAlbum>> getAlbums({
String? search,
Expand Down
127 changes: 123 additions & 4 deletions lib/blocs/album_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,142 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';
import 'package:reaxit/api/api_repository.dart';
import 'package:reaxit/api/exceptions.dart';
import 'package:reaxit/blocs.dart';
import 'package:reaxit/models.dart';

typedef AlbumState = DetailState<Album>;

class AlbumCubit extends Cubit<AlbumState> {
class AlbumScreenState extends Equatable {
/// This can only be null when [isLoading] or [hasException] is true.
final Album? album;
final bool isOpen;
final int? initialGalleryIndex;

final String? message;
final bool isLoading;
bool get hasException => message != null;

@protected
const AlbumScreenState({
required this.album,
required this.isLoading,
required this.message,
this.isOpen = false,
this.initialGalleryIndex,
}) : assert(
album != null || isLoading || message != null,
'album can only be null when isLoading or hasException is true.',
),
assert(
isOpen || initialGalleryIndex == null,
'initialGalleryIndex can only be set when isOpen is true.',
),
assert(
initialGalleryIndex != null || !isOpen,
'initialGalleryIndex must be set when isOpen is true.',
),
assert(
!isOpen || album != null,
'album must be set when isOpen is true.',
);

@override
List<Object?> get props => [
album,
message,
isLoading,
isOpen,
initialGalleryIndex,
];

AlbumScreenState copyWith({
Album? album,
List<AdminEventRegistration>? registrations,
bool? isLoading,
String? message,
bool? isOpen,
int? initialGalleryIndex,
}) =>
AlbumScreenState(
album: album ?? this.album,
isLoading: isLoading ?? this.isLoading,
message: message ?? this.message,
isOpen: isOpen ?? this.isOpen,
initialGalleryIndex: (isOpen ?? this.isOpen)
? (initialGalleryIndex ?? this.initialGalleryIndex)
: null,
);

const AlbumScreenState.result(
{required this.album, required this.isOpen, this.initialGalleryIndex})
: message = null,
isLoading = false;

const AlbumScreenState.loading()
: message = null,
album = null,
isLoading = true,
isOpen = false,
initialGalleryIndex = null;

const AlbumScreenState.failure({required String this.message})
: album = null,
isLoading = false,
isOpen = false,
initialGalleryIndex = null;
}

class AlbumCubit extends Cubit<AlbumScreenState> {
final ApiRepository api;

AlbumCubit(this.api) : super(const AlbumState.loading());
AlbumCubit(this.api) : super(const AlbumScreenState.loading());

Future<void> updateLike({required bool liked, required int index}) async {
if (state.album == null) {
return;
}

// Emit expected state after (un)liking.
final oldphoto = state.album!.photos[index];
AlbumPhoto newphoto = oldphoto.copyWith(
liked: liked,
numLikes: oldphoto.numLikes + (liked ? 1 : -1),
);
List<AlbumPhoto> newphotos = [...state.album!.photos];
newphotos[index] = newphoto;
emit(AlbumScreenState.result(
album: state.album!.copyWith(photos: newphotos),
isOpen: state.isOpen,
initialGalleryIndex: state.initialGalleryIndex,
));

try {
await api.updateLiked(newphoto.pk, liked);
} on ApiException {
// Revert to state before (un)liking.
emit(state);
rethrow;
}
}

void openGallery(int index) {
if (state.album == null) return;
emit(state.copyWith(isOpen: true, initialGalleryIndex: index));
}

void closeGallery() {
emit(state.copyWith(isOpen: false));
}

Future<void> load(String slug) async {
emit(state.copyWith(isLoading: true));
try {
final album = await api.getAlbum(slug: slug);
emit(AlbumState.result(result: album));
emit(AlbumScreenState.result(album: album, isOpen: false));
} on ApiException catch (exception) {
emit(AlbumState.failure(
emit(AlbumScreenState.failure(
message: exception.getMessage(notFound: 'The album does not exist.'),
));
}
Expand Down
Loading

0 comments on commit 28a6de6

Please sign in to comment.