diff --git a/appimage/Extera.AppDir/bundle/data/flutter_assets/NOTICES.Z b/appimage/Extera.AppDir/bundle/data/flutter_assets/NOTICES.Z index 51c691b..c0a54f2 100644 Binary files a/appimage/Extera.AppDir/bundle/data/flutter_assets/NOTICES.Z and b/appimage/Extera.AppDir/bundle/data/flutter_assets/NOTICES.Z differ diff --git a/appimage/Extera.AppDir/bundle/data/flutter_assets/version.json b/appimage/Extera.AppDir/bundle/data/flutter_assets/version.json index c578de2..6061039 100644 --- a/appimage/Extera.AppDir/bundle/data/flutter_assets/version.json +++ b/appimage/Extera.AppDir/bundle/data/flutter_assets/version.json @@ -1 +1 @@ -{"app_name":"extera_next","version":"2.0.1","package_name":"extera_next"} \ No newline at end of file +{"app_name":"extera_next","version":"2.0.2","package_name":"extera_next"} \ No newline at end of file diff --git a/appimage/Extera.AppDir/bundle/extera_next b/appimage/Extera.AppDir/bundle/extera_next index fcc8c70..e5c4e3b 100755 Binary files a/appimage/Extera.AppDir/bundle/extera_next and b/appimage/Extera.AppDir/bundle/extera_next differ diff --git a/appimage/Extera.AppDir/bundle/lib/libapp.so b/appimage/Extera.AppDir/bundle/lib/libapp.so index d398166..cae6925 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libapp.so and b/appimage/Extera.AppDir/bundle/lib/libapp.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libdesktop_drop_plugin.so b/appimage/Extera.AppDir/bundle/lib/libdesktop_drop_plugin.so index 0cfdc62..7085787 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libdesktop_drop_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libdesktop_drop_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libdynamic_color_plugin.so b/appimage/Extera.AppDir/bundle/lib/libdynamic_color_plugin.so index 0fd22d8..223a9f3 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libdynamic_color_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libdynamic_color_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libemoji_picker_flutter_plugin.so b/appimage/Extera.AppDir/bundle/lib/libemoji_picker_flutter_plugin.so index bebd365..2cc4038 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libemoji_picker_flutter_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libemoji_picker_flutter_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libfile_selector_linux_plugin.so b/appimage/Extera.AppDir/bundle/lib/libfile_selector_linux_plugin.so index 5e78878..d558aed 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libfile_selector_linux_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libfile_selector_linux_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libflutter_linux_gtk.so b/appimage/Extera.AppDir/bundle/lib/libflutter_linux_gtk.so index 00bb1a8..07edaef 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libflutter_linux_gtk.so and b/appimage/Extera.AppDir/bundle/lib/libflutter_linux_gtk.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libflutter_secure_storage_linux_plugin.so b/appimage/Extera.AppDir/bundle/lib/libflutter_secure_storage_linux_plugin.so index 4759d19..951a372 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libflutter_secure_storage_linux_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libflutter_secure_storage_linux_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libflutter_webrtc_plugin.so b/appimage/Extera.AppDir/bundle/lib/libflutter_webrtc_plugin.so index 0135e10..ea0fc54 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libflutter_webrtc_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libflutter_webrtc_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libgtk_plugin.so b/appimage/Extera.AppDir/bundle/lib/libgtk_plugin.so index da9a983..e391f46 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libgtk_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libgtk_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libhandy_flutter.so b/appimage/Extera.AppDir/bundle/lib/libhandy_flutter.so index e783d91..71561be 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libhandy_flutter.so and b/appimage/Extera.AppDir/bundle/lib/libhandy_flutter.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libhandy_window_plugin.so b/appimage/Extera.AppDir/bundle/lib/libhandy_window_plugin.so index d20758a..9cbee29 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libhandy_window_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libhandy_window_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libpasteboard_plugin.so b/appimage/Extera.AppDir/bundle/lib/libpasteboard_plugin.so index 7f9781a..e727972 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libpasteboard_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libpasteboard_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/librecord_linux_plugin.so b/appimage/Extera.AppDir/bundle/lib/librecord_linux_plugin.so index c1e1ee2..c1acb45 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/librecord_linux_plugin.so and b/appimage/Extera.AppDir/bundle/lib/librecord_linux_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libscreen_retriever_linux_plugin.so b/appimage/Extera.AppDir/bundle/lib/libscreen_retriever_linux_plugin.so new file mode 100644 index 0000000..70ed06f Binary files /dev/null and b/appimage/Extera.AppDir/bundle/lib/libscreen_retriever_linux_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libsqlcipher_flutter_libs_plugin.so b/appimage/Extera.AppDir/bundle/lib/libsqlcipher_flutter_libs_plugin.so index 84df94e..c541a20 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libsqlcipher_flutter_libs_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libsqlcipher_flutter_libs_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/liburl_launcher_linux_plugin.so b/appimage/Extera.AppDir/bundle/lib/liburl_launcher_linux_plugin.so index 7137a19..2371cd5 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/liburl_launcher_linux_plugin.so and b/appimage/Extera.AppDir/bundle/lib/liburl_launcher_linux_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libvodozemac_bindings_dart.so b/appimage/Extera.AppDir/bundle/lib/libvodozemac_bindings_dart.so index 2c547ec..718dc36 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libvodozemac_bindings_dart.so and b/appimage/Extera.AppDir/bundle/lib/libvodozemac_bindings_dart.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libwebcrypto.so b/appimage/Extera.AppDir/bundle/lib/libwebcrypto.so new file mode 100644 index 0000000..6696921 Binary files /dev/null and b/appimage/Extera.AppDir/bundle/lib/libwebcrypto.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libwebcrypto_plugin.so b/appimage/Extera.AppDir/bundle/lib/libwebcrypto_plugin.so new file mode 100644 index 0000000..8aa1633 Binary files /dev/null and b/appimage/Extera.AppDir/bundle/lib/libwebcrypto_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libwindow_manager_plugin.so b/appimage/Extera.AppDir/bundle/lib/libwindow_manager_plugin.so new file mode 100644 index 0000000..c8d1594 Binary files /dev/null and b/appimage/Extera.AppDir/bundle/lib/libwindow_manager_plugin.so differ diff --git a/appimage/Extera.AppDir/bundle/lib/libwindow_to_front_plugin.so b/appimage/Extera.AppDir/bundle/lib/libwindow_to_front_plugin.so index f535034..950a125 100644 Binary files a/appimage/Extera.AppDir/bundle/lib/libwindow_to_front_plugin.so and b/appimage/Extera.AppDir/bundle/lib/libwindow_to_front_plugin.so differ diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 9e2c7ca..a0cd35f 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -52,6 +52,8 @@ "type": "String", "placeholders": {} }, + "doNotSendIfCantClean": "Strictly no EXIF", + "doNotSendIfCantCleanDescription": "Do not send the image if there was an error cleaning EXIF metadata", "repeatPassword": "Repeat password", "@repeatPassword": {}, "notAnImage": "Not an image file.", diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 57b22f5..f4d31e4 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -52,6 +52,8 @@ "type": "String", "placeholders": {} }, + "doNotSendIfCantClean": "Не отправлять неочищенные картинки", + "doNotSendIfCantCleanDescription": "Не отправлять картинку, если не удаётся удалить метаданные EXIF", "repeatPassword": "Повторите пароль", "@repeatPassword": {}, "notAnImage": "Это не картинка.", diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 631feef..9ec57ad 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -17,6 +17,7 @@ abstract class AppConfig { static bool displayNavigationRail = true; static bool enableGradient = true; static bool cleanExif = true; + static bool doNotSendIfCantClean = true; static String? httpProxy; diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index ebeb3b2..0455f73 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -3,6 +3,7 @@ import 'package:shared_preferences/shared_preferences.dart'; abstract class SettingKeys { static const String httpProxy = 'xyz.extera.next.httpProxy'; static const String cleanExif = 'xyz.extera.next.cleanExif'; + static const String doNotSendIfCantClean = 'xyz.extera.next.doNotSendIfCantClean'; static const String displayNavigationRail = 'chat.fluffy.displayNavigationRail'; static const String hideAvatarsInInvites = 'xyz.extera.next.hideAvatarsInInvites'; static const String pureBlack = 'xyz.extera.next.pureBlack'; diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0d12ef5..868cfb6 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -140,46 +140,32 @@ class ChatInputRow extends StatelessWidget { contentPadding: const EdgeInsets.all(0), ), ), - // PopupMenuItem( - // value: 'image', - // child: ListTile( - // leading: CircleAvatar( - // backgroundColor: - // theme.colorScheme.onPrimaryContainer, - // foregroundColor: - // theme.colorScheme.primaryContainer, - // child: const Icon(Icons.photo_outlined), - // ), - // title: Text(L10n.of(context).sendImage), - // contentPadding: const EdgeInsets.all(0), - // ), - // ), - // PopupMenuItem( - // value: 'video', - // child: ListTile( - // leading: CircleAvatar( - // backgroundColor: - // theme.colorScheme.onPrimaryContainer, - // foregroundColor: - // theme.colorScheme.primaryContainer, - // child: - // const Icon(Icons.video_camera_back_outlined), - // ), - // title: Text(L10n.of(context).sendVideo), - // contentPadding: const EdgeInsets.all(0), - // ), - // ), PopupMenuItem( - value: 'poll', + value: 'image', child: ListTile( leading: CircleAvatar( backgroundColor: theme.colorScheme.onPrimaryContainer, foregroundColor: theme.colorScheme.primaryContainer, - child: const Icon(Icons.poll_outlined), + child: const Icon(Icons.photo_outlined), ), - title: Text(L10n.of(context).createPoll), + title: Text(L10n.of(context).sendImage), + contentPadding: const EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: 'video', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: + const Icon(Icons.video_camera_back_outlined), + ), + title: Text(L10n.of(context).sendVideo), contentPadding: const EdgeInsets.all(0), ), ), @@ -197,6 +183,20 @@ class ChatInputRow extends StatelessWidget { contentPadding: const EdgeInsets.all(0), ), ), + PopupMenuItem( + value: 'poll', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: const Icon(Icons.poll_outlined), + ), + title: Text(L10n.of(context).createPoll), + contentPadding: const EdgeInsets.all(0), + ), + ), ], ), ), diff --git a/lib/pages/chat/send_poll_dialog.dart b/lib/pages/chat/send_poll_dialog.dart index 3142829..40cabc3 100644 --- a/lib/pages/chat/send_poll_dialog.dart +++ b/lib/pages/chat/send_poll_dialog.dart @@ -73,6 +73,7 @@ class SendPollDialogState extends State { 'question': { 'org.matrix.msc1767.text': question, 'm.text': question, + 'body': question, }, 'answers': answers .map( @@ -117,21 +118,33 @@ class SendPollDialogState extends State { @override Widget build(BuildContext context) { + final l10n = L10n.of(context); + // Ensure max slider value is at least 1 to prevent division by zero errors + final double maxAnswers = _answerControllers.isNotEmpty + ? _answerControllers.length.toDouble() + : 1.0; + return AlertDialog( - title: Text(L10n.of(context).createPoll), + title: Text(l10n.createPoll), + // In M3, the surface tint color is often used, but we keep default styling here content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + // --- Question Input --- TextField( controller: _questionController, decoration: InputDecoration( - labelText: L10n.of(context).question, + labelText: l10n.question, border: const OutlineInputBorder(), + alignLabelWithHint: true, ), maxLines: 2, ), const SizedBox(height: 16), + + // --- Answer Inputs --- ..._answerControllers.asMap().entries.map((entry) { final index = entry.key; final controller = entry.value; @@ -143,58 +156,102 @@ class SendPollDialogState extends State { child: TextField( controller: controller, decoration: InputDecoration( - labelText: '${L10n.of(context).answer} ${index + 1}', + labelText: '${l10n.answer} ${index + 1}', border: const OutlineInputBorder(), ), ), ), + const SizedBox(width: 8), IconButton( - icon: const Icon(Icons.remove_circle), + // M3 uses standard variant colors for destructive actions + icon: const Icon(Icons.remove_circle_outline), onPressed: () => _removeAnswer(index), ), ], ), ); }), + const SizedBox(height: 8), - OutlinedButton( - onPressed: _addAnswer, - child: Text(L10n.of(context).addAnswer), + + // Add Answer Button + Center( + child: OutlinedButton.icon( + onPressed: _addAnswer, + icon: const Icon(Icons.add), + label: Text(l10n.addAnswer), + ), ), - const SizedBox(height: 16), - DropdownButtonFormField( - initialValue: _maxSelections, - decoration: InputDecoration( - labelText: L10n.of(context).maxSelections, - border: const OutlineInputBorder(), + + const Divider(height: 32), + + // --- Max Selections (Slider) --- + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + l10n.maxSelections, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + '$_maxSelections', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], ), - items: List.generate( - _answerControllers.length, - (i) => DropdownMenuItem( - value: i + 1, - child: Text('${i + 1}'), - ), - ), - onChanged: (value) => setState(() => _maxSelections = value!), ), + Slider( + value: _maxSelections.toDouble().clamp(1.0, maxAnswers), + min: 1, + max: maxAnswers, + divisions: (maxAnswers > 1) ? (maxAnswers - 1).toInt() : 1, + label: '$_maxSelections', + onChanged: (value) { + setState(() { + _maxSelections = value.toInt(); + }); + }, + ), + const SizedBox(height: 16), - DropdownButtonFormField( - initialValue: _kind, - decoration: InputDecoration( - labelText: L10n.of(context).pollType, - border: const OutlineInputBorder(), + + // --- Poll Type (Segmented Button) --- + Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 8.0), + child: Text( + l10n.pollType, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + SizedBox( + width: double.infinity, + child: SegmentedButton( + showSelectedIcon: false, // Cleaner look for text-only segments + segments: [ + ButtonSegment( + value: 'org.matrix.msc3381.disclosed', + label: Text(l10n.publicPoll), + icon: const Icon(Icons.visibility_outlined), + ), + ButtonSegment( + value: 'org.matrix.msc3381.undisclosed', + label: Text(l10n.anonymousPoll), + icon: const Icon(Icons.visibility_off_outlined), + ), + ], + selected: {_kind}, + onSelectionChanged: (Set newSelection) { + setState(() { + // SegmentedButton returns a Set, we just need the first (only) value + _kind = newSelection.first; + }); + }, ), - items: [ - DropdownMenuItem( - value: 'org.matrix.msc3381.disclosed', - child: Text(L10n.of(context).publicPoll), - ), - DropdownMenuItem( - value: 'org.matrix.msc3381.undisclosed', - child: Text(L10n.of(context).anonymousPoll), - ), - ], - onChanged: (value) => setState(() => _kind = value!), ), ], ), @@ -202,11 +259,11 @@ class SendPollDialogState extends State { actions: [ TextButton( onPressed: Navigator.of(context).pop, - child: Text(L10n.of(context).cancel), + child: Text(l10n.cancel), ), FilledButton( onPressed: _sendPoll, - child: Text(L10n.of(context).send), + child: Text(l10n.send), ), ], ); diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index 0150d36..62fe045 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -84,6 +84,14 @@ class SettingsSecurityView extends StatelessWidget { storeKey: SettingKeys.cleanExif, defaultValue: AppConfig.cleanExif, ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).doNotSendIfCantClean, + subtitle: + L10n.of(context).doNotSendIfCantCleanDescription, + onChanged: (b) => AppConfig.doNotSendIfCantClean = b, + storeKey: SettingKeys.doNotSendIfCantClean, + defaultValue: AppConfig.doNotSendIfCantClean, + ), SettingsSwitchListTile.adaptive( title: L10n.of(context).sendTypingNotifications, subtitle: diff --git a/lib/utils/clean_exif.dart b/lib/utils/clean_exif.dart index e7818b4..9abebac 100644 --- a/lib/utils/clean_exif.dart +++ b/lib/utils/clean_exif.dart @@ -1,13 +1,18 @@ import 'dart:typed_data'; +import 'package:extera_next/config/app_config.dart'; import 'package:image/image.dart'; class ExifCleaner { static List removeExifData(List imageBytes) { // Decode the image (this strips EXIF data) - final Image? image = decodeImage(Uint8List.fromList(imageBytes)); + final image = decodeImage(Uint8List.fromList(imageBytes)); if (image == null) { - throw Exception('Failed to decode image'); + if (AppConfig.doNotSendIfCantClean) { + throw Exception('Failed to decode image'); + } else { + return imageBytes; + } } // Encode back to bytes without EXIF based on detected format diff --git a/pubspec.lock b/pubspec.lock index 2f7350e..b96f133 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -957,10 +957,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" image_picker_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b3cac69..4c49e59 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: html: ^0.15.4 http: ^1.2.0 image: ^4.5.4 - image_picker: ^1.1.0 + image_picker: ^1.2.1 intl: ^0.20.2 just_audio: ^0.9.39 latlong2: ^0.9.1