mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-24 23:32:13 +00:00
restructure code
This commit is contained in:
parent
ba2f9644c0
commit
1c902bb64d
252 changed files with 1579 additions and 3173 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:twonly/core/frb_generated.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
28
lib/app.dart
28
lib/app.dart
|
|
@ -1,24 +1,26 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/themes/dark.dart';
|
||||
import 'package:twonly/src/themes/light.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/pow.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/app_outdated.dart';
|
||||
import 'package:twonly/src/views/home.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/register.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
|
||||
import 'package:twonly/src/views/unlock_twonly.view.dart';
|
||||
import 'package:twonly/src/visual/components/app_outdated.comp.dart';
|
||||
import 'package:twonly/src/visual/themes/dark.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/views/home.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
|
||||
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
|
@ -131,9 +133,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
if (_isUserCreated) {
|
||||
if (_isTwonlyLocked) {
|
||||
// do not change in case twonly was already unlocked at some point
|
||||
_isTwonlyLocked = AppSession.currentUser.screenLockEnabled;
|
||||
_isTwonlyLocked = appSession.currentUser.screenLockEnabled;
|
||||
}
|
||||
if (AppSession.currentUser.appVersion < 62) {
|
||||
if (appSession.currentUser.appVersion < 62) {
|
||||
_showDatabaseMigration = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -176,7 +178,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
_isTwonlyLocked = false;
|
||||
}),
|
||||
);
|
||||
} else if (AppSession.currentUser.twonlySafeBackup == null &&
|
||||
} else if (appSession.currentUser.twonlySafeBackup == null &&
|
||||
!_skipBackup) {
|
||||
child = SetupBackupView(
|
||||
callBack: () => setState(() {
|
||||
|
|
@ -204,7 +206,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
const AppOutdated(),
|
||||
const AppOutdatedComp(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ import 'dart:async';
|
|||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
|
||||
class AppEnvironment {
|
||||
static late final String cacheDir;
|
||||
|
|
@ -29,17 +26,3 @@ class AppState {
|
|||
class AppGlobalKeys {
|
||||
static final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
}
|
||||
|
||||
class AppSession {
|
||||
static late UserData currentUser;
|
||||
|
||||
static final _userDataUpdateController = StreamController<void>.broadcast();
|
||||
static Stream<void> get onUserUpdated => _userDataUpdateController.stream;
|
||||
|
||||
static void triggerUserUpdate() {
|
||||
_userDataUpdateController.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
late ApiService apiService;
|
||||
late TwonlyDB twonlyDB;
|
||||
|
|
|
|||
17
lib/locator.dart
Normal file
17
lib/locator.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:get_it/get_it.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
|
||||
final GetIt locator = GetIt.instance;
|
||||
|
||||
void setupLocator() {
|
||||
locator
|
||||
..registerLazySingleton<UserService>(UserService.new)
|
||||
..registerLazySingleton<ApiService>(ApiService.new)
|
||||
..registerLazySingleton<TwonlyDB>(TwonlyDB.new);
|
||||
}
|
||||
|
||||
UserService get appSession => locator<UserService>();
|
||||
ApiService get apiService => locator<ApiService>();
|
||||
TwonlyDB get twonlyDB => locator<TwonlyDB>();
|
||||
|
|
@ -10,21 +10,21 @@ import 'package:twonly/app.dart';
|
|||
import 'package:twonly/core/bridge.dart' as bridge;
|
||||
import 'package:twonly/core/frb_generated.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/callbacks/callbacks.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/backup/create.backup.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -34,6 +34,7 @@ void main() async {
|
|||
|
||||
await AppEnvironment.init();
|
||||
Log.init();
|
||||
setupLocator();
|
||||
|
||||
await RustLib.init();
|
||||
|
||||
|
|
@ -60,7 +61,7 @@ void main() async {
|
|||
}
|
||||
|
||||
if (user != null) {
|
||||
AppSession.currentUser = user;
|
||||
appSession.currentUser = user;
|
||||
|
||||
if (user.allowErrorTrackingViaSentry) {
|
||||
AppState.allowErrorTrackingViaSentry = true;
|
||||
|
|
@ -87,18 +88,15 @@ void main() async {
|
|||
|
||||
unawaited(setupPushNotification());
|
||||
|
||||
apiService = ApiService();
|
||||
twonlyDB = TwonlyDB();
|
||||
|
||||
if (user != null) {
|
||||
if (AppSession.currentUser.appVersion < 90) {
|
||||
if (appSession.currentUser.appVersion < 90) {
|
||||
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
|
||||
await updateUser((u) {
|
||||
u.appVersion = 90;
|
||||
});
|
||||
}
|
||||
if (AppSession.currentUser.appVersion < 91) {
|
||||
if (appSession.currentUser.appVersion < 91) {
|
||||
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
|
||||
await makeMigrationToVersion91();
|
||||
await updateUser((u) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
|
|||
// ignore: implementation_imports
|
||||
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
|
||||
import 'package:twonly/core/bridge.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
|
|
@ -140,7 +140,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
t.userDiscoveryVersion.isNotNull() &
|
||||
t.userDiscoveryExcluded.equals(false) &
|
||||
t.mediaSendCounter.isBiggerOrEqualValue(
|
||||
AppSession.currentUser.minimumRequiredImagesExchanged,
|
||||
appSession.currentUser.minimumRequiredImagesExchanged,
|
||||
),
|
||||
))
|
||||
.watch();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
|
|
@ -113,7 +113,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
int contactId,
|
||||
GroupsCompanion group,
|
||||
) async {
|
||||
final groupIdDirectChat = getUUIDforDirectChat(contactId, AppSession.currentUser.userId);
|
||||
final groupIdDirectChat = getUUIDforDirectChat(contactId, appSession.currentUser.userId);
|
||||
final insertGroup = group.copyWith(
|
||||
groupId: Value(groupIdDirectChat),
|
||||
isDirectChat: const Value(true),
|
||||
|
|
@ -209,7 +209,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
}
|
||||
|
||||
Stream<Group?> watchDirectChat(int contactId) {
|
||||
final groupId = getUUIDforDirectChat(contactId, AppSession.currentUser.userId);
|
||||
final groupId = getUUIDforDirectChat(contactId, appSession.currentUser.userId);
|
||||
return (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/reactions.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||
|
||||
part 'reactions.dao.g.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
|||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/tables/receipts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
part 'receipts.dao.g.dart';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||
ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
|
||||
class SignalIdentityKeyStore extends IdentityKeyStore {
|
||||
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
|
||||
|
||||
final IdentityKeyPair identityKeyPair;
|
||||
final int localRegistrationId;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class ConnectPreKeyStore extends PreKeyStore {
|
||||
class SignalPreKeyStore extends PreKeyStore {
|
||||
@override
|
||||
Future<bool> containsPreKey(int preKeyId) async {
|
||||
final preKeyRecord = await (twonlyDB.select(
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/src/database/signal/connect_identity_key_store.dart';
|
||||
import 'package:twonly/src/database/signal/connect_pre_key_store.dart';
|
||||
import 'package:twonly/src/database/signal/connect_session_store.dart';
|
||||
import 'package:twonly/src/database/signal/connect_signed_pre_key_store.dart';
|
||||
import 'package:twonly/src/database/signal/signal_identity_key_store.dart';
|
||||
import 'package:twonly/src/database/signal/signal_pre_key_store.dart';
|
||||
import 'package:twonly/src/database/signal/signal_session_store.dart';
|
||||
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
|
||||
|
||||
class ConnectSignalProtocolStore implements SignalProtocolStore {
|
||||
ConnectSignalProtocolStore(
|
||||
class SignalSignalProtocolStore implements SignalProtocolStore {
|
||||
SignalSignalProtocolStore(
|
||||
IdentityKeyPair identityKeyPair,
|
||||
int registrationId,
|
||||
) {
|
||||
_identityKeyStore = ConnectIdentityKeyStore(
|
||||
_identityKeyStore = SignalIdentityKeyStore(
|
||||
identityKeyPair,
|
||||
registrationId,
|
||||
);
|
||||
}
|
||||
|
||||
final preKeyStore = ConnectPreKeyStore();
|
||||
final sessionStore = ConnectSessionStore();
|
||||
final signedPreKeyStore = ConnectSignedPreKeyStore();
|
||||
final preKeyStore = SignalPreKeyStore();
|
||||
final sessionStore = SignalSessionStore();
|
||||
final signedPreKeyStore = SignalSignedPreKeyStore();
|
||||
|
||||
late IdentityKeyStore _identityKeyStore;
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
class ConnectSenderKeyStore extends SenderKeyStore {
|
||||
class SignalSenderKeyStore extends SenderKeyStore {
|
||||
@override
|
||||
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
||||
final identity =
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
class ConnectSessionStore extends SessionStore {
|
||||
class SignalSessionStore extends SessionStore {
|
||||
@override
|
||||
Future<bool> containsSession(SignalProtocolAddress address) async {
|
||||
final sessions =
|
||||
|
|
@ -3,9 +3,9 @@ import 'dart:convert';
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
|
||||
class ConnectSignedPreKeyStore extends SignedPreKeyStore {
|
||||
class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
||||
Future<HashMap<int, Uint8List>> getStore() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final storeSerialized = await storage.read(
|
||||
|
|
@ -3,7 +3,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'signal_identity.g.dart';
|
||||
part 'signal_identity.model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class SignalIdentity {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'signal_identity.dart';
|
||||
part of 'signal_identity.model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'userdata.g.dart';
|
||||
part 'userdata.model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class UserData {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'userdata.dart';
|
||||
part of 'userdata.model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
|
||||
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
CustomChangeProvider() {
|
||||
_connSub = apiService.onConnectionStateUpdated.listen(
|
||||
updateConnectionState,
|
||||
);
|
||||
// The API is connected before the subscription has started so ensure that the connection state is correct
|
||||
_isConnected = apiService.isConnected;
|
||||
}
|
||||
bool _isConnected = false;
|
||||
bool get isConnected => _isConnected;
|
||||
late bool _isConnected;
|
||||
late StreamSubscription<bool> _connSub;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/subscription.keys.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/model/purchases/purchasable_product.dart';
|
||||
import 'package:twonly/src/model/purchasable_product.model.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
// Gives the option to override in tests.
|
||||
|
|
|
|||
|
|
@ -2,50 +2,50 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:twonly/app.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/views/camera/camera_qr_scanner.view.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to.view.dart';
|
||||
import 'package:twonly/src/views/chats/add_new_user.view.dart';
|
||||
import 'package:twonly/src/views/chats/archived_chats.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer.view.dart';
|
||||
import 'package:twonly/src/views/chats/start_new_chat.view.dart';
|
||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||
import 'package:twonly/src/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/views/groups/group_create_select_members.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/recover.view.dart';
|
||||
import 'package:twonly/src/views/public_profile.view.dart';
|
||||
import 'package:twonly/src/views/settings/account.view.dart';
|
||||
import 'package:twonly/src/views/settings/appearance.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup_server.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
|
||||
import 'package:twonly/src/views/settings/chat/chat_reactions.view.dart';
|
||||
import 'package:twonly/src/views/settings/chat/chat_settings.view.dart';
|
||||
import 'package:twonly/src/views/settings/data_and_storage.view.dart';
|
||||
import 'package:twonly/src/views/settings/data_and_storage/export_media.view.dart';
|
||||
import 'package:twonly/src/views/settings/data_and_storage/import_media.view.dart';
|
||||
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart';
|
||||
import 'package:twonly/src/views/settings/developer/developer.view.dart';
|
||||
import 'package:twonly/src/views/settings/developer/reduce_flames.view.dart';
|
||||
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/changelog.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/contact_us.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/credits.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/diagnostics.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/faq.view.dart';
|
||||
import 'package:twonly/src/views/settings/help/faq/verifybadge.dart';
|
||||
import 'package:twonly/src/views/settings/help/help.view.dart';
|
||||
import 'package:twonly/src/views/settings/notification.view.dart';
|
||||
import 'package:twonly/src/views/settings/privacy.view.dart';
|
||||
import 'package:twonly/src/views/settings/privacy/block_users.view.dart';
|
||||
import 'package:twonly/src/views/settings/privacy/user_discovery.view.dart';
|
||||
import 'package:twonly/src/views/settings/profile/modify_avatar.view.dart';
|
||||
import 'package:twonly/src/views/settings/profile/profile.view.dart';
|
||||
import 'package:twonly/src/views/settings/settings_main.view.dart';
|
||||
import 'package:twonly/src/views/settings/share_with_friends.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_questionnaire.view.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_welcome.view.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_qr_scanner.view.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/add_new_user.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/archived_chats.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/media_viewer.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/start_new_chat.view.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact.view.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/recover.view.dart';
|
||||
import 'package:twonly/src/visual/views/public_profile.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/account.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/appearance.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/chat/chat_settings.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/export_media.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/import_media.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/retransmission_data.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/changelog.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/contact_us.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/credits.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/settings_main.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/share_with_friends.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart';
|
||||
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
|
||||
|
||||
final routerProvider = GoRouter(
|
||||
routes: [
|
||||
|
|
@ -175,9 +175,7 @@ final routerProvider = GoRouter(
|
|||
),
|
||||
GoRoute(
|
||||
path: 'setup',
|
||||
builder: (context, state) => SetupBackupView(
|
||||
isPasswordChangeOnly: state.extra as bool? ?? false,
|
||||
),
|
||||
builder: (context, state) => const SetupBackupView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
|
||||
class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
late ThemeMode _themeMode;
|
||||
|
|
|
|||
|
|
@ -18,18 +18,19 @@ import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
|
|||
import 'package:mutex/mutex.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||
as server;
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/server_messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/api/server_messages.api.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
|
|
@ -37,12 +38,12 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||
import 'package:twonly/src/services/user_study.service.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_data_collection.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
|
||||
final lockConnecting = Mutex();
|
||||
|
|
@ -126,7 +127,7 @@ class ApiService {
|
|||
|
||||
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
|
||||
|
||||
if (AppSession.currentUser.userStudyParticipantsToken != null) {
|
||||
if (appSession.currentUser.userStudyParticipantsToken != null) {
|
||||
// In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection
|
||||
unawaited(handleUserStudyUpload());
|
||||
}
|
||||
|
|
@ -211,7 +212,7 @@ class ApiService {
|
|||
});
|
||||
}
|
||||
|
||||
bool get isConnected => _channel != null && _channel!.closeCode != null;
|
||||
bool get isConnected => _channel != null && _channel!.closeCode == null;
|
||||
|
||||
Future<void> _onDone() async {
|
||||
_reconnectionDelay = 3;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:clock/clock.dart' show clock;
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleAdditionalDataMessage(
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart' hide Message;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -161,7 +162,7 @@ Future<void> handleGroupUpdate(
|
|||
case GroupActionType.demoteToMember:
|
||||
int? affectedContactId = update.affectedContactId.toInt();
|
||||
|
||||
if (affectedContactId == AppSession.currentUser.userId) {
|
||||
if (affectedContactId == appSession.currentUser.userId) {
|
||||
affectedContactId = null;
|
||||
if (actionType == GroupActionType.removedMember) {
|
||||
// Oh no, I just got removed from the group...
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
hide Message;
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleMessageUpdate(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleReaction(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleTextMessage(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -34,17 +34,18 @@ Future<void> handleUserDiscoveryRequest(
|
|||
) async {
|
||||
Log.info('Got a user discovery request');
|
||||
|
||||
if (!AppSession.currentUser.isUserDiscoveryEnabled) {
|
||||
if (!appSession.currentUser.isUserDiscoveryEnabled) {
|
||||
Log.warn('Got a user discovery request while it is disabled');
|
||||
return;
|
||||
}
|
||||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||
if (contact == null) return;
|
||||
|
||||
if (contact.mediaSendCounter < AppSession.currentUser.minimumRequiredImagesExchanged ||
|
||||
if (contact.mediaSendCounter <
|
||||
appSession.currentUser.minimumRequiredImagesExchanged ||
|
||||
contact.userDiscoveryExcluded) {
|
||||
Log.warn(
|
||||
'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${AppSession.currentUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}',
|
||||
'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${appSession.currentUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -72,7 +73,7 @@ Future<void> handleUserDiscoveryUpdate(
|
|||
int fromUserId,
|
||||
EncryptedContent_UserDiscoveryUpdate update,
|
||||
) async {
|
||||
if (!AppSession.currentUser.isUserDiscoveryEnabled) {
|
||||
if (!appSession.currentUser.isUserDiscoveryEnabled) {
|
||||
Log.warn('Got a user discovery update while it is disabled');
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
|
|
@ -8,11 +9,11 @@ import 'package:drift/drift.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -96,7 +97,8 @@ Future<bool> isAllowedToDownload(MediaType type) async {
|
|||
}
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
final options = AppSession.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||
final options =
|
||||
appSession.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||
|
||||
if (connectivityResult.contains(ConnectivityResult.mobile)) {
|
||||
if (type == MediaType.video) {
|
||||
|
|
@ -3,11 +3,11 @@ import 'dart:async';
|
|||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/backup/create.backup.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
|
@ -11,15 +12,16 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -357,7 +359,7 @@ Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
|
|||
|
||||
// if the user has enabled auto storing and the file
|
||||
// was send with unlimited counter not in twonly-Mode then store the file
|
||||
if (AppSession.currentUser.autoStoreAllSendUnlimitedMediaFiles &&
|
||||
if (appSession.currentUser.autoStoreAllSendUnlimitedMediaFiles &&
|
||||
!mediaService.mediaFile.requiresAuthentication &&
|
||||
!mediaService.storedPath.existsSync() &&
|
||||
mediaService.mediaFile.displayLimitInMilliseconds == null) {
|
||||
|
|
@ -595,7 +597,7 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
|
|||
|
||||
if (AppState.isInBackgroundTask ||
|
||||
!connectivityResult.contains(ConnectivityResult.mobile) &&
|
||||
!connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
!connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
// no internet, directly put it into the background...
|
||||
await FileDownloader().enqueue(task);
|
||||
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -345,12 +346,12 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
return null;
|
||||
}
|
||||
}
|
||||
encryptedContent.senderProfileCounter = Int64(AppSession.currentUser.avatarCounter);
|
||||
encryptedContent.senderProfileCounter = Int64(appSession.currentUser.avatarCounter);
|
||||
|
||||
if (AppSession.currentUser.isUserDiscoveryEnabled && messageId != null) {
|
||||
if (appSession.currentUser.isUserDiscoveryEnabled && messageId != null) {
|
||||
final contact = await twonlyDB.contactsDao.getContactById(contactId);
|
||||
if (contact != null &&
|
||||
contact.mediaSendCounter >= AppSession.currentUser.minimumRequiredImagesExchanged &&
|
||||
contact.mediaSendCounter >= appSession.currentUser.minimumRequiredImagesExchanged &&
|
||||
!contact.userDiscoveryExcluded) {
|
||||
final version = await UserDiscoveryService.getCurrentVersion();
|
||||
if (version != null) {
|
||||
|
|
@ -406,7 +407,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
}
|
||||
|
||||
Future<void> sendTypingIndication(String groupId, bool isTyping) async {
|
||||
if (!AppSession.currentUser.typingIndicators) return;
|
||||
if (!appSession.currentUser.typingIndicators) return;
|
||||
await sendCipherTextToGroup(
|
||||
groupId,
|
||||
pb.EncryptedContent(
|
||||
|
|
@ -462,15 +463,15 @@ Future<void> notifyContactAboutOpeningMessage(
|
|||
|
||||
Future<void> sendContactMyProfileData(int contactId) async {
|
||||
List<int>? avatarSvgCompressed;
|
||||
if (AppSession.currentUser.avatarSvg != null) {
|
||||
avatarSvgCompressed = gzip.encode(utf8.encode(AppSession.currentUser.avatarSvg!));
|
||||
if (appSession.currentUser.avatarSvg != null) {
|
||||
avatarSvgCompressed = gzip.encode(utf8.encode(appSession.currentUser.avatarSvg!));
|
||||
}
|
||||
final encryptedContent = pb.EncryptedContent(
|
||||
contactUpdate: pb.EncryptedContent_ContactUpdate(
|
||||
type: pb.EncryptedContent_ContactUpdate_Type.UPDATE,
|
||||
avatarSvgCompressed: avatarSvgCompressed,
|
||||
displayName: AppSession.currentUser.displayName,
|
||||
username: AppSession.currentUser.username,
|
||||
displayName: appSession.currentUser.displayName,
|
||||
username: appSession.currentUser.username,
|
||||
),
|
||||
);
|
||||
await sendCipherText(contactId, encryptedContent);
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart' hide Message;
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
||||
|
|
@ -25,7 +27,7 @@ import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart';
|
|||
import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
|
||||
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
|
||||
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
|
|
@ -263,7 +265,8 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId);
|
||||
|
||||
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
|
||||
if (AppSession.currentUser.isUserDiscoveryEnabled && content.hasSenderUserDiscoveryVersion()) {
|
||||
if (appSession.currentUser.isUserDiscoveryEnabled &&
|
||||
content.hasSenderUserDiscoveryVersion()) {
|
||||
await checkForUserDiscoveryChanges(
|
||||
fromUserId,
|
||||
content.senderUserDiscoveryVersion,
|
||||
|
|
@ -351,7 +354,8 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
|
||||
/// Verify that the user is (still) in that group...
|
||||
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
|
||||
if (getUUIDforDirectChat(AppSession.currentUser.userId, fromUserId) == content.groupId) {
|
||||
if (getUUIDforDirectChat(appSession.currentUser.userId, fromUserId) ==
|
||||
content.groupId) {
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
||||
|
|
@ -11,7 +11,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
|
|||
as server;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
|
||||
hide Message;
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/exclusive_access.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
// ignore: unreachable_from_main
|
||||
|
|
@ -56,7 +56,7 @@ Future<bool> initBackgroundExecution() async {
|
|||
// stay alive for multiple hours between task executions
|
||||
final user = await getUser();
|
||||
if (user == null) return false;
|
||||
AppSession.currentUser = user;
|
||||
appSession.currentUser = user;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ Future<bool> initBackgroundExecution() async {
|
|||
|
||||
final user = await getUser();
|
||||
if (user == null) return false;
|
||||
AppSession.currentUser = user;
|
||||
|
||||
twonlyDB = TwonlyDB();
|
||||
apiService = ApiService();
|
||||
setupLocator();
|
||||
appSession.currentUser = user;
|
||||
|
||||
AppState.isInBackgroundTask = true;
|
||||
|
||||
_isInitialized = true;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/hashlib.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/services/backup/create.backup.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> enableTwonlySafe(String password) async {
|
||||
final (backupId, encryptionKey) = await getMasterKey(
|
||||
password,
|
||||
AppSession.currentUser.username,
|
||||
appSession.currentUser.username,
|
||||
);
|
||||
|
||||
await updateUser((user) {
|
||||
|
|
@ -65,10 +66,10 @@ Future<(Uint8List, Uint8List)> getMasterKey(
|
|||
}
|
||||
|
||||
String? getTwonlySafeBackupUrl() {
|
||||
if (AppSession.currentUser.twonlySafeBackup == null) return null;
|
||||
if (appSession.currentUser.twonlySafeBackup == null) return null;
|
||||
return getTwonlySafeBackupUrlFromServer(
|
||||
AppSession.currentUser.twonlySafeBackup!.backupId,
|
||||
AppSession.currentUser.backupServer,
|
||||
appSession.currentUser.twonlySafeBackup!.backupId,
|
||||
appSession.currentUser.backupServer,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
|
|
@ -11,28 +12,29 @@ import 'package:drift_flutter/drift_flutter.dart';
|
|||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
||||
import 'package:twonly/src/services/backup/common.backup.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> performTwonlySafeBackup({bool force = false}) async {
|
||||
if (AppSession.currentUser.twonlySafeBackup == null) {
|
||||
if (appSession.currentUser.twonlySafeBackup == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppSession.currentUser.twonlySafeBackup!.backupUploadState ==
|
||||
if (appSession.currentUser.twonlySafeBackup!.backupUploadState ==
|
||||
LastBackupUploadState.pending) {
|
||||
Log.warn('Backup upload is already pending.');
|
||||
return;
|
||||
}
|
||||
|
||||
final lastUpdateTime =
|
||||
AppSession.currentUser.twonlySafeBackup!.lastBackupDone;
|
||||
appSession.currentUser.twonlySafeBackup!.lastBackupDone;
|
||||
if (!force && lastUpdateTime != null) {
|
||||
if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
|
||||
return;
|
||||
|
|
@ -120,8 +122,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
|
||||
|
||||
if (AppSession.currentUser.twonlySafeBackup!.lastBackupDone == null ||
|
||||
AppSession.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter(
|
||||
if (appSession.currentUser.twonlySafeBackup!.lastBackupDone == null ||
|
||||
appSession.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter(
|
||||
clock.now().subtract(const Duration(days: 90)),
|
||||
)) {
|
||||
force = true;
|
||||
|
|
@ -150,7 +152,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
final secretBox = await chacha20.encrypt(
|
||||
backupBytes,
|
||||
secretKey: SecretKey(
|
||||
AppSession.currentUser.twonlySafeBackup!.encryptionKey,
|
||||
appSession.currentUser.twonlySafeBackup!.encryptionKey,
|
||||
),
|
||||
nonce: nonce,
|
||||
);
|
||||
|
|
@ -173,9 +175,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.',
|
||||
);
|
||||
|
||||
if (AppSession.currentUser.backupServer != null) {
|
||||
if (appSession.currentUser.backupServer != null) {
|
||||
if (encryptedBackupBytes.length >
|
||||
AppSession.currentUser.backupServer!.maxBackupBytes) {
|
||||
appSession.currentUser.backupServer!.maxBackupBytes) {
|
||||
Log.error('Backup is to big for the alternative backup server.');
|
||||
await updateUser((user) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
|
@ -9,12 +10,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
||||
import 'package:twonly/src/services/backup/common.backup.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> recoverBackup(
|
||||
String username,
|
||||
|
|
@ -23,10 +24,7 @@ Future<void> recoverBackup(
|
|||
) async {
|
||||
final (backupId, encryptionKey) = await getMasterKey(password, username);
|
||||
|
||||
final backupServerUrl = await getTwonlySafeBackupUrlFromServer(
|
||||
backupId,
|
||||
server,
|
||||
);
|
||||
final backupServerUrl = getTwonlySafeBackupUrlFromServer(backupId, server);
|
||||
|
||||
if (backupServerUrl == null) {
|
||||
Log.error('Could not create backup url');
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import 'package:clock/clock.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> syncFlameCounters({String? forceForGroup}) async {
|
||||
final groups = await twonlyDB.groupsDao.getAllGroups();
|
||||
|
|
@ -17,7 +17,7 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
|||
(x) => x.totalMediaCounter == maxMessageCounter,
|
||||
);
|
||||
|
||||
if (AppSession.currentUser.myBestFriendGroupId != bestFriend.groupId) {
|
||||
if (appSession.currentUser.myBestFriendGroupId != bestFriend.groupId) {
|
||||
await updateUser((user) {
|
||||
user.myBestFriendGroupId = bestFriend.groupId;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
|
|
@ -13,13 +14,13 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -42,8 +43,8 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
|||
final memberIds = members.map((x) => Int64(x.userId)).toList();
|
||||
|
||||
final groupState = EncryptedGroupState(
|
||||
memberIds: [Int64(AppSession.currentUser.userId)] + memberIds,
|
||||
adminIds: [Int64(AppSession.currentUser.userId)],
|
||||
memberIds: [Int64(appSession.currentUser.userId)] + memberIds,
|
||||
adminIds: [Int64(appSession.currentUser.userId)],
|
||||
groupName: groupName,
|
||||
deleteMessagesAfterMilliseconds: Int64(
|
||||
defaultDeleteMessagesAfterMilliseconds,
|
||||
|
|
@ -283,9 +284,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
final myPubKey = keyPair.getPublicKey().serialize().toList();
|
||||
|
||||
if (listEquals(appendedPubKey, myPubKey)) {
|
||||
adminIds.remove(Int64(AppSession.currentUser.userId));
|
||||
adminIds.remove(Int64(appSession.currentUser.userId));
|
||||
memberIds.remove(
|
||||
Int64(AppSession.currentUser.userId),
|
||||
Int64(appSession.currentUser.userId),
|
||||
); // -> Will remove the user later...
|
||||
} else {
|
||||
Log.info('A non admin left the group!!!');
|
||||
|
|
@ -303,7 +304,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
}
|
||||
}
|
||||
|
||||
if (!memberIds.contains(Int64(AppSession.currentUser.userId))) {
|
||||
if (!memberIds.contains(Int64(appSession.currentUser.userId))) {
|
||||
// OH no, I am no longer a member of this group...
|
||||
// Return from the group...
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
|
|
@ -316,7 +317,10 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
}
|
||||
|
||||
final isGroupAdmin =
|
||||
adminIds.firstWhereOrNull((t) => t.toInt() == AppSession.currentUser.userId) != null;
|
||||
adminIds.firstWhereOrNull(
|
||||
(t) => t.toInt() == appSession.currentUser.userId,
|
||||
) !=
|
||||
null;
|
||||
|
||||
if (!listEquals(memberIds, encryptedGroupState.memberIds)) {
|
||||
if (isGroupAdmin) {
|
||||
|
|
@ -368,7 +372,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
|
||||
// First find and insert NEW members
|
||||
for (final memberId in memberIds) {
|
||||
if (memberId == Int64(AppSession.currentUser.userId)) {
|
||||
if (memberId == Int64(appSession.currentUser.userId)) {
|
||||
continue;
|
||||
}
|
||||
if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) {
|
||||
|
|
@ -838,7 +842,9 @@ Future<bool> removeMemberFromGroup(
|
|||
groupId: Value(group.groupId),
|
||||
type: const Value(GroupActionType.removedMember),
|
||||
affectedContactId: Value(
|
||||
removeContactId == AppSession.currentUser.userId ? null : removeContactId,
|
||||
removeContactId == appSession.currentUser.userId
|
||||
? null
|
||||
: removeContactId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -945,7 +951,7 @@ Future<bool> leaveAsNonAdminFromGroup(Group group) async {
|
|||
EncryptedContent(
|
||||
groupUpdate: EncryptedContent_GroupUpdate(
|
||||
groupActionType: groupActionType.name,
|
||||
affectedContactId: Int64(AppSession.currentUser.userId),
|
||||
affectedContactId: Int64(appSession.currentUser.userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,23 +2,24 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/views/chats/add_new_user.view.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/visual/views/chats/add_new_user.view.dart';
|
||||
|
||||
Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||
if (!uri.scheme.startsWith('http')) return false;
|
||||
|
|
@ -32,7 +33,7 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
|
||||
if (!context.mounted) return false;
|
||||
|
||||
if (username == AppSession.currentUser.username) {
|
||||
if (username == appSession.currentUser.username) {
|
||||
await context.push(Routes.settingsPublicProfile);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -115,7 +116,7 @@ Future<void> handleIntentMediaFile(
|
|||
|
||||
final newMediaService = await initializeMediaUpload(
|
||||
type,
|
||||
AppSession.currentUser.defaultShowTime,
|
||||
appSession.currentUser.defaultShowTime,
|
||||
);
|
||||
if (newMediaService == null) {
|
||||
Log.error('Could not create new media file for intent shared file');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:pro_video_editor/pro_video_editor.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/channels/video_compression.channel.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/mediafiles/compression.service.dart';
|
||||
|
|
@ -237,7 +239,7 @@ class MediaFileService {
|
|||
}
|
||||
if (tempPath.existsSync()) {
|
||||
await tempPath.copy(storedPath.path);
|
||||
if (AppSession.currentUser.storeMediaFilesInGallery) {
|
||||
if (appSession.currentUser.storeMediaFilesInGallery) {
|
||||
if (mediaFile.type == MediaType.video) {
|
||||
await saveVideoToGallery(storedPath.path);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:cryptography_plus/cryptography_plus.dart';
|
|||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations_de.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations_en.dart';
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import 'package:firebase_core/firebase_core.dart';
|
|||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
import '../../../firebase_options.dart';
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ Future<void> checkForTokenUpdates() async {
|
|||
}
|
||||
|
||||
Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
||||
if (AppSession.currentUser.updateFCMToken || force) {
|
||||
if (appSession.currentUser.updateFCMToken || force) {
|
||||
const storage = FlutterSecureStorage();
|
||||
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
|
||||
if (storedToken != null) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
|
|
@ -9,14 +10,14 @@ import 'package:fixnum/fixnum.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
|||
import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
|
||||
import 'package:twonly/src/model/json/signal_identity.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/database/signal/signal_protocol_store.dart';
|
||||
import 'package:twonly/src/model/json/signal_identity.model.dart';
|
||||
import 'package:twonly/src/services/signal/consts.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
|
||||
final signalIdentity = await getSignalIdentity();
|
||||
|
|
@ -20,10 +21,10 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
|
|||
// This function runs after the clients authenticated with the server.
|
||||
// It then checks if it should update a new session key
|
||||
Future<void> signalHandleNewServerConnection() async {
|
||||
if (AppSession.currentUser.signalLastSignedPreKeyUpdated != null) {
|
||||
if (appSession.currentUser.signalLastSignedPreKeyUpdated != null) {
|
||||
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
|
||||
final isYoungerThan48Hours =
|
||||
(AppSession.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
|
||||
(appSession.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
|
||||
fortyEightHoursAgo,
|
||||
);
|
||||
if (isYoungerThan48Hours) {
|
||||
|
|
@ -104,7 +105,7 @@ Future<void> createIfNotExistsSignalIdentity() async {
|
|||
final identityKeyPair = generateIdentityKeyPair();
|
||||
final registrationId = generateRegistrationId(true);
|
||||
|
||||
final signalStore = ConnectSignalProtocolStore(
|
||||
final signalStore = SignalSignalProtocolStore(
|
||||
identityKeyPair,
|
||||
registrationId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/services/signal/consts.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
|
||||
import 'package:twonly/src/model/json/signal_identity.dart';
|
||||
import 'package:twonly/src/database/signal/signal_protocol_store.dart';
|
||||
import 'package:twonly/src/model/json/signal_identity.model.dart';
|
||||
import 'package:twonly/src/services/signal/consts.signal.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
|
||||
Future<ConnectSignalProtocolStore?> getSignalStore() async {
|
||||
Future<SignalSignalProtocolStore?> getSignalStore() async {
|
||||
return getSignalStoreFromIdentity((await getSignalIdentity())!);
|
||||
}
|
||||
|
||||
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
|
||||
Future<SignalSignalProtocolStore> getSignalStoreFromIdentity(
|
||||
SignalIdentity signalIdentity,
|
||||
) async {
|
||||
final identityKeyPair = IdentityKeyPair.fromSerialized(
|
||||
signalIdentity.identityKeyPairU8List,
|
||||
);
|
||||
|
||||
return ConnectSignalProtocolStore(
|
||||
return SignalSignalProtocolStore(
|
||||
identityKeyPair,
|
||||
signalIdentity.registrationId,
|
||||
);
|
||||
|
|
|
|||
91
lib/src/services/user.service.dart
Normal file
91
lib/src/services/user.service.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class UserService {
|
||||
late UserData currentUser;
|
||||
|
||||
final _userDataUpdateController = StreamController<void>.broadcast();
|
||||
Stream<void> get onUserUpdated => _userDataUpdateController.stream;
|
||||
|
||||
void triggerUserUpdate() {
|
||||
_userDataUpdateController.add(null);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_userDataUpdateController.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isUserCreated() async {
|
||||
final user = await getUser();
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
appSession.currentUser = user;
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<UserData?> getUser() async {
|
||||
try {
|
||||
final userJson = await const FlutterSecureStorage().read(
|
||||
key: SecureStorageKeys.userData,
|
||||
);
|
||||
if (userJson == null) {
|
||||
return null;
|
||||
}
|
||||
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
|
||||
final user = UserData.fromJson(userMap);
|
||||
return user;
|
||||
} catch (e) {
|
||||
Log.error('Error getting user: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUsersPlan(
|
||||
BuildContext context,
|
||||
SubscriptionPlan plan,
|
||||
) async {
|
||||
context.read<PurchasesProvider>().plan = plan;
|
||||
|
||||
await updateUser((user) {
|
||||
user.subscriptionPlan = plan.name;
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
context.read<PurchasesProvider>().updatePlan(plan);
|
||||
}
|
||||
|
||||
Mutex updateProtection = Mutex();
|
||||
|
||||
Future<void> updateUser(
|
||||
void Function(UserData userData) updateUser,
|
||||
) async {
|
||||
await updateProtection.protect(() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.defaultShowTime == 999999) {
|
||||
// This was the old version for infinity -> change it to null
|
||||
user.defaultShowTime = null;
|
||||
}
|
||||
updateUser(user);
|
||||
await const FlutterSecureStorage().write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: jsonEncode(user),
|
||||
);
|
||||
appSession.currentUser = user;
|
||||
});
|
||||
|
||||
appSession.triggerUserUpdate();
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/qr.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class UserDiscoveryService {
|
||||
static Future<void> checkForNewAnnouncedUsers() async {
|
||||
|
|
@ -53,7 +54,7 @@ class UserDiscoveryService {
|
|||
try {
|
||||
await FlutterUserDiscovery.initializeOrUpdate(
|
||||
threshold: threshold,
|
||||
userId: AppSession.currentUser.userId,
|
||||
userId: appSession.currentUser.userId,
|
||||
publicKey: await getUserPublicKey(),
|
||||
);
|
||||
await updateUser(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
const userStudySurveyKey = 'user_study_survey';
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ const surveyUrlBase = 'https://survey.twonly.org/upload.php';
|
|||
|
||||
Future<void> handleUserStudyUpload() async {
|
||||
try {
|
||||
final token = AppSession.currentUser.userStudyParticipantsToken;
|
||||
final token = appSession.currentUser.userStudyParticipantsToken;
|
||||
if (token == null) return;
|
||||
|
||||
// in case the survey was taken offline try again
|
||||
|
|
@ -35,8 +35,8 @@ Future<void> handleUserStudyUpload() async {
|
|||
await KeyValueStore.delete(userStudySurveyKey);
|
||||
}
|
||||
|
||||
if (AppSession.currentUser.lastUserStudyDataUpload != null &&
|
||||
isToday(AppSession.currentUser.lastUserStudyDataUpload!)) {
|
||||
if (appSession.currentUser.lastUserStudyDataUpload != null &&
|
||||
isToday(appSession.currentUser.lastUserStudyDataUpload!)) {
|
||||
// Only send updates once a day.
|
||||
// This enables to see if improvements to actually work.
|
||||
return;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import 'dart:ui';
|
||||
|
||||
class DefaultColors {
|
||||
static const messageSelf = Color.fromARGB(255, 58, 136, 102);
|
||||
static const messageOther = Color.fromARGB(233, 68, 137, 255);
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
|
||||
String getAvatarSvg(Uint8List avatarSvgCompressed) {
|
||||
return utf8.decode(gzip.decode(avatarSvgCompressed));
|
||||
}
|
||||
|
||||
Future<void> createPushAvatars({int? forceForUserId}) async {
|
||||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
|
|
@ -47,13 +53,13 @@ File avatarPNGFile(int contactId) {
|
|||
}
|
||||
|
||||
Future<Uint8List> getUserAvatar() async {
|
||||
if (AppSession.currentUser.avatarSvg == null) {
|
||||
if (appSession.currentUser.avatarSvg == null) {
|
||||
final data = await rootBundle.load('assets/images/default_avatar.png');
|
||||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
final pictureInfo = await vg.loadPicture(
|
||||
SvgStringLoader(AppSession.currentUser.avatarSvg!),
|
||||
SvgStringLoader(appSession.currentUser.avatarSvg!),
|
||||
null,
|
||||
);
|
||||
|
||||
|
|
@ -62,7 +68,8 @@ Future<Uint8List> getUserAvatar() async {
|
|||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
final file = avatarPNGFile(AppSession.currentUser.userId)..writeAsBytesSync(pngBytes);
|
||||
final file = avatarPNGFile(appSession.currentUser.userId)
|
||||
..writeAsBytesSync(pngBytes);
|
||||
pictureInfo.picture.dispose();
|
||||
|
||||
return file.readAsBytesSync();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:clock/clock.dart';
|
||||
|
|
@ -11,8 +10,6 @@ import 'package:gal/gal.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
|
|
@ -131,30 +128,6 @@ String formatDuration(BuildContext context, int seconds) {
|
|||
}
|
||||
}
|
||||
|
||||
InputDecoration getInputDecoration(BuildContext context, String hintText) {
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
borderSide: BorderSide(color: primaryColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
||||
final result = await FlutterImageCompress.compressWithList(
|
||||
imageBytes,
|
||||
quality: 90,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> authenticateUser(
|
||||
String localizedReason, {
|
||||
bool force = true,
|
||||
|
|
@ -180,27 +153,6 @@ Future<bool> authenticateUser(
|
|||
return false;
|
||||
}
|
||||
|
||||
Uint8List intToBytes(int value) {
|
||||
final byteData = ByteData(4)..setInt32(0, value);
|
||||
return byteData.buffer.asUint8List();
|
||||
}
|
||||
|
||||
int bytesToInt(Uint8List bytes) {
|
||||
final byteData = ByteData.sublistView(bytes);
|
||||
return byteData.getInt32(0);
|
||||
}
|
||||
|
||||
List<Uint8List>? removeLastXBytes(Uint8List original, int count) {
|
||||
if (original.length < count) {
|
||||
return null;
|
||||
}
|
||||
final newList = Uint8List(original.length - count)
|
||||
..setAll(0, original.sublist(0, original.length - count));
|
||||
|
||||
final lastXBytes = original.sublist(original.length - count);
|
||||
return [newList, lastXBytes];
|
||||
}
|
||||
|
||||
bool isDarkMode(BuildContext context) {
|
||||
final selectedTheme = context.read<SettingsChangeProvider>().themeMode;
|
||||
|
||||
|
|
@ -218,31 +170,6 @@ bool isToday(DateTime lastImageSend) {
|
|||
lastImageSend.day == now.day;
|
||||
}
|
||||
|
||||
InputDecoration inputTextMessageDeco(BuildContext context) {
|
||||
return InputDecoration(
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: const BorderSide(color: Colors.grey, width: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String truncateString(String input, {int maxLength = 20}) {
|
||||
if (input.length > maxLength) {
|
||||
return '${input.characters.take(maxLength)}...';
|
||||
|
|
@ -301,35 +228,6 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList(
|
|||
),
|
||||
);
|
||||
|
||||
Color getMessageColorFromType(
|
||||
Message message,
|
||||
MediaFile? mediaFile,
|
||||
BuildContext context,
|
||||
) {
|
||||
Color color;
|
||||
|
||||
if (message.type == MessageType.restoreFlameCounter.name) {
|
||||
color = Colors.orange;
|
||||
} else if (message.type == MessageType.text.name) {
|
||||
color = Colors.blueAccent;
|
||||
} else if (mediaFile != null) {
|
||||
if (mediaFile.requiresAuthentication) {
|
||||
color = context.color.primary;
|
||||
} else {
|
||||
if (mediaFile.type == MediaType.video) {
|
||||
color = const Color.fromARGB(255, 243, 33, 208);
|
||||
} else if (mediaFile.type == MediaType.audio) {
|
||||
color = const Color.fromARGB(255, 252, 149, 85);
|
||||
} else {
|
||||
color = Colors.redAccent;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (isDarkMode(context)) ? Colors.white : Colors.black;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
String getUUIDforDirectChat(int a, int b) {
|
||||
if (a < 0 || b < 0) {
|
||||
throw ArgumentError('Inputs must be non-negative integers.');
|
||||
|
|
@ -385,17 +283,6 @@ String friendlyDateTime(
|
|||
return '$timePart $datePart';
|
||||
}
|
||||
|
||||
String getAvatarSvg(Uint8List avatarSvgCompressed) {
|
||||
final raw = gzip.decode(avatarSvgCompressed);
|
||||
return utf8.decode(raw);
|
||||
}
|
||||
|
||||
void printWrapped(String text) {
|
||||
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
|
||||
// ignore: avoid_print
|
||||
pattern.allMatches(text).forEach((match) => print(match.group(0)));
|
||||
}
|
||||
|
||||
Future<List<int>> sha256File(File file) async {
|
||||
final input = file.openRead();
|
||||
final sha256Sink = AccumulatorSink<Digest>();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
|
||||
|
|
@ -17,8 +17,8 @@ Future<Uint8List> getProfileQrCodeData() async {
|
|||
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
|
||||
|
||||
final publicProfile = PublicProfile(
|
||||
userId: Int64(AppSession.currentUser.userId),
|
||||
username: AppSession.currentUser.username,
|
||||
userId: Int64(appSession.currentUser.userId),
|
||||
username: appSession.currentUser.username,
|
||||
publicIdentityKey: (await signalStore.getIdentityKeyPair())
|
||||
.getPublicKey()
|
||||
.serialize(),
|
||||
|
|
|
|||
|
|
@ -1,78 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<bool> isUserCreated() async {
|
||||
final user = await getUser();
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
AppSession.currentUser = user;
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<UserData?> getUser() async {
|
||||
try {
|
||||
final userJson = await const FlutterSecureStorage().read(
|
||||
key: SecureStorageKeys.userData,
|
||||
);
|
||||
if (userJson == null) {
|
||||
return null;
|
||||
}
|
||||
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
|
||||
final user = UserData.fromJson(userMap);
|
||||
return user;
|
||||
} catch (e) {
|
||||
Log.error('Error getting user: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUsersPlan(
|
||||
BuildContext context,
|
||||
SubscriptionPlan plan,
|
||||
) async {
|
||||
context.read<PurchasesProvider>().plan = plan;
|
||||
|
||||
await updateUser((user) {
|
||||
user.subscriptionPlan = plan.name;
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
context.read<PurchasesProvider>().updatePlan(plan);
|
||||
}
|
||||
|
||||
Mutex updateProtection = Mutex();
|
||||
|
||||
Future<void> updateUser(
|
||||
void Function(UserData userData) updateUser,
|
||||
) async {
|
||||
await updateProtection.protect(() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.defaultShowTime == 999999) {
|
||||
// This was the old version for infinity -> change it to null
|
||||
user.defaultShowTime = null;
|
||||
}
|
||||
updateUser(user);
|
||||
await const FlutterSecureStorage().write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: jsonEncode(user),
|
||||
);
|
||||
AppSession.currentUser = user;
|
||||
});
|
||||
|
||||
AppSession.triggerUserUpdate();
|
||||
}
|
||||
|
||||
Future<bool> deleteLocalUserData() async {
|
||||
final appDir = await getApplicationSupportDirectory();
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FingerprintText extends StatelessWidget {
|
||||
const FingerprintText(this.longString, {super.key});
|
||||
final String longString;
|
||||
|
||||
String formatString(String input) {
|
||||
final formattedString = StringBuffer();
|
||||
var blockCount = 0;
|
||||
|
||||
for (var i = 0; i < input.length; i += 4) {
|
||||
final block = input.substring(
|
||||
i,
|
||||
i + 4 > input.length ? input.length : i + 4,
|
||||
);
|
||||
formattedString.write(block);
|
||||
blockCount++;
|
||||
|
||||
if (blockCount == 5) {
|
||||
formattedString.writeln();
|
||||
blockCount = 0;
|
||||
} else {
|
||||
formattedString.write(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return formattedString.toString().trim();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectableText(
|
||||
formatString(longString),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/account/refund_credits.view.dart';
|
||||
|
||||
class AccountView extends StatefulWidget {
|
||||
const AccountView({super.key});
|
||||
|
||||
@override
|
||||
State<AccountView> createState() => _AccountViewState();
|
||||
}
|
||||
|
||||
class _AccountViewState extends State<AccountView> {
|
||||
String? formattedBallance;
|
||||
bool hasRemainingBallance = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final ballance = await apiService.loadPlanBalance(useCache: false);
|
||||
if (ballance == null || !mounted) return;
|
||||
var ballanceInCents = ballance.transactions
|
||||
.where(
|
||||
(x) =>
|
||||
x.transactionType != Response_TransactionTypes.ThanksForTesting ||
|
||||
!kReleaseMode,
|
||||
)
|
||||
.map((a) => a.depositCents.toInt())
|
||||
.sum;
|
||||
if (ballanceInCents < 0) {
|
||||
ballanceInCents = 0;
|
||||
}
|
||||
hasRemainingBallance = ballanceInCents > 0;
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
formattedBallance = NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(ballanceInCents / 100);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsAccount),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
// ListTile(
|
||||
// title: const Text('Transfer account'),
|
||||
// subtitle: const Text('Coming soon'),
|
||||
// onTap: () async {
|
||||
// await showAlertDialog(
|
||||
// context,
|
||||
// 'Coming soon',
|
||||
// 'This feature is not yet implemented!',
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// const Divider(),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
|
||||
child: const Text(
|
||||
'Danger Zone',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.lang.settingsAccountDeleteAccount,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
subtitle: (formattedBallance == null)
|
||||
? Text(context.lang.settingsAccountDeleteAccountNoInternet)
|
||||
: hasRemainingBallance
|
||||
? Text(
|
||||
context.lang.settingsAccountDeleteAccountWithBallance(
|
||||
formattedBallance!,
|
||||
),
|
||||
)
|
||||
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
|
||||
onTap: (formattedBallance == null)
|
||||
? null
|
||||
: () async {
|
||||
if (hasRemainingBallance) {
|
||||
final canGoNext =
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return RefundCreditsView(
|
||||
formattedBalance: formattedBallance!,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
as bool?;
|
||||
unawaited(initAsync());
|
||||
if (canGoNext == null || !canGoNext) return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
final ok = await showAlertDialog(
|
||||
context,
|
||||
context.lang.settingsAccountDeleteModalTitle,
|
||||
context.lang.settingsAccountDeleteModalBody,
|
||||
);
|
||||
if (ok) {
|
||||
final res = await apiService.deleteAccount();
|
||||
if (res.isError) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Could not delete the account. Please ensure you have a internet connection!',
|
||||
),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await deleteLocalUserData();
|
||||
await Restart.restartApp(
|
||||
notificationTitle: 'Account successfully deleted',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
forceKill: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
// import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||
// import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class RefundCreditsView extends StatefulWidget {
|
||||
const RefundCreditsView({required this.formattedBalance, super.key});
|
||||
final String formattedBalance;
|
||||
|
||||
@override
|
||||
State<RefundCreditsView> createState() => _RefundCreditsViewState();
|
||||
}
|
||||
|
||||
class _RefundCreditsViewState extends State<RefundCreditsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Refund Credits'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'Remaining balance: ${widget.formattedBalance}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20), // Space between balance and options
|
||||
|
||||
ListTile(
|
||||
title: const Text('Create a Voucher'),
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const VoucherView();
|
||||
},
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, false);
|
||||
}
|
||||
},
|
||||
),
|
||||
// ListTile(
|
||||
// title: Text("Spend to an Open Source Project"),
|
||||
// onTap: () async {},
|
||||
// ),
|
||||
// ListTile(
|
||||
// title: Text("Spend to an NGO"),
|
||||
// onTap: () async {},
|
||||
// ),
|
||||
// ListTile(
|
||||
// title: Text("Spend to twonly"),
|
||||
// onTap: () async {},
|
||||
// ),
|
||||
// Divider(),
|
||||
// ListTile(
|
||||
// title: Text(
|
||||
// "Learn more about your donation",
|
||||
// ),
|
||||
// subtitle: Text(
|
||||
// "This will open our webpage which will provide you more informations where we will donate your remaining ballance if you choose this option.",
|
||||
// ),
|
||||
// onTap: () {
|
||||
// launchUrl(Uri.parse("https://twonly.eu/de/donation/"));
|
||||
// },
|
||||
// trailing: FaIcon(
|
||||
// FontAwesomeIcons.arrowUpRightFromSquare,
|
||||
// size: 15,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/backup/create.backup.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class BackupView extends StatefulWidget {
|
||||
const BackupView({super.key});
|
||||
|
||||
@override
|
||||
State<BackupView> createState() => _BackupViewState();
|
||||
}
|
||||
|
||||
BackupServer defaultBackupServer = BackupServer(
|
||||
serverUrl: 'Default',
|
||||
retentionDays: 180,
|
||||
maxBackupBytes: 2097152,
|
||||
);
|
||||
|
||||
class _BackupViewState extends State<BackupView> {
|
||||
bool isLoading = false;
|
||||
|
||||
int activePageIdx = 0;
|
||||
|
||||
final PageController pageController = PageController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {}
|
||||
|
||||
String backupStatus(LastBackupUploadState status) {
|
||||
switch (status) {
|
||||
case LastBackupUploadState.none:
|
||||
return context.lang.backupPending;
|
||||
case LastBackupUploadState.pending:
|
||||
return context.lang.backupPending;
|
||||
case LastBackupUploadState.failed:
|
||||
return context.lang.backupFailed;
|
||||
case LastBackupUploadState.success:
|
||||
return context.lang.backupSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changeTwonlySafePassword() async {
|
||||
await context.push(Routes.settingsBackupSetup, extra: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<void>(
|
||||
stream: AppSession.onUserUpdated,
|
||||
builder: (context, _) {
|
||||
final backupServer =
|
||||
AppSession.currentUser.backupServer ?? defaultBackupServer;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsBackup),
|
||||
),
|
||||
body: PageView(
|
||||
controller: pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
activePageIdx = index;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
BackupOption(
|
||||
title: 'twonly Backup',
|
||||
description: context.lang.backupTwonlySafeDesc,
|
||||
bottomButton: FilledButton(
|
||||
onPressed: changeTwonlySafePassword,
|
||||
child: Text(context.lang.backupChangePassword),
|
||||
),
|
||||
child: (AppSession.currentUser.twonlySafeBackup == null)
|
||||
? null
|
||||
: Column(
|
||||
children: [
|
||||
Table(
|
||||
defaultVerticalAlignment:
|
||||
TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
...[
|
||||
(
|
||||
context.lang.backupServer,
|
||||
(backupServer.serverUrl.contains('@'))
|
||||
? backupServer.serverUrl.split('@')[1]
|
||||
: backupServer.serverUrl.replaceAll(
|
||||
'https://',
|
||||
'',
|
||||
),
|
||||
),
|
||||
(
|
||||
context.lang.backupMaxBackupSize,
|
||||
formatBytes(backupServer.maxBackupBytes),
|
||||
),
|
||||
(
|
||||
context.lang.backupStorageRetention,
|
||||
'${backupServer.retentionDays} Days',
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupDate,
|
||||
formatDateTime(
|
||||
context,
|
||||
AppSession
|
||||
.currentUser
|
||||
.twonlySafeBackup!
|
||||
.lastBackupDone,
|
||||
),
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupSize,
|
||||
formatBytes(
|
||||
AppSession
|
||||
.currentUser
|
||||
.twonlySafeBackup!
|
||||
.lastBackupSize,
|
||||
),
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupResult,
|
||||
backupStatus(
|
||||
AppSession
|
||||
.currentUser
|
||||
.twonlySafeBackup!
|
||||
.backupUploadState,
|
||||
),
|
||||
),
|
||||
].map((pair) {
|
||||
return TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
// padding: EdgeInsets.all(4),
|
||||
child: Text(pair.$1),
|
||||
),
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
),
|
||||
child: Text(
|
||||
pair.$2,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
OutlinedButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
await performTwonlySafeBackup(force: true);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
},
|
||||
child: Text(context.lang.backupTwonlySaveNow),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BackupOption(
|
||||
title: '${context.lang.backupData} (Coming Soon)',
|
||||
description: context.lang.backupDataDesc,
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
showSelectedLabels: true,
|
||||
showUnselectedLabels: true,
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.inverseSurface.withAlpha(150),
|
||||
),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.inverseSurface,
|
||||
),
|
||||
items: [
|
||||
const BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
|
||||
label: 'twonly Backup',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const FaIcon(Icons.archive_outlined, size: 17),
|
||||
label: context.lang.backupData,
|
||||
),
|
||||
],
|
||||
onTap: (index) async {
|
||||
activePageIdx = index;
|
||||
await pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.bounceIn,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
currentIndex: activePageIdx,
|
||||
// ),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BackupOption extends StatelessWidget {
|
||||
const BackupOption({
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.bottomButton,
|
||||
super.key,
|
||||
this.child,
|
||||
});
|
||||
final String title;
|
||||
final String description;
|
||||
final Widget? child;
|
||||
final Widget? bottomButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(description),
|
||||
const SizedBox(height: 8),
|
||||
if (child != null) child! else Container(),
|
||||
Expanded(child: Container()),
|
||||
if (bottomButton != null) Center(child: bottomButton),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/select_payment.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
|
||||
class CheckoutView extends StatefulWidget {
|
||||
const CheckoutView({
|
||||
required this.plan,
|
||||
super.key,
|
||||
this.disableMonthlyOption,
|
||||
});
|
||||
|
||||
final SubscriptionPlan plan;
|
||||
final bool? disableMonthlyOption;
|
||||
|
||||
@override
|
||||
State<CheckoutView> createState() => _CheckoutViewState();
|
||||
}
|
||||
|
||||
class _CheckoutViewState extends State<CheckoutView> {
|
||||
int checkoutInCents = 0;
|
||||
bool paidMonthly = false;
|
||||
bool tryAutoRenewal = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setCheckout(init: true);
|
||||
}
|
||||
|
||||
void setCheckout({bool init = false}) {
|
||||
checkoutInCents = getPlanPrice(widget.plan, paidMonthly: paidMonthly);
|
||||
if (!init) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalPrice =
|
||||
'${localePrizing(context, checkoutInCents)}/${paidMonthly ? context.lang.month : context.lang.year}';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.checkoutOptions),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
PlanCard(plan: widget.plan),
|
||||
if (widget.disableMonthlyOption == null ||
|
||||
!widget.disableMonthlyOption!)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListTile(
|
||||
title: Text(context.lang.checkoutPayYearly),
|
||||
onTap: () {
|
||||
paidMonthly = !paidMonthly;
|
||||
setCheckout();
|
||||
},
|
||||
trailing: Checkbox(
|
||||
value: !paidMonthly,
|
||||
onChanged: (a) {
|
||||
paidMonthly = !paidMonthly;
|
||||
setCheckout();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.checkoutTotal,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
totalPrice,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
final success =
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SelectPaymentView(
|
||||
plan: widget.plan,
|
||||
payMonthly: paidMonthly,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
as bool?;
|
||||
if (success != null && success && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(context.lang.selectPaymentMethod),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.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/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
|
||||
class ManageSubscriptionView extends StatefulWidget {
|
||||
const ManageSubscriptionView({
|
||||
required this.ballance,
|
||||
required this.nextPayment,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Response_PlanBallance? ballance;
|
||||
final DateTime? nextPayment;
|
||||
|
||||
@override
|
||||
State<ManageSubscriptionView> createState() => _ManageSubscriptionViewState();
|
||||
}
|
||||
|
||||
class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
|
||||
Response_PlanBallance? ballance;
|
||||
bool? autoRenewal;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ballance = widget.ballance;
|
||||
if (ballance != null) {
|
||||
autoRenewal = ballance!.autoRenewal;
|
||||
}
|
||||
unawaited(initAsync(true));
|
||||
}
|
||||
|
||||
Future<void> initAsync(bool force) async {
|
||||
if (force) {
|
||||
ballance = await loadPlanBalance(useCache: false);
|
||||
if (ballance != null) {
|
||||
autoRenewal = ballance!.autoRenewal;
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> toggleRenewalOption() async {
|
||||
final res = await apiService.updatePlanOptions(!autoRenewal!);
|
||||
if (res.isError) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorCodeToText(context, res.error as ErrorCode)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
await initAsync(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final plan = context.watch<PurchasesProvider>().plan;
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.manageSubscription),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
PlanCard(plan: plan, paidMonthly: paidMonthly),
|
||||
if (isPayingUser(plan)) const SizedBox(height: 20),
|
||||
if (widget.nextPayment != null && isPayingUser(plan))
|
||||
ListTile(
|
||||
title: Text(
|
||||
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}',
|
||||
),
|
||||
),
|
||||
if (autoRenewal != null && isPayingUser(plan))
|
||||
ListTile(
|
||||
title: Text(context.lang.autoRenewal),
|
||||
subtitle: Text(
|
||||
context.lang.autoRenewalLongDesc,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
onTap: toggleRenewalOption,
|
||||
trailing: Switch(
|
||||
value: autoRenewal!,
|
||||
onChanged: (a) async {
|
||||
await toggleRenewalOption();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SelectPaymentView extends StatefulWidget {
|
||||
const SelectPaymentView({
|
||||
super.key,
|
||||
this.plan,
|
||||
this.payMonthly,
|
||||
this.valueInCents,
|
||||
});
|
||||
|
||||
final SubscriptionPlan? plan;
|
||||
final bool? payMonthly;
|
||||
final int? valueInCents;
|
||||
|
||||
@override
|
||||
State<SelectPaymentView> createState() => _SelectPaymentViewState();
|
||||
}
|
||||
|
||||
enum PaymentMethods {
|
||||
twonlyCredit,
|
||||
googleSubscription,
|
||||
appleSubscription,
|
||||
}
|
||||
|
||||
class _SelectPaymentViewState extends State<SelectPaymentView> {
|
||||
int? balanceInCents;
|
||||
int checkoutInCents = 0;
|
||||
bool tryAutoRenewal = true;
|
||||
|
||||
PaymentMethods paymentMethods = PaymentMethods.twonlyCredit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setCheckout(true);
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final balance = await loadPlanBalance();
|
||||
if (balance == null) {
|
||||
balanceInCents = 0;
|
||||
} else {
|
||||
balanceInCents = balance.transactions
|
||||
.map((a) => a.depositCents.toInt())
|
||||
.sum;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setCheckout(bool init) {
|
||||
if (widget.valueInCents != null && widget.valueInCents! > 0) {
|
||||
checkoutInCents = widget.valueInCents!;
|
||||
} else if (widget.plan != null) {
|
||||
checkoutInCents = getPlanPrice(
|
||||
widget.plan!,
|
||||
paidMonthly: widget.payMonthly!,
|
||||
);
|
||||
} else {
|
||||
/// Nothing to checkout for...
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
if (!init) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalPrice = (widget.plan != null && widget.payMonthly != null)
|
||||
? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}'
|
||||
: localePrizing(context, checkoutInCents);
|
||||
final canPay =
|
||||
paymentMethods == PaymentMethods.twonlyCredit &&
|
||||
(balanceInCents == null || balanceInCents! >= checkoutInCents);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.selectPaymentMethod),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.lang.testPaymentMethod,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Card(
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.lang.twonlyCredit),
|
||||
if (balanceInCents != null)
|
||||
Text(
|
||||
'${context.lang.currentBalance}: ${localePrizing(context, balanceInCents!)}',
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
Checkbox(
|
||||
value:
|
||||
paymentMethods == PaymentMethods.twonlyCredit,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
paymentMethods = PaymentMethods.twonlyCredit;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!canPay) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.lang.notEnoughCredit,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const VoucherView();
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
child: Text(context.lang.chargeCredit),
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListTile(
|
||||
title: Text(context.lang.autoRenewal),
|
||||
subtitle: Text(context.lang.autoRenewalDesc),
|
||||
onTap: () {
|
||||
tryAutoRenewal = !tryAutoRenewal;
|
||||
setCheckout(false);
|
||||
},
|
||||
trailing: Checkbox(
|
||||
value: tryAutoRenewal,
|
||||
onChanged: (a) {
|
||||
tryAutoRenewal = !tryAutoRenewal;
|
||||
setCheckout(false);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.checkoutTotal,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
totalPrice,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: FilledButton(
|
||||
onPressed: canPay
|
||||
? () async {
|
||||
final res = await apiService.switchToPayedPlan(
|
||||
widget.plan!.name,
|
||||
widget.payMonthly!,
|
||||
tryAutoRenewal,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (res.isSuccess) {
|
||||
await updateUsersPlan(context, widget.plan!);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.lang.planSuccessUpgraded),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
errorCodeToText(
|
||||
context,
|
||||
res.error as ErrorCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(context.lang.checkoutSubmit),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse(
|
||||
'https://twonly.eu/de/legal/#revocation-policy',
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Widerrufsbelehrung',
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse('https://twonly.eu/de/legal/agb.html'),
|
||||
),
|
||||
child: const Text(
|
||||
'ABG',
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,473 +0,0 @@
|
|||
// ignore_for_file: inference_failure_on_instance_creation
|
||||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/checkout.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/manage_subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/transaction.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||
|
||||
String localePrizing(BuildContext context, int cents) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final euros = cents / 100;
|
||||
|
||||
if (euros == euros.toInt()) {
|
||||
return '${euros.toInt()}€';
|
||||
}
|
||||
|
||||
return NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(cents / 100);
|
||||
}
|
||||
|
||||
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
|
||||
final ballance = await apiService.getPlanBallance();
|
||||
if (ballance != null) {
|
||||
await updateUser((u) => u.lastPlanBallance = ballance.writeToJson());
|
||||
return ballance;
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user != null && user.lastPlanBallance != null && useCache) {
|
||||
try {
|
||||
return Response_PlanBallance.fromJson(
|
||||
user.lastPlanBallance!,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('from json: $e');
|
||||
}
|
||||
}
|
||||
return ballance;
|
||||
}
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
const int MONTHLY_PAYMENT_DAYS = 30;
|
||||
// ignore: constant_identifier_names
|
||||
const int YEARLY_PAYMENT_DAYS = 365;
|
||||
|
||||
class SubscriptionCustomView extends StatefulWidget {
|
||||
const SubscriptionCustomView({super.key, this.redirectError});
|
||||
|
||||
final ErrorCode? redirectError;
|
||||
|
||||
@override
|
||||
State<SubscriptionCustomView> createState() => _SubscriptionCustomViewState();
|
||||
}
|
||||
|
||||
class _SubscriptionCustomViewState extends State<SubscriptionCustomView> {
|
||||
bool loaded = false;
|
||||
bool testerRequested = true;
|
||||
Response_PlanBallance? ballance;
|
||||
String? additionalOwnerName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
ballance = await loadPlanBalance();
|
||||
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
||||
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(ownerId)
|
||||
.getSingleOrNull();
|
||||
if (contact != null) {
|
||||
additionalOwnerName = getContactDisplayName(contact);
|
||||
} else {
|
||||
additionalOwnerName = ownerId.toString();
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
String? formattedBalance;
|
||||
DateTime? nextPayment;
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
|
||||
if (ballance != null) {
|
||||
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
|
||||
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
|
||||
);
|
||||
if (isPayingUser(currentPlan)) {
|
||||
nextPayment = lastPaymentDateTime.add(
|
||||
Duration(days: ballance!.paymentPeriodDays.toInt()),
|
||||
);
|
||||
}
|
||||
final ballanceInCents = ballance!.transactions
|
||||
.map((a) => a.depositCents.toInt())
|
||||
.sum;
|
||||
formattedBalance = NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(ballanceInCents / 100);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsSubscription),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
if (widget.redirectError != null)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orangeAccent,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
(widget.redirectError == ErrorCode.PlanLimitReached)
|
||||
? context.lang.planLimitReached
|
||||
: context.lang.planNotAllowed,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
child: Text(
|
||||
currentPlan.name,
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (additionalOwnerName != null)
|
||||
Center(
|
||||
child: Text(
|
||||
context.lang.partOfPaidPlanOf(additionalOwnerName!),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
if (!isPayingUser(currentPlan))
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Text(
|
||||
context.lang.upgradeToPaidPlan,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester)
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Pro,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const CheckoutView(
|
||||
plan: SubscriptionPlan.Pro,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
if (currentPlan != SubscriptionPlan.Family)
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Family,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CheckoutView(
|
||||
plan: SubscriptionPlan.Family,
|
||||
disableMonthlyOption:
|
||||
currentPlan == SubscriptionPlan.Pro &&
|
||||
ballance!.paymentPeriodDays.toInt() ==
|
||||
YEARLY_PAYMENT_DAYS,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (currentPlan != SubscriptionPlan.Family) const Divider(),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.gears,
|
||||
text: context.lang.manageSubscription,
|
||||
subtitle: (nextPayment != null)
|
||||
? Text(
|
||||
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}',
|
||||
)
|
||||
: null,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ManageSubscriptionView(
|
||||
ballance: ballance,
|
||||
nextPayment: nextPayment,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.moneyBillTransfer,
|
||||
text: context.lang.transactionHistory,
|
||||
subtitle: (formattedBalance != null)
|
||||
? Text('${context.lang.currentBalance}: $formattedBalance')
|
||||
: null,
|
||||
onTap: () async {
|
||||
if (formattedBalance == null) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return TransactionView(
|
||||
transactions: ballance?.transactions,
|
||||
formattedBalance: formattedBalance!,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester)
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userPlus,
|
||||
text: context.lang.manageAdditionalUsers,
|
||||
subtitle: loaded ? Text('${context.lang.open}: 3') : null,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return AdditionalUsersView(
|
||||
ballance: ballance,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.ticket,
|
||||
text: context.lang.createOrRedeemVoucher,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const VoucherView();
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.Pro:
|
||||
return paidMonthly ? 100 : 1000;
|
||||
case SubscriptionPlan.Family:
|
||||
return paidMonthly ? 200 : 2000;
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class PlanCard extends StatelessWidget {
|
||||
const PlanCard({
|
||||
required this.plan,
|
||||
super.key,
|
||||
this.refund,
|
||||
this.onTap,
|
||||
this.paidMonthly,
|
||||
});
|
||||
final SubscriptionPlan plan;
|
||||
final void Function()? onTap;
|
||||
final int? refund;
|
||||
final bool? paidMonthly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final yearlyPrice = getPlanPrice(plan, paidMonthly: false);
|
||||
final monthlyPrice = getPlanPrice(plan, paidMonthly: true);
|
||||
var features = <String>[];
|
||||
|
||||
switch (plan.name) {
|
||||
case 'Free':
|
||||
features = [context.lang.freeFeature1];
|
||||
case 'Plus':
|
||||
features = [context.lang.plusFeature1, context.lang.plusFeature2];
|
||||
case 'Tester':
|
||||
case 'Pro':
|
||||
features = [
|
||||
context.lang.proFeature1,
|
||||
context.lang.proFeature2,
|
||||
context.lang.proFeature3,
|
||||
context.lang.proFeature4,
|
||||
];
|
||||
case 'Family':
|
||||
features = [
|
||||
context.lang.proFeature1,
|
||||
context.lang.familyFeature2,
|
||||
context.lang.proFeature3,
|
||||
context.lang.proFeature4,
|
||||
];
|
||||
default:
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
plan.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (yearlyPrice != 0) const SizedBox(height: 10),
|
||||
if (yearlyPrice != 0 && paidMonthly == null)
|
||||
Column(
|
||||
children: [
|
||||
if (paidMonthly == null || paidMonthly!)
|
||||
Text(
|
||||
'${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (paidMonthly == null || !paidMonthly!)
|
||||
Text(
|
||||
'${localePrizing(context, monthlyPrice)}/${context.lang.month}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (paidMonthly != null)
|
||||
Text(
|
||||
(paidMonthly!)
|
||||
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'
|
||||
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...features.map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Text(
|
||||
feature,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (refund != null && refund! > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 7),
|
||||
child: Text(
|
||||
context.lang.subscriptionRefund(
|
||||
localePrizing(context, refund!),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: FilledButton.icon(
|
||||
onPressed: onTap,
|
||||
label: Text(
|
||||
context.lang.upgradeToPaidPlanButton(plan.name, ''),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class TransactionView extends StatefulWidget {
|
||||
const TransactionView({
|
||||
required this.transactions,
|
||||
required this.formattedBalance,
|
||||
super.key,
|
||||
});
|
||||
final List<Response_Transaction>? transactions;
|
||||
final String formattedBalance;
|
||||
|
||||
@override
|
||||
State<TransactionView> createState() => _TransactionViewState();
|
||||
}
|
||||
|
||||
class _TransactionViewState extends State<TransactionView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.transactionHistory),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
child: Text(
|
||||
widget.formattedBalance,
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.transactions != null)
|
||||
...widget.transactions!.map((x) => TransactionCard(transaction: x)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionCard extends StatefulWidget {
|
||||
const TransactionCard({required this.transaction, super.key});
|
||||
final Response_Transaction transaction;
|
||||
|
||||
@override
|
||||
State<TransactionCard> createState() => _TransactionCardState();
|
||||
}
|
||||
|
||||
class _TransactionCardState extends State<TransactionCard> {
|
||||
String typeToText(Response_TransactionTypes type) {
|
||||
switch (type) {
|
||||
case Response_TransactionTypes.Cash:
|
||||
return context.lang.transactionCash;
|
||||
case Response_TransactionTypes.PlanUpgrade:
|
||||
return context.lang.transactionPlanUpgrade;
|
||||
case Response_TransactionTypes.Refund:
|
||||
return context.lang.transactionRefund;
|
||||
case Response_TransactionTypes.ThanksForTesting:
|
||||
return context.lang.transactionThanksForTesting;
|
||||
case Response_TransactionTypes.Unknown:
|
||||
return context.lang.transactionUnknown;
|
||||
case Response_TransactionTypes.VoucherCreated:
|
||||
return context.lang.transactionVoucherCreated;
|
||||
case Response_TransactionTypes.VoucherRedeemed:
|
||||
return context.lang.transactionVoucherRedeemed;
|
||||
case Response_TransactionTypes.AutoRenewal:
|
||||
return context.lang.transactionAutoRenewal;
|
||||
}
|
||||
return type.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final formattedValue = NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(widget.transaction.depositCents.toInt() / 100);
|
||||
|
||||
final timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||
widget.transaction.createdAtUnixTimestamp.toInt() * 1000,
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(10),
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
typeToText(widget.transaction.transactionType),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat.yMMMMd(myLocale.toString()).format(timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
formattedValue,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (widget.transaction.depositCents < 0)
|
||||
? Colors.red
|
||||
: context.color.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.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/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class VoucherView extends StatefulWidget {
|
||||
const VoucherView({super.key});
|
||||
|
||||
@override
|
||||
State<VoucherView> createState() => _VoucherViewState();
|
||||
}
|
||||
|
||||
class _VoucherViewState extends State<VoucherView> {
|
||||
List<Response_Voucher> vouchers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final resVouchers = await apiService.getVoucherList();
|
||||
setState(() {
|
||||
vouchers = resVouchers?.vouchers ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final openVoucher = vouchers.where((x) => !x.redeemed && !x.requested);
|
||||
final redeemedVoucher = vouchers.where((x) => x.redeemed);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.createOrRedeemVoucher),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.lang.redeemVoucher),
|
||||
onTap: () async {
|
||||
await redeemVoucher(context);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.createVoucher),
|
||||
onTap: () async {
|
||||
await showBuyVoucher(context);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
if (openVoucher.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.lang.openVouchers,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
...openVoucher.map((x) => VoucherCard(voucher: x)),
|
||||
if (redeemedVoucher.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.lang.redeemedVouchers,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
...redeemedVoucher.map((x) => VoucherCard(voucher: x)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VoucherCard extends StatefulWidget {
|
||||
const VoucherCard({required this.voucher, super.key});
|
||||
final Response_Voucher voucher;
|
||||
|
||||
@override
|
||||
State<VoucherCard> createState() => _VoucherCardState();
|
||||
}
|
||||
|
||||
class _VoucherCardState extends State<VoucherCard> {
|
||||
Future<void> _copyVoucherId() async {
|
||||
if (!widget.voucher.redeemed) {
|
||||
await Clipboard.setData(ClipboardData(text: widget.voucher.voucherId));
|
||||
await HapticFeedback.heavyImpact();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${widget.voucher.voucherId} copied.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isRedeemed = widget.voucher.redeemed || widget.voucher.requested;
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final formattedValue = NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(widget.voucher.valueCents.toInt() / 100);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _copyVoucherId,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(10),
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.voucher.voucherId.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isRedeemed ? Colors.grey : context.color.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formattedValue,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isRedeemed ? Colors.grey : context.color.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> redeemVoucher(BuildContext context) async {
|
||||
var voucherCode = '';
|
||||
//
|
||||
// ignore: inference_failure_on_function_invocation
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.lang.redeemVoucher),
|
||||
content: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
// Convert to uppercase
|
||||
setState(() {
|
||||
voucherCode = value.toUpperCase();
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: context.lang.enterVoucherCode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
// Set the text to be uppercase
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final res = await apiService.redeemVoucher(voucherCode);
|
||||
if (!context.mounted) return;
|
||||
if (res.isSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.lang.voucherRedeemed)),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
errorCodeToText(
|
||||
context,
|
||||
res.error as ErrorCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(context.lang.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showBuyVoucher(BuildContext context) async {
|
||||
var quantity = 1000;
|
||||
//
|
||||
// ignore: inference_failure_on_function_invocation
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.lang.createVoucher),
|
||||
content: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(context.lang.createVoucherDesc),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
if (quantity > 1) {
|
||||
setState(() {
|
||||
if (quantity <= 100) return;
|
||||
if (quantity <= 1000) {
|
||||
quantity -= 100;
|
||||
} else {
|
||||
quantity -= 500;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
NumberFormat.currency(
|
||||
locale: Localizations.localeOf(context).toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(quantity / 100),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (quantity >= 1000) {
|
||||
quantity += 500;
|
||||
} else {
|
||||
quantity += 100;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Close the dialog
|
||||
},
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final res = await apiService.buyVoucher(quantity);
|
||||
if (!context.mounted) return;
|
||||
if (res.isSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.lang.voucherCreated)),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
errorCodeToText(
|
||||
context,
|
||||
res.error as ErrorCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop(); // Close the dialog
|
||||
},
|
||||
child: Text(context.lang.buy),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -17,8 +17,12 @@ bool isOneEmoji(String character) {
|
|||
return false;
|
||||
}
|
||||
|
||||
class EmojiAnimation extends StatelessWidget {
|
||||
const EmojiAnimation({required this.emoji, super.key, this.repeat = true});
|
||||
class EmojiAnimationComp extends StatelessWidget {
|
||||
const EmojiAnimationComp({
|
||||
required this.emoji,
|
||||
super.key,
|
||||
this.repeat = true,
|
||||
});
|
||||
final String emoji;
|
||||
final bool repeat;
|
||||
static final Map<String, String> animatedIcons = {
|
||||
|
|
@ -279,7 +283,7 @@ class EmojiAnimationFlying extends StatelessWidget {
|
|||
padding: EdgeInsets.only(bottom: 20 * value),
|
||||
child: SizedBox(
|
||||
width: size + 30 * value,
|
||||
child: EmojiAnimation(emoji: emoji),
|
||||
child: EmojiAnimationComp(emoji: emoji),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AppOutdated extends StatefulWidget {
|
||||
const AppOutdated({super.key});
|
||||
class AppOutdatedComp extends StatefulWidget {
|
||||
const AppOutdatedComp({super.key});
|
||||
|
||||
@override
|
||||
State<AppOutdated> createState() => _AppOutdatedState();
|
||||
State<AppOutdatedComp> createState() => _AppOutdatedCompState();
|
||||
}
|
||||
|
||||
class _AppOutdatedState extends State<AppOutdated> {
|
||||
class _AppOutdatedCompState extends State<AppOutdatedComp> {
|
||||
bool appIsOutdated = false;
|
||||
bool newDeviceRegistered = false;
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:vector_graphics/vector_graphics.dart';
|
||||
|
||||
class AvatarIcon extends StatefulWidget {
|
||||
|
|
@ -93,19 +93,19 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
setState(() {});
|
||||
});
|
||||
} else if (widget.myAvatar) {
|
||||
_userSub = AppSession.onUserUpdated.listen((_) {
|
||||
_userSub = appSession.onUserUpdated.listen((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (AppSession.currentUser.avatarSvg != null) {
|
||||
_avatarSvg = AppSession.currentUser.avatarSvg;
|
||||
if (appSession.currentUser.avatarSvg != null) {
|
||||
_avatarSvg = appSession.currentUser.avatarSvg;
|
||||
} else {
|
||||
_avatarContacts = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (AppSession.currentUser.avatarSvg != null) {
|
||||
_avatarSvg = AppSession.currentUser.avatarSvg;
|
||||
if (appSession.currentUser.avatarSvg != null) {
|
||||
_avatarSvg = appSession.currentUser.avatarSvg;
|
||||
}
|
||||
} else if (widget.contactId != null) {
|
||||
contactStream = twonlyDB.contactsDao
|
||||
|
|
@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/loader/ripple.loader.dart';
|
||||
import 'package:twonly/src/visual/loader/ripple.loader.dart';
|
||||
|
||||
class ConnectionStatusBadge extends StatelessWidget {
|
||||
const ConnectionStatusBadge({
|
||||
class ConnectionStatusComp extends StatelessWidget {
|
||||
const ConnectionStatusComp({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/context_menu.component.dart';
|
||||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
||||
|
||||
class UserContextMenu extends StatelessWidget {
|
||||
const UserContextMenu({
|
||||
|
|
@ -2,7 +2,7 @@ import 'dart:io';
|
|||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
||||
|
||||
class EmojiPickerBottom extends StatelessWidget {
|
||||
const EmojiPickerBottom({super.key});
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||
|
||||
class FlameCounterWidget extends StatefulWidget {
|
||||
const FlameCounterWidget({
|
||||
|
|
@ -48,7 +49,8 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
}
|
||||
if (groupId != null && group != null) {
|
||||
isBestFriend =
|
||||
AppSession.currentUser.myBestFriendGroupId == groupId && group.alsoBestFriend;
|
||||
appSession.currentUser.myBestFriendGroupId == groupId &&
|
||||
group.alsoBestFriend;
|
||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||
flameCounterSub = stream.listen((counter) {
|
||||
if (mounted) {
|
||||
|
|
@ -85,7 +87,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
child: EmojiAnimation(
|
||||
child: EmojiAnimationComp(
|
||||
emoji: flameEmoji,
|
||||
),
|
||||
),
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationBadge extends StatelessWidget {
|
||||
const NotificationBadge({
|
||||
class NotificationBadgeComp extends StatelessWidget {
|
||||
const NotificationBadgeComp({
|
||||
required this.count,
|
||||
required this.child,
|
||||
this.backgroundColor = Colors.red,
|
||||
|
|
@ -4,15 +4,15 @@ import 'package:drift/drift.dart' show Value;
|
|||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
|
||||
class SelectChatDeletionTimeListTitle extends StatefulWidget {
|
||||
const SelectChatDeletionTimeListTitle({
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/views/components/svg_icon.dart';
|
||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||
|
||||
class VerifiedShield extends StatefulWidget {
|
||||
const VerifiedShield({
|
||||
class VerificationBadgeComp extends StatefulWidget {
|
||||
const VerificationBadgeComp({
|
||||
this.contact,
|
||||
this.group,
|
||||
super.key,
|
||||
|
|
@ -23,10 +24,10 @@ class VerifiedShield extends StatefulWidget {
|
|||
final bool clickable;
|
||||
|
||||
@override
|
||||
State<VerifiedShield> createState() => _VerifiedShieldState();
|
||||
State<VerificationBadgeComp> createState() => _VerificationBadgeCompState();
|
||||
}
|
||||
|
||||
class _VerifiedShieldState extends State<VerifiedShield> {
|
||||
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||
bool isVerified = false;
|
||||
Contact? contact;
|
||||
|
||||
|
|
@ -2,12 +2,12 @@ import 'package:drift/drift.dart' hide Column;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/context_menu.component.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
||||
|
||||
class GroupContextMenu extends StatelessWidget {
|
||||
const GroupContextMenu({
|
||||
31
lib/src/visual/context_menu/user.context_menu.dart
Normal file
31
lib/src/visual/context_menu/user.context_menu.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
|
||||
|
||||
class UserContextMenu extends StatelessWidget {
|
||||
const UserContextMenu({
|
||||
required this.contact,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
final Widget child;
|
||||
final Contact contact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContextMenu(
|
||||
items: [
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuUserProfile,
|
||||
onTap: () => context.push(Routes.profileContact(contact.userId)),
|
||||
icon: FontAwesomeIcons.user,
|
||||
),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/src/visual/decorations/input_text.decoration.dart
Normal file
42
lib/src/visual/decorations/input_text.decoration.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
InputDecoration inputTextMessageDeco(BuildContext context, String hintText) {
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: const BorderSide(color: Colors.grey, width: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration getInputDecoration(BuildContext context, String hintText) {
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
borderSide: BorderSide(color: primaryColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class HeadLineComponent extends StatelessWidget {
|
||||
const HeadLineComponent(this.text, {super.key});
|
||||
class HeadLineComp extends StatelessWidget {
|
||||
const HeadLineComp(this.text, {super.key});
|
||||
final String text;
|
||||
|
||||
@override
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaViewSizing extends StatefulWidget {
|
||||
const MediaViewSizing({
|
||||
class MediaViewSizingHelper extends StatefulWidget {
|
||||
const MediaViewSizingHelper({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.requiredHeight,
|
||||
|
|
@ -15,10 +15,10 @@ class MediaViewSizing extends StatefulWidget {
|
|||
final Widget child;
|
||||
|
||||
@override
|
||||
State<MediaViewSizing> createState() => _MediaViewSizingState();
|
||||
State<MediaViewSizingHelper> createState() => _MediaViewSizingHelperState();
|
||||
}
|
||||
|
||||
class _MediaViewSizingState extends State<MediaViewSizing> {
|
||||
class _MediaViewSizingHelperState extends State<MediaViewSizingHelper> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var needToDownSizeImage = false;
|
||||
|
|
@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/rendering.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class ScreenshotImage {
|
||||
ScreenshotImage({
|
||||
class ScreenshotImageHelper {
|
||||
ScreenshotImageHelper({
|
||||
this.image,
|
||||
this.imageBytes,
|
||||
this.imageBytesFuture,
|
||||
|
|
@ -46,7 +46,7 @@ class ScreenshotController {
|
|||
}
|
||||
late GlobalKey _containerKey;
|
||||
|
||||
Future<ScreenshotImage?> capture({double? pixelRatio}) async {
|
||||
Future<ScreenshotImageHelper?> capture({double? pixelRatio}) async {
|
||||
try {
|
||||
final findRenderObject = _containerKey.currentContext?.findRenderObject();
|
||||
if (findRenderObject == null) {
|
||||
|
|
@ -62,7 +62,7 @@ class ScreenshotController {
|
|||
}
|
||||
}
|
||||
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
|
||||
return ScreenshotImage(image: image);
|
||||
return ScreenshotImageHelper(image: image);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
|
|
@ -4,18 +4,18 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoPlayerWrapper extends StatefulWidget {
|
||||
const VideoPlayerWrapper({
|
||||
class VideoPlayerHelper extends StatefulWidget {
|
||||
const VideoPlayerHelper({
|
||||
required this.videoPath,
|
||||
super.key,
|
||||
});
|
||||
final File videoPath;
|
||||
|
||||
@override
|
||||
State<VideoPlayerWrapper> createState() => _VideoPlayerWrapperState();
|
||||
State<VideoPlayerHelper> createState() => _VideoPlayerHelperState();
|
||||
}
|
||||
|
||||
class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
|
||||
class _VideoPlayerHelperState extends State<VideoPlayerHelper> {
|
||||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue