Skip to content

Commit 3109b42

Browse files
committed
added image_tags_page
1 parent 69447ad commit 3109b42

1 file changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:piwigo_ng/components/lists/image_grid_view.dart';
3+
import 'package:piwigo_ng/components/popup_list_item.dart';
4+
import 'package:piwigo_ng/models/album_model.dart';
5+
import 'package:piwigo_ng/models/image_model.dart';
6+
import 'package:piwigo_ng/models/tag_model.dart';
7+
import 'package:piwigo_ng/network/api_error.dart';
8+
import 'package:piwigo_ng/network/images.dart';
9+
import 'package:piwigo_ng/services/preferences_service.dart';
10+
import 'package:piwigo_ng/utils/image_actions.dart';
11+
import 'package:piwigo_ng/utils/localizations.dart';
12+
import 'package:piwigo_ng/utils/settings.dart';
13+
import 'package:piwigo_ng/views/image/image_page.dart';
14+
import 'package:pull_to_refresh/pull_to_refresh.dart';
15+
16+
class ImageTagsPage extends StatefulWidget {
17+
const ImageTagsPage({
18+
Key? key,
19+
required this.tag,
20+
this.isAdmin = false
21+
}) : super(key: key);
22+
23+
static const String routeName = '/images/tags';
24+
25+
final TagModel tag;
26+
final bool isAdmin;
27+
28+
@override
29+
State<ImageTagsPage> createState() => _ImageTagsPageState();
30+
}
31+
32+
class _ImageTagsPageState extends State<ImageTagsPage> {
33+
final RefreshController _refreshController =
34+
RefreshController(initialRefresh: false);
35+
final ScrollController _scrollController = ScrollController();
36+
37+
late final Future<ApiResponse<Map>> _imageFuture;
38+
List<ImageModel>? _imageList;
39+
List<ImageModel> _selectedList = [];
40+
41+
int _nbImages = 0;
42+
int _page = 0;
43+
44+
@override
45+
void initState() {
46+
super.initState();
47+
_imageFuture = fetchTagImages(widget.tag.id).then((response) {
48+
if (response.hasData) {
49+
setState(() {
50+
final int? total = response.data!['total_count'];
51+
if (total != null) {
52+
_nbImages = total;
53+
}
54+
_imageList = response.data!['images'].cast<ImageModel>() ?? [];
55+
});
56+
}
57+
return response;
58+
});
59+
}
60+
61+
@override
62+
void dispose() {
63+
_refreshController.dispose();
64+
_scrollController.dispose();
65+
super.dispose();
66+
}
67+
68+
bool get _hasNonFavorites =>
69+
_selectedList.where((image) => !image.favorite).isNotEmpty;
70+
71+
Future<bool> _onWillPop() async {
72+
if (_selectedList.isNotEmpty) {
73+
setState(() {
74+
_selectedList.clear();
75+
});
76+
return false;
77+
}
78+
return true;
79+
}
80+
81+
Future<void> _onRefresh() async {
82+
final ApiResponse<Map> result = await fetchTagImages(widget.tag.id);
83+
if (!result.hasData) {
84+
_refreshController.refreshFailed();
85+
await Future.delayed(const Duration(milliseconds: 500));
86+
return _refreshController.refreshCompleted();
87+
}
88+
final int? total = result.data!['total_count'];
89+
setState(() {
90+
_page = 0;
91+
if (total != null) {
92+
_nbImages = total;
93+
}
94+
_imageList = result.data!['images'].cast<ImageModel>() ?? [];
95+
_selectedList.clear();
96+
});
97+
return _refreshController.refreshCompleted();
98+
}
99+
100+
Future<void> _loadMoreImages() async {
101+
if (_imageList == null || _nbImages <= _imageList!.length) return;
102+
ApiResponse<Map> result = await fetchTagImages(widget.tag.id, _page + 1);
103+
if (result.hasError || !result.hasData) {
104+
_refreshController.loadFailed();
105+
await Future.delayed(const Duration(milliseconds: 500));
106+
return _refreshController.loadComplete();
107+
}
108+
final int? total = result.data!['total_count'];
109+
setState(() {
110+
if (total != null) {
111+
_nbImages = total;
112+
}
113+
_imageList!.addAll(result.data!['images'].cast<ImageModel>() ?? []);
114+
});
115+
_refreshController.loadComplete();
116+
}
117+
118+
void _onTapPhoto(ImageModel image) => Navigator.of(context).pushNamed(
119+
ImagePage.routeName,
120+
arguments: {
121+
'images': _imageList,
122+
'startId': image.id,
123+
'album': AlbumModel(
124+
id: -1,
125+
name: '',
126+
nbImages: _nbImages,
127+
nbTotalImages: _nbImages,
128+
),
129+
},
130+
).then((images) {
131+
if (images == null || images is! List<ImageModel>) return;
132+
setState(() {
133+
_imageList = images;
134+
_page =
135+
((images.length - 1) / Settings.defaultElementPerPage).floor();
136+
});
137+
});
138+
void _onEditPhotos() => onEditPhotos(context, _selectedList).then((success) {
139+
if (success == true) {
140+
_selectedList.clear();
141+
_onRefresh();
142+
}
143+
});
144+
void _onLikePhotos() =>
145+
onLikePhotos(_selectedList, false).whenComplete(() => _onRefresh());
146+
_onDeletePhotos() => onDeletePhotos(context, _selectedList).then((success) {
147+
if (success) _onRefresh();
148+
});
149+
150+
@override
151+
Widget build(BuildContext context) {
152+
return WillPopScope(
153+
onWillPop: _onWillPop,
154+
child: Scaffold(
155+
body: SafeArea(
156+
child: SmartRefresher(
157+
controller: _refreshController,
158+
scrollController: _scrollController,
159+
enablePullUp: _imageList != null && _nbImages > _imageList!.length,
160+
onLoading: _loadMoreImages,
161+
onRefresh: _onRefresh,
162+
header: MaterialClassicHeader(
163+
backgroundColor: Theme.of(context).cardColor,
164+
color: Theme.of(context).colorScheme.secondary,
165+
),
166+
footer: ClassicFooter(
167+
loadingText: appStrings.loadingHUD_label,
168+
noDataText: appStrings.categoryImageList_noDataError,
169+
failedText: appStrings.errorHUD_label,
170+
idleText: '',
171+
canLoadingText: appStrings.loadMoreHUD_label,
172+
),
173+
child: CustomScrollView(
174+
controller: _scrollController,
175+
slivers: [
176+
_appBar,
177+
SliverToBoxAdapter(
178+
child: _taggedImageGrid,
179+
),
180+
],
181+
),
182+
),
183+
),
184+
bottomNavigationBar: AnimatedSlide(
185+
duration: const Duration(milliseconds: 300),
186+
curve: Curves.easeInOut,
187+
offset: _selectedList.isEmpty ? Offset(0, 1) : Offset.zero,
188+
child: _bottomBar,
189+
),
190+
),
191+
);
192+
}
193+
194+
Widget get _appBar {
195+
Orientation orientation = MediaQuery.of(context).orientation;
196+
return SliverAppBar(
197+
pinned: true,
198+
centerTitle: false,
199+
titleSpacing: 0.0,
200+
leading: BackButton(
201+
onPressed: () => Navigator.of(context).pop(),
202+
),
203+
// title: Text(appStrings.categoryDiscoverFavorites_title),
204+
title: Text(widget.tag.name),
205+
actions: [
206+
if (_selectedList.isNotEmpty)
207+
IconButton(
208+
onPressed: () => setState(() {
209+
_selectedList.clear();
210+
}),
211+
tooltip: appStrings.categoryImageList_deselectButton,
212+
icon: Icon(Icons.cancel),
213+
),
214+
if (orientation == Orientation.landscape) ..._actions,
215+
if (widget.isAdmin)
216+
PopupMenuButton(
217+
tooltip: appStrings.imageOptions_title,
218+
enabled: _selectedList.isNotEmpty,
219+
position: PopupMenuPosition.under,
220+
itemBuilder: (context) => [
221+
PopupMenuItem(
222+
onTap: () => Future.delayed(
223+
const Duration(seconds: 0),
224+
() => share(_selectedList),
225+
),
226+
child: PopupListItem(
227+
icon: Icons.share,
228+
text: appStrings.imageOptions_share,
229+
),
230+
),
231+
if (Preferences.getUserStatus != 'guest')
232+
PopupMenuItem(
233+
onTap: () => Future.delayed(
234+
const Duration(seconds: 0),
235+
_onLikePhotos,
236+
),
237+
child: PopupListItem(
238+
icon: Icons.remove_circle,
239+
text: appStrings.imageOptions_removeFavorites,
240+
color: Theme.of(context).colorScheme.error,
241+
),
242+
),
243+
PopupMenuItem(
244+
onTap: () => Future.delayed(
245+
const Duration(seconds: 0),
246+
() => downloadImages(_selectedList),
247+
),
248+
child: PopupListItem(
249+
icon: Icons.download,
250+
text: appStrings.downloadImage_title(_selectedList.length),
251+
),
252+
),
253+
],
254+
),
255+
],
256+
);
257+
}
258+
259+
Widget get _taggedImageGrid {
260+
return FutureBuilder<ApiResponse<Map>>(
261+
future: _imageFuture,
262+
builder: (context, snapshot) {
263+
if (snapshot.hasData) {
264+
if (!snapshot.data!.hasData) {
265+
return Center(
266+
child: Padding(
267+
padding: const EdgeInsets.all(8.0),
268+
child: Text(appStrings.categoryImageList_noDataError),
269+
),
270+
);
271+
}
272+
return _buildImageGrid(snapshot);
273+
}
274+
return Center(child: CircularProgressIndicator());
275+
},
276+
);
277+
}
278+
279+
Widget _buildImageGrid(AsyncSnapshot snapshot) {
280+
final ApiResponse<Map> result = snapshot.data!;
281+
if (_imageList == null) {
282+
_nbImages = result.data!['total_count'];
283+
_imageList = result.data!['images'].cast<ImageModel>() ?? [];
284+
}
285+
286+
_selectedList =
287+
_imageList!.where((image) => _selectedList.contains(image)).toList();
288+
289+
if (_imageList!.isEmpty) {
290+
return Center(
291+
child: Padding(
292+
padding: const EdgeInsets.all(8.0),
293+
child: Text(appStrings.noImages),
294+
),
295+
);
296+
}
297+
return ImageGridView(
298+
imageList: _imageList!,
299+
selectedList: _selectedList,
300+
onSelectImage: (image) => setState(() {
301+
_selectedList.add(image);
302+
}),
303+
onDeselectImage: (image) => setState(() {
304+
_selectedList.remove(image);
305+
}),
306+
onTapImage: _onTapPhoto,
307+
);
308+
}
309+
310+
Widget get _bottomBar {
311+
return OrientationBuilder(builder: (context, orientation) {
312+
return AnimatedContainer(
313+
duration: const Duration(milliseconds: 300),
314+
curve: Curves.easeInOut,
315+
height: _selectedList.isEmpty || orientation == Orientation.landscape
316+
? 0
317+
: 56.0,
318+
child: BottomAppBar(
319+
height: 56.0,
320+
child: Row(
321+
children:
322+
_actions.map((action) => Expanded(child: action)).toList(),
323+
),
324+
),
325+
);
326+
});
327+
}
328+
329+
List<Widget> get _actions {
330+
List<Widget> adminActions = [
331+
IconButton(
332+
onPressed: _onEditPhotos,
333+
tooltip: appStrings.imageOptions_edit,
334+
icon: Icon(Icons.edit),
335+
),
336+
IconButton(
337+
onPressed: _onDeletePhotos,
338+
tooltip: appStrings.deleteImage_delete,
339+
icon: Icon(Icons.delete),
340+
),
341+
];
342+
List<Widget> userActions = [
343+
IconButton(
344+
onPressed: () => share(_selectedList),
345+
tooltip: appStrings.imageOptions_share,
346+
icon: Icon(Icons.share),
347+
),
348+
if (Preferences.getUserStatus != 'guest') // Todo: enum roles
349+
IconButton(
350+
onPressed: _onLikePhotos,
351+
tooltip: _hasNonFavorites
352+
? appStrings.imageOptions_addFavorites
353+
: appStrings.imageOptions_removeFavorites,
354+
isSelected: !_hasNonFavorites,
355+
selectedIcon: Icon(Icons.favorite),
356+
icon: Icon(Icons.favorite_border),
357+
),
358+
IconButton(
359+
onPressed: () => downloadImages(_selectedList),
360+
tooltip: appStrings.imageOptions_download,
361+
icon: Icon(Icons.download),
362+
),
363+
];
364+
365+
return widget.isAdmin ? adminActions : userActions;
366+
}
367+
}

0 commit comments

Comments
 (0)