mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
finish onboarding and some more stuff
This commit is contained in:
parent
c3f429a90a
commit
5f2a9890ba
34 changed files with 559 additions and 114 deletions
|
|
@ -10,6 +10,7 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure
|
|||
|
||||
## Maybe
|
||||
- Send a picture first to only one person -> Kamera button
|
||||
- Response to a image
|
||||
- MediaView:
|
||||
- Bei weiteren geladenen Bildern -> Direkt anzeigen ohne pop
|
||||
|
||||
|
|
|
|||
|
|
@ -30,12 +30,33 @@
|
|||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
|
||||
<service
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:foregroundServiceType="dataSync|remoteMessaging"
|
||||
android:exported="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="eu.twonly.service.TWONLY_LOGO"
|
||||
android:resource="@drawable/ic_launcher_foreground" />
|
||||
</application>
|
||||
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
|
|
|||
BIN
android/app/src/main/res/drawable/ic_launcher_foreground.png
Normal file
BIN
android/app/src/main/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
1
assets/animations/takephoto.json
Normal file
1
assets/animations/takephoto.json
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/images/onboarding/ricky_the_greedy_racoon.png
Normal file
BIN
assets/images/onboarding/ricky_the_greedy_racoon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
5
lib/globals.dart
Normal file
5
lib/globals.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
|
||||
late DbProvider dbProvider;
|
||||
late ApiProvider apiProvider;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
|
|
@ -13,9 +15,6 @@ import 'package:twonly/src/services/notification_service.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'src/app.dart';
|
||||
|
||||
late DbProvider dbProvider;
|
||||
late ApiProvider apiProvider;
|
||||
|
||||
void main() async {
|
||||
final settingsController = SettingsChangeProvider();
|
||||
|
||||
|
|
@ -39,23 +38,11 @@ void main() async {
|
|||
await initMediaStorage();
|
||||
|
||||
dbProvider = DbProvider();
|
||||
// Database is just a file, so this will not block the loading of the app much
|
||||
await dbProvider.ready;
|
||||
|
||||
var apiUrl = "ws://api.twonly.eu/api/client";
|
||||
var backupApiUrl = "ws://api2.twonly.eu/api/client";
|
||||
// if (!kReleaseMode) {
|
||||
// Overwrite the domain in your local network so you can test the app locally
|
||||
apiUrl = "ws://10.99.0.6:3030/api/client";
|
||||
// }
|
||||
apiProvider = ApiProvider();
|
||||
|
||||
apiProvider = ApiProvider(apiUrl: apiUrl, backupApiUrl: backupApiUrl);
|
||||
|
||||
// Workmanager.executeTask((task, inputData) async {
|
||||
// await _HomeState().manager();
|
||||
// print('Background Services are Working!');//This is Working
|
||||
// return true;
|
||||
// });
|
||||
FlutterForegroundTask.initCommunicationPort();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
|
|
|
|||
116
lib/src/app.dart
116
lib/src/app.dart
|
|
@ -1,9 +1,12 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/providers/contacts_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/settings_change_provider.dart';
|
||||
import 'package:twonly/src/tasks/websocket_foreground_task.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/onboarding/onboarding_view.dart';
|
||||
import 'package:twonly/src/views/home_view.dart';
|
||||
|
|
@ -72,24 +75,127 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
|
||||
// connect async to the backend api
|
||||
apiProvider.connect();
|
||||
FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_requestPermissions();
|
||||
_initService();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
Future<void> _requestPermissions() async {
|
||||
// Android 13+, you need to allow notification permission to display foreground service notification.
|
||||
//
|
||||
// iOS: If you need notification, ask for permission.
|
||||
final NotificationPermission notificationPermission =
|
||||
await FlutterForegroundTask.checkNotificationPermission();
|
||||
if (notificationPermission != NotificationPermission.granted) {
|
||||
await FlutterForegroundTask.requestNotificationPermission();
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
// 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.
|
||||
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
|
||||
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
|
||||
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
|
||||
}
|
||||
|
||||
// Use this utility only if you provide services that require long-term survival,
|
||||
// such as exact alarm service, healthcare service, or Bluetooth communication.
|
||||
//
|
||||
// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
|
||||
// Using this permission may make app distribution difficult due to Google policy.
|
||||
// if (!await FlutterForegroundTask.canScheduleExactAlarms) {
|
||||
// When you call this function, will be gone to the settings page.
|
||||
// So you need to explain to the user why set it.
|
||||
// await FlutterForegroundTask.openAlarmsAndRemindersSettings();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
void _onReceiveTaskData(Object data) {
|
||||
if (data is Map<String, dynamic>) {
|
||||
final dynamic timestampMillis = data["timestampMillis"];
|
||||
if (timestampMillis != null) {
|
||||
final DateTime timestamp =
|
||||
DateTime.fromMillisecondsSinceEpoch(timestampMillis, isUtc: true);
|
||||
print('timestamp: ${timestamp.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _initService() {
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'foreground_service',
|
||||
channelName: 'Foreground Service Notification',
|
||||
channelDescription:
|
||||
'This notification appears when the foreground service is running.',
|
||||
onlyAlertOnce: true,
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(
|
||||
showNotification: false,
|
||||
playSound: false,
|
||||
),
|
||||
foregroundTaskOptions: ForegroundTaskOptions(
|
||||
eventAction: ForegroundTaskEventAction.repeat(5000),
|
||||
autoRunOnBoot: true,
|
||||
autoRunOnMyPackageReplaced: true,
|
||||
allowWakeLock: true,
|
||||
allowWifiLock: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ServiceRequestResult> _startService() async {
|
||||
if (await FlutterForegroundTask.isRunningService) {
|
||||
return FlutterForegroundTask.restartService();
|
||||
} else {
|
||||
return FlutterForegroundTask.startService(
|
||||
serviceId: 256,
|
||||
notificationTitle: 'Foreground Service is running',
|
||||
notificationText: 'Tap to return to the app',
|
||||
notificationIcon:
|
||||
NotificationIcon(metaDataName: "eu.twonly.service.TWONLY_LOGO"),
|
||||
notificationInitialRoute: '/',
|
||||
callback: startCallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future _stopService() async {
|
||||
await FlutterForegroundTask.stopService();
|
||||
if (!apiProvider.isAuthenticated) {
|
||||
apiProvider.connect();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
print("STATE: $state");
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_stopService();
|
||||
//apiProvider.connect();
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
apiProvider.close(() {
|
||||
_startService();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
print("STATE: dispose");
|
||||
// apiProvider.close(() {});
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
// disable globalCallbacks to the flutter tree
|
||||
globalCallbackConnectionState = (a) {};
|
||||
globalCallBackOnDownloadChange = (a, b) {};
|
||||
globalCallBackOnContactChange = () {};
|
||||
globalCallBackOnMessageChange = (a) {};
|
||||
FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,13 +39,25 @@ class InitialsAvatar extends StatelessWidget {
|
|||
double proSize = (fontSize == null) ? 40 : (fontSize! * 2);
|
||||
|
||||
return isPro
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0), //or 15.0
|
||||
child: Container(
|
||||
height: proSize,
|
||||
width: proSize,
|
||||
color: avatarColor,
|
||||
child: Center(child: child),
|
||||
? //or 15.0
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 2 * (fontSize ?? 20),
|
||||
minWidth: 2 * (fontSize ?? 20),
|
||||
maxWidth: 2 * (fontSize ?? 20),
|
||||
maxHeight: 2 * (fontSize ?? 20),
|
||||
),
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
height: proSize,
|
||||
width: proSize,
|
||||
//padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
color: avatarColor,
|
||||
child: Center(child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircleAvatar(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PermissionHandlerView extends StatefulWidget {
|
||||
|
|
@ -44,6 +47,28 @@ class PermissionHandlerViewState extends State<PermissionHandlerView> {
|
|||
// if (statuses[Permission.camera]!.isDenied) {
|
||||
// }
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
// 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.
|
||||
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
|
||||
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
|
||||
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
|
||||
}
|
||||
|
||||
// Use this utility only if you provide services that require long-term survival,
|
||||
// such as exact alarm service, healthcare service, or Bluetooth communication.
|
||||
//
|
||||
// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
|
||||
// Using this permission may make app distribution difficult due to Google policy.
|
||||
// if (!await FlutterForegroundTask.canScheduleExactAlarms) {
|
||||
// When you call this function, will be gone to the settings page.
|
||||
// So you need to explain to the user why set it.
|
||||
// await FlutterForegroundTask.openAlarmsAndRemindersSettings();
|
||||
// }
|
||||
}
|
||||
|
||||
/*{Permission.camera: PermissionStatus.granted, Permission.storage: PermissionStatus.granted}*/
|
||||
return statuses;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||
import 'package:twonly/src/views/contact/contact_verify_view.dart';
|
||||
|
||||
class UserContextMenu extends StatefulWidget {
|
||||
|
|
@ -33,6 +34,17 @@ class _UserContextMenuState extends State<UserContextMenu> {
|
|||
? FaIcon(FontAwesomeIcons.shieldHeart)
|
||||
: const Icon(Icons.gpp_maybe_rounded), // Can be any widget
|
||||
),
|
||||
PieAction(
|
||||
tooltip: const Text('Open chat'),
|
||||
onSelect: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatItemDetailsView(user: widget.user);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidComments),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: const Text('Send image'),
|
||||
onSelect: () {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,97 @@
|
|||
{
|
||||
"@@locale": "de",
|
||||
"registerTitle": "Willkommen bei twonly",
|
||||
"@registerTitle": {},
|
||||
"registerSlogan": "Sende Bilder in Echtzeit an Freunde und sei dir sicher, dass nur ihr sie sehen könnt."
|
||||
"registerTitle": "Willkommen bei twonly!",
|
||||
"registerSlogan": "twonly, ein datenschutzfreundlicher Weg, um sich mit Freunden durch sicheren, spontanen Bildaustausch zu verbinden",
|
||||
"onboardingWelcomeTitle": "Willkommen bei twonly!",
|
||||
"onboardingWelcomeBody": "Erlebe eine datenschutzfreundliche Möglichkeit sich mit Freunden durch sicheren, spontanen Bildaustausch zu verbinden.",
|
||||
"onboardingE2eTitle": "Ende-zu-Ende-Verschlüsselung",
|
||||
"onboardingE2eBody": "Deine Privatsphäre ist uns wichtig! Nur aus diesem Grund wurde twonly entwickelt. Genieße durch die Ende-zu-Ende-Verschlüsselung die Gewissheit, dass nur du und deine Freunde die geteilten Momente sehen können.",
|
||||
"onboardingFocusTitle": "Fokussiere dich auf das Teilen von Momenten",
|
||||
"onboardingFocusBody": "Verabschiede dich von süchtig machenden Funktionen! Unsere App wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.",
|
||||
"onboardingSendTwonliesTitle": "Twonlies senden",
|
||||
"onboardingSendTwonliesBody": "Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!",
|
||||
"onboardingNotProductTitle": "Du bist nicht das Produkt!",
|
||||
"onboardingNotProductBody": "Zahlst du für eine App nicht, dann werden deine Daten verkauft. Das entspricht nicht unseren Werten, deshalb haben wir uns entschieden, ein nachhaltiges Geschäftsmodell zu entwickeln, von dem alle profitieren. Du kannst deine Daten privat halten und wir können eine schöne App erstellen.",
|
||||
"onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei",
|
||||
"onboardingBuyOneGetTwoBody": "Um eine werbefreie, datenschutzorientierte App zu schaffen, brauchen wir dich! Wir versuchen, dir den besten Preis anzubieten, damit du twonly für nur 0,99 € / monatlich oder 9,99 € / jährlich erhalten kannst und sogar eine zweite Lizenz kostenlos für deinen twonly-Partner bekommst!",
|
||||
"onboardingGetStartedTitle": "Lass uns anfangen!",
|
||||
"onboardingGetStartedBody": "Du kannst twonly 14 Tage lang kostenlos testen und dann entscheiden, ob es dir wert ist.",
|
||||
"onboardingTryForFree": "Kostenlos testen",
|
||||
"registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!",
|
||||
"registerUsernameDecoration": "Benutzername",
|
||||
"registerUsernameLimits": "Der Benutzername muss 4 bis 12 Zeichen lang sein und darf nur aus Buchstaben (a-z) und Zahlen (0-9) bestehen.",
|
||||
"registerSubmitButton": "Jetzt registrieren!",
|
||||
"newMessageTitle": "Neue Nachricht",
|
||||
"chatsTitle": "Chats",
|
||||
"shareImageTitle": "Teilen mit",
|
||||
"shareImageBestFriends": "Beste Freunde",
|
||||
"shareImagedEditorSendImage": "Senden",
|
||||
"shareImagedEditorShareWith": "Teilen mit",
|
||||
"shareImagedEditorSaveImage": "Speichern",
|
||||
"shareImagedEditorSavedImage": "Gespeichert",
|
||||
"shareImageAllUsers": "Alle Kontakte",
|
||||
"shareImageAllTwonlyWarning": "Twonlies können nur an verifizierte Kontakte gesendet werden!",
|
||||
"searchUsernameInput": "Benutzername",
|
||||
"searchUsernameTitle": "Benutzernamen suchen",
|
||||
"searchUsernameNotFound": "Benutzername nicht gefunden",
|
||||
"searchUsernameNewFollowerTitle": "Folgeanfragen",
|
||||
"searchUsernameQrCodeBtn": "QR-Code scannen",
|
||||
"chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!",
|
||||
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
|
||||
"chatListDetailInput": "Nachricht eingeben",
|
||||
"messageSendState_Received": "Empfangen",
|
||||
"messageSendState_Opened": "Geöffnet",
|
||||
"messageSendState_Send": "Senden",
|
||||
"messageSendState_Sending": "Wird gesendet",
|
||||
"messageSendState_TapToLoad": "Tippe zum Laden",
|
||||
"messageSendState_Loading": "Herunterladen",
|
||||
"imageEditorDrawOk": "Zeichnung machen",
|
||||
"settingsTitle": "Einstellungen",
|
||||
"settingsAccount": "Konto",
|
||||
"settingsSubscription": "Abonnement",
|
||||
"settingsAppearance": "Erscheinungsbild",
|
||||
"settingsPrivacy": "Datenschutz",
|
||||
"settingsPrivacyBlockUsers": "Benutzer blockieren",
|
||||
"settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.",
|
||||
"settingsPrivacyBlockUsersCount": "{len} Kontakt(e)",
|
||||
"settingsNotification": "Benachrichtigung",
|
||||
"settingsHelp": "Hilfe",
|
||||
"settingsHelpSupport": "Support-Center",
|
||||
"settingsHelpVersion": "Version",
|
||||
"settingsHelpLicenses": "Lizenzen",
|
||||
"settingsHelpLegal": "Nutzungsbedingungen & Datenschutzrichtlinie",
|
||||
"settingsAppearanceTheme": "Theme",
|
||||
"settingsAccountDeleteAccount": "Konto löschen",
|
||||
"settingsAccountDeleteModalTitle": "Bist du sicher?",
|
||||
"settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.",
|
||||
"contactVerifyNumberTitle": "Sicherheitsnummer verifizieren",
|
||||
"contactVerifyNumberMarkAsVerified": "Als verifiziert markieren",
|
||||
"contactVerifyNumberClearVerification": "Verifizierung aufheben",
|
||||
"contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.",
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederholen",
|
||||
"next": "Weiter",
|
||||
"close": "Schließen",
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "Ok",
|
||||
"switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.",
|
||||
"addTextItem": "Text",
|
||||
"protectAsARealTwonly": "Als echtes twonly senden!",
|
||||
"addDrawing": "Zeichnung",
|
||||
"addEmoji": "Emoji",
|
||||
"toogleFlashLight": "Taschenlampe umschalten",
|
||||
"searchUsernameNotFoundLong": "\"{username}\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.",
|
||||
"errorUnknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.",
|
||||
"errorBadRequest": "Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.",
|
||||
"errorTooManyRequests": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.",
|
||||
"errorInternalError": "Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.",
|
||||
"errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.",
|
||||
"errorUsernameAlreadyTaken": "Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.",
|
||||
"errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.",
|
||||
"errorUsernameNotFound": "Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.",
|
||||
"errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.",
|
||||
"errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.",
|
||||
"errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.",
|
||||
"errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.",
|
||||
"errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren."
|
||||
}
|
||||
|
|
@ -1,7 +1,22 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"registerTitle": "Welcome to twonly!",
|
||||
"registerSlogan": "Send pictures to friends in real time and be sure you are the only people who can see them.",
|
||||
"registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing",
|
||||
"onboardingWelcomeTitle": "Welcome to twonly!",
|
||||
"onboardingWelcomeBody": "Experience a privacy friendly way to connect with friends through secure, spontaneous image sharing.",
|
||||
"onboardingE2eTitle": "End-to-End Encryption",
|
||||
"onboardingE2eBody": "Your privacy matters! In fact, twonly was only created because there is no secure alternative. Enjoy peace of mind with end-to-end encryption that ensures only you and your friends can see your pictures.",
|
||||
"onboardingFocusTitle": "Focus on sharing moments",
|
||||
"onboardingFocusBody": "Say goodbye to addictive features! Our app was created for sharing moments, free from useless distractions or ads.",
|
||||
"onboardingSendTwonliesTitle": "Send twonlies",
|
||||
"onboardingSendTwonliesBody": "Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!",
|
||||
"onboardingNotProductTitle": "You are not the product!",
|
||||
"onboardingNotProductBody": "If you don't pay, your data is the product that is sold. So we decided to develop a sustainable business model where everyone wins. You can keep your data private and we can create a beautiful app.",
|
||||
"onboardingBuyOneGetTwoTitle": "Buy one get two",
|
||||
"onboardingBuyOneGetTwoBody": "To create a ad-free, privacy-focused app, we need you! We try to offer the best price for you, so you can get twonly for only 0,99€ / monthly or 9,99€ / yearly and even get a second license for free for your twonly partner!",
|
||||
"onboardingGetStartedTitle": "Let's get started!",
|
||||
"onboardingGetStartedBody": "You can test twonly free for 14 days and then decide if it is worth to you.",
|
||||
"onboardingTryForFree": "Try for free",
|
||||
"registerUsernameSlogan": "Please select a username so others can find you!",
|
||||
"registerUsernameDecoration": "Username",
|
||||
"registerUsernameLimits": "Username must be 4 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).",
|
||||
|
|
@ -55,6 +70,7 @@
|
|||
"contactVerifyNumberLongDesc": "To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"next": "Next",
|
||||
"close": "Close",
|
||||
"cancel": "Cancel",
|
||||
"ok": "Ok",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:fixnum/fixnum.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'dart:convert';
|
|||
import 'package:cv/cv.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/components/message_send_state_icon.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
|
@ -130,12 +130,17 @@ Future uploadMediaFile(
|
|||
|
||||
if (uploadToken == null) return;
|
||||
|
||||
bool wasSend = await apiProvider.uploadData(uploadToken, encryptedMedia, 0);
|
||||
|
||||
Logger("api.dart").shout("UPDATE...");
|
||||
// TODO: fragmented upload...
|
||||
if (!await apiProvider.uploadData(uploadToken, encryptedMedia, 0)) {
|
||||
if (!wasSend) {
|
||||
Logger("api.dart").shout("error while uploading media");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger("api.dart").shout("DOING UPDATE");
|
||||
|
||||
box.delete("retransmit-$messageId-media");
|
||||
box.delete("retransmit-$messageId-uploadtoken");
|
||||
await DbContacts.checkAndUpdateFlames(target.toInt());
|
||||
|
|
@ -256,6 +261,7 @@ Future<Uint8List?> getDownloadedMedia(
|
|||
box.delete(mediaToken.toString());
|
||||
box.put("${mediaToken}_downloaded", "deleted");
|
||||
box.delete("${mediaToken}_fromUserId");
|
||||
box.delete("${mediaToken}_messageId");
|
||||
|
||||
return media;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/app.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
|
@ -54,6 +54,25 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
|
|||
final box = await getMediaStorage();
|
||||
|
||||
String boxId = data.uploadToken.toString();
|
||||
if (data.fin && data.data.isEmpty) {
|
||||
// media file was deleted by the server. remove the media from device
|
||||
|
||||
int? messageId = box.get("${data.uploadToken}_messageId");
|
||||
if (messageId != null) {
|
||||
await DbMessages.deleteMessageById(messageId);
|
||||
box.delete(boxId);
|
||||
int? fromUserId = box.get("${data.uploadToken}_fromUserId");
|
||||
if (fromUserId != null) {
|
||||
globalCallBackOnMessageChange(fromUserId);
|
||||
}
|
||||
box.delete("${data.uploadToken}_fromUserId");
|
||||
box.delete("${data.uploadToken}_downloaded");
|
||||
globalCallBackOnDownloadChange(data.uploadToken, false);
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List? buffered = box.get(boxId);
|
||||
Uint8List downloadedBytes;
|
||||
if (buffered != null) {
|
||||
|
|
@ -164,6 +183,7 @@ Future<client.Response> handleNewMessage(
|
|||
List<int> downloadToken = content.downloadToken;
|
||||
Box box = await getMediaStorage();
|
||||
box.put("${downloadToken}_fromUserId", fromUserId.toInt());
|
||||
box.put("${downloadToken}_messageId", messageId);
|
||||
tryDownloadMedia(downloadToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
|||
/// It handles errors and does automatically tries to reconnect on
|
||||
/// errors or network changes.
|
||||
class ApiProvider {
|
||||
final String apiUrl;
|
||||
final String? backupApiUrl;
|
||||
final String apiUrl = "ws://10.99.0.6:3030/api/client";
|
||||
// ws://api.twonly.eu/api/client
|
||||
final String? backupApiUrl = "ws://10.99.0.6:3030/api/client";
|
||||
bool isAuthenticated = false;
|
||||
ApiProvider({required this.apiUrl, required this.backupApiUrl});
|
||||
ApiProvider();
|
||||
|
||||
final log = Logger("ApiProvider");
|
||||
|
||||
|
|
@ -62,6 +63,15 @@ class ApiProvider {
|
|||
tryTransmitMessages();
|
||||
}
|
||||
|
||||
Future close(Function callback) async {
|
||||
if (_channel != null) {
|
||||
await _channel!.sink.close();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
Future<bool> connect() async {
|
||||
if (_channel != null && _channel!.closeCode != null) {
|
||||
return true;
|
||||
|
|
@ -94,7 +104,7 @@ class ApiProvider {
|
|||
globalCallbackConnectionState(false);
|
||||
_channel = null;
|
||||
isAuthenticated = false;
|
||||
tryToReconnect();
|
||||
// tryToReconnect();
|
||||
}
|
||||
|
||||
void _onError(dynamic e) {
|
||||
|
|
@ -157,7 +167,9 @@ class ApiProvider {
|
|||
}
|
||||
|
||||
Future sendResponse(ClientToServer response) async {
|
||||
_channel!.sink.add(response.writeToBuffer());
|
||||
if (_channel != null) {
|
||||
_channel!.sink.add(response.writeToBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result> _sendRequestV0(ClientToServer request,
|
||||
|
|
@ -184,6 +196,7 @@ class ApiProvider {
|
|||
}
|
||||
|
||||
Future authenticate() async {
|
||||
print("try authenticate $isAuthenticated");
|
||||
if (isAuthenticated) return;
|
||||
if (await SignalHelper.getSignalIdentity() == null) {
|
||||
return;
|
||||
|
|
@ -192,6 +205,7 @@ class ApiProvider {
|
|||
var handshake = Handshake()..getchallenge = Handshake_GetChallenge();
|
||||
var req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
print("try authenticate send to server");
|
||||
final result = await _sendRequestV0(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
log.shout("Error auth", result);
|
||||
|
|
|
|||
|
|
@ -144,10 +144,10 @@ String getPushNotificationText(String key, String userName) {
|
|||
pushNotificationText = {
|
||||
"newTextMessage": "%userName% hat die eine Nachricht gesendet.",
|
||||
"newTwonly": "%userName% hat dir einen twonly gesendet.",
|
||||
"newVideo": "%userName% hat die eine Video gesendet.",
|
||||
"newImage": "%userName% hat die eine Bild gesendet.",
|
||||
"newVideo": "%userName% hat dir eine Video gesendet.",
|
||||
"newImage": "%userName% hat dir eine Bild gesendet.",
|
||||
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
|
||||
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
|
||||
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
|
||||
};
|
||||
} else {
|
||||
pushNotificationText = {
|
||||
|
|
@ -214,6 +214,6 @@ Future localPushNotificationNewMessage(
|
|||
user.displayName,
|
||||
msg,
|
||||
notificationDetails,
|
||||
// payload: 'test',
|
||||
payload: message.kind.index.toString(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/identity_key_store_model.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/pre_key_model.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/sender_key_store_model.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/session_store_model.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
|
||||
|
|
|
|||
64
lib/src/tasks/websocket_foreground_task.dart
Normal file
64
lib/src/tasks/websocket_foreground_task.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// The callback function should always be a top-level or static function.
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void startCallback() {
|
||||
FlutterForegroundTask.setTaskHandler(WebsocketForegroundTask());
|
||||
}
|
||||
|
||||
class WebsocketForegroundTask extends TaskHandler {
|
||||
// Called when the task is started.
|
||||
@override
|
||||
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
|
||||
print('onStart(starter: ${starter.name})');
|
||||
|
||||
dbProvider = DbProvider();
|
||||
await dbProvider.ready;
|
||||
apiProvider = ApiProvider();
|
||||
apiProvider.connect();
|
||||
}
|
||||
|
||||
// Called based on the eventAction set in ForegroundTaskOptions.
|
||||
@override
|
||||
void onRepeatEvent(DateTime timestamp) {
|
||||
// Send data to main isolate.
|
||||
final Map<String, dynamic> data = {
|
||||
"timestampMillis": timestamp.millisecondsSinceEpoch,
|
||||
};
|
||||
FlutterForegroundTask.sendDataToMain(data);
|
||||
}
|
||||
|
||||
// Called when the task is destroyed.
|
||||
@override
|
||||
Future<void> onDestroy(DateTime timestamp) async {
|
||||
await apiProvider.close(() {});
|
||||
print('onDestroy');
|
||||
}
|
||||
|
||||
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.
|
||||
@override
|
||||
void onReceiveData(Object data) {
|
||||
print('onReceiveData: $data');
|
||||
}
|
||||
|
||||
// Called when the notification button is pressed.
|
||||
@override
|
||||
void onNotificationButtonPressed(String id) {
|
||||
print('onNotificationButtonPressed: $id');
|
||||
}
|
||||
|
||||
// Called when the notification itself is pressed.
|
||||
@override
|
||||
void onNotificationPressed() {
|
||||
print('onNotificationPressed');
|
||||
}
|
||||
|
||||
// Called when the notification itself is dismissed.
|
||||
@override
|
||||
void onNotificationDismissed() {
|
||||
print('onNotificationDismissed');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/json/user_data.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class UserList extends StatelessWidget {
|
|||
child: VerifiedShield(user),
|
||||
),
|
||||
Text(user.displayName),
|
||||
if (flameCounter >= 0)
|
||||
if (flameCounter >= 1)
|
||||
FlameCounterWidget(
|
||||
user,
|
||||
flameCounter,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/model/messages_model.dart';
|
|||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/providers/download_change_provider.dart';
|
||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer_view.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/contact/contact_view.dart';
|
||||
|
|
@ -138,25 +139,17 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
|
|||
}
|
||||
|
||||
Future _loadAsync({bool updateOpenStatus = false}) async {
|
||||
if (_messages.isEmpty) {
|
||||
_messages =
|
||||
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
|
||||
} else {
|
||||
int lastMessageId = _messages.first.messageId;
|
||||
List<DbMessage> toAppend =
|
||||
await DbMessages.getAllMessagesForUserWithHigherMessageId(
|
||||
widget.user.userId.toInt(), lastMessageId);
|
||||
_messages.insertAll(0, toAppend);
|
||||
}
|
||||
|
||||
try {
|
||||
if (context.mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
// state should be disposed
|
||||
return;
|
||||
}
|
||||
// if (_messages.isEmpty || updateOpenStatus) {
|
||||
_messages =
|
||||
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
|
||||
// } else {
|
||||
// will not update older message states like when they now downloaded...
|
||||
// int lastMessageId = _messages.first.messageId;
|
||||
// List<DbMessage> toAppend =
|
||||
// await DbMessages.getAllMessagesForUserWithHigherMessageId(
|
||||
// widget.user.userId.toInt(), lastMessageId);
|
||||
// _messages.insertAll(0, toAppend);
|
||||
// }
|
||||
|
||||
if (updateOpenStatus) {
|
||||
_messages.where((x) => x.messageOpenedAt == null).forEach((message) {
|
||||
|
|
@ -165,11 +158,20 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
|
|||
if (!alreadyReportedOpened.contains(message.messageOtherId!)) {
|
||||
userOpenedOtherMessage(
|
||||
message.otherUserId, message.messageOtherId!);
|
||||
flutterLocalNotificationsPlugin.cancel(message.messageId);
|
||||
alreadyReportedOpened.add(message.messageOtherId!);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (context.mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
// state should be disposed
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future _sendMessage() async {
|
||||
|
|
@ -184,6 +186,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
|
|||
final changeCounter = context.watch<MessagesChangeProvider>().changeCounter;
|
||||
if (changeCounter.containsKey(widget.user.userId.toInt())) {
|
||||
if (changeCounter[widget.user.userId.toInt()] != lastChangeCounter) {
|
||||
print("FORCE reload");
|
||||
_loadAsync(updateOpenStatus: true);
|
||||
lastChangeCounter = changeCounter[widget.user.userId.toInt()]!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
context.watch<MessagesChangeProvider>().lastMessage;
|
||||
|
||||
List<Contact> allUsers = context
|
||||
.read<ContactChangeProvider>()
|
||||
.watch<ContactChangeProvider>()
|
||||
.allContacts
|
||||
.where((c) => c.accepted)
|
||||
.toList();
|
||||
|
|
@ -106,20 +106,20 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: OutlinedButton.icon(
|
||||
icon: Icon((activeUsers.isEmpty)
|
||||
icon: Icon((allUsers.isEmpty)
|
||||
? Icons.person_add
|
||||
: Icons.camera_alt),
|
||||
onPressed: () {
|
||||
(activeUsers.isEmpty)
|
||||
(allUsers.isEmpty)
|
||||
? Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SearchUsernameView(),
|
||||
),
|
||||
)
|
||||
: globalUpdateOfHomeViewPageIndex(1);
|
||||
: globalUpdateOfHomeViewPageIndex(0);
|
||||
},
|
||||
label: Text((activeUsers.isEmpty)
|
||||
label: Text((allUsers.isEmpty)
|
||||
? context.lang.chatListViewSearchUserNameBtn
|
||||
: context.lang.chatListViewSendFirstTwonly)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:twonly/src/model/contacts_model.dart';
|
|||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/messages_model.dart';
|
||||
import 'package:twonly/src/providers/api/api.dart';
|
||||
import 'package:twonly/src/services/notification_service.dart';
|
||||
|
||||
final _noScreenshot = NoScreenshot.instance;
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
}
|
||||
|
||||
flutterLocalNotificationsPlugin.cancel(widget.message.messageId);
|
||||
List<int> token = content.downloadToken;
|
||||
_imageByte =
|
||||
await getDownloadedMedia(token, widget.message.messageOtherId!);
|
||||
|
|
@ -202,7 +204,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (_imageByte != null)
|
||||
if (_imageByte != null && false)
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:twonly/src/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/components/headline.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:introduction_screen/introduction_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
// Slide 1: Welcome to [App Name]
|
||||
// Text: "Experience a new way to connect with friends through secure, spontaneous image sharing."
|
||||
|
|
@ -29,23 +30,21 @@ class OnboardingView extends StatelessWidget {
|
|||
bodyPadding: EdgeInsets.only(top: 75, left: 10, right: 10),
|
||||
pages: [
|
||||
PageViewModel(
|
||||
title: "Welcome to twonly!",
|
||||
body:
|
||||
"Experience a new way to connect with friends through secure, spontaneous image sharing.",
|
||||
title: context.lang.onboardingWelcomeTitle,
|
||||
body: context.lang.onboardingWelcomeBody,
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
// child: Image.asset('assets/animations/messages.gif'),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/messages.json',
|
||||
'assets/animations/selfie2.json',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: "End-to-End Encryption",
|
||||
body:
|
||||
"Your privacy matters. Enjoy peace of mind with end-to-end encryption, ensuring only you and your friends can see your images.",
|
||||
title: context.lang.onboardingE2eTitle,
|
||||
body: context.lang.onboardingE2eBody,
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
|
|
@ -57,22 +56,19 @@ class OnboardingView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: "Focus on sharing moments",
|
||||
body:
|
||||
"Say goodbye to addictive features! Our app is designed for sharing moments, no useless distractions or ads.",
|
||||
title: context.lang.onboardingFocusTitle,
|
||||
body: context.lang.onboardingFocusBody,
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/selfie2.json',
|
||||
),
|
||||
child: Lottie.asset('assets/animations/takephoto.json',
|
||||
repeat: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: "Send twonlies",
|
||||
body:
|
||||
"Share moments securely with just one other person. twonly ensures that only you and your chosen friend can view the picture, keeping your moments private.",
|
||||
title: context.lang.onboardingSendTwonliesTitle,
|
||||
body: context.lang.onboardingSendTwonliesBody,
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
|
|
@ -84,44 +80,40 @@ class OnboardingView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: "You are not the product!",
|
||||
body:
|
||||
"If you don't pay, your data is the product that is sold. So we decided to develop a sustainable business model where everyone wins. You can keep your data private and we can create a beautiful app.",
|
||||
title: context.lang.onboardingNotProductTitle,
|
||||
body: context.lang.onboardingNotProductBody,
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/product.json',
|
||||
child: Image.asset(
|
||||
'assets/images/onboarding/ricky_the_greedy_racoon.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: "Pricing",
|
||||
title: context.lang.onboardingBuyOneGetTwoTitle,
|
||||
bodyWidget: Column(
|
||||
children: [
|
||||
Text(
|
||||
"To be able to create a sustainable privacy focused app which does not show ads, we have to rely on you! You can get twonly for only 0,99€ / monthly or 9,99€ / yearly. As twonly is for at least two, you get a second user for free, so your twonly partner does not have to pay!",
|
||||
context.lang.onboardingBuyOneGetTwoBody,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/selfie.json',
|
||||
),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/present.lottie.json',
|
||||
),
|
||||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: "Let's get started!",
|
||||
title: context.lang.onboardingGetStartedTitle,
|
||||
bodyWidget: Column(
|
||||
children: [
|
||||
Text(
|
||||
"You can test twonly free for 14 days and then decide if it is worth to you.",
|
||||
context.lang.onboardingGetStartedBody,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
|
|
@ -133,7 +125,7 @@ class OnboardingView extends StatelessWidget {
|
|||
callbackOnSuccess();
|
||||
// On button pressed
|
||||
},
|
||||
child: const Text("Try for free"),
|
||||
child: Text(context.lang.onboardingTryForFree),
|
||||
)),
|
||||
],
|
||||
),
|
||||
|
|
@ -148,8 +140,8 @@ class OnboardingView extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
showNextButton: true,
|
||||
done: const Text("Our plans"),
|
||||
next: const Text("Next"),
|
||||
done: Text("Our plans"),
|
||||
next: Text(context.lang.next),
|
||||
// done: RegisterView(callbackOnSuccess: callbackOnSuccess),
|
||||
onDone: () {
|
||||
callbackOnSuccess();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twonly/src/components/alert_dialog.dart';
|
||||
|
|
|
|||
64
pubspec.lock
64
pubspec.lock
|
|
@ -318,6 +318,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_foreground_task:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_foreground_task
|
||||
sha256: "206017ee1bf864f34b8d7bce664a172717caa21af8da23f55866470dfe316644"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.17.0"
|
||||
flutter_image_compress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1101,6 +1109,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ dependencies:
|
|||
fixnum: ^1.1.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_foreground_task: ^8.17.0
|
||||
flutter_image_compress: ^2.4.0
|
||||
flutter_local_notifications: ^18.0.1
|
||||
flutter_localizations:
|
||||
|
|
@ -82,9 +83,11 @@ flutter:
|
|||
- assets/images/logo.jpg
|
||||
- assets/
|
||||
- assets/images/
|
||||
- assets/images/onboarding/ricky_the_greedy_racoon.png
|
||||
- assets/animations/present.lottie.json
|
||||
- assets/animations/selfie2.json
|
||||
- assets/animations/selfie.json
|
||||
- assets/animations/messages.json
|
||||
- assets/animations/takephoto.json
|
||||
- assets/animations/local.json
|
||||
- assets/animations/test.json
|
||||
- assets/animations/product.json
|
||||
|
|
|
|||
Loading…
Reference in a new issue