This commit is contained in:
otsmr 2025-06-22 15:52:04 +02:00
parent ff96a80373
commit 434945cb54
10 changed files with 117 additions and 30 deletions

View file

@ -1,3 +1,4 @@
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -5,6 +6,7 @@ import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/misc.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';
@ -12,6 +14,7 @@ import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'dart:async'; import 'dart:async';
import 'package:url_launcher/url_launcher.dart';
// these two callbacks are called on updated to the corresponding database // these two callbacks are called on updated to the corresponding database
@ -24,6 +27,7 @@ class App extends StatefulWidget {
class _AppState extends State<App> with WidgetsBindingObserver { class _AppState extends State<App> with WidgetsBindingObserver {
bool wasPaused = false; bool wasPaused = false;
bool appIsOutdated = false;
@override @override
void initState() { void initState() {
@ -62,6 +66,12 @@ class _AppState extends State<App> with WidgetsBindingObserver {
Future initAsync() async { Future initAsync() async {
setUserPlan(); setUserPlan();
globalCallbackAppIsOutdated = () async {
context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
appIsOutdated = true;
});
};
await apiService.connect(force: true); await apiService.connect(force: true);
apiService.listenToNetworkChanges(); apiService.listenToNetworkChanges();
// call this function so invalid media files are get purged // call this function so invalid media files are get purged
@ -87,6 +97,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = (a) {}; globalCallbackConnectionState = (a) {};
globalCallbackAppIsOutdated = () {};
super.dispose(); super.dispose();
} }
@ -133,8 +144,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
themeMode: context.watch<SettingsChangeProvider>().themeMode, themeMode: context.watch<SettingsChangeProvider>().themeMode,
initialRoute: '/', initialRoute: '/',
routes: { routes: {
"/": (context) => AppMainWidget(initialPage: 1), "/": (context) =>
"/chats": (context) => AppMainWidget(initialPage: 0) AppMainWidget(initialPage: 1, appIsOutdated: appIsOutdated),
"/chats": (context) =>
AppMainWidget(initialPage: 0, appIsOutdated: appIsOutdated)
}, },
); );
}, },
@ -143,8 +156,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
} }
class AppMainWidget extends StatefulWidget { class AppMainWidget extends StatefulWidget {
const AppMainWidget({super.key, required this.initialPage}); const AppMainWidget(
{super.key, required this.initialPage, required this.appIsOutdated});
final int initialPage; final int initialPage;
final bool appIsOutdated;
@override @override
State<AppMainWidget> createState() => _AppMainWidgetState(); State<AppMainWidget> createState() => _AppMainWidgetState();
} }
@ -187,6 +202,55 @@ class _AppMainWidgetState extends State<AppMainWidget> {
); );
}, },
), ),
if (widget.appIsOutdated)
Positioned(
top: 60,
left: 30,
right: 30,
child: SafeArea(
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.appOutdated,
textAlign: TextAlign.center,
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
if (Platform.isAndroid) SizedBox(height: 5),
if (Platform.isAndroid)
ElevatedButton(
onPressed: () {
launchUrl(Uri.parse(
"https://play.google.com/store/apps/details?id=eu.twonly"));
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
context.lang.appOutdatedBtn,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
),
],
),
),
),
),
], ],
); );
} }

View file

@ -16,6 +16,7 @@ bool gIsDemoUser = false;
// This callback called by the apiProvider // This callback called by the apiProvider
Function(bool) globalCallbackConnectionState = (a) {}; Function(bool) globalCallbackConnectionState = (a) {};
Function() globalCallbackAppIsOutdated = () {};
bool globalIsAppInBackground = true; bool globalIsAppInBackground = true;
int globalBestFriendUserId = -1; int globalBestFriendUserId = -1;

View file

@ -298,5 +298,7 @@
"backupTwonlySaveNow": "Jetzt speichern", "backupTwonlySaveNow": "Jetzt speichern",
"inviteFriends": "Freunde einladen", "inviteFriends": "Freunde einladen",
"inviteFriendsShareBtn": "Teilen", "inviteFriendsShareBtn": "Teilen",
"inviteFriendsShareText": "Wechseln wir zu twonly: {url}" "inviteFriendsShareText": "Wechseln wir zu twonly: {url}",
"appOutdated": "Deine Version von twonly ist veraltet.",
"appOutdatedBtn": "Jetzt aktualisieren."
} }

