Skip to content

Commit 7ff06b2

Browse files
authored
Merge pull request #50 from flutter-news-app-full-source-code/enhance-content-management
Enhance content management
2 parents 260f514 + 600be46 commit 7ff06b2

25 files changed

+1745
-249
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:core/core.dart';
3+
import 'package:data_repository/data_repository.dart';
4+
import 'package:equatable/equatable.dart';
5+
6+
part 'archived_headlines_event.dart';
7+
part 'archived_headlines_state.dart';
8+
9+
class ArchivedHeadlinesBloc
10+
extends Bloc<ArchivedHeadlinesEvent, ArchivedHeadlinesState> {
11+
ArchivedHeadlinesBloc({
12+
required DataRepository<Headline> headlinesRepository,
13+
}) : _headlinesRepository = headlinesRepository,
14+
super(const ArchivedHeadlinesState()) {
15+
on<LoadArchivedHeadlinesRequested>(_onLoadArchivedHeadlinesRequested);
16+
on<RestoreHeadlineRequested>(_onRestoreHeadlineRequested);
17+
on<DeleteHeadlineForeverRequested>(_onDeleteHeadlineForeverRequested);
18+
}
19+
20+
final DataRepository<Headline> _headlinesRepository;
21+
22+
Future<void> _onLoadArchivedHeadlinesRequested(
23+
LoadArchivedHeadlinesRequested event,
24+
Emitter<ArchivedHeadlinesState> emit,
25+
) async {
26+
emit(state.copyWith(status: ArchivedHeadlinesStatus.loading));
27+
try {
28+
final isPaginating = event.startAfterId != null;
29+
final previousHeadlines = isPaginating ? state.headlines : <Headline>[];
30+
31+
final paginatedHeadlines = await _headlinesRepository.readAll(
32+
filter: {'status': ContentStatus.archived.name},
33+
sort: [const SortOption('updatedAt', SortOrder.desc)],
34+
pagination: PaginationOptions(
35+
cursor: event.startAfterId,
36+
limit: event.limit,
37+
),
38+
);
39+
emit(
40+
state.copyWith(
41+
status: ArchivedHeadlinesStatus.success,
42+
headlines: [...previousHeadlines, ...paginatedHeadlines.items],
43+
cursor: paginatedHeadlines.cursor,
44+
hasMore: paginatedHeadlines.hasMore,
45+
),
46+
);
47+
} on HttpException catch (e) {
48+
emit(
49+
state.copyWith(
50+
status: ArchivedHeadlinesStatus.failure,
51+
exception: e,
52+
),
53+
);
54+
} catch (e) {
55+
emit(
56+
state.copyWith(
57+
status: ArchivedHeadlinesStatus.failure,
58+
exception: UnknownException('An unexpected error occurred: $e'),
59+
),
60+
);
61+
}
62+
}
63+
64+
Future<void> _onRestoreHeadlineRequested(
65+
RestoreHeadlineRequested event,
66+
Emitter<ArchivedHeadlinesState> emit,
67+
) async {
68+
final originalHeadlines = List<Headline>.from(state.headlines);
69+
final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id);
70+
if (headlineIndex == -1) return;
71+
72+
final headlineToRestore = originalHeadlines[headlineIndex];
73+
final updatedHeadlines = originalHeadlines..removeAt(headlineIndex);
74+
75+
emit(state.copyWith(headlines: updatedHeadlines));
76+
77+
try {
78+
await _headlinesRepository.update(
79+
id: event.id,
80+
item: headlineToRestore.copyWith(status: ContentStatus.active),
81+
);
82+
} on HttpException catch (e) {
83+
emit(state.copyWith(headlines: originalHeadlines, exception: e));
84+
} catch (e) {
85+
emit(
86+
state.copyWith(
87+
headlines: originalHeadlines,
88+
exception: UnknownException('An unexpected error occurred: $e'),
89+
),
90+
);
91+
}
92+
}
93+
94+
Future<void> _onDeleteHeadlineForeverRequested(
95+
DeleteHeadlineForeverRequested event,
96+
Emitter<ArchivedHeadlinesState> emit,
97+
) async {
98+
final originalHeadlines = List<Headline>.from(state.headlines);
99+
final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id);
100+
if (headlineIndex == -1) return;
101+
102+
final updatedHeadlines = originalHeadlines..removeAt(headlineIndex);
103+
emit(state.copyWith(headlines: updatedHeadlines));
104+
105+
try {
106+
await _headlinesRepository.delete(id: event.id);
107+
} on HttpException catch (e) {
108+
emit(state.copyWith(headlines: originalHeadlines, exception: e));
109+
} catch (e) {
110+
emit(
111+
state.copyWith(
112+
headlines: originalHeadlines,
113+
exception: UnknownException('An unexpected error occurred: $e'),
114+
),
115+
);
116+
}
117+
}
118+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
part of 'archived_headlines_bloc.dart';
2+
3+
sealed class ArchivedHeadlinesEvent extends Equatable {
4+
const ArchivedHeadlinesEvent();
5+
6+
@override
7+
List<Object?> get props => [];
8+
}
9+
10+
/// Event to request loading of archived headlines.
11+
final class LoadArchivedHeadlinesRequested extends ArchivedHeadlinesEvent {
12+
const LoadArchivedHeadlinesRequested({this.startAfterId, this.limit});
13+
14+
final String? startAfterId;
15+
final int? limit;
16+
17+
@override
18+
List<Object?> get props => [startAfterId, limit];
19+
}
20+
21+
/// Event to restore an archived headline.
22+
final class RestoreHeadlineRequested extends ArchivedHeadlinesEvent {
23+
const RestoreHeadlineRequested(this.id);
24+
25+
final String id;
26+
27+
@override
28+
List<Object?> get props => [id];
29+
}
30+
31+
/// Event to permanently delete an archived headline.
32+
final class DeleteHeadlineForeverRequested extends ArchivedHeadlinesEvent {
33+
const DeleteHeadlineForeverRequested(this.id);
34+
35+
final String id;
36+
37+
@override
38+
List<Object?> get props => [id];
39+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
part of 'archived_headlines_bloc.dart';
2+
3+
/// Represents the status of archived content operations.
4+
enum ArchivedHeadlinesStatus {
5+
initial,
6+
loading,
7+
success,
8+
failure,
9+
}
10+
11+
/// The state for the archived content feature.
12+
class ArchivedHeadlinesState extends Equatable {
13+
const ArchivedHeadlinesState({
14+
this.status = ArchivedHeadlinesStatus.initial,
15+
this.headlines = const [],
16+
this.cursor,
17+
this.hasMore = false,
18+
this.exception,
19+
});
20+
21+
final ArchivedHeadlinesStatus status;
22+
final List<Headline> headlines;
23+
final String? cursor;
24+
final bool hasMore;
25+
final HttpException? exception;
26+
27+
ArchivedHeadlinesState copyWith({
28+
ArchivedHeadlinesStatus? status,
29+
List<Headline>? headlines,
30+
String? cursor,
31+
bool? hasMore,
32+
HttpException? exception,
33+
}) {
34+
return ArchivedHeadlinesState(
35+
status: status ?? this.status,
36+
headlines: headlines ?? this.headlines,
37+
cursor: cursor ?? this.cursor,
38+
hasMore: hasMore ?? this.hasMore,
39+
exception: exception ?? this.exception,
40+
);
41+
}
42+
43+
@override
44+
List<Object?> get props => [
45+
status,
46+
headlines,
47+
cursor,
48+
hasMore,
49+
exception,
50+
];
51+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:core/core.dart';
3+
import 'package:data_repository/data_repository.dart';
4+
import 'package:equatable/equatable.dart';
5+
6+
part 'archived_sources_event.dart';
7+
part 'archived_sources_state.dart';
8+
9+
class ArchivedSourcesBloc
10+
extends Bloc<ArchivedSourcesEvent, ArchivedSourcesState> {
11+
ArchivedSourcesBloc({
12+
required DataRepository<Source> sourcesRepository,
13+
}) : _sourcesRepository = sourcesRepository,
14+
super(const ArchivedSourcesState()) {
15+
on<LoadArchivedSourcesRequested>(_onLoadArchivedSourcesRequested);
16+
on<RestoreSourceRequested>(_onRestoreSourceRequested);
17+
}
18+
19+
final DataRepository<Source> _sourcesRepository;
20+
21+
Future<void> _onLoadArchivedSourcesRequested(
22+
LoadArchivedSourcesRequested event,
23+
Emitter<ArchivedSourcesState> emit,
24+
) async {
25+
emit(state.copyWith(status: ArchivedSourcesStatus.loading));
26+
try {
27+
final isPaginating = event.startAfterId != null;
28+
final previousSources = isPaginating ? state.sources : <Source>[];
29+
30+
final paginatedSources = await _sourcesRepository.readAll(
31+
filter: {'status': ContentStatus.archived.name},
32+
sort: [const SortOption('updatedAt', SortOrder.desc)],
33+
pagination: PaginationOptions(
34+
cursor: event.startAfterId,
35+
limit: event.limit,
36+
),
37+
);
38+
emit(
39+
state.copyWith(
40+
status: ArchivedSourcesStatus.success,
41+
sources: [...previousSources, ...paginatedSources.items],
42+
cursor: paginatedSources.cursor,
43+
hasMore: paginatedSources.hasMore,
44+
),
45+
);
46+
} on HttpException catch (e) {
47+
emit(
48+
state.copyWith(
49+
status: ArchivedSourcesStatus.failure,
50+
exception: e,
51+
),
52+
);
53+
} catch (e) {
54+
emit(
55+
state.copyWith(
56+
status: ArchivedSourcesStatus.failure,
57+
exception: UnknownException('An unexpected error occurred: $e'),
58+
),
59+
);
60+
}
61+
}
62+
63+
Future<void> _onRestoreSourceRequested(
64+
RestoreSourceRequested event,
65+
Emitter<ArchivedSourcesState> emit,
66+
) async {
67+
final originalSources = List<Source>.from(state.sources);
68+
final sourceIndex = originalSources.indexWhere((s) => s.id == event.id);
69+
if (sourceIndex == -1) return;
70+
71+
final sourceToRestore = originalSources[sourceIndex];
72+
final updatedSources = originalSources..removeAt(sourceIndex);
73+
74+
emit(state.copyWith(sources: updatedSources));
75+
76+
try {
77+
await _sourcesRepository.update(
78+
id: event.id,
79+
item: sourceToRestore.copyWith(status: ContentStatus.active),
80+
);
81+
} on HttpException catch (e) {
82+
emit(state.copyWith(sources: originalSources, exception: e));
83+
} catch (e) {
84+
emit(
85+
state.copyWith(
86+
sources: originalSources,
87+
exception: UnknownException('An unexpected error occurred: $e'),
88+
),
89+
);
90+
}
91+
}
92+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
part of 'archived_sources_bloc.dart';
2+
3+
sealed class ArchivedSourcesEvent extends Equatable {
4+
const ArchivedSourcesEvent();
5+
6+
@override
7+
List<Object?> get props => [];
8+
}
9+
10+
/// Event to request loading of archived sources.
11+
final class LoadArchivedSourcesRequested extends ArchivedSourcesEvent {
12+
const LoadArchivedSourcesRequested({this.startAfterId, this.limit});
13+
14+
final String? startAfterId;
15+
final int? limit;
16+
17+
@override
18+
List<Object?> get props => [startAfterId, limit];
19+
}
20+
21+
/// Event to restore an archived source.
22+
final class RestoreSourceRequested extends ArchivedSourcesEvent {
23+
const RestoreSourceRequested(this.id);
24+
25+
final String id;
26+
27+
@override
28+
List<Object?> get props => [id];
29+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
part of 'archived_sources_bloc.dart';
2+
3+
/// Represents the status of archived content operations.
4+
enum ArchivedSourcesStatus {
5+
initial,
6+
loading,
7+
success,
8+
failure,
9+
}
10+
11+
/// The state for the archived content feature.
12+
class ArchivedSourcesState extends Equatable {
13+
const ArchivedSourcesState({
14+
this.status = ArchivedSourcesStatus.initial,
15+
this.sources = const [],
16+
this.cursor,
17+
this.hasMore = false,
18+
this.exception,
19+
});
20+
21+
final ArchivedSourcesStatus status;
22+
final List<Source> sources;
23+
final String? cursor;
24+
final bool hasMore;
25+
final HttpException? exception;
26+
27+
ArchivedSourcesState copyWith({
28+
ArchivedSourcesStatus? status,
29+
List<Source>? sources,
30+
String? cursor,
31+
bool? hasMore,
32+
HttpException? exception,
33+
}) {
34+
return ArchivedSourcesState(
35+
status: status ?? this.status,
36+
sources: sources ?? this.sources,
37+
cursor: cursor ?? this.cursor,
38+
hasMore: hasMore ?? this.hasMore,
39+
exception: exception ?? this.exception,
40+
);
41+
}
42+
43+
@override
44+
List<Object?> get props => [
45+
status,
46+
sources,
47+
cursor,
48+
hasMore,
49+
exception,
50+
];
51+
}

0 commit comments

Comments
 (0)