@@ -2,8 +2,9 @@ import 'dart:convert';
22import 'dart:io' ;
33
44import 'package:dio/dio.dart' ;
5+ import 'package:extended_text/extended_text.dart' ;
56import 'package:file_picker/file_picker.dart' ;
6- import 'package:flutter/material .dart' ;
7+ import 'package:flutter/foundation .dart' ;
78import 'package:flutter_local_notifications/flutter_local_notifications.dart' ;
89import 'package:image_picker/image_picker.dart' ;
910import 'package:path/path.dart' as path;
@@ -21,7 +22,9 @@ import 'package:share_plus/share_plus.dart';
2122import 'albums.dart' ;
2223import 'api_client.dart' ;
2324
24- Future <ApiResult <ImageModel >> getImage (int imageId) async {
25+ Future <ApiResult <ImageModel >> getImage (
26+ int imageId,
27+ ) async {
2528 Map <String , dynamic > queries = {
2629 'format' : 'json' ,
2730 'method' : 'pwg.images.getInfo' ,
@@ -48,8 +51,10 @@ Future<ApiResult<ImageModel>> getImage(int imageId) async {
4851 return ApiResult (error: ApiErrors .error);
4952}
5053
51- Future <ApiResult <List <ImageModel >>> fetchImages (int albumID,
52- [int page = 0 ]) async {
54+ Future <ApiResult <List <ImageModel >>> fetchImages (
55+ int albumID, [
56+ int page = 0 ,
57+ ]) async {
5358 Map <String , dynamic > queries = {
5459 'format' : 'json' ,
5560 'method' : 'pwg.categories.getImages' ,
@@ -78,7 +83,10 @@ Future<ApiResult<List<ImageModel>>> fetchImages(int albumID,
7883 return ApiResult (error: ApiErrors .fetchImagesError);
7984}
8085
81- Future <ApiResult <Map >> searchImages (String searchQuery, [int page = 0 ]) async {
86+ Future <ApiResult <Map >> searchImages (
87+ String searchQuery, [
88+ int page = 0 ,
89+ ]) async {
8290 Map <String , dynamic > query = {
8391 'format' : 'json' ,
8492 'method' : 'pwg.images.search' ,
@@ -116,7 +124,9 @@ Future<ApiResult<Map>> searchImages(String searchQuery, [int page = 0]) async {
116124 return ApiResult (error: ApiErrors .searchImagesError);
117125}
118126
119- Future <ApiResult <Map >> fetchFavorites ([int page = 0 ]) async {
127+ Future <ApiResult <Map >> fetchFavorites ([
128+ int page = 0 ,
129+ ]) async {
120130 Map <String , dynamic > query = {
121131 'format' : 'json' ,
122132 'method' : 'pwg.users.favorites.getList' ,
@@ -160,8 +170,10 @@ Future<String?> pickDirectoryPath() async {
160170 return await FilePicker .platform.getDirectoryPath ();
161171}
162172
163- Future <void > _showDownloadNotification (
164- {bool success = true , String ? payload}) async {
173+ Future <void > _showDownloadNotification ({
174+ bool success = true ,
175+ String ? payload,
176+ }) async {
165177 if (! Preferences .getDownloadNotification) return ;
166178 final android = AndroidNotificationDetails (
167179 'piwigo-ng-download' ,
@@ -183,7 +195,9 @@ Future<void> _showDownloadNotification(
183195 );
184196}
185197
186- Future <bool > share (List <ImageModel > images) async {
198+ Future <bool > share (
199+ List <ImageModel > images,
200+ ) async {
187201 List <XFile >? filesPath = await downloadImages (
188202 images,
189203 showNotification: false ,
@@ -233,7 +247,10 @@ Future<List<XFile>?> downloadImages(
233247 return files;
234248}
235249
236- Future <XFile ?> downloadImage (String dirPath, ImageModel image) async {
250+ Future <XFile ?> downloadImage (
251+ String dirPath,
252+ ImageModel image,
253+ ) async {
237254 String localPath = path.join (dirPath, image.file);
238255 try {
239256 await ApiClient .download (
@@ -276,7 +293,9 @@ Future<int> deleteImages(
276293 return nbSuccess;
277294}
278295
279- Future <bool > deleteImage (ImageModel image) async {
296+ Future <bool > deleteImage (
297+ ImageModel image,
298+ ) async {
280299 Map <String , String > queries = {
281300 'format' : 'json' ,
282301 'method' : 'pwg.images.delete' ,
@@ -302,7 +321,10 @@ Future<bool> deleteImage(ImageModel image) async {
302321 return false ;
303322}
304323
305- Future <int > removeImages (List <ImageModel > images, int albumId) async {
324+ Future <int > removeImages (
325+ List <ImageModel > images,
326+ int albumId,
327+ ) async {
306328 int nbSuccess = 0 ;
307329 for (ImageModel image in images) {
308330 bool response = await removeImage (image, albumId);
@@ -313,7 +335,10 @@ Future<int> removeImages(List<ImageModel> images, int albumId) async {
313335 return nbSuccess;
314336}
315337
316- Future <bool > removeImage (ImageModel image, int albumId) async {
338+ Future <bool > removeImage (
339+ ImageModel image,
340+ int albumId,
341+ ) async {
317342 final List <int > albums =
318343 image.categories.map <int >((album) => album['id' ]).toList ();
319344 albums.removeWhere ((album) => album == albumId);
@@ -348,7 +373,10 @@ Future<bool> removeImage(ImageModel image, int albumId) async {
348373}
349374
350375Future <int > moveImages (
351- List <ImageModel > images, int oldAlbumId, int newAlbumId) async {
376+ List <ImageModel > images,
377+ int oldAlbumId,
378+ int newAlbumId,
379+ ) async {
352380 int nbMoved = 0 ;
353381 for (var image in images) {
354382 bool response = await moveImage (image, oldAlbumId, newAlbumId);
@@ -359,7 +387,11 @@ Future<int> moveImages(
359387 return nbMoved;
360388}
361389
362- Future <bool > moveImage (ImageModel image, int oldAlbumId, int newAlbumId) async {
390+ Future <bool > moveImage (
391+ ImageModel image,
392+ int oldAlbumId,
393+ int newAlbumId,
394+ ) async {
363395 final List <int > albums =
364396 image.categories.map <int >((album) => album['id' ]).toList ();
365397 albums.removeWhere ((id) => id == oldAlbumId);
@@ -390,7 +422,10 @@ Future<bool> moveImage(ImageModel image, int oldAlbumId, int newAlbumId) async {
390422 return false ;
391423}
392424
393- Future <int > assignImages (List <ImageModel > images, int albumId) async {
425+ Future <int > assignImages (
426+ List <ImageModel > images,
427+ int albumId,
428+ ) async {
394429 int nbAssigned = 0 ;
395430 for (ImageModel image in images) {
396431 final List <int > categories =
@@ -404,7 +439,10 @@ Future<int> assignImages(List<ImageModel> images, int albumId) async {
404439 return nbAssigned;
405440}
406441
407- Future <bool > assignImage (int imageId, List <int > categories) async {
442+ Future <bool > assignImage (
443+ int imageId,
444+ List <int > categories,
445+ ) async {
408446 final Map <String , dynamic > queries = {
409447 'format' : 'json' ,
410448 'method' : 'pwg.images.setInfo' ,
@@ -491,8 +529,10 @@ Future<bool> editImage(
491529}
492530
493531/// Return a list of files that are not in the server
494- Future <List <File >> checkImagesNotExist (List <File > files,
495- {bool returnExistFiles = false }) async {
532+ Future <List <File >> checkImagesNotExist (
533+ List <File > files, {
534+ bool returnExistFiles = false ,
535+ }) async {
496536 Map <String , File > md5sumList = {};
497537
498538 for (File file in files) {
@@ -528,3 +568,122 @@ Future<List<File>> checkImagesNotExist(List<File> files,
528568 }
529569 return [];
530570}
571+
572+ String ? removeUrlProtocol (String ? url) {
573+ if (url == null ) return null ;
574+ url = url.replaceFirst ('http://' , '' );
575+ url = url.replaceFirst ('https://' , '' );
576+ return url;
577+ }
578+
579+ String ? cleanImageUrl (String ? originalUrl) {
580+ if (originalUrl == null ) return null ;
581+ final String okUrl = originalUrl;
582+
583+ // TEMPORARY PATCH for case where $conf['original_url_protection'] = 'images';
584+ /// See https://github.com/Piwigo/Piwigo-Mobile/issues/503
585+ String patchedUrl = okUrl.replaceAll ('&part=' , '&part=' );
586+
587+ // Servers may return incorrect URLs
588+ /// See https://tools.ietf.org/html/rfc3986#section-2
589+ Uri ? serverUrl = Uri .tryParse (patchedUrl);
590+
591+ print ("$patchedUrl " );
592+
593+ if (serverUrl == null ) {
594+ // URL not RFC compliant!
595+ String leftUrl = patchedUrl;
596+
597+ // Remove protocol header
598+ leftUrl = removeUrlProtocol (patchedUrl) ?? leftUrl;
599+
600+ // Retrieve authority
601+ int endAuthority = leftUrl.indexOf ('/' );
602+ // No path, incomplete URL —> return image.jpg but should never happen
603+ if (endAuthority == - 1 ) return null ;
604+ leftUrl = leftUrl.substring (endAuthority);
605+
606+ // The Piwigo server may not be in the root e.g. example.com/piwigo/…
607+ // So we remove the path to avoid a duplicate if necessary
608+ String ? loginUrl = appPreferences.getString (Preferences .serverUrlKey);
609+ loginUrl = removeUrlProtocol (loginUrl);
610+ if (loginUrl != null &&
611+ loginUrl.isNotEmpty &&
612+ leftUrl.startsWith (loginUrl)) {
613+ leftUrl = leftUrl.substring (loginUrl.length);
614+ }
615+
616+ // Retrieve path
617+ int endQuery = leftUrl.indexOf ('?' );
618+ if (endQuery != - 1 ) {
619+ String query = leftUrl.substring (0 , endQuery);
620+ query.replaceAll ('??' , '?' );
621+ String encodedQuery = Uri .encodeComponent (query);
622+ leftUrl.substring (0 , query.length);
623+ String encodedPath = Uri .encodeComponent (leftUrl);
624+ serverUrl = Uri .tryParse ("$loginUrl $encodedQuery $encodedPath " );
625+ } else {
626+ // No query -> remaining string is a path
627+ String encodedPath = Uri .encodeComponent (leftUrl);
628+ serverUrl = Uri .tryParse ("$loginUrl $encodedPath " );
629+ }
630+
631+ // Last check
632+ if (serverUrl == null ) {
633+ // Could not apply percent encoding —> return nil
634+ return null ;
635+ }
636+ }
637+
638+ // Servers may return image URLs different from those used to login (e.g. wrong server settings)
639+ // We only keep the path+query because we only accept to download images from the same server.
640+ String cleanPath = serverUrl.path;
641+ // todo : parameterString
642+ String query = serverUrl.query;
643+ if (query.isNotEmpty) {
644+ cleanPath.joinChar ("?$query " );
645+ }
646+ String fragment = serverUrl.fragment;
647+ if (fragment.isNotEmpty) {
648+ cleanPath.joinChar ("#$fragment " );
649+ }
650+
651+ // The Piwigo server may not be in the root e.g. example.com/piwigo/…
652+ // and images may not be in the same path
653+ String ? loginPath = appPreferences.getString (Preferences .serverUrlKey);
654+ loginPath = removeUrlProtocol (loginPath);
655+ if (loginPath != null && loginPath.isNotEmpty) {
656+ if (cleanPath.startsWith (loginPath)) {
657+ cleanPath = cleanPath.substring (loginPath.length);
658+ } else if (cleanPath.endsWith (loginPath)) {
659+ cleanPath = cleanPath.substring (0 , cleanPath.length - loginPath.length);
660+ }
661+ }
662+
663+ // Remove the .php?, i? prefixes if any
664+ String prefix = '' ;
665+ int pos = cleanPath.indexOf ('?' );
666+ if (pos != - 1 ) {
667+ prefix = cleanPath.substring (0 , pos);
668+ }
669+
670+ // Path may not be encoded
671+ String decodedPath = Uri .decodeComponent (cleanPath);
672+ if (cleanPath == decodedPath) {
673+ String test = Uri .encodeComponent (cleanPath);
674+ cleanPath = test;
675+ }
676+
677+ // Compile final URL using the one provided at login
678+ String encodedImageUrl = "${serverUrl .scheme }://$loginPath $prefix $cleanPath " ;
679+ if (kDebugMode) {
680+ if (encodedImageUrl != originalUrl) {
681+ print ("=> originalURL:$originalUrl " );
682+ print (" encodedURL:$encodedImageUrl " );
683+ print (
684+ " path=${serverUrl .path }, parameterString=${serverUrl .data }, query:${serverUrl .query }, fragment:${serverUrl .fragment }" );
685+ }
686+ }
687+
688+ return encodedImageUrl;
689+ }
0 commit comments