View file

@ -455,5 +455,7 @@
"twonlySafeRecoverBtn": "Restore backup", "twonlySafeRecoverBtn": "Restore backup",
"inviteFriends": "Invite your friends", "inviteFriends": "Invite your friends",
"inviteFriendsShareBtn": "Share", "inviteFriendsShareBtn": "Share",
"inviteFriendsShareText": "Let's switch to twonly: {url}" "inviteFriendsShareText": "Let's switch to twonly: {url}",
"appOutdated": "Your version of twonly is out of date.",
"appOutdatedBtn": "Update Now"
} }

View file

@ -1831,6 +1831,18 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Let\'s switch to twonly: {url}'** /// **'Let\'s switch to twonly: {url}'**
String inviteFriendsShareText(Object url); String inviteFriendsShareText(Object url);
/// No description provided for @appOutdated.
///
/// In en, this message translates to:
/// **'Your version of twonly is out of date.'**
String get appOutdated;
/// No description provided for @appOutdatedBtn.
///
/// In en, this message translates to:
/// **'Update Now'**
String get appOutdatedBtn;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -974,4 +974,10 @@ class AppLocalizationsDe extends AppLocalizations {
String inviteFriendsShareText(Object url) { String inviteFriendsShareText(Object url) {
return 'Wechseln wir zu twonly: $url'; return 'Wechseln wir zu twonly: $url';
} }
@override
String get appOutdated => 'Deine Version von twonly ist veraltet.';
@override
String get appOutdatedBtn => 'Jetzt aktualisieren.';
} }

View file

@ -968,4 +968,10 @@ class AppLocalizationsEn extends AppLocalizations {
String inviteFriendsShareText(Object url) { String inviteFriendsShareText(Object url) {
return 'Let\'s switch to twonly: $url'; return 'Let\'s switch to twonly: $url';
} }
@override
String get appOutdated => 'Your version of twonly is out of date.';
@override
String get appOutdatedBtn => 'Update Now';
} }

View file

