diff --git a/android/.gitignore b/android/.gitignore index 0a741cb..7ae15b8 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -9,3 +9,4 @@ GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties +/build/.last_build_id diff --git a/android/app/build.gradle b/android/app/build.gradle index bc9c6cf..60f3056 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { @@ -37,13 +34,13 @@ android { compileOptions { // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true - // Sets Java compatibility to Java 8 - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + // Sets Java compatibility to Java 11 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '11' } sourceSets { @@ -81,7 +78,6 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.window:window:1.0.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' } diff --git a/android/build.gradle b/android/build.gradle index d1b666a..b42eb3a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bc..138ec95 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.1.3" apply false + id "org.jetbrains.kotlin.android" version "1.9.22" apply false +} + +include ":app" \ No newline at end of file diff --git a/l10n/app_de.arb b/l10n/app_de.arb index 588d83c..99ebe7c 100644 --- a/l10n/app_de.arb +++ b/l10n/app_de.arb @@ -1,6 +1,5 @@ { "tabBar_albums": "Alben", - "settings_language": "Wählen Sie die Sprache", "tabBar_upload": "Hochladen", "tabBar_preferences": "Einstellungen", "alertOkButton": "OK", diff --git a/l10n/app_zh.arb b/l10n/app_zh.arb index 4b9bbab..31287e4 100644 --- a/l10n/app_zh.arb +++ b/l10n/app_zh.arb @@ -20,7 +20,7 @@ "loadingHUD_label": "正在加载…", "completeHUD_label": "完成", "errorHUD_label": "出错", - "loadMoreHUD_label": "Release to load more", + "loadMoreHUD_label": "松开以加载更多", "uploadRights_title": "需要上传权限", "uploadRights_message": "您必须拥有上传权限才能上传照片或视频。", "internetErrorGeneral_title": "连接出错", @@ -48,16 +48,16 @@ "login_newSession": "正在打开会话", "login_communityParameters": "社区参数", "login_serverParameters": "Piwigo参数", - "login_advancedParameters": "Authentication Settings", + "login_advancedParameters": "验证设置", "login_connectionChanged": "连接已变更!", - "login_rememberCredentials": "Remember credentials", + "login_rememberCredentials": "记住凭据", "loginHTTP_title": "HTTP 证书", "loginHTTP_message": "Piwigo服务器需要基本访问验证:", - "loginHTTP_enable": "Enable HTTP Basic", + "loginHTTP_enable": "使能 HTTP Basic", "loginHTTPuser_placeholder": "用户名", "loginHTTPpwd_placeholder": "密码", - "loginCert_title": "Self Signed Certificates", - "loginCert_enable": "Allow SSL", + "loginCert_title": "自签名证书", + "loginCert_enable": "允许 SSL", "loginCertFailed_title": "非私密连接", "loginCertFailed_message": "Piwigo警告网站证书无效。您仍然想要接受此证书吗?", "loginHTTPSfailed_title": "安全连接失败", @@ -153,7 +153,7 @@ "categoryUpload_loadSubCategories": "加载", "categoryUpload_images": "上传图片", "categoryUpload_videos": "上传视频", - "categoryUpload_camera": "Camera", + "categoryUpload_camera": "相机", "categoryUpload_takePhoto": "拍照", "categoryUpload_takeVideo": "录制", "uploadList_title": "上传状态", @@ -242,23 +242,23 @@ "moveCategoryHUD_moved": "相册已移动", "moveCategoryError_title": "移动失败", "moveCategoryError_message": "移动相册失败", - "categoryPrivacy": "Manage Permissions", - "categoryPrivacy_subtitle": "Manage access permissions of \"{album_name}\".", + "categoryPrivacy": "管理权限", + "categoryPrivacy_subtitle": "管理 \"{album_name}\" 的访问权限", "@categoryPrivacy_subtitle": { "placeholders": { "album_name": {} } }, - "categoryPrivacyMode_public": "Public", - "categoryPrivacyMode_publicMessage": "Every user can see this album.", - "categoryPrivacyMode_private": "Private", - "categoryPrivacyMode_privateMessage": "Visitors must log in and have the necessary permissions to see this album.", - "categoryPrivacyGroups": "Group permissions", - "categoryPrivacyGroups_add": "Authorize groups", - "categoryPrivacyUsers": "User permissions", - "categoryPrivacyUsers_message": "To manage user permissions, go to your web administration.", - "categoryPrivacyRecursive": "Apply to sub-albums", - "categoryPrivacyRecursive_message": "After confirmation, all modifications will be applied to sub-albums.", + "categoryPrivacyMode_public": "公开的", + "categoryPrivacyMode_publicMessage": "每个用户都可以看到此相册。", + "categoryPrivacyMode_private": "私有的", + "categoryPrivacyMode_privateMessage": "访客必须登录并拥有查看此相册的必要权限。", + "categoryPrivacyGroups": "组权限", + "categoryPrivacyGroups_add": "授权组", + "categoryPrivacyUsers": "用户权限", + "categoryPrivacyUsers_message": "要管理用户权限,请前往您的网站管理界面。", + "categoryPrivacyRecursive": "应用到子相册", + "categoryPrivacyRecursive_message": "确认后,所有修改都将应用于子相册。", "categorySelection_setThumbnail": "请选择要使用照片 {photo} 作为缩略图的相册。", "@categorySelection_setThumbnail": { "placeholders": { @@ -364,17 +364,17 @@ "tagsAdd_placeholder": "新标签", "tagsAddHUD_label": "创建标签中...", "tagsAddHUD_created": "标签已创建", - "group": "Group", - "groups": "Groups", - "groupsTitle_selectOne": "Select a Group", - "groupsHeader_selected": "Selected", - "groupsHeader_notSelected": "Not Selected", - "groupsHeader_all": "All Groups", - "groupsAdd_title": "Add Group", - "groupsAdd_message": "Enter a name for this new group", - "groupsAdd_placeholder": "New group", - "groupsAddHUD_label": "Creating Group…", - "groupsAddHUD_created": "Group Created", + "group": "组", + "groups": "组", + "groupsTitle_selectOne": "选择一个组", + "groupsHeader_selected": "已选中", + "groupsHeader_notSelected": "未选中", + "groupsHeader_all": "所有组", + "groupsAdd_title": "添加组", + "groupsAdd_message": "为新增的组设置名称", + "groupsAdd_placeholder": "新增组", + "groupsAddHUD_label": "正在创建组…", + "groupsAddHUD_created": "已创建组", "tagsAddError_message": "无法创建新标签", "tagsAddError_title": "创建失败", "selectImages": "选择图片", diff --git a/lib/app.dart b/lib/app.dart index e7f02d0..8224c75 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -16,6 +16,7 @@ import 'package:piwigo_ng/views/image/edit_image_page.dart'; import 'package:piwigo_ng/views/image/image_favorites_page.dart'; import 'package:piwigo_ng/views/image/image_page.dart'; import 'package:piwigo_ng/views/image/image_search_page.dart'; +import 'package:piwigo_ng/views/image/image_tags_page.dart'; import 'package:piwigo_ng/views/image/video_player_page.dart'; import 'package:piwigo_ng/views/settings/auto_upload_page.dart'; import 'package:piwigo_ng/views/settings/privacy_policy_page.dart'; @@ -147,6 +148,14 @@ Route generateRoute(RouteSettings settings) { ), settings: settings, ); + case ImageTagsPage.routeName: + return MaterialPageRoute( + builder: (_) => ImageTagsPage( + isAdmin: arguments['isAdmin'] ?? isAdmin, + tag: arguments["tag"], + ), + settings: settings, + ); case ImageFavoritesPage.routeName: return MaterialPageRoute( builder: (_) => ImageFavoritesPage( diff --git a/lib/components/appbars/root_search_app_bar.dart b/lib/components/appbars/root_search_app_bar.dart index 1208940..d12413f 100644 --- a/lib/components/appbars/root_search_app_bar.dart +++ b/lib/components/appbars/root_search_app_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:piwigo_ng/components/modals/open_tag_modal.dart'; import 'package:piwigo_ng/components/notification_dot.dart'; import 'package:piwigo_ng/components/popup_list_item.dart'; import 'package:piwigo_ng/services/preferences_service.dart'; @@ -40,9 +41,7 @@ class _RootSearchAppBarState extends State { if (widget.scrollController.offset > _expandedHeight * _opacityScale) { return 0.0; } - return (_expandedHeight * _opacityScale - - widget.scrollController.offset) / - (_expandedHeight * _opacityScale); + return (_expandedHeight * _opacityScale - widget.scrollController.offset) / (_expandedHeight * _opacityScale); } return 1.0; } @@ -58,8 +57,7 @@ class _RootSearchAppBarState extends State { } // In case 0%-100% of the expanded height is viewed - double scrollDelta = - (_expandedHeight - widget.scrollController.offset) / _expandedHeight; + double scrollDelta = (_expandedHeight - widget.scrollController.offset) / _expandedHeight; double scrollPercent = (scrollDelta * 2 - 1); return (1 - scrollPercent) * delta * basePadding + basePadding; } @@ -71,8 +69,7 @@ class _RootSearchAppBarState extends State { Widget build(BuildContext context) { return SliverAppBar( leading: IconButton( - onPressed: () => - Navigator.of(context).pushNamed(SettingsPage.routeName), + onPressed: () => Navigator.of(context).pushNamed(SettingsPage.routeName), icon: const Icon(Icons.settings), ), pinned: true, @@ -90,7 +87,6 @@ class _RootSearchAppBarState extends State { child: AppField( padding: const EdgeInsets.symmetric(vertical: 8.0), prefix: Icon(Icons.search), - hint: "Search...", ), ), ), @@ -108,7 +104,7 @@ class _RootSearchAppBarState extends State { ), title: Text( appStrings.tabBar_albums, - textScaleFactor: 1, + textScaler: TextScaler.linear(1), style: Theme.of(context).appBarTheme.titleTextStyle, ), ), @@ -125,8 +121,7 @@ class _RootSearchAppBarState extends State { PopupMenuItem( onTap: () => Future.delayed( const Duration(seconds: 0), - () => - Navigator.of(context).pushNamed(UploadStatusPage.routeName), + () => Navigator.of(context).pushNamed(UploadStatusPage.routeName), ), child: Stack( children: [ @@ -137,8 +132,7 @@ class _RootSearchAppBarState extends State { Positioned( top: 14.0, left: 0.0, - child: Consumer( - builder: (context, uploadNotifier, child) { + child: Consumer(builder: (context, uploadNotifier, child) { return NotificationDot( isShown: uploadNotifier.uploadList.isNotEmpty, ); @@ -147,12 +141,21 @@ class _RootSearchAppBarState extends State { ], ), ), + PopupMenuItem( + onTap: () => Future.delayed( + const Duration(seconds: 0), + () => showOpenTagModal(context), + ), + child: PopupListItem( + icon: Icons.local_offer_outlined, + text: appStrings.tags, + ), + ), if (Preferences.getUserStatus != 'guest') PopupMenuItem( onTap: () => Future.delayed( const Duration(seconds: 0), - () => Navigator.of(context) - .pushNamed(ImageFavoritesPage.routeName), + () => Navigator.of(context).pushNamed(ImageFavoritesPage.routeName), ), child: PopupListItem( icon: Icons.favorite, @@ -164,8 +167,7 @@ class _RootSearchAppBarState extends State { Positioned( top: 12.0, left: 12.0, - child: Consumer( - builder: (context, uploadNotifier, child) { + child: Consumer(builder: (context, uploadNotifier, child) { return NotificationDot( isShown: uploadNotifier.uploadList.isNotEmpty, ); diff --git a/lib/components/appbars/settings_app_bar.dart b/lib/components/appbars/settings_app_bar.dart index 7ea496a..b260357 100644 --- a/lib/components/appbars/settings_app_bar.dart +++ b/lib/components/appbars/settings_app_bar.dart @@ -33,8 +33,7 @@ class _SettingsAppBarState extends State { } // In case 0%-100% of the expanded height is viewed - double scrollDelta = - (_expandedHeight - widget.scrollController.offset) / _expandedHeight; + double scrollDelta = (_expandedHeight - widget.scrollController.offset) / _expandedHeight; double scrollPercent = (scrollDelta * 2 - 1); return (1 - scrollPercent) * delta * basePadding + basePadding; } @@ -59,7 +58,7 @@ class _SettingsAppBarState extends State { ), title: Text( appStrings.tabBar_preferences, - textScaleFactor: 1, + textScaler: TextScaler.linear(1), style: Theme.of(context).appBarTheme.titleTextStyle, ), ), diff --git a/lib/components/cards/image_card.dart b/lib/components/cards/image_card.dart index 1c3a469..0cad66a 100644 --- a/lib/components/cards/image_card.dart +++ b/lib/components/cards/image_card.dart @@ -80,7 +80,7 @@ class ImageCard extends StatelessWidget { end: Alignment.topCenter), ), child: AutoSizeText( - image.name, + image.name ?? "", maxLines: 1, maxFontSize: 14, minFontSize: 8, diff --git a/lib/components/cards/image_details_card.dart b/lib/components/cards/image_details_card.dart index 197a75b..9197448 100644 --- a/lib/components/cards/image_details_card.dart +++ b/lib/components/cards/image_details_card.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'dart:ui' as ui show Codec, FrameInfo, Image; +import 'dart:ui' as ui show Image; import 'package:auto_size_text/auto_size_text.dart'; import 'package:extended_text/extended_text.dart'; @@ -17,8 +17,7 @@ import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; class ImageDetailsCard extends StatelessWidget { - const ImageDetailsCard({Key? key, required this.image, this.onRemove}) - : super(key: key); + const ImageDetailsCard({Key? key, required this.image, this.onRemove}) : super(key: key); final ImageModel image; final Function()? onRemove; @@ -57,9 +56,7 @@ class ImageDetailsCard extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(5.0), child: Builder(builder: (context) { - final String? imageUrl = image - .getDerivativeFromString(Preferences.getImageThumbnailSize) - ?.url; + final String? imageUrl = image.getDerivativeFromString(Preferences.getImageThumbnailSize)?.url; return ImageNetworkDisplay( imageUrl: imageUrl, ); @@ -91,10 +88,7 @@ class ImageDetailsCard extends StatelessWidget { children: [ Flexible( child: Text( - image.file - .replaceAll('', '\u200B') - .split(path.extension(image.file)) - .first, + image.file.replaceAll('', '\u200B').split(path.extension(image.file)).first, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, @@ -112,13 +106,11 @@ class ImageDetailsCard extends StatelessWidget { const Spacer(), if (image.dateAvailable != null) Builder(builder: (context) { - LocaleNotifier localeNotifier = - Provider.of(context, listen: false); + LocaleNotifier localeNotifier = Provider.of(context, listen: false); String date = - DateFormat.yMMMMd(localeNotifier.locale.languageCode) - .format(DateTime.parse(image.dateAvailable!)); - String time = DateFormat.Hms(localeNotifier.locale.languageCode) - .format(DateTime.parse(image.dateAvailable!)); + DateFormat.yMMMMd(localeNotifier.locale.languageCode).format(DateTime.parse(image.dateAvailable!)); + String time = + DateFormat.Hms(localeNotifier.locale.languageCode).format(DateTime.parse(image.dateAvailable!)); return AutoSizeText( "$date $time", maxLines: 1, @@ -150,8 +142,7 @@ class ImageDetailsCard extends StatelessWidget { } class LocalImageDetailsCard extends StatefulWidget { - const LocalImageDetailsCard( - {Key? key, required this.image, this.onRemove, this.isDuplicate = false}) + const LocalImageDetailsCard({Key? key, required this.image, this.onRemove, this.isDuplicate = false}) : super(key: key); final File image; @@ -214,17 +205,12 @@ class _LocalImageDetailsCardState extends State { fit: StackFit.expand, children: [ LayoutBuilder(builder: (context, constraints) { - List? mimeType = - mime(widget.image.path.split('/').last)?.split('/'); + List? mimeType = mime(widget.image.path.split('/').last)?.split('/'); if (mimeType?.first == 'image') { _checkMemory(); - double? cacheWidth = constraints.maxWidth.isInfinite - ? constraints.maxWidth - : null; - double? cacheHeight = constraints.maxHeight.isInfinite - ? constraints.maxHeight - : null; + double? cacheWidth = constraints.maxWidth.isInfinite ? constraints.maxWidth : null; + double? cacheHeight = constraints.maxHeight.isInfinite ? constraints.maxHeight : null; return Image.file( widget.image, fit: BoxFit.cover, @@ -327,8 +313,7 @@ class _LocalImageDetailsCardState extends State { } class LocalVideoDetailsCard extends StatefulWidget { - const LocalVideoDetailsCard( - {Key? key, required this.video, this.onRemove, this.isDuplicate = false}) + const LocalVideoDetailsCard({Key? key, required this.video, this.onRemove, this.isDuplicate = false}) : super(key: key); final File video; @@ -361,9 +346,7 @@ class _LocalVideoDetailsCardState extends State { final Duration duration = _controller.value.duration; int hours = duration.inHours; int minutes = (duration - Duration(hours: hours)).inMinutes; - int seconds = - (duration - Duration(hours: hours) - Duration(minutes: minutes)) - .inSeconds; + int seconds = (duration - Duration(hours: hours) - Duration(minutes: minutes)).inSeconds; return '${hours > 0 ? '$hours:' : ''}${minutes < 10 ? '0$minutes' : '$minutes'}:${seconds < 10 ? '0$seconds' : '$seconds'}'; } @@ -433,17 +416,12 @@ class _LocalVideoDetailsCardState extends State { bottom: 2.0, left: 2.0, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 2), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: AppColors.black.withOpacity(0.7)), + borderRadius: BorderRadius.circular(5), color: AppColors.black.withOpacity(0.7)), child: Text( _duration, - style: TextStyle( - color: AppColors.white, - fontSize: 10, - fontWeight: FontWeight.bold), + style: TextStyle(color: AppColors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), diff --git a/lib/components/dialogs/confirm_dialog.dart b/lib/components/dialogs/confirm_dialog.dart index d335380..a2fdef4 100644 --- a/lib/components/dialogs/confirm_dialog.dart +++ b/lib/components/dialogs/confirm_dialog.dart @@ -27,12 +27,10 @@ class ConfirmDialog extends StatelessWidget { actions: [ TextButton( style: ButtonStyle( - foregroundColor: MaterialStateColor.resolveWith( - (states) => - Theme.of(context).textTheme.bodySmall?.color ?? - AppColors.disabled, + foregroundColor: WidgetStateColor.resolveWith( + (states) => Theme.of(context).textTheme.bodySmall?.color ?? AppColors.disabled, ), - overlayColor: MaterialStateColor.resolveWith( + overlayColor: WidgetStateColor.resolveWith( (states) => AppColors.accent.withOpacity(0.3), ), ), @@ -43,10 +41,10 @@ class ConfirmDialog extends StatelessWidget { ), TextButton( style: ButtonStyle( - foregroundColor: MaterialStateColor.resolveWith( + foregroundColor: WidgetStateColor.resolveWith( (states) => confirmColor ?? AppColors.accent, ), - overlayColor: MaterialStateColor.resolveWith( + overlayColor: WidgetStateColor.resolveWith( (states) => AppColors.accent.withOpacity(0.3), ), ), diff --git a/lib/components/dialogs/image_comment_dialog.dart b/lib/components/dialogs/image_comment_dialog.dart index 00b8c42..5544ab6 100644 --- a/lib/components/dialogs/image_comment_dialog.dart +++ b/lib/components/dialogs/image_comment_dialog.dart @@ -59,7 +59,7 @@ class _ImageCommentDialogState extends State { curve: Curves.ease, opacity: isNameHidden ? 1.0 : 0.0, child: Text( - widget.image.name, + widget.image.name ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium, @@ -78,7 +78,7 @@ class _ImageCommentDialogState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - widget.image.name, + widget.image.name ?? "", textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), diff --git a/lib/components/modals/image_info_modal.dart b/lib/components/modals/image_info_modal.dart index 9777cef..c5bef46 100644 --- a/lib/components/modals/image_info_modal.dart +++ b/lib/components/modals/image_info_modal.dart @@ -45,7 +45,7 @@ class ImageInfoModal extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - image.name, + image.name ?? "", style: Theme.of(context).textTheme.titleMedium, ), ), diff --git a/lib/components/modals/move_or_copy_modal.dart b/lib/components/modals/move_or_copy_modal.dart index 01f1e5c..f52a667 100644 --- a/lib/components/modals/move_or_copy_modal.dart +++ b/lib/components/modals/move_or_copy_modal.dart @@ -26,7 +26,6 @@ class MoveOrCopyModal extends StatefulWidget { } class _MoveOrCopyModalState extends State { - final ScrollController _scrollController = ScrollController(); late final Future>> _albumFuture; late final List _disabledAlbums; @@ -39,8 +38,7 @@ class _MoveOrCopyModalState extends State { _disabledAlbums = [ if (widget.album != null) widget.album!.id, if (parentAlbums.length == 1 || widget.isImage) 0, - if (!widget.isImage && parentAlbums.length > 1) - int.parse(parentAlbums[parentAlbums.length - 2]), + if (!widget.isImage && parentAlbums.length > 1) int.parse(parentAlbums[parentAlbums.length - 2]), ]; super.initState(); @@ -213,9 +211,7 @@ class _ExpansionAlbumTileState extends State { child: Text( "${List.generate(widget.index, (index) => '.').join()}${widget.index > 0 ? ' ' : ''}${widget.album.name}", overflow: TextOverflow.ellipsis, - style: _disabled - ? Theme.of(context).textTheme.bodySmall - : Theme.of(context).textTheme.bodyMedium, + style: _disabled ? Theme.of(context).textTheme.bodySmall : Theme.of(context).textTheme.bodyMedium, ), ), ), @@ -235,9 +231,7 @@ class _ExpansionAlbumTileState extends State { child: Text( appStrings.albumCount(widget.album.nbCategories), style: TextStyle( - color: Theme.of(context) - .primaryColor - .withOpacity(0.7), + color: Theme.of(context).primaryColor.withOpacity(0.7), fontSize: 14, ), ), diff --git a/lib/components/modals/open_tag_modal.dart b/lib/components/modals/open_tag_modal.dart new file mode 100644 index 0000000..5e3a6b1 --- /dev/null +++ b/lib/components/modals/open_tag_modal.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:piwigo_ng/components/fields/app_field.dart'; +import 'package:piwigo_ng/models/tag_model.dart'; +import 'package:piwigo_ng/network/api_error.dart'; +import 'package:piwigo_ng/network/tags.dart'; +import 'package:piwigo_ng/utils/localizations.dart'; +import 'package:piwigo_ng/views/image/image_tags_page.dart'; + +class OpenTagModal extends StatefulWidget { + const OpenTagModal({super.key}); + + @override + _OpenTagModalState createState() => _OpenTagModalState(); +} + +class _OpenTagModalState extends State { + final ScrollController _scrollController = ScrollController(); + late final Future _tagsFuture; + + String _searchQuery = ''; + List? _tagList; + + @override + void initState() { + super.initState(); + _tagsFuture = _onRefresh(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Future _onRefresh() async { + try { + final ApiResponse> result = await getTags(); + if (!result.hasData) return; + setState(() { + _tagList = result.data!.where((tag) => tag.counter > 0).toList()..sort((a, b) => a.name.compareTo(b.name)); + }); + } catch (e) { + setState(() { + _tagList = null; + }); + } + } + + void _onSelectTag(TagModel tag) { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed( + ImageTagsPage.routeName, + arguments: { + 'tag': tag, + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(15.0), + ), + ), + elevation: 0.0, + scrolledUnderElevation: 5.0, + leading: IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text(appStrings.tags), + ), + body: ListView( + controller: ModalScrollController.of(context), + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: AppField( + padding: const EdgeInsets.symmetric(vertical: 8.0), + prefix: Icon(Icons.search), + onChanged: (query) => setState(() { + _searchQuery = query; + }), + ), + ), + FutureBuilder( + future: _tagsFuture, + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return _buildTagList(); + default: + return Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ], + ), + ); + } + + Widget _buildTagList() { + if (_tagList == null) { + return Center( + child: Text(appStrings.coreDataFetch_TagError), + ); + } + + List tags = + _tagList!.where((tag) => tag.name.toLowerCase().contains(_searchQuery.toLowerCase())).toList(); + if (tags.isEmpty) { + return Center( + child: Text(appStrings.none), + ); + } + + return Column( + children: tags.map((tag) => _buildItem(tag)).toList(), + ); + } + + Widget _buildItem(TagModel tag) => ListTile( + visualDensity: VisualDensity.compact, + shape: Border( + bottom: BorderSide(color: Theme.of(context).scaffoldBackgroundColor), + ), + title: Text(tag.name), + trailing: Text(appStrings.imageCount(tag.counter)), + onTap: () => _onSelectTag(tag), + ); +} + +Future showOpenTagModal(BuildContext context) async { + return showMaterialModalBottomSheet( + context: context, + enableDrag: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(30.0)), + ), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + builder: (context) => OpenTagModal(), + ); +} diff --git a/lib/components/modals/select_tags_modal.dart b/lib/components/modals/select_tags_modal.dart index 69e3d9e..56d291b 100644 --- a/lib/components/modals/select_tags_modal.dart +++ b/lib/components/modals/select_tags_modal.dart @@ -41,11 +41,10 @@ class _SelectTagsModalState extends State { super.dispose(); } - List get _unselectedTagList => - _tagList!.where((t) => !_selectedTagList.contains(t)).toList(); + List get _unselectedTagList => _tagList!.where((t) => !_selectedTagList.contains(t)).toList(); Future _onRefresh() async { - final ApiResponse> result = await getTags(); + final ApiResponse> result = await getAdminTags(); if (!result.hasData) return; setState(() { _tagList = result.data!; diff --git a/lib/models/image_model.dart b/lib/models/image_model.dart index 2754887..4b719af 100644 --- a/lib/models/image_model.dart +++ b/lib/models/image_model.dart @@ -8,7 +8,7 @@ class ImageModel { int hit; bool favorite; String file; - String name; + String? name; String? comment; String? dateCreation; String? dateAvailable; @@ -25,7 +25,7 @@ class ImageModel { this.hit = 0, this.favorite = false, this.file = '', - required this.name, + this.name, this.comment, this.dateCreation, this.dateAvailable, @@ -43,7 +43,7 @@ class ImageModel { hit = int.tryParse(json['hit'].toString()) ?? 0, favorite = json['is_favorite'] ?? false, file = json['file'].toString(), - name = json['name'].toString(), + name = json['name']?.toString(), comment = json['comment'], dateCreation = json['date_creation'], dateAvailable = json['date_available'], diff --git a/lib/network/albums.dart b/lib/network/albums.dart index c601a9b..fa80a34 100644 --- a/lib/network/albums.dart +++ b/lib/network/albums.dart @@ -27,12 +27,10 @@ Future>> fetchAlbums(int albumID) async { ); if (response.statusCode == 200) { - List jsonAlbums = - tryParseJson(response.data)['result']['categories']; + List jsonAlbums = tryParseJson(response.data)['result']['categories']; List albums = List.from(jsonAlbums.map( (album) { - bool canUpload = - appPreferences.getBool(Preferences.isAdminKey) ?? false; + bool canUpload = appPreferences.getBool(Preferences.isAdminKey) ?? false; album['can_upload'] = canUpload; return AlbumModel.fromJson(album); }, @@ -47,8 +45,7 @@ Future>> fetchAlbums(int albumID) async { } if (communityResult.hasData) { for (AlbumModel communityAlbum in communityResult.data) { - int index = - albums.indexWhere((album) => album.id == communityAlbum.id); + int index = albums.indexWhere((album) => album.id == communityAlbum.id); if (index >= 0) { AlbumModel newAlbum = albums.elementAt(index); newAlbum.canUpload = true; @@ -65,7 +62,7 @@ Future>> fetchAlbums(int albumID) async { data: albums, ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint("$e"); @@ -86,8 +83,7 @@ Future>> fetchCommunityAlbums(int albumID) async { ); if (response.statusCode == 200) { - List jsonAlbums = - json.decode(response.data)['result']['categories']; + List jsonAlbums = json.decode(response.data)['result']['categories']; List albums = List.from(jsonAlbums.map( (album) => AlbumModel.fromJson(album), )); @@ -96,7 +92,7 @@ Future>> fetchCommunityAlbums(int albumID) async { data: albums, ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint("$e"); @@ -129,7 +125,7 @@ Future>> getAlbumTree([int? startId]) async { data: albums, ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint("$e"); @@ -164,7 +160,7 @@ Future> addAlbum({ } return ApiResponse(data: true); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint("$e"); @@ -197,7 +193,7 @@ Future> moveAlbum(int catId, int parentCatId) async { } return ApiResponse(data: true); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("${e.message}"); } catch (e) { debugPrint("$e"); @@ -239,7 +235,7 @@ Future> editAlbum({ print(data); return ApiResponse(data: true); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("${e.message}"); } catch (e) { debugPrint("$e"); @@ -276,7 +272,7 @@ Future> deleteAlbum( } return ApiResponse(data: true); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("${e.message}"); } catch (e) { debugPrint("$e"); diff --git a/lib/network/api_client.dart b/lib/network/api_client.dart index 812d306..b472b9f 100644 --- a/lib/network/api_client.dart +++ b/lib/network/api_client.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; -import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:piwigo_ng/services/preferences_service.dart'; @@ -17,13 +17,9 @@ class ApiClient { ..interceptors.add(CookieManager(cookieJar)) ..httpClientAdapter = sslHttpClientAdapter; - static HttpClientAdapter get sslHttpClientAdapter { - return DefaultHttpClientAdapter() - ..onHttpClientCreate = (HttpClient client) { - client.badCertificateCallback = piwigoSSLBypass; - return client; - }; - } + static HttpClientAdapter get sslHttpClientAdapter => IOHttpClientAdapter( + createHttpClient: () => HttpClient()..badCertificateCallback = piwigoSSLBypass, + ); static bool piwigoSSLBypass(X509Certificate cert, String host, int port) { if (Preferences.getEnableSSL) { @@ -138,8 +134,7 @@ class ApiClient { class SSLHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) { - return super.createHttpClient(context) - ..badCertificateCallback = ApiClient.piwigoSSLBypass; + return super.createHttpClient(context)..badCertificateCallback = ApiClient.piwigoSSLBypass; } } diff --git a/lib/network/api_interceptor.dart b/lib/network/api_interceptor.dart index 7abe58b..a84a1e1 100644 --- a/lib/network/api_interceptor.dart +++ b/lib/network/api_interceptor.dart @@ -21,8 +21,7 @@ class ApiInterceptor extends Interceptor { if (prefs.getBool(Preferences.enableBasicAuthKey) ?? false) { String? username = prefs.getString(Preferences.basicUsernameKey) ?? ''; String? password = prefs.getString(Preferences.basicPasswordKey) ?? ''; - String basicAuth = - "Basic ${base64.encode(utf8.encode('$username:$password'))}"; + String basicAuth = "Basic ${base64.encode(utf8.encode('$username:$password'))}"; options.headers['authorization'] = basicAuth; } return super.onRequest(options, handler); @@ -33,18 +32,16 @@ class ApiInterceptor extends Interceptor { Response response, ResponseInterceptorHandler handler, ) async { - debugPrint( - "[${response.statusCode}] ${response.requestOptions.queryParameters['method']}"); + debugPrint("[${response.statusCode}] ${response.requestOptions.queryParameters['method']}"); return super.onResponse(response, handler); } @override void onError( - DioError err, + DioException err, ErrorInterceptorHandler handler, ) async { - debugPrint( - "[${err.response?.statusCode}] ${err.requestOptions.queryParameters['method']}"); + debugPrint("[${err.response?.statusCode}] ${err.requestOptions.queryParameters['method']}"); debugPrint('${err.error}\n${err.response?.data}\n${err.stackTrace}'); switch (err.response?.statusCode) { @@ -61,8 +58,7 @@ class ApiInterceptor extends Interceptor { if (err.error is HandshakeException) { HandshakeException handshakeError = err.error as HandshakeException; String? message = handshakeError.osError?.message; - if (message != null && - message.contains('CERTIFICATE_VERIFY_FAILED')) { + if (message != null && message.contains('CERTIFICATE_VERIFY_FAILED')) { App.scaffoldMessengerKey.currentState?.showSnackBar( errorSnackBar( message: appStrings.loginCertFailed_title, diff --git a/lib/network/authentication.dart b/lib/network/authentication.dart index bf82f48..f20468c 100644 --- a/lib/network/authentication.dart +++ b/lib/network/authentication.dart @@ -23,7 +23,7 @@ Future> pingAPI() async { if (data['stat'] == 'ok') { return ApiResponse(data: data['result']); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -88,15 +88,14 @@ Future> loginUser( } ApiResponse status = await sessionStatus(); if (status.hasData) { - Preferences.saveId(status.data!, - username: username, password: password); + Preferences.saveId(status.data!, username: username, password: password); } askMediaPermission(); return ApiResponse( data: true, ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -108,10 +107,7 @@ Future> loginUser( } Future> sessionStatus() async { - Map queries = { - 'format': 'json', - 'method': 'pwg.session.getStatus' - }; + Map queries = {'format': 'json', 'method': 'pwg.session.getStatus'}; try { Response response = await ApiClient.get(queryParameters: queries); @@ -125,7 +121,7 @@ Future> sessionStatus() async { data: StatusModel.fromJson(data['result']), ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Session Status Error: $e'); @@ -139,10 +135,7 @@ Future> sessionStatus() async { } Future communityStatus() async { - Map queries = { - 'format': 'json', - 'method': 'community.session.getStatus' - }; + Map queries = {'format': 'json', 'method': 'community.session.getStatus'}; try { Response response = await ApiClient.get(queryParameters: queries); @@ -150,7 +143,7 @@ Future communityStatus() async { if (data['stat'] == 'ok') { return data['result']['real_user_status']; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -169,7 +162,7 @@ Future> getInfo() async { data: InfoModel.fromJson(data['result']), ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -180,18 +173,14 @@ Future> getInfo() async { } Future>> getMethods() async { - Map queries = { - 'format': 'json', - 'method': 'reflection.getMethodList' - }; + Map queries = {'format': 'json', 'method': 'reflection.getMethodList'}; try { Response response = await ApiClient.get(queryParameters: queries); Map data = json.decode(response.data); - final List methods = - data['result']['methods'].map((e) => e.toString()).toList(); + final List methods = data['result']['methods'].map((e) => e.toString()).toList(); return ApiResponse>(data: methods); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); diff --git a/lib/network/groups.dart b/lib/network/groups.dart index 6f45009..b3fd988 100644 --- a/lib/network/groups.dart +++ b/lib/network/groups.dart @@ -57,7 +57,7 @@ Future?> getAllGroups({ } return groupsResponse; - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch all groups: ${e.message}'); } on Error catch (e) { debugPrint('Fetch all groups: ${e.stackTrace}'); @@ -89,7 +89,7 @@ Future?>> getGroups([int page = 0]) async { data: groups, ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch groups: ${e.message}'); } on Error catch (e) { debugPrint('Fetch groups: ${e.stackTrace}'); diff --git a/lib/network/images.dart b/lib/network/images.dart index 1cee573..033a7a6 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -44,7 +44,7 @@ Future> getImage( ImageModel image = ImageModel.fromJson(jsonImage); return ApiResponse(data: image); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch images: ${e.message}'); } on Error catch (e) { debugPrint('Fetch images: $e\n${e.stackTrace}'); @@ -76,7 +76,7 @@ Future>> fetchImages( return ApiResponse>(data: images); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch images: ${e.message}'); } catch (e) { debugPrint('Fetch images: $e'); @@ -117,7 +117,7 @@ Future> searchImages( 'images': images, }); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Search images: ${e.message}'); } on Error catch (e) { debugPrint('Search images: ${e.stackTrace}'); @@ -159,7 +159,7 @@ Future> fetchFavorites([ 'images': images, }); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch favorites: ${e.message}'); } on Error catch (e) { debugPrint('Fetch favorites: ${e.stackTrace}'); @@ -167,6 +167,49 @@ Future> fetchFavorites([ return ApiResponse(error: ApiErrors.error); } +Future> fetchTagImages(int tagID, [int page = 0]) async { + Map query = { + "format": "json", + "method": "pwg.tags.getImages", + "tag_id": tagID.toString(), + "per_page": "100", + "page": page.toString(), + }; + + try { + Response response = await ApiClient.get(queryParameters: query); + + if (response.statusCode == 200) { + final Map result = json.decode(response.data); + if (result['stat'] == 'fail') { + return ApiResponse(data: { + 'total_count': 0, + 'images': [], + }); + } + final jsonImages = result['result']['images']; + List images = List.from( + jsonImages.map((image) { + image['tags'] = null; + return ImageModel.fromJson(image); + }), + ); + + print(result['result']['paging']); + + return ApiResponse(data: { + 'total_count': result['result']['paging']['total_count'], + 'images': images, + }); + } + } on DioException catch (e) { + debugPrint('Fetch tag images: ${e.message}'); + } on Error catch (e) { + debugPrint('Fetch tag images: ${e.stackTrace}'); + } + return ApiResponse(error: ApiErrors.error); +} + Future pickDirectoryPath() async { return await FilePicker.platform.getDirectoryPath(); } @@ -185,12 +228,8 @@ Future _showDownloadNotification({ ); await showLocalNotification( id: 0, - title: success - ? appStrings.downloadImageSuccess_title - : appStrings.downloadImageFail_title, - body: success - ? appStrings.downloadImageSuccess_message - : appStrings.deleteImageFail_message, + title: success ? appStrings.downloadImageSuccess_title : appStrings.downloadImageFail_title, + body: success ? appStrings.downloadImageSuccess_message : appStrings.deleteImageFail_message, details: android, payload: payload, ); @@ -263,7 +302,7 @@ Future downloadImage( name: image.name, ); return XFile(localPath); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("Download images: ${e.message}"); } on Error catch (e) { debugPrint("Download images: ${e.stackTrace}"); @@ -318,7 +357,7 @@ Future deleteImage( if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Delete images: ${e.message}'); } on Error catch (e) { debugPrint('Delete images: ${e.stackTrace}'); @@ -344,8 +383,7 @@ Future removeImage( ImageModel image, int albumId, ) async { - final List albums = - image.categories.map((album) => album['id']).toList(); + final List albums = image.categories.map((album) => album['id']).toList(); albums.removeWhere((album) => album == albumId); if (albums.isEmpty) { @@ -363,13 +401,12 @@ Future removeImage( }); try { - Response response = - await ApiClient.post(data: formData, queryParameters: queries); + Response response = await ApiClient.post(data: formData, queryParameters: queries); if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Remove images: ${e.message}'); } on Error catch (e) { debugPrint('Remove images: ${e.stackTrace}'); @@ -397,8 +434,7 @@ Future moveImage( int oldAlbumId, int newAlbumId, ) async { - final List albums = - image.categories.map((album) => album['id']).toList(); + final List albums = image.categories.map((album) => album['id']).toList(); albums.removeWhere((id) => id == oldAlbumId); albums.add(newAlbumId); Map queries = { @@ -413,13 +449,12 @@ Future moveImage( }); try { - Response response = - await ApiClient.post(data: formData, queryParameters: queries); + Response response = await ApiClient.post(data: formData, queryParameters: queries); if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Move images: ${e.message}'); } on Error catch (e) { debugPrint('Move images: ${e.stackTrace}'); @@ -433,8 +468,7 @@ Future assignImages( ) async { int nbAssigned = 0; for (ImageModel image in images) { - final List categories = - image.categories.map((album) => album['id']).toList(); + final List categories = image.categories.map((album) => album['id']).toList(); categories.add(albumId); bool response = await assignImage(image.id, categories); if (response == true) { @@ -460,13 +494,12 @@ Future assignImage( }); try { - Response response = - await ApiClient.post(data: formData, queryParameters: queries); + Response response = await ApiClient.post(data: formData, queryParameters: queries); if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Assign images: ${e.message}'); } on Error catch (e) { debugPrint('Assign images: ${e.stackTrace}'); @@ -525,7 +558,7 @@ Future editImage( if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Edit images: ${e.message}'); } on Error catch (e) { debugPrint('Edit images: ${e.stackTrace}'); @@ -566,7 +599,7 @@ Future> checkImagesNotExist( existResult.removeWhere((key, value) => value != null); } return existResult.keys.map((md5sum) => md5sumList[md5sum]!).toList(); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Edit images: ${e.message}'); } on Error catch (e) { debugPrint('Edit images: ${e.stackTrace}'); @@ -610,9 +643,7 @@ String? cleanImageUrl(String? originalUrl) { // So we remove the path to avoid a duplicate if necessary String? loginUrl = appPreferences.getString(Preferences.serverUrlKey); loginUrl = removeUrlProtocol(loginUrl); - if (loginUrl != null && - loginUrl.isNotEmpty && - leftUrl.startsWith(loginUrl)) { + if (loginUrl != null && loginUrl.isNotEmpty && leftUrl.startsWith(loginUrl)) { leftUrl = leftUrl.substring(loginUrl.length); } diff --git a/lib/network/permissions.dart b/lib/network/permissions.dart index 10faeeb..b474b3c 100644 --- a/lib/network/permissions.dart +++ b/lib/network/permissions.dart @@ -10,8 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart'; Future getAlbumPermissions( int albumId, ) async { - List? permissions = - await getPermissions(byAlbum: albumId); + List? permissions = await getPermissions(byAlbum: albumId); if (permissions == null || permissions.isEmpty) { return null; } @@ -52,13 +51,12 @@ Future?> getPermissions({ Map data = json.decode(response.data); var jsonPermissions = data['result']['categories']; List permissions = List.from( - jsonPermissions - .map((permission) => AlbumPermissionModel.fromJson(permission)), + jsonPermissions.map((permission) => AlbumPermissionModel.fromJson(permission)), ); return permissions; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch permissions: ${e.message}'); } on Error catch (e) { debugPrint('Fetch permissions: ${e.stackTrace}'); @@ -103,7 +101,7 @@ Future addPermission({ } return false; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Add permission: ${e.message}'); } on Error catch (e) { debugPrint('Add permission: ${e.stackTrace}'); @@ -146,7 +144,7 @@ Future removePermission({ } return false; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Add permission: ${e.message}'); } on Error catch (e) { debugPrint('Add permission: ${e.stackTrace}'); diff --git a/lib/network/tags.dart b/lib/network/tags.dart index c7ba74c..c038000 100644 --- a/lib/network/tags.dart +++ b/lib/network/tags.dart @@ -4,10 +4,36 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:piwigo_ng/models/tag_model.dart'; import 'package:piwigo_ng/network/api_error.dart'; +import 'package:piwigo_ng/services/preferences_service.dart'; import 'api_client.dart'; Future>> getTags() async { + Map queries = { + 'format': 'json', + 'method': 'pwg.tags.getList', + }; + + Response response = await ApiClient.get(queryParameters: queries); + + try { + if (response.statusCode == 200) { + var data = json.decode(response.data); + if (data['stat'] == 'fail') { + return ApiResponse(error: ApiErrors.error); + } + List tags = data['result']['tags'].map((tag) => TagModel.fromJson(tag)).toList(); + return ApiResponse(data: tags); + } + } on DioException catch (e) { + debugPrint('Get tags: ${e.message}'); + } on Error catch (e) { + debugPrint('Get tags: $e\n${e.stackTrace}'); + } + return ApiResponse(error: ApiErrors.error); +} + +Future>> getAdminTags() async { Map queries = { 'format': 'json', 'method': 'pwg.tags.getAdminList', @@ -21,12 +47,10 @@ Future>> getTags() async { if (data['stat'] == 'fail') { return ApiResponse(error: ApiErrors.error); } - List tags = data['result']['tags'] - .map((tag) => TagModel.fromJson(tag)) - .toList(); + List tags = data['result']['tags'].map((tag) => TagModel.fromJson(tag)).toList(); return ApiResponse(data: tags); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Get tags: ${e.message}'); } on Error catch (e) { debugPrint('Get tags: $e\n${e.stackTrace}'); @@ -53,10 +77,38 @@ Future> createTag(String name) async { data: TagModel.fromJson(data['result']), ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch tags: ${e.message}'); } on Error catch (e) { debugPrint('Fetch tags: $e\n${e.stackTrace}'); } return ApiResponse(error: ApiErrors.error); } + +Future editTag(int tagId, String tagName) async { + Map queries = { + "format": "json", + "method": "pwg.tags.rename", + }; + FormData formData = FormData.fromMap({ + "tag_id": tagId, + "new_name": tagName, + 'pwg_token': appPreferences.getString('PWG_TOKEN'), + }); + Response response = await ApiClient.post(data: formData, queryParameters: queries); + + try { + if (response.statusCode == 200) { + var data = json.decode(response.data); + if (data['stat'] == 'fail') { + return ApiResponse(error: ApiErrors.error); + } + return ApiResponse(data: true); + } + } on DioException catch (e) { + debugPrint('Get tags: ${e.message}'); + } on Error catch (e) { + debugPrint('Get tags: $e\n${e.stackTrace}'); + } + return ApiResponse(error: ApiErrors.error); +} diff --git a/lib/network/upload.dart b/lib/network/upload.dart index 9471efe..c1aa2ca 100644 --- a/lib/network/upload.dart +++ b/lib/network/upload.dart @@ -85,8 +85,7 @@ Future> uploadPhotos( if (url == null) return []; String? username = await storage.read(key: Preferences.usernameKey); String? password = await storage.read(key: Preferences.passwordKey); - UploadNotifier uploadNotifier = - App.appKey.currentContext!.read(); + UploadNotifier uploadNotifier = App.appKey.currentContext!.read(); int nbError = 0; // Creates Upload Item list for the upload notifier @@ -106,8 +105,7 @@ Future> uploadPhotos( App.navigatorKey.currentState?.popAndPushNamed(UploadStatusPage.routeName); // Iterate on each item - await Future.wait(List>.generate(items.length, (index) async { - UploadItem item = items[index]; + for (UploadItem item in items) { try { // Upload image Response? response = await uploadChunk( @@ -139,7 +137,7 @@ Future> uploadPhotos( // todo: delete real file path, not the cached one. } } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("${e.message}"); debugPrint("${e.stackTrace}"); uploadNotifier.itemUploadCompleted(item, error: true); @@ -152,7 +150,7 @@ Future> uploadPhotos( uploadNotifier.itemUploadCompleted(item, error: true); nbError++; } - })); + } // Send notifications showUploadNotification(nbError, result.length); @@ -166,7 +164,7 @@ Future> uploadPhotos( if (await methodExist('community.images.uploadCompleted')) { await communityUploadCompleted(result, albumId); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } @@ -202,11 +200,9 @@ Future uploadChunk({ // Filter fields if (info['name'] != '' && info['name'] != null) fields['name'] = info['name']; - if (info['comment'] != '' && info['comment'] != null) - fields['comment'] = info['comment']; - if (info['tag_ids']?.isNotEmpty ?? false) - fields['tag_ids'] = info['tag_ids'].join(','); - if (info['level'] != -1) fields['level'] = info['level']; + if (info['comment'] != '' && info['comment'] != null) fields['comment'] = info['comment']; + if (info['tag_ids']?.isNotEmpty ?? false) fields['tag_ids'] = info['tag_ids'].join(','); + if (info['level'] != -1 && info['level'] != null) fields['level'] = info['level']; // Create dio client Dio dio = Dio( @@ -247,12 +243,11 @@ Future uploadCompleted(List imageId, int categoryId) async { }); try { - Response response = - await ApiClient.post(data: formData, queryParameters: queries); + Response response = await ApiClient.post(data: formData, queryParameters: queries); if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("$e"); } return false; @@ -270,12 +265,11 @@ Future communityUploadCompleted(List imageId, int categoryId) async { 'category_id': categoryId, }); try { - Response response = - await ApiClient.post(data: formData, queryParameters: queries); + Response response = await ApiClient.post(data: formData, queryParameters: queries); if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("$e"); } return false; diff --git a/lib/network/users.dart b/lib/network/users.dart index e507ddb..784c3bc 100644 --- a/lib/network/users.dart +++ b/lib/network/users.dart @@ -58,7 +58,7 @@ Future?> getAllUsers({ } return usersResponse; - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch all users: ${e.message}'); } on Error catch (e) { debugPrint('Fetch all users: ${e.stackTrace}'); @@ -90,7 +90,7 @@ Future>> getUsers([int page = 0]) async { data: users, ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch users: ${e.message}'); } on Error catch (e) { debugPrint('Fetch users: ${e.stackTrace}'); @@ -140,7 +140,7 @@ Future>> getAllAdmins() async { return ApiResponse>( data: users, ); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch admins: ${e.message}'); } on Error catch (e) { debugPrint('Fetch admins: ${e.stackTrace}'); @@ -167,7 +167,7 @@ Future>> fetchFavorites(int page) async { return ApiResponse>(data: images); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Fetch favorites: ${e.message}'); } on Error catch (e) { debugPrint('Fetch favorites: ${e.stackTrace}'); @@ -200,7 +200,7 @@ Future addFavorite(int imageId) async { if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Add favorite: ${e.message}'); } on Error catch (e) { debugPrint('Add favorite: ${e.stackTrace}'); @@ -234,7 +234,7 @@ Future removeFavorite(int imageId) async { if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Remove favorite: ${e.message}'); } on Error catch (e) { debugPrint('Remove favorite: ${e.stackTrace}'); diff --git a/lib/services/auto_upload_manager.dart b/lib/services/auto_upload_manager.dart index ea0eb3e..381ba66 100644 --- a/lib/services/auto_upload_manager.dart +++ b/lib/services/auto_upload_manager.dart @@ -56,8 +56,7 @@ class AutoUploadManager { // Save a copy of the current account credentials await AutoUploadPreferences.saveCredentials(); // Get task frequency - int hours = prefs.getInt(AutoUploadPreferences.frequencyKey) ?? - Settings.defaultAutoUploadFrequency; + int hours = prefs.getInt(AutoUploadPreferences.frequencyKey) ?? Settings.defaultAutoUploadFrequency; // Enable auto upload prefs.setBool(AutoUploadPreferences.enabledKey, true); // Register task @@ -90,10 +89,7 @@ class AutoUploadManager { List dirFiles = appDocDir.listSync(recursive: true); // Remove folders and links - List files = dirFiles - .where((file) => file is File) - .map((e) => e as File) - .toList(); + List files = dirFiles.where((file) => file is File).map((e) => e as File).toList(); // Convert .heic files to .jpg for (File file in files) { @@ -138,10 +134,8 @@ class AutoUploadManager { cookieJar.delete(Uri.parse(url)); // Get server credentials - String? username = - await storage.read(key: AutoUploadPreferences.usernameKey); - String? password = - await storage.read(key: AutoUploadPreferences.passwordKey); + String? username = await storage.read(key: AutoUploadPreferences.usernameKey); + String? password = await storage.read(key: AutoUploadPreferences.passwordKey); // Get destination album String? albumJson = prefs.getString(AutoUploadPreferences.destinationKey); @@ -189,7 +183,7 @@ class AutoUploadManager { // todo: delete file } } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("Dio Error ${e.type}"); nbError++; } on Error catch (e) { @@ -221,13 +215,10 @@ class AutoUploadManager { ); } - String? username = - await secureStorage.read(key: AutoUploadPreferences.usernameKey); - String? password = - await secureStorage.read(key: AutoUploadPreferences.passwordKey); + String? username = await secureStorage.read(key: AutoUploadPreferences.usernameKey); + String? password = await secureStorage.read(key: AutoUploadPreferences.passwordKey); - if ((username == null || username.isEmpty) && - (password == null || password.isEmpty)) { + if ((username == null || username.isEmpty) && (password == null || password.isEmpty)) { return ApiResponse( data: false, error: ApiErrors.wrongServerUrl, @@ -278,7 +269,7 @@ class AutoUploadManager { data: false, error: ApiErrors.wrongLoginId, ); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -290,10 +281,7 @@ class AutoUploadManager { } Future> _sessionStatus() async { - Map queries = { - 'format': 'json', - 'method': 'pwg.session.getStatus' - }; + Map queries = {'format': 'json', 'method': 'pwg.session.getStatus'}; try { Response response = await dio.get('ws.php', queryParameters: queries); @@ -308,7 +296,7 @@ class AutoUploadManager { data: StatusModel.fromJson(data['result']), ); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -319,10 +307,7 @@ class AutoUploadManager { } Future _communityStatus() async { - Map queries = { - 'format': 'json', - 'method': 'community.session.getStatus' - }; + Map queries = {'format': 'json', 'method': 'community.session.getStatus'}; try { Response response = await dio.get('ws.php', queryParameters: queries); @@ -330,7 +315,7 @@ class AutoUploadManager { if (data['stat'] == 'ok') { return data['result']['real_user_status']; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -371,7 +356,7 @@ class AutoUploadManager { existResult.removeWhere((key, value) => value != null); } return existResult.keys.map((md5sum) => md5sumList[md5sum]!).toList(); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint('Edit images: ${e.message}'); } on Error catch (e) { debugPrint('Edit images: ${e.stackTrace}'); @@ -386,16 +371,13 @@ class AutoUploadManager { if (result.data?.contains('community.images.uploadCompleted') ?? false) { await communityAutoUploadCompleted(idList, destinationId); } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } } Future>> _getMethods() async { - Map queries = { - 'format': 'json', - 'method': 'reflection.getMethodList' - }; + Map queries = {'format': 'json', 'method': 'reflection.getMethodList'}; try { Response response = await dio.get( @@ -403,10 +385,9 @@ class AutoUploadManager { queryParameters: queries, ); Map data = json.decode(response.data); - final List methods = - data['result']['methods'].map((e) => e.toString()).toList(); + final List methods = data['result']['methods'].map((e) => e.toString()).toList(); return ApiResponse>(data: methods); - } on DioError catch (e) { + } on DioException catch (e) { debugPrint(e.message); } catch (e) { debugPrint('Error $e'); @@ -425,8 +406,7 @@ class AutoUploadManager { }; FormData formData = FormData.fromMap({ 'image_id': imageId, - 'pwg_token': - await secureStorage.read(key: AutoUploadPreferences.tokenKey), + 'pwg_token': await secureStorage.read(key: AutoUploadPreferences.tokenKey), 'category_id': categoryId, }); @@ -439,7 +419,7 @@ class AutoUploadManager { if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("$e"); } return false; @@ -456,8 +436,7 @@ class AutoUploadManager { }; FormData formData = FormData.fromMap({ 'image_id': imageId, - 'pwg_token': - await secureStorage.read(key: AutoUploadPreferences.tokenKey), + 'pwg_token': await secureStorage.read(key: AutoUploadPreferences.tokenKey), 'category_id': categoryId, }); try { @@ -469,7 +448,7 @@ class AutoUploadManager { if (response.statusCode == 200) { return true; } - } on DioError catch (e) { + } on DioException catch (e) { debugPrint("$e"); } return false; diff --git a/lib/services/chunked_uploader.dart b/lib/services/chunked_uploader.dart index 23e78ce..7207bd4 100644 --- a/lib/services/chunked_uploader.dart +++ b/lib/services/chunked_uploader.dart @@ -91,7 +91,11 @@ class UploadRequest { 'chunk': i, 'chunk_sum': chunkSums[i], 'original_sum': originalSum, - 'file': MultipartFile(chunkStream, end - start, filename: fileName), + 'file': MultipartFile.fromStream( + () => chunkStream, + end - start, + filename: fileName, + ), ...data }); @@ -108,16 +112,14 @@ class UploadRequest { onSendProgress: (current, total) => _updateProgress(i, current, total), ); - if (response.data != null && - json.decode(response.data)?['result']?['id'] != null) { + if (response.data != null && json.decode(response.data)?['result']?['id'] != null) { finalResponse = response; } } return finalResponse; } - Stream> _getChunkStream(int start, int end) => - _file.openRead(start, end); + Stream> _getChunkStream(int start, int end) => _file.openRead(start, end); _updateProgress(int chunkIndex, int chunkCurrent, int chunkTotal) { int totalUploadedSize = (chunkIndex * _maxChunkSize) + chunkCurrent; @@ -127,11 +129,9 @@ class UploadRequest { int _getChunkStart(int chunkIndex) => chunkIndex * _maxChunkSize; - int _getChunkEnd(int chunkIndex) => - min((chunkIndex + 1) * _maxChunkSize, _fileSize); + int _getChunkEnd(int chunkIndex) => min((chunkIndex + 1) * _maxChunkSize, _fileSize); - Map _getHeaders(int start, int end) => - {'Content-Range': 'bytes $start-${end - 1}/$_fileSize'}; + Map _getHeaders(int start, int end) => {'Content-Range': 'bytes $start-${end - 1}/$_fileSize'}; int get _chunksCount => (_fileSize / _maxChunkSize).ceil(); } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 705fd22..0538629 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -10,16 +10,13 @@ import 'package:piwigo_ng/utils/localizations.dart'; import 'package:piwigo_ng/utils/settings.dart'; import 'package:shared_preferences/shared_preferences.dart'; -final FlutterLocalNotificationsPlugin localNotification = - FlutterLocalNotificationsPlugin(); +final FlutterLocalNotificationsPlugin localNotification = FlutterLocalNotificationsPlugin(); Future initLocalNotifications() async { - const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings( + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings( '@mipmap/ic_launcher', ); - final initSettings = - InitializationSettings(android: initializationSettingsAndroid); + final initSettings = InitializationSettings(android: initializationSettingsAndroid); localNotification.initialize( initSettings, onDidReceiveNotificationResponse: onSelectNotification, @@ -36,9 +33,8 @@ Future onSelectNotification(NotificationResponse response) async { Future askNotificationPermissions() async { return localNotification - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestPermission(); + .resolvePlatformSpecificImplementation() + ?.requestNotificationsPermission(); } Future showLocalNotification({ @@ -88,9 +84,7 @@ Future showUploadNotification([int nbError = 0, int nbImage = 0]) async { } else if (nbError == 0 && nbImage > 0) { // Upload completed title = appStrings.imageUploadCompleted_title; - message = nbImage == 1 - ? appStrings.imageUploadCompleted_message - : appStrings.imageUploadCompleted_message1; + message = nbImage == 1 ? appStrings.imageUploadCompleted_message : appStrings.imageUploadCompleted_message1; } else if (nbError > 0 && nbImage != nbError) { // Upload partially completed title = appStrings.coreDataStore_WarningTitle; @@ -118,10 +112,8 @@ Future showAutoUploadNotification([ if (!(prefs.getBool(AutoUploadPreferences.notificationKey) ?? false)) return; // Find localizations - Locale locale = Locale(prefs.getString(LocaleNotifier.key) ?? - Platform.localeName.split('_').first); - AppLocalizations backgroundStrings = - await AppLocalizations.delegate.load(locale); + Locale locale = Locale(prefs.getString(LocaleNotifier.key) ?? Platform.localeName.split('_').first); + AppLocalizations backgroundStrings = await AppLocalizations.delegate.load(locale); // Init notifications final android = AndroidNotificationDetails( @@ -139,9 +131,8 @@ Future showAutoUploadNotification([ if (nbError == 0 && nbImage > 0) { // Upload completed title = backgroundStrings.imageUploadCompleted_title; - message = nbImage == 1 - ? backgroundStrings.imageUploadCompleted_message - : backgroundStrings.imageUploadCompleted_message1; + message = + nbImage == 1 ? backgroundStrings.imageUploadCompleted_message : backgroundStrings.imageUploadCompleted_message1; } else if (nbError > 0 && nbImage != nbError) { // Upload partially completed title = backgroundStrings.coreDataStore_WarningTitle; diff --git a/lib/utils/image_actions.dart b/lib/utils/image_actions.dart index 97362c8..4e16d09 100644 --- a/lib/utils/image_actions.dart +++ b/lib/utils/image_actions.dart @@ -188,7 +188,7 @@ Future onMovePhotos(BuildContext context, List images, title: appStrings.moveImage_title, subtitle: appStrings.moveImage_selectAlbum( images.length, - images.first.name, + images.first.name ?? "", ), isImage: true, album: origin, diff --git a/lib/utils/themes.dart b/lib/utils/themes.dart index 80761d3..00e809e 100644 --- a/lib/utils/themes.dart +++ b/lib/utils/themes.dart @@ -19,7 +19,7 @@ final ThemeData lightTheme = ThemeData.light(useMaterial3: true).copyWith( primary: AppColors.white, secondary: AppColors.accent, error: AppColors.error, - background: AppColors.backgroundLight, + surface: AppColors.backgroundLight, outline: AppColors.fieldLight, ), progressIndicatorTheme: const ProgressIndicatorThemeData( @@ -101,12 +101,12 @@ final ThemeData lightTheme = ThemeData.light(useMaterial3: true).copyWith( backgroundColor: Colors.black.withOpacity(0), ), switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.all(AppColors.backgroundLight), - overlayColor: MaterialStateProperty.all(AppColors.backgroundLight), - trackColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + thumbColor: WidgetStateProperty.all(AppColors.backgroundLight), + overlayColor: WidgetStateProperty.all(AppColors.backgroundLight), + trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return AppColors.accent; - } else if (states.contains(MaterialState.disabled)) { + } else if (states.contains(WidgetState.disabled)) { return AppColors.disabled; } return AppColors.fieldLight; @@ -190,7 +190,7 @@ final ThemeData darkTheme = ThemeData.dark(useMaterial3: true).copyWith( primary: AppColors.white, secondary: AppColors.accent, error: AppColors.error, - background: AppColors.backgroundDark, + surface: AppColors.backgroundDark, outline: AppColors.fieldDark, ), progressIndicatorTheme: const ProgressIndicatorThemeData( @@ -272,11 +272,11 @@ final ThemeData darkTheme = ThemeData.dark(useMaterial3: true).copyWith( backgroundColor: Colors.black.withOpacity(0), ), switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.all(const Color(0x80FFFFFF)), - trackColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + thumbColor: WidgetStateProperty.all(const Color(0x80FFFFFF)), + trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return AppColors.accent; - } else if (states.contains(MaterialState.disabled)) { + } else if (states.contains(WidgetState.disabled)) { return AppColors.disabled; } return AppColors.backgroundDark; diff --git a/lib/views/album/album_page.dart b/lib/views/album/album_page.dart index 80c7ad8..6e3a59b 100644 --- a/lib/views/album/album_page.dart +++ b/lib/views/album/album_page.dart @@ -40,8 +40,7 @@ class AlbumPage extends StatefulWidget { } class _AlbumPageState extends State { - final RefreshController _refreshController = - RefreshController(initialRefresh: false); + final RefreshController _refreshController = RefreshController(initialRefresh: false); final ScrollController _scrollController = ScrollController(); late AlbumModel _currentAlbum; @@ -63,8 +62,7 @@ class _AlbumPageState extends State { super.initState(); } - bool get _hasNonFavorites => - _selectedList.where((image) => !image.favorite).isNotEmpty; + bool get _hasNonFavorites => _selectedList.where((image) => !image.favorite).isNotEmpty; bool get _enableLoad { if (_imageList == null || _imageList!.isEmpty) return false; @@ -85,8 +83,7 @@ class _AlbumPageState extends State { Future _loadMoreImages() async { if (_imageList == null) return; if (_currentAlbum.nbImages <= _imageList!.length) return; - ApiResponse> result = - await fetchImages(widget.album.id, _page + 1); + ApiResponse> result = await fetchImages(widget.album.id, _page + 1); if (result.hasError || !result.hasData) { _refreshController.loadFailed(); await Future.delayed(const Duration(milliseconds: 500)); @@ -100,12 +97,9 @@ class _AlbumPageState extends State { } Future _onRefresh() async { - final result = await Future.wait( - [fetchAlbums(widget.album.id), fetchImages(widget.album.id, 0)]); - final ApiResponse> albumsResult = - result.first as ApiResponse>; - final ApiResponse> imagesResult = - result.last as ApiResponse>; + final result = await Future.wait([fetchAlbums(widget.album.id), fetchImages(widget.album.id, 0)]); + final ApiResponse> albumsResult = result.first as ApiResponse>; + final ApiResponse> imagesResult = result.last as ApiResponse>; if (!albumsResult.hasData || !imagesResult.hasData) { _refreshController.refreshFailed(); await Future.delayed(const Duration(milliseconds: 500)); @@ -120,14 +114,14 @@ class _AlbumPageState extends State { _refreshController.refreshCompleted(); } - void _onAddAlbum() => - onAddAlbum(context, widget.album.id).whenComplete(() => _onRefresh()); - void _onTapAlbum(AlbumModel album) => - onOpenAlbum(context, album).whenComplete(() => _onRefresh()); - void _onEditAlbum(AlbumModel album) => - onEditAlbum(context, album).whenComplete(() => _onRefresh()); - void _onMoveAlbum(AlbumModel album) => - onMoveAlbum(context, album).whenComplete(() => _onRefresh()); + void _onAddAlbum() => onAddAlbum(context, widget.album.id).whenComplete(() => _onRefresh()); + + void _onTapAlbum(AlbumModel album) => onOpenAlbum(context, album).whenComplete(() => _onRefresh()); + + void _onEditAlbum(AlbumModel album) => onEditAlbum(context, album).whenComplete(() => _onRefresh()); + + void _onMoveAlbum(AlbumModel album) => onMoveAlbum(context, album).whenComplete(() => _onRefresh()); + Future _onDeleteAlbum(AlbumModel album) async { return onDeleteAlbum(context, album).then((success) { if (success) _onRefresh(); @@ -135,8 +129,7 @@ class _AlbumPageState extends State { }); } - void _onAlbumPrivacy(AlbumModel album) => - onEditAlbumPrivacy(context, album).whenComplete(() => _onRefresh()); + void _onAlbumPrivacy(AlbumModel album) => onEditAlbumPrivacy(context, album).whenComplete(() => _onRefresh()); void _onTapPhoto(ImageModel image) { Navigator.of(context).pushNamed( @@ -165,13 +158,11 @@ class _AlbumPageState extends State { } Future _onMovePhotos() async { - onMovePhotos(context, _selectedList, _currentAlbum) - .whenComplete(() => _onRefresh()); + onMovePhotos(context, _selectedList, _currentAlbum).whenComplete(() => _onRefresh()); } void _onLikePhotos() { - onLikePhotos(_selectedList, _hasNonFavorites) - .whenComplete(() => _onRefresh()); + onLikePhotos(_selectedList, _hasNonFavorites).whenComplete(() => _onRefresh()); } void _onDeletePhotos() { @@ -206,100 +197,107 @@ class _AlbumPageState extends State { }).then((value) => _refreshController.requestRefresh()); } + void _onWillPop(bool pop) { + if (_selectedList.isNotEmpty) { + setState(() { + _selectedList.clear(); + }); + } + } + @override Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: SmartRefresher( - controller: _refreshController, - scrollController: _scrollController, - enablePullUp: _enableLoad, - onLoading: _loadMoreImages, - onRefresh: _onRefresh, - header: MaterialClassicHeader( - backgroundColor: Theme.of(context).cardColor, - color: Theme.of(context).colorScheme.secondary, - ), - footer: ClassicFooter( - loadingText: appStrings.loadingHUD_label, - noDataText: appStrings.categoryImageList_noDataError, - failedText: appStrings.errorHUD_label, - idleText: '', - canLoadingText: appStrings.loadMoreHUD_label, - ), - child: CustomScrollView( - controller: _scrollController, - slivers: [ - _appBar, - SliverToBoxAdapter( - child: FutureBuilder>( - future: _data, - builder: (context, snapshot) { - if (snapshot.hasData) { - if (snapshot.data!.first.hasError && - snapshot.data!.last.hasError) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 8.0), - child: Text( - appStrings.categoryImageList_noDataError, - textAlign: TextAlign.center, - ), - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _albumGrid(snapshot), - _imageGrid(snapshot), - SizedBox( - height: 72.0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - appStrings - .imageCount(_currentAlbum.nbTotalImages), - style: Theme.of(context).textTheme.titleSmall, + return PopScope( + canPop: _selectedList.isEmpty, + onPopInvoked: _onWillPop, + child: Scaffold( + body: SafeArea( + child: SmartRefresher( + controller: _refreshController, + scrollController: _scrollController, + enablePullUp: _enableLoad, + onLoading: _loadMoreImages, + onRefresh: _onRefresh, + header: MaterialClassicHeader( + backgroundColor: Theme.of(context).cardColor, + color: Theme.of(context).colorScheme.secondary, + ), + footer: ClassicFooter( + loadingText: appStrings.loadingHUD_label, + noDataText: appStrings.categoryImageList_noDataError, + failedText: appStrings.errorHUD_label, + idleText: '', + canLoadingText: appStrings.loadMoreHUD_label, + ), + child: CustomScrollView( + controller: _scrollController, + slivers: [ + _appBar, + SliverToBoxAdapter( + child: FutureBuilder>( + future: _data, + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot.data!.first.hasError && snapshot.data!.last.hasError) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text( + appStrings.categoryImageList_noDataError, + textAlign: TextAlign.center, + ), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _albumGrid(snapshot), + _imageGrid(snapshot), + SizedBox( + height: 72.0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + appStrings.imageCount(_currentAlbum.nbTotalImages), + style: Theme.of(context).textTheme.titleSmall, + ), ), ), - ), - ], + ], + ); + } + return const Center( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), ); - } - return const Center( - child: Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(), - ), - ); - }, + }, + ), ), - ), - ], + ], + ), ), ), + bottomNavigationBar: AnimatedSlide( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + offset: _selectedList.isEmpty ? Offset(0, 1) : Offset.zero, + child: _bottomBar, + ), + floatingActionButton: _adminActionsSpeedDial, ), - bottomNavigationBar: AnimatedSlide( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - offset: _selectedList.isEmpty ? Offset(0, 1) : Offset.zero, - child: _bottomBar, - ), - floatingActionButton: _adminActionsSpeedDial, ); } Widget get _appBar { Orientation orientation = MediaQuery.of(context).orientation; return SliverAppBar( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, pinned: true, titleSpacing: 0, centerTitle: _selectedList.isNotEmpty, title: Text( - _selectedList.isEmpty - ? _currentAlbum.name - : _selectedList.length.toString(), + _selectedList.isEmpty ? _currentAlbum.name : _selectedList.length.toString(), style: Theme.of(context).appBarTheme.titleTextStyle, ), actions: [ @@ -311,8 +309,7 @@ class _AlbumPageState extends State { tooltip: appStrings.categoryImageList_deselectButton, icon: Icon(Icons.cancel), ), - if (orientation == Orientation.landscape && _selectedList.isNotEmpty) - ..._imageActions, + if (orientation == Orientation.landscape && _selectedList.isNotEmpty) ..._imageActions, if (widget.isAdmin || _currentAlbum.canUpload) PopupMenuButton( tooltip: appStrings.imageOptions_title, @@ -329,17 +326,14 @@ class _AlbumPageState extends State { text: appStrings.imageOptions_share, ), ), - if (_selectedList.isNotEmpty && - Preferences.getUserStatus != 'guest') + if (_selectedList.isNotEmpty && Preferences.getUserStatus != 'guest') PopupMenuItem( onTap: () => Future.delayed( const Duration(seconds: 0), _onLikePhotos, ), child: PopupListItem( - icon: _hasNonFavorites - ? Icons.favorite_border - : Icons.favorite, + icon: _hasNonFavorites ? Icons.favorite_border : Icons.favorite, text: _hasNonFavorites ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, @@ -400,8 +394,7 @@ class _AlbumPageState extends State { Widget _albumGrid(AsyncSnapshot snapshot) { // initialize album list if (_albumList == null) { - final ApiResponse> result = - snapshot.data!.first as ApiResponse>; + final ApiResponse> result = snapshot.data!.first as ApiResponse>; // if only albums has error if (!result.hasData) { return Center( @@ -424,8 +417,7 @@ class _AlbumPageState extends State { Widget _imageGrid(AsyncSnapshot snapshot) { // Initialize image list if (_imageList == null) { - final ApiResponse> result = - snapshot.data!.last as ApiResponse>; + final ApiResponse> result = snapshot.data!.last as ApiResponse>; // if only images has error if (!result.hasData) { return Center( @@ -434,13 +426,11 @@ class _AlbumPageState extends State { } _imageList = result.data!; // Refresh after build (for _enableLoad) - WidgetsBinding.instance - .addPostFrameCallback((timeStamp) => setState(() {})); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => setState(() {})); } if (_imageList!.isEmpty) return const SizedBox(); // rebuild current selection with new images - _selectedList = - _imageList!.where((image) => _selectedList.contains(image)).toList(); + _selectedList = _imageList!.where((image) => _selectedList.contains(image)).toList(); return ImageGridView( album: _currentAlbum, imageList: _imageList!, @@ -465,9 +455,7 @@ class _AlbumPageState extends State { builder: (context, uploadNotifier, child) { bool uploading = uploadNotifier.uploadList.isNotEmpty; return FloatingActionButton( - tooltip: uploading - ? appStrings.uploadList_title - : appStrings.categorySelection_root, + tooltip: uploading ? appStrings.uploadList_title : appStrings.categorySelection_root, shape: uploading ? CircleBorder() : null, backgroundColor: Theme.of(context).disabledColor.withOpacity(0.7), onPressed: () { @@ -562,14 +550,11 @@ class _AlbumPageState extends State { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: _selectedList.isEmpty || orientation == Orientation.landscape - ? 0 - : 56.0, + height: _selectedList.isEmpty || orientation == Orientation.landscape ? 0 : 56.0, child: BottomAppBar( height: 56.0, child: Row( - children: - _imageActions.map((action) => Expanded(child: action)).toList(), + children: _imageActions.map((action) => Expanded(child: action)).toList(), ), ), ); @@ -603,9 +588,7 @@ class _AlbumPageState extends State { if (Preferences.getUserStatus != 'guest') // Todo: enum roles IconButton( onPressed: _onLikePhotos, - tooltip: _hasNonFavorites - ? appStrings.imageOptions_addFavorites - : appStrings.imageOptions_removeFavorites, + tooltip: _hasNonFavorites ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, isSelected: !_hasNonFavorites, selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_border), @@ -617,8 +600,6 @@ class _AlbumPageState extends State { ), ]; - return widget.isAdmin || _currentAlbum.canUpload - ? adminActions - : userActions; + return widget.isAdmin || _currentAlbum.canUpload ? adminActions : userActions; } } diff --git a/lib/views/album/album_privacy_page.dart b/lib/views/album/album_privacy_page.dart index 0402e37..68d3d37 100644 --- a/lib/views/album/album_privacy_page.dart +++ b/lib/views/album/album_privacy_page.dart @@ -81,18 +81,16 @@ class _AlbumPrivacyPageState extends State { if (!editResult.hasData || !editResult.data) return; if (_selectedMode == AlbumStatus.private) { - List newGroups = - _groups.where((e) => !_allowedGroups.contains(e)).toList(); - List removedGroups = - _allowedGroups.where((e) => !_groups.contains(e)).toList(); + List newGroups = _groups.where((e) => !_allowedGroups.contains(e)).toList(); + List removedGroups = _allowedGroups.where((e) => !_groups.contains(e)).toList(); - bool addSuccess = await addPermission( + await addPermission( albumId: widget.album.id, groups: newGroups.map((group) => group.id).toList(), recursive: _recursive, ); - bool removeSuccess = await removePermission( + await removePermission( albumId: widget.album.id, groups: removedGroups.map((group) => group.id).toList(), ); @@ -153,8 +151,7 @@ class _AlbumPrivacyPageState extends State { groupValue: _selectedMode, activeColor: Theme.of(context).colorScheme.secondary, title: Text(appStrings.categoryPrivacyMode_public), - subtitle: - Text(appStrings.categoryPrivacyMode_publicMessage), + subtitle: Text(appStrings.categoryPrivacyMode_publicMessage), onChanged: _onChangeMode, ), RadioListTile( @@ -162,8 +159,7 @@ class _AlbumPrivacyPageState extends State { groupValue: _selectedMode, activeColor: Theme.of(context).colorScheme.secondary, title: Text(appStrings.categoryPrivacyMode_private), - subtitle: - Text(appStrings.categoryPrivacyMode_privateMessage), + subtitle: Text(appStrings.categoryPrivacyMode_privateMessage), onChanged: _onChangeMode, ), ], diff --git a/lib/views/authentication/login_form_view.dart b/lib/views/authentication/login_form_view.dart index f65cf1c..a2dcf35 100644 --- a/lib/views/authentication/login_form_view.dart +++ b/lib/views/authentication/login_form_view.dart @@ -32,8 +32,7 @@ class _LoginFormViewState extends State { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final GlobalKey _urlKey = GlobalKey(); - final RoundedLoadingButtonController _btnController = - RoundedLoadingButtonController(); + final RoundedLoadingButtonController _btnController = RoundedLoadingButtonController(); String _url = ''; String _username = ''; @@ -169,8 +168,7 @@ class _LoginFormViewState extends State { AppField( key: _urlKey, margin: const EdgeInsets.symmetric(vertical: 4.0), - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), controller: _urlController, onChanged: (value) { bool isError = !_urlValidator(value); @@ -189,8 +187,7 @@ class _LoginFormViewState extends State { ), AppField( margin: const EdgeInsets.symmetric(vertical: 4.0), - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), controller: _usernameController, onChanged: (value) { if (_idError) { @@ -224,8 +221,7 @@ class _LoginFormViewState extends State { ), AppField( margin: const EdgeInsets.symmetric(vertical: 4.0), - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), controller: _passwordController, onFieldSubmitted: (String value) { FocusScope.of(context).unfocus(); @@ -282,7 +278,7 @@ class _LoginFormViewState extends State { ), TextButton( style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (states) => Theme.of(context).colorScheme.secondary, ), ), @@ -319,10 +315,7 @@ class _LoginFormViewState extends State { top: Theme.of(context).textTheme.bodyMedium?.fontSize, child: Text( !_isSecured ? 'https' : 'http', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontSize: 11), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 11), ), ), ], diff --git a/lib/views/authentication/login_settings_page.dart b/lib/views/authentication/login_settings_page.dart index bab9cb8..6e4af7a 100644 --- a/lib/views/authentication/login_settings_page.dart +++ b/lib/views/authentication/login_settings_page.dart @@ -21,10 +21,8 @@ class _LoginSettingsPageState extends State { @override void initState() { - _basicAuthUsername = - appPreferences.getString(Preferences.basicUsernameKey) ?? ''; - _basicAuthPassword = - appPreferences.getString(Preferences.basicPasswordKey) ?? ''; + _basicAuthUsername = appPreferences.getString(Preferences.basicUsernameKey) ?? ''; + _basicAuthPassword = appPreferences.getString(Preferences.basicPasswordKey) ?? ''; _sslEnabled = Preferences.getEnableSSL; _basiAuth = Preferences.getEnableBasicAuth; _rememberCredentials = Preferences.getRememberCredentials; @@ -37,7 +35,7 @@ class _LoginSettingsPageState extends State { appBar: AppBar( title: Text( appStrings.tabBar_preferences, - textScaleFactor: 1, + textScaler: TextScaler.linear(1), style: Theme.of(context).appBarTheme.titleTextStyle, ), ), diff --git a/lib/views/image/edit_image_page.dart b/lib/views/image/edit_image_page.dart index 4678576..8f9047c 100644 --- a/lib/views/image/edit_image_page.dart +++ b/lib/views/image/edit_image_page.dart @@ -51,7 +51,7 @@ class _EditImagePageState extends State { _authorController = TextEditingController(text: Preferences.getUploadAuthor); if (_imageList.length == 1) { - _titleController.text = _imageList.first.name; + _titleController.text = _imageList.first.name ?? ""; _descriptionController.text = _imageList.first.comment ?? ''; _tags = _imageList.first.tags; } diff --git a/lib/views/image/image_favorites_page.dart b/lib/views/image/image_favorites_page.dart index 63636ab..5ea6f64 100644 --- a/lib/views/image/image_favorites_page.dart +++ b/lib/views/image/image_favorites_page.dart @@ -24,8 +24,7 @@ class ImageFavoritesPage extends StatefulWidget { } class _ImageFavoritesPageState extends State { - final RefreshController _refreshController = - RefreshController(initialRefresh: false); + final RefreshController _refreshController = RefreshController(initialRefresh: false); final ScrollController _scrollController = ScrollController(); late final Future> _imageFuture; @@ -38,7 +37,18 @@ class _ImageFavoritesPageState extends State { @override void initState() { super.initState(); - _imageFuture = fetchFavorites(); + _imageFuture = fetchFavorites().then((response) { + if (response.hasData) { + setState(() { + final int? total = response.data!['total_count']; + if (total != null) { + _nbImages = total; + } + _imageList = response.data!['images'].cast() ?? []; + }); + } + return response; + }); } @override @@ -48,17 +58,14 @@ class _ImageFavoritesPageState extends State { super.dispose(); } - bool get _hasNonFavorites => - _selectedList.where((image) => !image.favorite).isNotEmpty; + bool get _hasNonFavorites => _selectedList.where((image) => !image.favorite).isNotEmpty; - Future _onWillPop() async { + void _onWillPop(bool pop) async { if (_selectedList.isNotEmpty) { setState(() { _selectedList.clear(); }); - return false; } - return true; } Future _onRefresh() async { @@ -114,26 +121,28 @@ class _ImageFavoritesPageState extends State { if (images == null || images is! List) return; setState(() { _imageList = images; - _page = - ((images.length - 1) / Settings.defaultElementPerPage).floor(); + _page = ((images.length - 1) / Settings.defaultElementPerPage).floor(); }); }); + void _onEditPhotos() => onEditPhotos(context, _selectedList).then((success) { if (success == true) { _selectedList.clear(); _onRefresh(); } }); - void _onLikePhotos() => - onLikePhotos(_selectedList, false).whenComplete(() => _onRefresh()); + + void _onLikePhotos() => onLikePhotos(_selectedList, false).whenComplete(() => _onRefresh()); + _onDeletePhotos() => onDeletePhotos(context, _selectedList).then((success) { if (success) _onRefresh(); }); @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, + return PopScope( + canPop: _selectedList.isEmpty, + onPopInvoked: _onWillPop, child: Scaffold( body: SafeArea( child: SmartRefresher( @@ -265,8 +274,7 @@ class _ImageFavoritesPageState extends State { _imageList = result.data!['images'].cast() ?? []; } - _selectedList = - _imageList!.where((image) => _selectedList.contains(image)).toList(); + _selectedList = _imageList!.where((image) => _selectedList.contains(image)).toList(); if (_imageList!.isEmpty) { return Center( @@ -294,14 +302,11 @@ class _ImageFavoritesPageState extends State { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: _selectedList.isEmpty || orientation == Orientation.landscape - ? 0 - : 56.0, + height: _selectedList.isEmpty || orientation == Orientation.landscape ? 0 : 56.0, child: BottomAppBar( height: 56.0, child: Row( - children: - _actions.map((action) => Expanded(child: action)).toList(), + children: _actions.map((action) => Expanded(child: action)).toList(), ), ), ); @@ -330,9 +335,7 @@ class _ImageFavoritesPageState extends State { if (Preferences.getUserStatus != 'guest') // Todo: enum roles IconButton( onPressed: _onLikePhotos, - tooltip: _hasNonFavorites - ? appStrings.imageOptions_addFavorites - : appStrings.imageOptions_removeFavorites, + tooltip: _hasNonFavorites ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, isSelected: !_hasNonFavorites, selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_border), diff --git a/lib/views/image/image_page.dart b/lib/views/image/image_page.dart index 6c9a46a..d883f00 100644 --- a/lib/views/image/image_page.dart +++ b/lib/views/image/image_page.dart @@ -82,11 +82,9 @@ class _ImagePageState extends State { void initState() { _imageList = widget.images.sublist(0); _album = widget.album; - _imagePage = - ((_imageList.length - 1) / Settings.defaultElementPerPage).floor(); + _imagePage = ((_imageList.length - 1) / Settings.defaultElementPerPage).floor(); - final ImageModel? startImage = - _imageList.firstWhere((image) => image.id == widget.startId); + final ImageModel? startImage = _imageList.firstWhere((image) => image.id == widget.startId); if (startImage != null) { _page = _imageList.indexOf(startImage); if (_imageList.last == startImage) { @@ -117,9 +115,7 @@ class _ImagePageState extends State { systemNavigationBarColor: Colors.black.withOpacity(0.001), statusBarColor: Colors.black.withOpacity(0.001), statusBarIconBrightness: - App.appKey.currentContext?.read().isDark ?? false - ? Brightness.light - : Brightness.dark, + App.appKey.currentContext?.read().isDark ?? false ? Brightness.light : Brightness.dark, )); super.dispose(); } @@ -129,8 +125,7 @@ class _ImagePageState extends State { Future _loadMoreImages() async { if (_album.id == -1) return; if (_album.nbImages <= _imageList.length) return; - ApiResponse> result = - await fetchImages(_album.id, _imagePage + 1); + ApiResponse> result = await fetchImages(_album.id, _imagePage + 1); if (result.hasError || !result.hasData) return; setState(() { _imagePage += 1; @@ -145,10 +140,8 @@ class _ImagePageState extends State { if (serverUrl == null) return {}; // Get server cookies - List cookies = - await ApiClient.cookieJar.loadForRequest(Uri.parse(serverUrl)); - String cookiesStr = - cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; '); + List cookies = await ApiClient.cookieJar.loadForRequest(Uri.parse(serverUrl)); + String cookiesStr = cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; '); // Get HTTP Basic id SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -184,15 +177,14 @@ class _ImagePageState extends State { /// Handler before closing the page. /// * If overlay is hidden, show it. /// * Otherwise, close the page. - Future _onWillPop() async { + void _onWillPop(bool pop) { if (!_showOverlay) { setState(() { _showOverlay = true; }); - return false; + } else { + Navigator.of(context).pop(_imageList); } - Navigator.of(context).pop(_imageList); - return false; } /// Toggle overlay action (orientation was necessary, *see comments*). @@ -289,8 +281,9 @@ class _ImagePageState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, + return PopScope( + canPop: !_showOverlay, + onPopInvoked: _onWillPop, child: Scaffold( backgroundColor: Colors.black, resizeToAvoidBottomInset: true, @@ -345,7 +338,7 @@ class _ImagePageState extends State { ), Expanded( child: AutoSizeText( - '${_currentImage.name}', + '${_currentImage.name ?? ""}', softWrap: true, maxLines: 1, maxFontSize: 16.0, @@ -354,9 +347,7 @@ class _ImagePageState extends State { style: TextStyle(fontSize: 16.0, color: Colors.white), ), ), - if (MediaQuery.of(context).orientation == - Orientation.landscape) - ..._actions, + if (MediaQuery.of(context).orientation == Orientation.landscape) ..._actions, if (widget.isAdmin) PopupMenuButton( position: PopupMenuPosition.under, @@ -385,9 +376,7 @@ class _ImagePageState extends State { _onLike, ), child: PopupListItem( - icon: !_currentImage.favorite - ? Icons.favorite_border - : Icons.favorite, + icon: !_currentImage.favorite ? Icons.favorite_border : Icons.favorite, text: !_currentImage.favorite ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, @@ -495,10 +484,7 @@ class _ImagePageState extends State { imageUrl = image.elementUrl; imageUrl = HtmlUnescape().convert(imageUrl); } else { - imageUrl = image - .getDerivativeFromString(Preferences.getImageFullScreenSize) - ?.url ?? - ''; + imageUrl = image.getDerivativeFromString(Preferences.getImageFullScreenSize)?.url ?? ''; } // ApiClient.cookieJar.loadForRequest(Uri.parse(imageUrl)); @@ -520,10 +506,8 @@ class _ImagePageState extends State { child: IconButton( color: Colors.white, style: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( - (states) => Colors.black.withOpacity(0.5)), - shape: MaterialStateProperty.resolveWith( - (states) => CircleBorder()), + backgroundColor: WidgetStateProperty.resolveWith((states) => Colors.black.withOpacity(0.5)), + shape: WidgetStateProperty.resolveWith((states) => CircleBorder()), ), onPressed: () { Navigator.of(context).pushNamed( @@ -562,6 +546,7 @@ class _ImagePageState extends State { debugPrint("$o\n$s"); return const Icon(Icons.broken_image_outlined); }, + filterQuality: FilterQuality.medium, ); }, ); @@ -601,14 +586,11 @@ class _ImagePageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _pagination, - if (MediaQuery.of(context).orientation == - Orientation.portrait) + if (MediaQuery.of(context).orientation == Orientation.portrait) SizedBox( height: 56.0, child: Row( - children: _actions - .map((action) => Expanded(child: action)) - .toList(), + children: _actions.map((action) => Expanded(child: action)).toList(), ), ), ], @@ -667,8 +649,7 @@ class _ImagePageState extends State { duration: _overlayAnimationDuration, curve: _overlayAnimationCurve, child: Builder(builder: (context) { - if (_currentImage.comment == null || _currentImage.comment!.isEmpty) - return const SizedBox(); + if (_currentImage.comment == null || _currentImage.comment!.isEmpty) return const SizedBox(); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: _showImageDetails, diff --git a/lib/views/image/image_search_page.dart b/lib/views/image/image_search_page.dart index f590a58..41aa507 100644 --- a/lib/views/image/image_search_page.dart +++ b/lib/views/image/image_search_page.dart @@ -29,8 +29,7 @@ class ImageSearchPage extends StatefulWidget { class _ImageSearchPageState extends State { final TextEditingController _searchController = TextEditingController(); - final RefreshController _refreshController = - RefreshController(initialRefresh: false); + final RefreshController _refreshController = RefreshController(initialRefresh: false); final ScrollController _scrollController = ScrollController(); final FocusNode _focusNode = FocusNode(); @@ -62,17 +61,14 @@ class _ImageSearchPageState extends State { super.dispose(); } - bool get _hasNonFavorites => - _selectedList.where((image) => !image.favorite).isNotEmpty; + bool get _hasNonFavorites => _selectedList.where((image) => !image.favorite).isNotEmpty; - Future _onWillPop() async { + void _onWillPop(bool pop) { if (_selectedList.isNotEmpty) { setState(() { _selectedList.clear(); }); - return false; } - return true; } Future _onRefresh() async { @@ -132,8 +128,7 @@ class _ImageSearchPageState extends State { if (images == null || images is! List) return; setState(() { _searchList = images; - _page = - ((images.length - 1) / Settings.defaultElementPerPage).floor(); + _page = ((images.length - 1) / Settings.defaultElementPerPage).floor(); }); }); @@ -144,8 +139,7 @@ class _ImageSearchPageState extends State { } }); - void _onLikePhotos() => onLikePhotos(_selectedList, _hasNonFavorites) - .whenComplete(() => _onRefresh()); + void _onLikePhotos() => onLikePhotos(_selectedList, _hasNonFavorites).whenComplete(() => _onRefresh()); _onDeletePhotos() => onDeletePhotos(context, _selectedList).then((success) { if (success) _onRefresh(); @@ -153,16 +147,15 @@ class _ImageSearchPageState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, + return PopScope( + canPop: _selectedList.isEmpty, + onPopInvoked: _onWillPop, child: Scaffold( body: SafeArea( child: SmartRefresher( controller: _refreshController, scrollController: _scrollController, - enablePullUp: _searchList != null && - _searchList!.isNotEmpty && - _nbImages > _searchList!.length, + enablePullUp: _searchList != null && _searchList!.isNotEmpty && _nbImages > _searchList!.length, onLoading: _loadMoreImages, onRefresh: _onRefresh, header: MaterialClassicHeader( @@ -219,7 +212,6 @@ class _ImageSearchPageState extends State { controller: _searchController, focusNode: _focusNode, prefix: const Icon(Icons.search), - hint: "Search...", onChanged: (value) => setState(() { _searchText = value; _onRefresh(); @@ -263,9 +255,7 @@ class _ImageSearchPageState extends State { _onLikePhotos, ), child: PopupListItem( - icon: _hasNonFavorites - ? Icons.favorite_border - : Icons.favorite, + icon: _hasNonFavorites ? Icons.favorite_border : Icons.favorite, text: _hasNonFavorites ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, @@ -335,14 +325,11 @@ class _ImageSearchPageState extends State { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: _selectedList.isEmpty || orientation == Orientation.landscape - ? 0 - : 56.0, + height: _selectedList.isEmpty || orientation == Orientation.landscape ? 0 : 56.0, child: BottomAppBar( height: 56.0, child: Row( - children: - _actions.map((action) => Expanded(child: action)).toList(), + children: _actions.map((action) => Expanded(child: action)).toList(), ), ), ); @@ -371,9 +358,7 @@ class _ImageSearchPageState extends State { if (Preferences.getUserStatus != 'guest') // Todo: enum roles IconButton( onPressed: _onLikePhotos, - tooltip: _hasNonFavorites - ? appStrings.imageOptions_addFavorites - : appStrings.imageOptions_removeFavorites, + tooltip: _hasNonFavorites ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, isSelected: !_hasNonFavorites, selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_border), diff --git a/lib/views/image/image_tags_page.dart b/lib/views/image/image_tags_page.dart new file mode 100644 index 0000000..f909f17 --- /dev/null +++ b/lib/views/image/image_tags_page.dart @@ -0,0 +1,355 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/components/lists/image_grid_view.dart'; +import 'package:piwigo_ng/components/popup_list_item.dart'; +import 'package:piwigo_ng/models/album_model.dart'; +import 'package:piwigo_ng/models/image_model.dart'; +import 'package:piwigo_ng/models/tag_model.dart'; +import 'package:piwigo_ng/network/api_error.dart'; +import 'package:piwigo_ng/network/images.dart'; +import 'package:piwigo_ng/services/preferences_service.dart'; +import 'package:piwigo_ng/utils/image_actions.dart'; +import 'package:piwigo_ng/utils/localizations.dart'; +import 'package:piwigo_ng/utils/settings.dart'; +import 'package:piwigo_ng/views/image/image_page.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +class ImageTagsPage extends StatefulWidget { + const ImageTagsPage({Key? key, required this.tag, this.isAdmin = false}) : super(key: key); + + static const String routeName = '/images/tags'; + + final TagModel tag; + final bool isAdmin; + + @override + State createState() => _ImageTagsPageState(); +} + +class _ImageTagsPageState extends State { + final RefreshController _refreshController = RefreshController(initialRefresh: false); + final ScrollController _scrollController = ScrollController(); + + late final Future> _imageFuture; + List? _imageList; + List _selectedList = []; + + int _nbImages = 0; + int _page = 0; + + @override + void initState() { + super.initState(); + _imageFuture = fetchTagImages(widget.tag.id).then((response) { + if (response.hasData) { + setState(() { + final int? total = response.data!['total_count']; + if (total != null) { + _nbImages = total; + } + _imageList = response.data!['images'].cast() ?? []; + }); + } + return response; + }); + } + + @override + void dispose() { + _refreshController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + bool get _hasNonFavorites => _selectedList.where((image) => !image.favorite).isNotEmpty; + + void _onWillPop(bool pop) { + if (_selectedList.isNotEmpty) { + setState(() { + _selectedList.clear(); + }); + } + } + + Future _onRefresh() async { + final ApiResponse result = await fetchTagImages(widget.tag.id); + if (!result.hasData) { + _refreshController.refreshFailed(); + await Future.delayed(const Duration(milliseconds: 500)); + return _refreshController.refreshCompleted(); + } + final int? total = result.data!['total_count']; + setState(() { + _page = 0; + if (total != null) { + _nbImages = total; + } + _imageList = result.data!['images'].cast() ?? []; + _selectedList.clear(); + }); + return _refreshController.refreshCompleted(); + } + + Future _loadMoreImages() async { + if (_imageList == null || _nbImages <= _imageList!.length) return; + ApiResponse result = await fetchTagImages(widget.tag.id, _page + 1); + if (result.hasError || !result.hasData) { + _refreshController.loadFailed(); + await Future.delayed(const Duration(milliseconds: 500)); + return _refreshController.loadComplete(); + } + final int? total = result.data!['total_count']; + setState(() { + if (total != null) { + _nbImages = total; + } + _imageList!.addAll(result.data!['images'].cast() ?? []); + }); + _refreshController.loadComplete(); + } + + void _onTapPhoto(ImageModel image) => Navigator.of(context).pushNamed( + ImagePage.routeName, + arguments: { + 'images': _imageList, + 'startId': image.id, + 'album': AlbumModel( + id: -1, + name: '', + nbImages: _nbImages, + nbTotalImages: _nbImages, + ), + }, + ).then((images) { + if (images == null || images is! List) return; + setState(() { + _imageList = images; + _page = ((images.length - 1) / Settings.defaultElementPerPage).floor(); + }); + }); + + void _onEditPhotos() => onEditPhotos(context, _selectedList).then((success) { + if (success == true) { + _selectedList.clear(); + _onRefresh(); + } + }); + + void _onLikePhotos() => onLikePhotos(_selectedList, false).whenComplete(() => _onRefresh()); + + _onDeletePhotos() => onDeletePhotos(context, _selectedList).then((success) { + if (success) _onRefresh(); + }); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: _selectedList.isEmpty, + onPopInvoked: _onWillPop, + child: Scaffold( + body: SafeArea( + child: SmartRefresher( + controller: _refreshController, + scrollController: _scrollController, + enablePullUp: _imageList != null && _nbImages > _imageList!.length, + onLoading: _loadMoreImages, + onRefresh: _onRefresh, + header: MaterialClassicHeader( + backgroundColor: Theme.of(context).cardColor, + color: Theme.of(context).colorScheme.secondary, + ), + footer: ClassicFooter( + loadingText: appStrings.loadingHUD_label, + noDataText: appStrings.categoryImageList_noDataError, + failedText: appStrings.errorHUD_label, + idleText: '', + canLoadingText: appStrings.loadMoreHUD_label, + ), + child: CustomScrollView( + controller: _scrollController, + slivers: [ + _appBar, + SliverToBoxAdapter( + child: _taggedImageGrid, + ), + ], + ), + ), + ), + bottomNavigationBar: AnimatedSlide( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + offset: _selectedList.isEmpty ? Offset(0, 1) : Offset.zero, + child: _bottomBar, + ), + ), + ); + } + + Widget get _appBar { + Orientation orientation = MediaQuery.of(context).orientation; + return SliverAppBar( + pinned: true, + centerTitle: false, + titleSpacing: 0.0, + leading: BackButton( + onPressed: () => Navigator.of(context).pop(), + ), + // title: Text(appStrings.categoryDiscoverFavorites_title), + title: Text(widget.tag.name), + actions: [ + if (_selectedList.isNotEmpty) + IconButton( + onPressed: () => setState(() { + _selectedList.clear(); + }), + tooltip: appStrings.categoryImageList_deselectButton, + icon: Icon(Icons.cancel), + ), + if (orientation == Orientation.landscape) ..._actions, + if (widget.isAdmin) + PopupMenuButton( + tooltip: appStrings.imageOptions_title, + enabled: _selectedList.isNotEmpty, + position: PopupMenuPosition.under, + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => Future.delayed( + const Duration(seconds: 0), + () => share(_selectedList), + ), + child: PopupListItem( + icon: Icons.share, + text: appStrings.imageOptions_share, + ), + ), + if (Preferences.getUserStatus != 'guest') + PopupMenuItem( + onTap: () => Future.delayed( + const Duration(seconds: 0), + _onLikePhotos, + ), + child: PopupListItem( + icon: Icons.remove_circle, + text: appStrings.imageOptions_removeFavorites, + color: Theme.of(context).colorScheme.error, + ), + ), + PopupMenuItem( + onTap: () => Future.delayed( + const Duration(seconds: 0), + () => downloadImages(_selectedList), + ), + child: PopupListItem( + icon: Icons.download, + text: appStrings.downloadImage_title(_selectedList.length), + ), + ), + ], + ), + ], + ); + } + + Widget get _taggedImageGrid { + return FutureBuilder>( + future: _imageFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + if (!snapshot.data!.hasData) { + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(appStrings.categoryImageList_noDataError), + ), + ); + } + return _buildImageGrid(snapshot); + } + return Center(child: CircularProgressIndicator()); + }, + ); + } + + Widget _buildImageGrid(AsyncSnapshot snapshot) { + final ApiResponse result = snapshot.data!; + if (_imageList == null) { + _nbImages = result.data!['total_count']; + _imageList = result.data!['images'].cast() ?? []; + } + + _selectedList = _imageList!.where((image) => _selectedList.contains(image)).toList(); + + if (_imageList!.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(appStrings.noImages), + ), + ); + } + return ImageGridView( + imageList: _imageList!, + selectedList: _selectedList, + onSelectImage: (image) => setState(() { + _selectedList.add(image); + }), + onDeselectImage: (image) => setState(() { + _selectedList.remove(image); + }), + onTapImage: _onTapPhoto, + ); + } + + Widget get _bottomBar { + return OrientationBuilder(builder: (context, orientation) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: _selectedList.isEmpty || orientation == Orientation.landscape ? 0 : 56.0, + child: BottomAppBar( + height: 56.0, + child: Row( + children: _actions.map((action) => Expanded(child: action)).toList(), + ), + ), + ); + }); + } + + List get _actions { + List adminActions = [ + IconButton( + onPressed: _onEditPhotos, + tooltip: appStrings.imageOptions_edit, + icon: Icon(Icons.edit), + ), + IconButton( + onPressed: _onDeletePhotos, + tooltip: appStrings.deleteImage_delete, + icon: Icon(Icons.delete), + ), + ]; + List userActions = [ + IconButton( + onPressed: () => share(_selectedList), + tooltip: appStrings.imageOptions_share, + icon: Icon(Icons.share), + ), + if (Preferences.getUserStatus != 'guest') // Todo: enum roles + IconButton( + onPressed: _onLikePhotos, + tooltip: _hasNonFavorites ? appStrings.imageOptions_addFavorites : appStrings.imageOptions_removeFavorites, + isSelected: !_hasNonFavorites, + selectedIcon: Icon(Icons.favorite), + icon: Icon(Icons.favorite_border), + ), + IconButton( + onPressed: () => downloadImages(_selectedList), + tooltip: appStrings.imageOptions_download, + icon: Icon(Icons.download), + ), + ]; + + return widget.isAdmin ? adminActions : userActions; + } +} diff --git a/lib/views/image/video_player_page.dart b/lib/views/image/video_player_page.dart index 02c19eb..0c903d0 100644 --- a/lib/views/image/video_player_page.dart +++ b/lib/views/image/video_player_page.dart @@ -60,7 +60,7 @@ class _VideoPlayerPageState extends State { Future initializePlayer() async { if (widget.videoUrl == null) return; - _videoPlayerController = VideoPlayerController.network(widget.videoUrl!); + _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl!)); await _videoPlayerController.initialize(); _createChewieController(); setState(() {}); @@ -85,8 +85,7 @@ class _VideoPlayerPageState extends State { child: Text(appStrings.errorHUD_label), ); } - if (_chewieController == null || - !_chewieController!.videoPlayerController.value.isInitialized) { + if (_chewieController == null || !_chewieController!.videoPlayerController.value.isInitialized) { return Center( child: CircularProgressIndicator(), ); diff --git a/lib/views/image/video_view.dart b/lib/views/image/video_view.dart index 746fd09..4e5b0ee 100644 --- a/lib/views/image/video_view.dart +++ b/lib/views/image/video_view.dart @@ -43,8 +43,8 @@ class _VideoViewState extends State { @override void initState() { super.initState(); - _controller = VideoPlayerController.network( - widget.videoUrl ?? '', + _controller = VideoPlayerController.networkUrl( + Uri.parse(widget.videoUrl!), videoPlayerOptions: VideoPlayerOptions(), )..initialize().then((_) { debugPrint("---- controller initialized"); @@ -79,8 +79,7 @@ class _VideoViewState extends State { final Duration position = controller.value.position; if (!mounted) return; setState(() { - _progress = position.inMilliseconds.ceilToDouble() / - controller.value.duration.inMilliseconds.ceilToDouble(); + _progress = position.inMilliseconds.ceilToDouble() / controller.value.duration.inMilliseconds.ceilToDouble(); }); } } @@ -90,8 +89,7 @@ class _VideoViewState extends State { void _checkControllerEnd() async { if (!mounted) return; if (_controller.value.position.inMilliseconds > 0 && - _controller.value.position.inSeconds >= - _controller.value.duration.inSeconds) { + _controller.value.position.inSeconds >= _controller.value.duration.inSeconds) { setState(() { _isEnd = true; if (!widget.showOverlay) { @@ -190,8 +188,7 @@ class _VideoViewState extends State { Future _onVideoTimeChangeEnd(double value) async { // Parse slider time final double newValue = max(0, min(value, 99)) * 0.01; - final int millis = - (_controller.value.duration.inMilliseconds * newValue).toInt(); + final int millis = (_controller.value.duration.inMilliseconds * newValue).toInt(); // Change time await _controller.seekTo(Duration(milliseconds: millis)); // Resume player @@ -212,9 +209,7 @@ class _VideoViewState extends State { } int hours = duration.inHours; int minutes = (duration - Duration(hours: hours)).inMinutes; - int seconds = - (duration - Duration(hours: hours) - Duration(minutes: minutes)) - .inSeconds; + int seconds = (duration - Duration(hours: hours) - Duration(minutes: minutes)).inSeconds; return '${hours > 0 ? '$hours:' : ''}${minutes < 10 ? '0$minutes' : '$minutes'}:${seconds < 10 ? '0$seconds' : '$seconds'}'; } @@ -271,8 +266,7 @@ class _VideoViewState extends State { /// * Fast forward Widget get _overlayCenter { // Check if the player is processing / loading. - if ((_controller.value.isBuffering && !_isEnd) || - (_isEnd && _controller.value.isPlaying)) { + if ((_controller.value.isBuffering && !_isEnd) || (_isEnd && _controller.value.isPlaying)) { return Center(child: CircularProgressIndicator()); } // If player has ended, show the replay button @@ -362,10 +356,7 @@ class _VideoViewState extends State { padding: const EdgeInsets.all(8.0), child: Text( _durationText, - style: TextStyle( - fontSize: 14, - color: Colors.white, - fontWeight: FontWeight.w500), + style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500), ), ), Expanded( @@ -431,8 +422,7 @@ class _VideoViewState extends State { ), ), // Loading... - if (!_controller.value.isInitialized) - Center(child: CircularProgressIndicator()), + if (!_controller.value.isInitialized) Center(child: CircularProgressIndicator()), // Error while loading the video if (_controller.value.hasError) Center( @@ -494,7 +484,7 @@ class _VideoPlayerViewState extends State { Future initializePlayer() async { if (widget.videoUrl == null) return; - _videoPlayerController = VideoPlayerController.network(widget.videoUrl!); + _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl!)); await _videoPlayerController.initialize(); _createChewieController(); setState(() {}); @@ -518,8 +508,7 @@ class _VideoPlayerViewState extends State { child: Text(appStrings.errorHUD_label), ); } - if (_chewieController == null || - !_chewieController!.videoPlayerController.value.isInitialized) { + if (_chewieController == null || !_chewieController!.videoPlayerController.value.isInitialized) { return Center( child: CircularProgressIndicator(), ); diff --git a/lib/views/settings/settings_page.dart b/lib/views/settings/settings_page.dart index 7d9fb6e..8f54a89 100644 --- a/lib/views/settings/settings_page.dart +++ b/lib/views/settings/settings_page.dart @@ -47,9 +47,7 @@ class _SettingsPageState extends State { late SortMethods _imageSort; late bool _stripMetadata; late String _author; - late bool _compressBeforeUpload; late bool _wifiOnly; - late bool _deleteAfterUpload; late bool _downloadNotification; late bool _uploadNotification; late bool _autoUploadEnabled; @@ -67,8 +65,6 @@ class _SettingsPageState extends State { _availablePreviewSizes = Preferences.getAvailableSizes; _author = Preferences.getUploadAuthor ?? ''; _stripMetadata = Preferences.getRemoveMetadata; - _compressBeforeUpload = Preferences.getCompressUpload; - _deleteAfterUpload = Preferences.getDeleteAfterUpload; _wifiOnly = Preferences.getWifiUpload; _quality = Preferences.getUploadQuality; _downloadNotification = Preferences.getDownloadNotification; @@ -108,9 +104,7 @@ class _SettingsPageState extends State { try { if (cacheDir.existsSync()) { - cacheDir - .listSync(recursive: true, followLinks: false) - .forEach((FileSystemEntity entity) { + cacheDir.listSync(recursive: true, followLinks: false).forEach((FileSystemEntity entity) { if (entity is File) { totalSize = totalSize! + entity.lengthSync(); } @@ -171,14 +165,11 @@ class _SettingsPageState extends State { delegate: SliverChildListDelegate([ _serverSection, _logoutSection, - if (appPreferences.getString(Preferences.fileTypesKey) != - null) - _supportedFilesSection, + if (appPreferences.getString(Preferences.fileTypesKey) != null) _supportedFilesSection, _albumsSection, _photosSection, // Hide upload section for non admin / community users - if (appPreferences.getBool(Preferences.isAdminKey) ?? false) - _uploadSection, + if (appPreferences.getBool(Preferences.isAdminKey) ?? false) _uploadSection, // _privacySection, _appearanceSection, _cacheSection, @@ -194,8 +185,7 @@ class _SettingsPageState extends State { } Widget get _serverSection => SettingsSection( - title: appStrings - .settingsHeader_server(appPreferences.getString('VERSION') ?? ''), + title: appStrings.settingsHeader_server(appPreferences.getString('VERSION') ?? ''), children: [ SettingsSectionItemInfo( title: appStrings.settings_server, @@ -210,6 +200,7 @@ class _SettingsPageState extends State { ), ], ); + Widget get _logoutSection => SettingsSection( children: [ SettingsSectionButton( @@ -229,8 +220,9 @@ class _SettingsPageState extends State { ), ], ); + Widget get _supportedFilesSection { - String fileTypes = Preferences.getAvailableFileTypes.join(', ') ?? ''; + String fileTypes = Preferences.getAvailableFileTypes.join(', '); return SettingsSection( color: Colors.transparent, children: [ @@ -258,9 +250,7 @@ class _SettingsPageState extends State { if (size != null) { setState(() { _albumThumbnailSize = size; - appPreferences.setString( - Preferences.albumThumbnailSizeKey, - _albumThumbnailSize); + appPreferences.setString(Preferences.albumThumbnailSizeKey, _albumThumbnailSize); }); } }, @@ -289,6 +279,7 @@ class _SettingsPageState extends State { // ), ], ); + Widget get _photosSection => Column( children: [ SettingsSection( @@ -301,8 +292,7 @@ class _SettingsPageState extends State { if (size == null) return; setState(() { _imageThumbnailSize = size; - appPreferences.setString( - Preferences.imageThumbnailSizeKey, _imageThumbnailSize); + appPreferences.setString(Preferences.imageThumbnailSizeKey, _imageThumbnailSize); }); }, selectedItemBuilder: (context) { @@ -329,8 +319,7 @@ class _SettingsPageState extends State { if (size == null) return; setState(() { _imageFullScreenSize = size; - appPreferences.setString(Preferences.imageFullScreenSizeKey, - _imageFullScreenSize); + appPreferences.setString(Preferences.imageFullScreenSizeKey, _imageFullScreenSize); }); }, selectedItemBuilder: (context) { @@ -357,8 +346,7 @@ class _SettingsPageState extends State { if (sort == null) return; setState(() { _imageSort = sort; - appPreferences.setString( - Preferences.imageSortKey, _imageSort.value); + appPreferences.setString(Preferences.imageSortKey, _imageSort.value); }); }, selectedItemBuilder: (context) { @@ -380,10 +368,8 @@ class _SettingsPageState extends State { ), Builder(builder: (context) { final orientation = MediaQuery.of(context).orientation; - final int nbImages = - Settings.getImageCrossAxisCount(context, _imageRowNumber); - final int maxNbImages = - Settings.getImageCrossAxisCount(context, 6); + final int nbImages = Settings.getImageCrossAxisCount(context, _imageRowNumber); + final int maxNbImages = Settings.getImageCrossAxisCount(context, 6); return SettingsSectionItemSlider( enableField: false, title: appStrings.defaultNberOfThumbnailsShort, @@ -391,13 +377,11 @@ class _SettingsPageState extends State { textWidth: orientation == Orientation.portrait ? 24.0 : 40.0, min: Settings.minImageRowCount.toDouble(), max: Settings.maxImageRowCount.toDouble(), - divisions: - Settings.maxImageRowCount - Settings.minImageRowCount, + divisions: Settings.maxImageRowCount - Settings.minImageRowCount, value: _imageRowNumber.toDouble(), onChanged: (value) => setState(() { _imageRowNumber = value.round(); - appPreferences.setInt( - Preferences.imageRowCountKey, _imageRowNumber); + appPreferences.setInt(Preferences.imageRowCountKey, _imageRowNumber); }), ); }), @@ -406,8 +390,7 @@ class _SettingsPageState extends State { value: _thumbnailTitle, onChanged: (value) => setState(() { _thumbnailTitle = value; - appPreferences.setBool( - Preferences.showThumbnailTitleKey, _thumbnailTitle); + appPreferences.setBool(Preferences.showThumbnailTitleKey, _thumbnailTitle); }), ), ], @@ -419,6 +402,7 @@ class _SettingsPageState extends State { // ), ], ); + Widget get _uploadSection => SettingsSection( title: appStrings.settingsHeader_upload, children: [ @@ -488,11 +472,9 @@ class _SettingsPageState extends State { SettingsSectionItemSwitch( title: appStrings.settings_autoUpload, value: _autoUploadEnabled, - onChanged: (_) => Navigator.of(context) - .pushNamed(AutoUploadPage.routeName) - .then((_) => setState(() { - _autoUploadEnabled = AutoUploadPreferences.getEnabled; - })), + onChanged: (_) => Navigator.of(context).pushNamed(AutoUploadPage.routeName).then((_) => setState(() { + _autoUploadEnabled = AutoUploadPreferences.getEnabled; + })), ), // SettingsSectionItemSwitch( // title: appStrings.settings_deleteImage, @@ -529,15 +511,7 @@ class _SettingsPageState extends State { ), ], ); - Widget get _privacySection => SettingsSection( - title: appStrings.settings_defaultPrivacy, - children: [ - SettingsSectionItemButton( - title: "App Lock", - text: "Off", - ), - ], - ); // todo: use biometry unlock + Widget get _appearanceSection => SettingsSection( title: appStrings.settingsHeader_appearance, children: [ @@ -552,6 +526,7 @@ class _SettingsPageState extends State { ), ], ); + Widget get _cacheSection => SettingsSection( title: appStrings.settingsHeader_cache, children: [ @@ -560,8 +535,7 @@ class _SettingsPageState extends State { builder: (context, snapshot) { String cacheSize = appStrings.none; if (snapshot.hasData && snapshot.data != null) { - cacheSize = - '${snapshot.data!.toStringAsFixed(1)} ${appStrings.settings_cacheMegabytes}'; + cacheSize = '${snapshot.data!.toStringAsFixed(1)} ${appStrings.settings_cacheMegabytes}'; } return SettingsSectionItemInfo( title: appStrings.settings_cacheSize, @@ -587,6 +561,7 @@ class _SettingsPageState extends State { ), ], ); + Widget get _infoSection => SettingsSection( title: appStrings.settingsHeader_about, children: [ @@ -621,8 +596,7 @@ class _SettingsPageState extends State { SettingsSectionItemButton( title: appStrings.settings_language, icon: const Icon(Icons.language), - onPressed: () => - Navigator.of(context).pushNamed(SelectLanguagePage.routeName), + onPressed: () => Navigator.of(context).pushNamed(SelectLanguagePage.routeName), ), SettingsSectionItemButton( title: appStrings.settings_translateWithCrowdin, @@ -657,6 +631,7 @@ class _SettingsPageState extends State { ), ], ); + Widget get _content => SettingsSection( color: Colors.transparent, children: [ diff --git a/lib/views/upload/upload_page.dart b/lib/views/upload/upload_page.dart index a106db4..76ef769 100644 --- a/lib/views/upload/upload_page.dart +++ b/lib/views/upload/upload_page.dart @@ -22,8 +22,7 @@ import 'package:rounded_loading_button/rounded_loading_button.dart'; import 'package:video_player/video_player.dart'; class UploadPage extends StatefulWidget { - const UploadPage({Key? key, required this.imageList, required this.albumId}) - : super(key: key); + const UploadPage({Key? key, required this.imageList, required this.albumId}) : super(key: key); static const String routeName = '/upload'; final List imageList; @@ -33,15 +32,13 @@ class UploadPage extends StatefulWidget { State createState() => _UploadGalleryViewPage(); } -class _UploadGalleryViewPage extends State - with SingleTickerProviderStateMixin { +class _UploadGalleryViewPage extends State with SingleTickerProviderStateMixin { static const double maxCarouselElementWidth = 300.0; static const double carouselHeight = 128.0; final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); late final TextEditingController _authorController; - final RoundedLoadingButtonController _btnController = - RoundedLoadingButtonController(); + final RoundedLoadingButtonController _btnController = RoundedLoadingButtonController(); final List> _levelItems = []; List _tags = []; @@ -52,8 +49,7 @@ class _UploadGalleryViewPage extends State @override void initState() { _imageList = List.from(widget.imageList); - _authorController = - TextEditingController(text: Preferences.getUploadAuthor); + _authorController = TextEditingController(text: Preferences.getUploadAuthor); super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { setState(() { @@ -126,17 +122,10 @@ class _UploadGalleryViewPage extends State }); } - void _onDeselectTag(TagModel tag) { - setState(() { - _tags.remove(tag); - }); - } - Future _onUpload() async { _btnController.start(); List tagIds = _tags.map((tag) => tag.id).toList(); - List filesToUpload = - _imageList.where((e) => !_imageExistList.contains(e.path)).toList(); + List filesToUpload = _imageList.where((e) => !_imageExistList.contains(e.path)).toList(); var result = await uploadPhotos(filesToUpload, widget.albumId, info: { 'name': _titleController.text, 'comment': _descriptionController.text, @@ -219,9 +208,7 @@ class _UploadGalleryViewPage extends State disabled: _imageList.isEmpty, onPressed: _onUpload, child: Text( - _imageList.isEmpty - ? appStrings.noImages - : appStrings.imageUploadDetailsButton_title, + _imageList.isEmpty ? appStrings.noImages : appStrings.imageUploadDetailsButton_title, style: Theme.of(context).textTheme.displaySmall, ), ), @@ -343,8 +330,7 @@ class _UploadGalleryViewPage extends State right: index == _imageList.length - 1 ? 8.0 : 0.0, ), child: Builder(builder: (context) { - List? mimeType = - mime(file.path.split('/').last)?.split('/'); + List? mimeType = mime(file.path.split('/').last)?.split('/'); if (mimeType?.first == 'video') { return LocalVideoDetailsCard( video: File(file.path), @@ -397,9 +383,7 @@ class _VideoUploadItemState extends State { final Duration duration = _controller.value.duration; int hours = duration.inHours; int minutes = (duration - Duration(hours: hours)).inMinutes; - int seconds = - (duration - Duration(hours: hours) - Duration(minutes: minutes)) - .inSeconds; + int seconds = (duration - Duration(hours: hours) - Duration(minutes: minutes)).inSeconds; return '${hours > 0 ? '$hours:' : ''}${minutes < 10 ? '0$minutes' : '$minutes'}:${seconds < 10 ? '0$seconds' : '$seconds'}'; } @@ -438,15 +422,11 @@ class _VideoUploadItemState extends State { left: 2, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: AppColors.black.withOpacity(0.7)), + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(5), color: AppColors.black.withOpacity(0.7)), child: Text( _duration, - style: TextStyle( - color: AppColors.white, - fontSize: 10, - fontWeight: FontWeight.bold), + style: TextStyle(color: AppColors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 4bc3198..b7a4120 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,10 +3,11 @@ description: A Piwigo Android application publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 2.2.3+223 +version: 2.3.0+230 environment: - sdk: ">=2.17.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" + flutter: 3.22.2 dependencies: flutter: @@ -15,7 +16,7 @@ dependencies: # Network url_launcher: ^6.1.5 # Open links with device's applications webview_flutter: ^3.0.4 # Show web page (CGU) - dio: ^4.0.6 # Requests to API + dio: ^5.4.3+1 # Requests to API dio_cookie_manager: ^2.0.0 # Compatibility between dio and cookie_far cookie_jar: ^3.0.1 # Handles cookies connectivity_plus: ^3.0.2 # Check if Wifi is enabled @@ -24,15 +25,18 @@ dependencies: auto_size_text: ^3.0.0 # Text that auto-sizes (image thumbnail title) cupertino_icons: ^1.0.2 # iOS style icons (might be useless) font_awesome_flutter: ^10.1.0 # Font awesome icons (might be useless) - flutter_slidable: ^2.0.0 # Album card's sliding cations + flutter_slidable: ^3.0.1 # Album card's sliding cations drag_select_grid_view: ^0.6.1 # Drag to select image grid - rounded_loading_button: ^2.1.0 # Loading button animation + rounded_loading_button: # Loading button animation + git: + url: https://github.com/scopendo/flutter_rounded_loading_button # A custom fork that can work with the latest flutter SDK + ref: dd4b76a modal_bottom_sheet: ^3.0.0-pre # Custom modals (might be useless) cached_network_image: ^3.2.2 # Better cache for images (used for album's thumbnail) flutter_speed_dial: ^6.1.0+1 # Speed dial pull_to_refresh: ^2.0.0 # Top and bottom refresh gestures photo_view: ^0.14.0 # Zoom on fullscreen photos - extended_text: ^11.0.0 # Text overflow on left side + extended_text: ^13.0.0 # Text overflow on left side flutter_easyloading: ^3.0.5 # Show loading dialog # Storage @@ -45,7 +49,7 @@ dependencies: # Device device_info_plus: ^8.2.0 # Get device info (version) - flutter_local_notifications: ^12.0.2 # Throws notifications on download or upload + flutter_local_notifications: ^17.2.0 # Throws notifications on download or upload open_filex: ^4.3.2 # Open files with devices apps workmanager: ^0.5.0 # Background processes (auto upload) image_gallery_saver: ^2.0.3 # Download images @@ -64,7 +68,7 @@ dependencies: # Translations flutter_localizations: sdk: flutter - intl: ^0.18.0 # Used for translations + intl: ^0.19.0 # Used for translations html_unescape: ^2.0.0 dev_dependencies: