mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 13:08:42 +00:00
parent
baae1b04c0
commit
7db9daf41d
10 changed files with 388 additions and 138 deletions
1
assets/animated_icons/red_heart_fire.json
Normal file
1
assets/animated_icons/red_heart_fire.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/components/connection_state.dart';
|
import 'package:twonly/src/components/connection_state.dart';
|
||||||
|
|
@ -7,7 +5,6 @@ import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/settings_change_provider.dart';
|
import 'package:twonly/src/providers/settings_change_provider.dart';
|
||||||
import 'package:twonly/src/tasks/websocket_foreground_task.dart';
|
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/views/onboarding/onboarding_view.dart';
|
import 'package:twonly/src/views/onboarding/onboarding_view.dart';
|
||||||
import 'package:twonly/src/views/home_view.dart';
|
import 'package:twonly/src/views/home_view.dart';
|
||||||
|
|
@ -88,37 +85,37 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
apiProvider.connect();
|
apiProvider.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestPermissions() async {
|
// Future<void> _requestPermissions() async {
|
||||||
// Android 13+, you need to allow notification permission to display foreground service notification.
|
// // Android 13+, you need to allow notification permission to display foreground service notification.
|
||||||
//
|
// //
|
||||||
// iOS: If you need notification, ask for permission.
|
// // iOS: If you need notification, ask for permission.
|
||||||
final NotificationPermission notificationPermission =
|
// final NotificationPermission notificationPermission =
|
||||||
await FlutterForegroundTask.checkNotificationPermission();
|
// await FlutterForegroundTask.checkNotificationPermission();
|
||||||
if (notificationPermission != NotificationPermission.granted) {
|
// if (notificationPermission != NotificationPermission.granted) {
|
||||||
await FlutterForegroundTask.requestNotificationPermission();
|
// await FlutterForegroundTask.requestNotificationPermission();
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
// if (Platform.isAndroid) {
|
||||||
// Android 12+, there are restrictions on starting a foreground service.
|
// // Android 12+, there are restrictions on starting a foreground service.
|
||||||
//
|
// //
|
||||||
// To restart the service on device reboot or unexpected problem, you need to allow below permission.
|
// // To restart the service on device reboot or unexpected problem, you need to allow below permission.
|
||||||
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
|
// if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
|
||||||
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
|
// // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
|
||||||
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
|
// await FlutterForegroundTask.requestIgnoreBatteryOptimization();
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Use this utility only if you provide services that require long-term survival,
|
// // Use this utility only if you provide services that require long-term survival,
|
||||||
// such as exact alarm service, healthcare service, or Bluetooth communication.
|
// // such as exact alarm service, healthcare service, or Bluetooth communication.
|
||||||
//
|
// //
|
||||||
// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
|
// // This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
|
||||||
// Using this permission may make app distribution difficult due to Google policy.
|
// // Using this permission may make app distribution difficult due to Google policy.
|
||||||
// if (!await FlutterForegroundTask.canScheduleExactAlarms) {
|
// // if (!await FlutterForegroundTask.canScheduleExactAlarms) {
|
||||||
// When you call this function, will be gone to the settings page.
|
// // When you call this function, will be gone to the settings page.
|
||||||
// So you need to explain to the user why set it.
|
// // So you need to explain to the user why set it.
|
||||||
// await FlutterForegroundTask.openAlarmsAndRemindersSettings();
|
// // await FlutterForegroundTask.openAlarmsAndRemindersSettings();
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ class EmojiAnimation extends StatelessWidget {
|
||||||
"❤": "red_heart.json",
|
"❤": "red_heart.json",
|
||||||
"💪": "💪.json",
|
"💪": "💪.json",
|
||||||
"🔥": "🔥.json",
|
"🔥": "🔥.json",
|
||||||
"🤠": "🤠.json",
|
|
||||||
"🤯": "🤯.json",
|
|
||||||
"🥰": "🥰.json",
|
|
||||||
"😂": "😂.json",
|
"😂": "😂.json",
|
||||||
"😭": "😭.json",
|
"😭": "😭.json",
|
||||||
|
"🤯": "🤯.json",
|
||||||
|
"🥰": "🥰.json",
|
||||||
|
"🤠": "🤠.json",
|
||||||
|
"❤️🔥": "red_heart_fire.json"
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiAnimation({super.key, required this.emoji});
|
const EmojiAnimation({super.key, required this.emoji});
|
||||||
|
|
@ -34,8 +35,45 @@ class EmojiAnimation extends StatelessWidget {
|
||||||
} else {
|
} else {
|
||||||
return Text(
|
return Text(
|
||||||
emoji,
|
emoji,
|
||||||
style: TextStyle(fontSize: 100), // Adjust the size as needed
|
style: TextStyle(fontSize: 15), // Adjust the size as needed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EmojiAnimationFlying extends StatelessWidget {
|
||||||
|
final String emoji;
|
||||||
|
final Duration duration;
|
||||||
|
final double startPosition;
|
||||||
|
final int size;
|
||||||
|
|
||||||
|
const EmojiAnimationFlying({
|
||||||
|
super.key,
|
||||||
|
required this.emoji,
|
||||||
|
required this.duration,
|
||||||
|
required this.startPosition,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: startPosition, end: 1), // Adjust end value as needed
|
||||||
|
duration: duration,
|
||||||
|
curve: Curves.linearToEaseOut,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 20 * value),
|
||||||
|
child: Container(
|
||||||
|
// opacity: 1 - value,
|
||||||
|
child: SizedBox(
|
||||||
|
width: size + 30 * value,
|
||||||
|
child: EmojiAnimation(emoji: emoji),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/components/animate_icon.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
|
|
||||||
class FlameCounterWidget extends StatelessWidget {
|
class FlameCounterWidget extends StatelessWidget {
|
||||||
|
|
@ -26,9 +27,12 @@ class FlameCounterWidget extends StatelessWidget {
|
||||||
flameCounter.toString(),
|
flameCounter.toString(),
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
Text(
|
SizedBox(
|
||||||
(maxTotalMediaCounter == user.totalMediaCounter) ? "❤️🔥" : "🔥",
|
height: 15,
|
||||||
style: const TextStyle(fontSize: 14),
|
child: EmojiAnimation(
|
||||||
|
emoji: (maxTotalMediaCounter == user.totalMediaCounter)
|
||||||
|
? "❤️🔥"
|
||||||
|
: "🔥"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
"chatListViewSearchUserNameBtn": "Add your first twonly contact!",
|
"chatListViewSearchUserNameBtn": "Add your first twonly contact!",
|
||||||
"chatListViewSendFirstTwonly": "Send your first twonly!",
|
"chatListViewSendFirstTwonly": "Send your first twonly!",
|
||||||
"chatListDetailInput": "Type a message",
|
"chatListDetailInput": "Type a message",
|
||||||
|
"mediaViewerAuthReason": "Please authenticate to see this twonly!",
|
||||||
"messageSendState_Received": "Received",
|
"messageSendState_Received": "Received",
|
||||||
"messageSendState_Opened": "Opened",
|
"messageSendState_Opened": "Opened",
|
||||||
"messageSendState_Send": "Send",
|
"messageSendState_Send": "Send",
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,16 @@ class DbMessage {
|
||||||
|
|
||||||
bool get messageReceived => messageOtherId != null;
|
bool get messageReceived => messageOtherId != null;
|
||||||
|
|
||||||
|
bool isRealTwonly() {
|
||||||
|
final content = messageContent;
|
||||||
|
if (content is MediaMessageContent) {
|
||||||
|
if (content.isRealTwonly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isMedia() {
|
bool isMedia() {
|
||||||
return messageContent is MediaMessageContent;
|
return messageContent is MediaMessageContent;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
@ -203,3 +205,22 @@ Duration calculateTimeDifference(DateTime now, DateTime startTime) {
|
||||||
// Calculate the difference
|
// Calculate the difference
|
||||||
return nowInUTC.difference(startTimeInUTC);
|
return nowInUTC.difference(startTimeInUTC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> authenticateUser(String localizedReason,
|
||||||
|
{bool force = true}) async {
|
||||||
|
try {
|
||||||
|
final LocalAuthentication auth = LocalAuthentication();
|
||||||
|
bool didAuthenticate = await auth.authenticate(
|
||||||
|
localizedReason: localizedReason,
|
||||||
|
options: const AuthenticationOptions(useErrorDialogs: false));
|
||||||
|
if (didAuthenticate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
if (!force) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
|
||||||
context
|
context
|
||||||
.read<MessagesChangeProvider>()
|
.read<MessagesChangeProvider>()
|
||||||
.loadMessagesForUser(user.userId.toInt(), force: true);
|
.loadMessagesForUser(user.userId.toInt(), force: true);
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _sendMessage() async {
|
Future _sendMessage() async {
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,20 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:no_screenshot/no_screenshot.dart';
|
import 'package:no_screenshot/no_screenshot.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:twonly/src/components/animate_icon.dart';
|
||||||
import 'package:twonly/src/components/media_view_sizing.dart';
|
import 'package:twonly/src/components/media_view_sizing.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/model/messages_model.dart';
|
import 'package:twonly/src/model/messages_model.dart';
|
||||||
import 'package:twonly/src/providers/api/api.dart';
|
import 'package:twonly/src/providers/api/api.dart';
|
||||||
|
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/send_next_media_to.dart';
|
import 'package:twonly/src/providers/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||||
import 'package:twonly/src/views/home_view.dart';
|
import 'package:twonly/src/views/home_view.dart';
|
||||||
|
|
||||||
final _noScreenshot = NoScreenshot.instance;
|
final _noScreenshot = NoScreenshot.instance;
|
||||||
|
|
@ -27,82 +30,142 @@ class MediaViewerView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MediaViewerViewState extends State<MediaViewerView> {
|
class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
Uint8List? _imageByte;
|
Timer? nextMediaTimer;
|
||||||
|
Timer? progressTimer;
|
||||||
|
|
||||||
|
bool showShortReactions = false;
|
||||||
|
int selectedShortReaction = -1;
|
||||||
|
|
||||||
|
// current image related
|
||||||
|
Uint8List? imageBytes;
|
||||||
DateTime? canBeSeenUntil;
|
DateTime? canBeSeenUntil;
|
||||||
int maxShowTime = 999999;
|
int maxShowTime = 999999;
|
||||||
bool isRealTwonly = false;
|
|
||||||
double progress = 0;
|
double progress = 0;
|
||||||
Timer? _timer;
|
bool isRealTwonly = false;
|
||||||
Timer? _timer2;
|
bool isDownloading = false;
|
||||||
// DateTime opened;
|
|
||||||
|
List<DbMessage> allMediaFiles = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final content = widget.message.messageContent;
|
|
||||||
if (content is MediaMessageContent) {
|
allMediaFiles = [widget.message];
|
||||||
if (content.isRealTwonly) {
|
asyncLoadNextMedia();
|
||||||
isRealTwonly = true;
|
loadCurrentMediaFile();
|
||||||
}
|
|
||||||
}
|
|
||||||
loadMedia();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future loadMedia({bool force = false}) async {
|
Future asyncLoadNextMedia() async {
|
||||||
bool result = await _noScreenshot.screenshotOff();
|
await context
|
||||||
debugPrint('Screenshot Off: $result');
|
.read<MessagesChangeProvider>()
|
||||||
final content = widget.message.messageContent;
|
.loadMessagesForUser(widget.otherUser.userId.toInt());
|
||||||
if (content is MediaMessageContent) {
|
if (!context.mounted) return;
|
||||||
if (content.isRealTwonly) {
|
final allMessages = context
|
||||||
if (!force) {
|
.read<MessagesChangeProvider>()
|
||||||
|
.allMessagesFromUser[widget.otherUser.userId.toInt()];
|
||||||
|
if (allMessages == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
final nextMediaFiles = allMessages.where((x) =>
|
||||||
final LocalAuthentication auth = LocalAuthentication();
|
x.isMedia() &&
|
||||||
bool didAuthenticate = await auth.authenticate(
|
x.messageOtherId != null &&
|
||||||
localizedReason: 'Please authenticate to see this twonly!',
|
x.messageOpenedAt == null &&
|
||||||
options: const AuthenticationOptions(useErrorDialogs: false));
|
x.messageId != widget.message.messageId);
|
||||||
if (!didAuthenticate) {
|
allMediaFiles.addAll(nextMediaFiles.map((x) => x));
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future nextMediaOrExit() async {
|
||||||
|
nextMediaTimer?.cancel();
|
||||||
|
progressTimer?.cancel();
|
||||||
|
if (allMediaFiles.isEmpty || allMediaFiles.length == 1) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
}
|
allMediaFiles.removeAt(0);
|
||||||
} on PlatformException catch (e) {
|
loadCurrentMediaFile();
|
||||||
debugPrint(e.toString());
|
|
||||||
// these errors because of hardware not available or bio is not enrolled
|
|
||||||
// as this is just a nice gimig, do not interrupt the user experience
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutterLocalNotificationsPlugin.cancel(widget.message.messageId);
|
Future loadCurrentMediaFile({bool showTwonly = false}) async {
|
||||||
List<int> token = content.downloadToken;
|
await _noScreenshot.screenshotOff();
|
||||||
_imageByte = await getDownloadedMedia(
|
if (!context.mounted || allMediaFiles.isEmpty) return;
|
||||||
token, widget.message.messageOtherId!, widget.message.otherUserId);
|
|
||||||
if (_imageByte == null) {
|
final DbMessage current = allMediaFiles.first;
|
||||||
// image already deleted
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// image loading does require some time
|
|
||||||
Future.delayed(Duration(milliseconds: 200), () {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
mediaOpened();
|
// reset current image values
|
||||||
|
imageBytes = null;
|
||||||
|
canBeSeenUntil = null;
|
||||||
|
maxShowTime = 999999;
|
||||||
|
progress = 0;
|
||||||
|
isDownloading = false;
|
||||||
|
isRealTwonly = current.isRealTwonly();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This will show the extra screen for the twonly
|
||||||
|
if (current.isRealTwonly() && !showTwonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final content = current.messageContent;
|
||||||
|
if (content is MediaMessageContent) {
|
||||||
|
if (isRealTwonly) {
|
||||||
|
bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason,
|
||||||
|
force: false);
|
||||||
|
if (!isAuth) {
|
||||||
|
nextMediaOrExit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flutterLocalNotificationsPlugin.cancel(current.messageId);
|
||||||
|
if (!current.isDownloaded) {
|
||||||
|
setState(() {
|
||||||
|
isDownloading = true;
|
||||||
});
|
});
|
||||||
|
await tryDownloadMedia(
|
||||||
|
current.messageId, current.otherUserId, content.downloadToken,
|
||||||
|
force: true);
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
if (isDownloading) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
imageBytes = await getDownloadedMedia(
|
||||||
|
content.downloadToken,
|
||||||
|
current.messageOtherId!,
|
||||||
|
current.otherUserId,
|
||||||
|
);
|
||||||
|
} while (isDownloading && imageBytes == null);
|
||||||
|
|
||||||
|
isDownloading = false;
|
||||||
|
|
||||||
|
if (imageBytes == null) {
|
||||||
|
nextMediaOrExit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.maxShowTime != 999999) {
|
||||||
|
canBeSeenUntil = DateTime.now().add(
|
||||||
|
Duration(seconds: content.maxShowTime),
|
||||||
|
);
|
||||||
|
maxShowTime = content.maxShowTime;
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startTimer() {
|
startTimer() {
|
||||||
_timer = Timer(canBeSeenUntil!.difference(DateTime.now()), () {
|
nextMediaTimer?.cancel();
|
||||||
|
progressTimer?.cancel();
|
||||||
|
nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
nextMediaOrExit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_timer2 = Timer.periodic(Duration(milliseconds: 10), (timer) {
|
progressTimer = Timer.periodic(Duration(milliseconds: 10), (timer) {
|
||||||
if (canBeSeenUntil != null) {
|
if (canBeSeenUntil != null) {
|
||||||
Duration difference = canBeSeenUntil!.difference(DateTime.now());
|
Duration difference = canBeSeenUntil!.difference(DateTime.now());
|
||||||
// Calculate the progress as a value between 0.0 and 1.0
|
// Calculate the progress as a value between 0.0 and 1.0
|
||||||
|
|
@ -112,26 +175,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaOpened() {
|
|
||||||
if (canBeSeenUntil != null) return;
|
|
||||||
final content = widget.message.messageContent;
|
|
||||||
if (content is MediaMessageContent) {
|
|
||||||
if (content.maxShowTime != 999999) {
|
|
||||||
canBeSeenUntil = DateTime.now().add(
|
|
||||||
Duration(seconds: content.maxShowTime),
|
|
||||||
);
|
|
||||||
maxShowTime = content.maxShowTime;
|
|
||||||
startTimer();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_timer?.cancel();
|
nextMediaTimer?.cancel();
|
||||||
_timer2?.cancel();
|
progressTimer?.cancel();
|
||||||
_noScreenshot.screenshotOn();
|
_noScreenshot.screenshotOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,10 +190,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (_imageByte != null && (canBeSeenUntil == null || progress >= 0))
|
if (imageBytes != null && (canBeSeenUntil == null || progress >= 0))
|
||||||
MediaViewSizing(
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
nextMediaOrExit();
|
||||||
|
},
|
||||||
|
child: MediaViewSizing(
|
||||||
Image.memory(
|
Image.memory(
|
||||||
_imageByte!,
|
imageBytes!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
frameBuilder:
|
frameBuilder:
|
||||||
((context, child, frame, wasSynchronouslyLoaded) {
|
((context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
|
@ -157,17 +209,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
height: 60,
|
height: 60,
|
||||||
width: 60,
|
width: 60,
|
||||||
child: CircularProgressIndicator(strokeWidth: 6),
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 6),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isRealTwonly && _imageByte == null)
|
),
|
||||||
|
if (isRealTwonly && imageBytes == null)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
loadMedia(force: true);
|
loadCurrentMediaFile(showTwonly: true);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -188,7 +242,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
left: 10,
|
left: 10,
|
||||||
top: 10,
|
top: 10,
|
||||||
child: Row(
|
child: Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.close, size: 30),
|
icon: Icon(Icons.close, size: 30),
|
||||||
|
|
@ -200,6 +253,16 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isDownloading)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 20,
|
right: 20,
|
||||||
top: 27,
|
top: 27,
|
||||||
|
|
@ -217,7 +280,67 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_imageByte != null)
|
AnimatedPositioned(
|
||||||
|
duration: Duration(milliseconds: 200), // Animation duration
|
||||||
|
bottom: showShortReactions ? 130 : 90,
|
||||||
|
left: showShortReactions ? 0 : 150,
|
||||||
|
right: showShortReactions ? 0 : 150,
|
||||||
|
curve: Curves.linearToEaseOut,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: showShortReactions ? 1.0 : 0.0, // Fade in/out
|
||||||
|
duration: Duration(milliseconds: 150),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: List.generate(
|
||||||
|
6,
|
||||||
|
(index) {
|
||||||
|
final emoji =
|
||||||
|
EmojiAnimation.animatedIcons.keys.toList()[index];
|
||||||
|
return AnimatedSize(
|
||||||
|
duration:
|
||||||
|
Duration(milliseconds: 200), // Animation duration
|
||||||
|
curve: Curves.linearToEaseOut,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
sendTextMessage(widget.otherUser.userId, emoji);
|
||||||
|
setState(() {
|
||||||
|
selectedShortReaction = index;
|
||||||
|
});
|
||||||
|
Future.delayed(Duration(milliseconds: 300), () {
|
||||||
|
setState(() {
|
||||||
|
showShortReactions = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: (selectedShortReaction == index)
|
||||||
|
? EmojiAnimationFlying(
|
||||||
|
emoji: emoji,
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
startPosition: 0.0,
|
||||||
|
size: (showShortReactions) ? 40 : 10)
|
||||||
|
: AnimatedOpacity(
|
||||||
|
opacity: (selectedShortReaction == -1)
|
||||||
|
? 1
|
||||||
|
: 0, // Fade in/out
|
||||||
|
duration: Duration(milliseconds: 150),
|
||||||
|
child: SizedBox(
|
||||||
|
width: showShortReactions ? 40 : 10,
|
||||||
|
child: Center(
|
||||||
|
child: EmojiAnimation(
|
||||||
|
emoji: emoji,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (imageBytes != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 30,
|
bottom: 30,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|
@ -225,9 +348,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// const SizedBox(width: 20),
|
IconButton.outlined(
|
||||||
FilledButton.icon(
|
icon: FaIcon(FontAwesomeIcons.camera),
|
||||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
context.read<SendNextMediaTo>().updateSendNextMediaTo(
|
context.read<SendNextMediaTo>().updateSendNextMediaTo(
|
||||||
widget.otherUser.userId.toInt());
|
widget.otherUser.userId.toInt());
|
||||||
|
|
@ -239,9 +361,63 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(
|
),
|
||||||
"Respond",
|
SizedBox(width: 10),
|
||||||
style: TextStyle(fontSize: 17),
|
IconButton(
|
||||||
|
icon: SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
children: List.generate(
|
||||||
|
4,
|
||||||
|
(index) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
child: Center(
|
||||||
|
child: EmojiAnimation(
|
||||||
|
emoji: EmojiAnimation.animatedIcons.keys
|
||||||
|
.toList()[index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
showShortReactions = !showShortReactions;
|
||||||
|
selectedShortReaction = -1;
|
||||||
|
});
|
||||||
|
// context.read<SendNextMediaTo>().updateSendNextMediaTo(
|
||||||
|
// widget.otherUser.userId.toInt());
|
||||||
|
// globalUpdateOfHomeViewPageIndex(0);
|
||||||
|
// Navigator.popUntil(context, (route) => route.isFirst);
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
IconButton.outlined(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.message),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.popUntil(context, (route) => route.isFirst);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) {
|
||||||
|
return ChatItemDetailsView(user: widget.otherUser);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -85,4 +85,5 @@ flutter:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/images/onboarding/ricky_the_greedy_racoon.png
|
- assets/images/onboarding/ricky_the_greedy_racoon.png
|
||||||
- assets/animated_icons/
|
- assets/animated_icons/
|
||||||
|
- assets/animations/
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue