Skip to content

Commit b0077a7

Browse files
committed
Added header to image display and upload services
1 parent 7ac172e commit b0077a7

10 files changed

Lines changed: 351 additions & 161 deletions

lib/api/api_interceptor.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,22 @@ import 'package:piwigo_ng/app.dart';
88
import 'package:piwigo_ng/components/snackbars.dart';
99
import 'package:piwigo_ng/services/preferences_service.dart';
1010
import 'package:piwigo_ng/utils/localizations.dart';
11+
import 'package:shared_preferences/shared_preferences.dart';
1112

1213
class ApiInterceptor extends Interceptor {
1314
@override
1415
void onRequest(
1516
RequestOptions options,
1617
RequestInterceptorHandler handler,
1718
) async {
18-
print("[${options.method}] ${options.queryParameters['method']}");
19+
debugPrint("[${options.method}] ${options.queryParameters['method']}");
1920
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
21+
SharedPreferences prefs = await SharedPreferences.getInstance();
2022
options.baseUrl =
2123
(await secureStorage.read(key: Preferences.serverUrlKey))!;
2224
if (Preferences.getEnableBasicAuth) {
23-
String? username =
24-
appPreferences.getString(Preferences.basicUsernameKey) ??
25-
await secureStorage.read(key: Preferences.usernameKey);
26-
String? password =
27-
appPreferences.getString(Preferences.basicPasswordKey) ??
28-
await secureStorage.read(key: Preferences.passwordKey);
25+
String? username = prefs.getString(Preferences.basicUsernameKey) ?? '';
26+
String? password = prefs.getString(Preferences.basicPasswordKey) ?? '';
2927
String basicAuth =
3028
"Basic ${base64.encode(utf8.encode('$username:$password'))}";
3129
options.headers['authorization'] = basicAuth;

lib/api/upload.dart

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:image_picker/image_picker.dart';
1212
import 'package:path_provider/path_provider.dart';
1313
import 'package:permission_handler/permission_handler.dart';
1414
import 'package:piwigo_ng/api/api_client.dart';
15+
import 'package:piwigo_ng/api/api_interceptor.dart';
1516
import 'package:piwigo_ng/api/authentication.dart';
1617
import 'package:piwigo_ng/app.dart';
1718
import 'package:piwigo_ng/components/dialogs/confirm_dialog.dart';
@@ -25,6 +26,7 @@ import 'package:shared_preferences/shared_preferences.dart';
2526
import '../services/chunked_uploader.dart';
2627
import '../services/notification_service.dart';
2728

29+
/// Handle Android API 33 permissions
2830
Future<bool> askMediaPermission() async {
2931
bool storage = true;
3032
bool videos = true;
@@ -55,12 +57,13 @@ Future<bool> askMediaPermission() async {
5557
return false;
5658
}
5759

60+
/// Prepare and upload with [uploadChunk] a list of files.
5861
Future<List<int>> uploadPhotos(
5962
List<XFile> photos,
6063
int albumId, {
6164
Map<String, dynamic> info = const {},
6265
}) async {
63-
// Check if Wifi is enabled and working
66+
// Check if Wifi is enabled and working before processing
6467
if (Preferences.getWifiUpload) {
6568
var connectivity = await Connectivity().checkConnectivity();
6669
if (connectivity != ConnectivityResult.wifi) {
@@ -75,6 +78,7 @@ Future<List<int>> uploadPhotos(
7578
}
7679
}
7780

81+
// Initialize variables
7882
List<int> result = [];
7983
List<UploadItem> items = [];
8084
FlutterSecureStorage storage = const FlutterSecureStorage();
@@ -88,26 +92,31 @@ Future<List<int>> uploadPhotos(
8892

8993
// Creates Upload Item list for the upload notifier
9094
for (var photo in photos) {
91-
File? compressedFile;
92-
if (Preferences.getRemoveMetadata) {
93-
compressedFile = await compressFile(photo);
94-
} else {
95-
compressedFile = File(photo.path);
95+
File? uploadFile;
96+
97+
// Compress file
98+
uploadFile = await compressFile(photo);
99+
if (uploadFile == null) {
100+
uploadFile = File(photo.path);
96101
}
102+
97103
items.add(UploadItem(
98-
file: compressedFile,
104+
file: uploadFile,
99105
albumId: albumId,
100106
));
101107
}
102108

109+
// Add items to the queue
103110
uploadNotifier.addItems(items);
104111

112+
// Closes the Upload Configuration page and opens the Upload Status page
105113
App.navigatorKey.currentState?.popAndPushNamed(UploadStatusPage.routeName);
106114

115+
// Iterate on each item
107116
await Future.wait(List<Future<void>>.generate(items.length, (index) async {
108117
UploadItem item = items[index];
109118
try {
110-
// Make Request
119+
// Upload image
111120
Response? response = await uploadChunk(
112121
photo: item.file,
113122
category: albumId,
@@ -131,7 +140,7 @@ Future<List<int>> uploadPhotos(
131140
var data = json.decode(response.data);
132141
result.add(data['result']['id']);
133142

134-
// Notify provider upload completed
143+
// Notify provider the upload has completed.
135144
uploadNotifier.itemUploadCompleted(item);
136145
if (Preferences.getDeleteAfterUpload) {
137146
// todo: delete real file path, not the cached one.
@@ -143,18 +152,22 @@ Future<List<int>> uploadPhotos(
143152
uploadNotifier.itemUploadCompleted(item, error: true);
144153
nbError++;
145154
} catch (e) {
155+
debugPrint("$e");
146156
if (e is Error) {
147-
debugPrint("$e");
148157
debugPrint("${e.stackTrace}");
149158
}
150-
debugPrint("$e");
151159
uploadNotifier.itemUploadCompleted(item, error: true);
152160
nbError++;
153161
}
154162
}));
155163

164+
// Send notifications
156165
showUploadNotification(nbError, result.length);
166+
167+
// If no image was successfully uploaded, no call for "uploadCompleted"
157168
if (result.isEmpty) return [];
169+
170+
// Empty Piwigo lounge
158171
try {
159172
await uploadCompleted(result, albumId);
160173
if (await methodExist('community.images.uploadCompleted')) {
@@ -167,6 +180,7 @@ Future<List<int>> uploadPhotos(
167180
return result;
168181
}
169182

183+
/// Upload images as chunks using [ChunkedUploader]
170184
Future<Response?> uploadChunk({
171185
required File photo,
172186
required int category,
@@ -178,30 +192,40 @@ Future<Response?> uploadChunk({
178192
CancelToken? cancelToken,
179193
}) async {
180194
SharedPreferences prefs = await SharedPreferences.getInstance();
195+
196+
// Request query parameters
181197
Map<String, String> queries = {
182198
'format': 'json',
183199
'method': 'pwg.images.uploadAsync',
184200
};
201+
202+
// Initialize fields
185203
Map<String, dynamic> fields = {
186204
'username': username,
187205
'password': password,
188206
'filename': photo.path.split('/').last,
189207
'category': category,
190208
};
191209

210+
// Filter fields
192211
if (info['name'] != '' && info['name'] != null) fields['name'] = info['name'];
193212
if (info['comment'] != '' && info['comment'] != null)
194213
fields['comment'] = info['comment'];
195214
if (info['tag_ids']?.isNotEmpty ?? false)
196215
fields['tag_ids'] = info['tag_ids'].join(',');
197216
if (info['level'] != -1) fields['level'] = info['level'];
198217

199-
ChunkedUploader chunkedUploader = ChunkedUploader(Dio(
218+
// Create dio client
219+
Dio dio = Dio(
200220
BaseOptions(
201221
baseUrl: url,
202222
),
203-
));
223+
)..interceptors.add(ApiInterceptor());
204224

225+
// Initialize chunk uploader service
226+
ChunkedUploader chunkedUploader = ChunkedUploader(dio);
227+
228+
// Upload image as chunks
205229
return await chunkedUploader.upload(
206230
path: '/ws.php',
207231
filePath: photo.absolute.path,
@@ -217,25 +241,41 @@ Future<Response?> uploadChunk({
217241
);
218242
}
219243

220-
Future<File> compressFile(XFile file) async {
244+
/// Compress before upload, enabled with [Preferences.getCompressUpload] parameter.
245+
Future<File?> compressFile(XFile file) async {
221246
try {
247+
SharedPreferences prefs = await SharedPreferences.getInstance();
248+
249+
// Get original file path
222250
final filePath = file.path;
251+
252+
// Directory output
223253
var dir = await getTemporaryDirectory();
254+
255+
// Extract file name and extension
224256
final String filename = filePath.split('/').last;
257+
258+
// Output file path
225259
final outPath = "${dir.absolute.path}/$filename";
226260

261+
// Get compress parameters
262+
double quality = prefs.getDouble(Preferences.uploadQualityKey) ?? 1.0;
263+
bool removeMetadata = prefs.getBool(Preferences.removeMetadataKey) ?? false;
264+
265+
// Compress with quality parameter and exif metadata
227266
var result = await FlutterImageCompress.compressAndGetFile(
228267
filePath,
229268
outPath,
230-
quality: (Preferences.getUploadQuality * 100).round(),
231-
keepExif: false,
269+
quality: (quality * 100).round(),
270+
keepExif: removeMetadata,
232271
);
272+
233273
debugPrint("Upload Compress $result");
234-
if (result != null) return result;
274+
return result;
235275
} catch (e) {
236276
debugPrint(e.toString());
237277
}
238-
return File(file.path);
278+
return null;
239279
}
240280

241281
Future<bool> uploadCompleted(List<int> imageId, int categoryId) async {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:cached_network_image/cached_network_image.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
7+
import 'package:piwigo_ng/api/api_client.dart';
8+
import 'package:piwigo_ng/services/preferences_service.dart';
9+
import 'package:shared_preferences/shared_preferences.dart';
10+
11+
class AppImageDisplay extends StatefulWidget {
12+
const AppImageDisplay({
13+
Key? key,
14+
this.imageUrl,
15+
this.fit,
16+
}) : super(key: key);
17+
18+
final String? imageUrl;
19+
final BoxFit? fit;
20+
21+
@override
22+
State<AppImageDisplay> createState() => _AppImageDisplayState();
23+
}
24+
25+
class _AppImageDisplayState extends State<AppImageDisplay> {
26+
late final Future<Map<String, String>> _headers;
27+
28+
@override
29+
initState() {
30+
super.initState();
31+
_headers = _getHeaders();
32+
}
33+
34+
Future<Map<String, String>> _getHeaders() async {
35+
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
36+
String? serverUrl = await secureStorage.read(key: 'SERVER_URL');
37+
38+
if (serverUrl == null) return {};
39+
40+
// Get server cookies
41+
List<Cookie> cookies =
42+
await ApiClient.cookieJar.loadForRequest(Uri.parse(serverUrl));
43+
String cookiesStr =
44+
cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
45+
46+
// Get HTTP Basic id
47+
SharedPreferences prefs = await SharedPreferences.getInstance();
48+
String? basicAuth;
49+
// Fetch only if enabled
50+
if (Preferences.getEnableBasicAuth) {
51+
String? username = prefs.getString(Preferences.basicUsernameKey) ?? '';
52+
String? password = prefs.getString(Preferences.basicPasswordKey) ?? '';
53+
basicAuth = "Basic ${base64.encode(utf8.encode('$username:$password'))}";
54+
}
55+
56+
return {
57+
HttpHeaders.cookieHeader: cookiesStr,
58+
if (basicAuth != null) 'Authorization': basicAuth,
59+
};
60+
}
61+
62+
@override
63+
Widget build(BuildContext context) {
64+
if (widget.imageUrl == null) {
65+
return _buildNoImageWidget(context);
66+
}
67+
68+
return FutureBuilder<Map<String, String>>(
69+
future: _headers,
70+
builder: (context, snapshot) {
71+
if (snapshot.hasData) {
72+
return CachedNetworkImage(
73+
imageUrl: widget.imageUrl!,
74+
fadeInDuration: const Duration(milliseconds: 300),
75+
fit: widget.fit ?? BoxFit.cover,
76+
httpHeaders: snapshot.data!,
77+
imageBuilder: (context, provider) => Image(
78+
image: provider,
79+
fit: widget.fit ?? BoxFit.cover,
80+
errorBuilder: (context, o, s) {
81+
debugPrint("$o\n$s");
82+
return _buildErrorWidget(context, widget.imageUrl, o);
83+
},
84+
),
85+
progressIndicatorBuilder: _buildProgressIndicator,
86+
errorWidget: _buildErrorWidget,
87+
);
88+
}
89+
if (snapshot.hasError) {
90+
return _buildErrorWidget(context);
91+
}
92+
return Center(
93+
child: CircularProgressIndicator(),
94+
);
95+
});
96+
}
97+
98+
Widget _buildProgressIndicator(
99+
BuildContext context, String url, DownloadProgress download) {
100+
if (download.downloaded >= (download.totalSize ?? 0)) {
101+
return const SizedBox();
102+
}
103+
return Center(
104+
child: CircularProgressIndicator(
105+
value: download.progress,
106+
),
107+
);
108+
}
109+
110+
Widget _buildErrorWidget(BuildContext context, [String? url, dynamic error]) {
111+
debugPrint("[$url!] $error");
112+
return FittedBox(
113+
fit: BoxFit.cover,
114+
child: Container(
115+
padding: const EdgeInsets.all(16.0),
116+
decoration: BoxDecoration(
117+
color: Theme.of(context).scaffoldBackgroundColor,
118+
),
119+
child: const Icon(Icons.broken_image_outlined),
120+
),
121+
);
122+
}
123+
124+
Widget _buildNoImageWidget(BuildContext context) {
125+
return FittedBox(
126+
fit: BoxFit.cover,
127+
child: Container(
128+
padding: const EdgeInsets.all(16.0),
129+
decoration: BoxDecoration(
130+
color: Theme.of(context).scaffoldBackgroundColor,
131+
),
132+
child: const Icon(Icons.image_not_supported),
133+
),
134+
);
135+
}
136+
}

lib/components/cards/album_card.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:cached_network_image/cached_network_image.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_slidable/flutter_slidable.dart';
4+
import 'package:piwigo_ng/components/app_image_display.dart';
45
import 'package:piwigo_ng/components/clippers/album_card_clipper.dart';
56
import 'package:piwigo_ng/components/clippers/clip_shadow_path.dart';
67
import 'package:piwigo_ng/models/album_model.dart';
@@ -186,6 +187,9 @@ class AlbumCardContent extends StatelessWidget {
186187
child: ClipRRect(
187188
borderRadius: BorderRadius.circular(10.0),
188189
child: Builder(builder: (context) {
190+
return AppImageDisplay(
191+
imageUrl: album.urlRepresentative,
192+
);
189193
if (album.urlRepresentative == null) {
190194
return FittedBox(
191195
fit: BoxFit.cover,

0 commit comments

Comments
 (0)