@ -48,6 +48,7 @@ class ApiService {
final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu"; final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu";
final String apiSecure = (kDebugMode) ? "" : "s"; final String apiSecure = (kDebugMode) ? "" : "s";
bool appIsOutdated = false;
bool isAuthenticated = false; bool isAuthenticated = false;
ApiService(); ApiService();
@ -60,6 +61,7 @@ class ApiService {
StreamSubscription<List<ConnectivityResult>>? connectivitySubscription; StreamSubscription<List<ConnectivityResult>>? connectivitySubscription;
Future<bool> _connectTo(String apiUrl) async { Future<bool> _connectTo(String apiUrl) async {
if (appIsOutdated) return false;
try { try {
var channel = IOWebSocketChannel.connect( var channel = IOWebSocketChannel.connect(
Uri.parse(apiUrl), Uri.parse(apiUrl),
@ -303,6 +305,13 @@ class ApiService {
Result res = asResult(await _waitForResponse(seq)); Result res = asResult(await _waitForResponse(seq));
if (res.isError) { if (res.isError) {
Log.error("got error from server: ${res.error}"); Log.error("got error from server: ${res.error}");
if (res.error == ErrorCode.AppVersionOutdated) {
globalCallbackAppIsOutdated();
Log.error("App Version is OUTDATED.");
appIsOutdated = true;
await close(() {});
return Result.error(ErrorCode.InternalError);
}
if (res.error == ErrorCode.SessionNotAuthenticated) { if (res.error == ErrorCode.SessionNotAuthenticated) {
isAuthenticated = false; isAuthenticated = false;
if (authenticated) { if (authenticated) {

View file

@ -11,7 +11,7 @@ import 'package:twonly/src/utils/storage.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
final Future<Uint8List?> Function() getMergedImage; final Future<Uint8List?> Function() getMergedImage;
final String? sendNextMediaToUserName; final bool displayButtonLabel;
final File? videoFilePath; final File? videoFilePath;
final int? mediaUploadId; final int? mediaUploadId;
final bool isLoading; final bool isLoading;
@ -20,7 +20,7 @@ class SaveToGalleryButton extends StatefulWidget {
super.key, super.key,
required this.getMergedImage, required this.getMergedImage,
required this.isLoading, required this.isLoading,
this.sendNextMediaToUserName, required this.displayButtonLabel,
this.mediaUploadId, this.mediaUploadId,
this.videoFilePath, this.videoFilePath,
}); });
@ -107,8 +107,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
: _imageSaved : _imageSaved
? Icon(Icons.check) ? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk), : FaIcon(FontAwesomeIcons.floppyDisk),
if (widget.sendNextMediaToUserName == null) SizedBox(width: 10), if (widget.displayButtonLabel) SizedBox(width: 10),
if (widget.sendNextMediaToUserName == null) if (widget.displayButtonLabel)
Text(_imageSaved Text(_imageSaved
? context.lang.shareImagedEditorSavedImage ? context.lang.shareImagedEditorSavedImage
: context.lang.shareImagedEditorSaveImage) : context.lang.shareImagedEditorSaveImage)

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'
show ErrorCode; show ErrorCode;
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
@ -56,7 +55,6 @@ class ShareImageEditorView extends StatefulWidget {
class _ShareImageEditorView extends State<ShareImageEditorView> { class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _isRealTwonly = false; bool _isRealTwonly = false;
int maxShowTime = gMediaShowInfinite; int maxShowTime = gMediaShowInfinite;
String? sendNextMediaToUserName;
double tabDownPosition = 0; double tabDownPosition = 0;
bool sendingOrLoadingImage = true; bool sendingOrLoadingImage = true;
bool loadingImage = true; bool loadingImage = true;
@ -138,15 +136,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {}); setState(() {});
} }
Future updateAsync(int userId) async {
if (sendNextMediaToUserName != null) return;
Contact? contact =
await twonlyDB.contactsDao.getContactByUserId(userId).getSingleOrNull();
if (contact != null) {
sendNextMediaToUserName = getContactDisplayName(contact);
}
}
List<Widget> get actionsAtTheRight { List<Widget> get actionsAtTheRight {
if (layers.isNotEmpty && if (layers.isNotEmpty &&
layers.last.isEditing && layers.last.isEditing &&
@ -449,10 +438,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
pixelRatio = MediaQuery.of(context).devicePixelRatio; pixelRatio = MediaQuery.of(context).devicePixelRatio;
if (widget.sendTo != null) {
sendNextMediaToUserName = getContactDisplayName(widget.sendTo!);
}
return Scaffold( return Scaffold(
backgroundColor: backgroundColor:
widget.sharedFromGallery ? null : Colors.white.withAlpha(0), widget.sharedFromGallery ? null : Colors.white.withAlpha(0),
@ -553,11 +538,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
getMergedImage: getMergedImage, getMergedImage: getMergedImage,
mediaUploadId: mediaUploadId, mediaUploadId: mediaUploadId,
videoFilePath: widget.videoFilePath, videoFilePath: widget.videoFilePath,
sendNextMediaToUserName: sendNextMediaToUserName, displayButtonLabel: widget.sendTo == null,
isLoading: loadingImage, isLoading: loadingImage,
), ),
if (sendNextMediaToUserName != null) SizedBox(width: 10), if (widget.sendTo != null) SizedBox(width: 10),
if (sendNextMediaToUserName != null) if (widget.sendTo != null)
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
iconColor: Theme.of(context).colorScheme.primary, iconColor: Theme.of(context).colorScheme.primary,
@ -566,7 +551,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: pushShareImageView, onPressed: pushShareImageView,
child: FaIcon(FontAwesomeIcons.userPlus), child: FaIcon(FontAwesomeIcons.userPlus),
), ),
SizedBox(width: sendNextMediaToUserName == null ? 20 : 10), SizedBox(width: widget.sendTo == null ? 20 : 10),
FilledButton.icon( FilledButton.icon(
icon: sendingOrLoadingImage icon: sendingOrLoadingImage
? SizedBox( ? SizedBox(
@ -589,9 +574,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
), ),
label: Text( label: Text(
(sendNextMediaToUserName == null) (widget.sendTo == null)
? context.lang.shareImagedEditorShareWith ? context.lang.shareImagedEditorShareWith
: sendNextMediaToUserName!, : getContactDisplayName(widget.sendTo!),
style: TextStyle(fontSize: 17), style: TextStyle(fontSize: 17),
), ),
), ),