fix: media file appears as a white square and is not listed.
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-03-14 22:18:29 +01:00
parent cdaca940df
commit cc0b88718b
13 changed files with 285 additions and 192 deletions

View file

@ -25,8 +25,11 @@ jobs:
- name: Cloning sub-repos
run: git submodule update --init --recursive
- name: Check flutter code
run: |
flutter pub get
flutter analyze
flutter test
- name: flutter pub get
run: flutter pub get
- name: flutter analyze
run: flutter analyze
- name: flutter test
run: flutter test

View file

@ -1,6 +1,6 @@
# Changelog
## 0.0.98
## 0.0.99
- New: Groups can now collect flames as well
- New: Background execution to pre-load messages
@ -8,10 +8,12 @@
- Improve: Video compression with progress updates
- Improve: Show message "Flames restored"
- Improve: Show toast message if user was added via QR
- Fix: Media file appears as a white square and is not listed.
- Fix: Issue with media files required to be reuploaded
- Fix: Problem during contact requests
- Fix: Problem with deleting a contact
- Fix: Problem with restoring from backup
- Fix: Issue with the log file
## 0.0.96

View file

@ -29,6 +29,10 @@ import 'package:twonly/src/utils/storage.dart';
void main() async {
SentryWidgetsFlutterBinding.ensureInitialized();
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
await initFCMService();
final user = await getUser();
@ -53,10 +57,6 @@ void main() async {
await deleteLocalUserData();
}
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
initLogger();
final settingsController = SettingsChangeProvider();

View file

@ -1,6 +1,7 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
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';
@ -9,7 +10,9 @@ import 'package:twonly/src/utils/log.dart';
part 'receipts.dao.g.dart';
@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts])
@DriftAccessor(
tables: [Receipts, Messages, MessageActions, ReceivedReceipts, Contacts],
)
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
@ -60,6 +63,17 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)),
))
.go();
final deletedContacts = await (select(
contacts,
)..where((t) => t.accountDeleted.equals(true))).get();
for (final contact in deletedContacts) {
await (delete(receipts)..where(
(t) => t.contactId.equals(contact.userId),
))
.go();
}
}
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {

View file

@ -46,6 +46,7 @@ import 'package:web_socket_channel/io.dart';
final lockConnecting = Mutex();
final lockRetransStore = Mutex();
final lockAuthentication = Mutex();
/// The ApiProvider is responsible for communicating with the server.
/// It handles errors and does automatically tries to reconnect on
@ -86,7 +87,6 @@ class ApiService {
// Function is called after the user is authenticated at the server
Future<void> onAuthenticated() async {
isAuthenticated = true;
await initFCMAfterAuthenticated();
globalCallbackConnectionState(isConnected: true);
@ -157,8 +157,9 @@ class ApiService {
if (connectivitySubscription != null) {
return;
}
connectivitySubscription =
Connectivity().onConnectivityChanged.listen((result) async {
connectivitySubscription = Connectivity().onConnectivityChanged.listen((
result,
) async {
if (!result.contains(ConnectivityResult.none)) {
await connect();
}
@ -355,7 +356,6 @@ class ApiService {
return Result.error(ErrorCode.InternalError);
}
if (res.error == ErrorCode.SessionNotAuthenticated) {
isAuthenticated = false;
if (authenticated) {
await authenticate();
if (isAuthenticated) {
@ -387,8 +387,9 @@ class ApiService {
Future<bool> tryAuthenticateWithToken(int userId) async {
const storage = FlutterSecureStorage();
final apiAuthToken =
await storage.read(key: SecureStorageKeys.apiAuthToken);
final apiAuthToken = await storage.read(
key: SecureStorageKeys.apiAuthToken,
);
final user = await getUser();
if (apiAuthToken != null && user != null) {
@ -412,6 +413,7 @@ class ApiService {
if (result.isSuccess) {
Log.info('websocket is authenticated');
isAuthenticated = true;
if (globalIsInBackgroundTask) {
await onAuthenticated();
} else {
@ -433,60 +435,66 @@ class ApiService {
}
Future<void> authenticate() async {
if (isAuthenticated) return;
if (await getSignalIdentity() == null) {
return;
}
return lockAuthentication.protect(() async {
if (isAuthenticated) return;
if (await getSignalIdentity() == null) {
return;
}
final userData = await getUser();
if (userData == null) return;
final userData = await getUser();
if (userData == null) return;
if (await tryAuthenticateWithToken(userData.userId)) {
return;
}
if (await tryAuthenticateWithToken(userData.userId)) {
return;
}
final handshake = Handshake()
..getAuthChallenge = Handshake_GetAuthChallenge();
final req = createClientToServerFromHandshake(handshake);
final handshake = Handshake()
..getAuthChallenge = Handshake_GetAuthChallenge();
final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false);
if (result.isError) {
Log.warn('could not request auth challenge', result);
return;
}
final result = await sendRequestSync(req, authenticated: false);
if (result.isError) {
Log.warn('could not request auth challenge', result);
return;
}
final challenge = result.value.authchallenge;
final challenge = result.value.authchallenge;
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return;
final random = getRandomUint8List(32);
final signature = sign(privKey.serialize(), challenge as Uint8List, random);
privKey = null;
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return;
final random = getRandomUint8List(32);
final signature = sign(
privKey.serialize(),
challenge as Uint8List,
random,
);
privKey = null;
final getAuthToken = Handshake_GetAuthToken()
..response = signature
..userId = Int64(userData.userId);
final getAuthToken = Handshake_GetAuthToken()
..response = signature
..userId = Int64(userData.userId);
final getauthtoken = Handshake()..getAuthToken = getAuthToken;
final getauthtoken = Handshake()..getAuthToken = getAuthToken;
final req2 = createClientToServerFromHandshake(getauthtoken);
final req2 = createClientToServerFromHandshake(getauthtoken);
final result2 = await sendRequestSync(req2, authenticated: false);
if (result2.isError) {
Log.error('could not send auth response: ${result2.error}');
return;
}
final result2 = await sendRequestSync(req2, authenticated: false);
if (result2.isError) {
Log.error('could not send auth response: ${result2.error}');
return;
}
final apiAuthToken = result2.value.authtoken as Uint8List;
final apiAuthTokenB64 = base64Encode(apiAuthToken);
final apiAuthToken = result2.value.authtoken as Uint8List;
final apiAuthTokenB64 = base64Encode(apiAuthToken);
const storage = FlutterSecureStorage();
await storage.write(
key: SecureStorageKeys.apiAuthToken,
value: apiAuthTokenB64,
);
const storage = FlutterSecureStorage();
await storage.write(
key: SecureStorageKeys.apiAuthToken,
value: apiAuthTokenB64,
);
await tryAuthenticateWithToken(userData.userId);
await tryAuthenticateWithToken(userData.userId);
});
}
Future<Result> register(
@ -505,8 +513,9 @@ class ApiService {
final register = Handshake_Register()
..username = username
..publicIdentityKey =
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize()
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
.getPublicKey()
.serialize()
..registrationId = Int64(signalIdentity.registrationId)
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
..signedPrekeySignature = signedPreKey.signature
@ -526,8 +535,10 @@ class ApiService {
}
Future<void> checkForDeletedUsernames() async {
final users = await twonlyDB.contactsDao
.getContactsByUsername('[deleted]', username2: '[Unknown]');
final users = await twonlyDB.contactsDao.getContactsByUsername(
'[deleted]',
username2: '[Unknown]',
);
for (final user in users) {
final userData = await getUserById(user.userId);
if (userData != null) {

View file

@ -4,7 +4,8 @@ import 'package:twonly/globals.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';
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';
@ -31,7 +32,7 @@ Future<void> handleMedia(
message.senderId != fromUserId ||
message.mediaId == null) {
Log.warn(
'Got reupload for a message that either does not exists or sender != fromUserId or not a media file',
'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
);
return;
}
@ -53,8 +54,9 @@ Future<void> handleMedia(
),
);
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!,
);
if (mediaFile != null) {
unawaited(startDownloadMedia(mediaFile, false));
@ -89,56 +91,64 @@ Future<void> handleMedia(
}
}
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
downloadState: const Value(DownloadState.pending),
type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication),
displayLimitInMilliseconds: Value(
displayLimitInMilliseconds,
),
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
late MediaFile? mediaFile;
late Message? message;
if (mediaFile == null) {
Log.error('Could not insert media file into database');
return;
}
await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
downloadState: const Value(DownloadState.pending),
type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication),
displayLimitInMilliseconds: Value(
displayLimitInMilliseconds,
),
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(media.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
mediaId: Value(mediaFile.mediaId),
type: Value(MessageType.media.name),
additionalMessageData: Value.absentIfNull(
media.hasAdditionalMessageData()
? Uint8List.fromList(media.additionalMessageData)
: null,
if (mediaFile == null) {
Log.error('Could not insert media file into database');
return;
}
message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(media.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
mediaId: Value(mediaFile!.mediaId),
type: Value(MessageType.media.name),
additionalMessageData: Value.absentIfNull(
media.hasAdditionalMessageData()
? Uint8List.fromList(media.additionalMessageData)
: null,
),
quotesMessageId: Value(
media.hasQuoteMessageId() ? media.quoteMessageId : null,
),
createdAt: Value(fromTimestamp(media.timestamp)),
),
quotesMessageId: Value(
media.hasQuoteMessageId() ? media.quoteMessageId : null,
),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
);
});
if (message != null) {
await twonlyDB.groupsDao
.increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp));
Log.info('Inserted a new media message with ID: ${message.messageId}');
await twonlyDB.groupsDao.increaseLastMessageExchange(
groupId,
fromTimestamp(media.timestamp),
);
Log.info('Inserted a new media message with ID: ${message!.messageId}');
await incFlameCounter(
message.groupId,
message!.groupId,
true,
fromTimestamp(media.timestamp),
);
unawaited(startDownloadMedia(mediaFile, false));
unawaited(startDownloadMedia(mediaFile!, false));
}
}
@ -163,8 +173,9 @@ Future<void> handleMediaUpdate(
);
return;
}
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!,
);
if (mediaFile == null) {
Log.info(
'Got media file update, but media file was not found ${message.mediaId}',
@ -203,8 +214,9 @@ Future<void> handleMediaUpdate(
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
final mediaFileUpdated =
await MediaFileService.fromMediaId(mediaFile.mediaId);
final mediaFileUpdated = await MediaFileService.fromMediaId(
mediaFile.mediaId,
);
if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync();

View file

@ -10,7 +10,6 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -42,15 +41,16 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Log.info('Starting new twonly Backup!');
final baseDir = (await getApplicationSupportDirectory()).path;
final baseDir = globalApplicationSupportDirectory;
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
await backupDir.create(recursive: true);
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
final backupDatabaseFileCleaned =
File(join(backupDir.path, 'twonly.backup.cleaned.sqlite'));
final backupDatabaseFileCleaned = File(
join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
);
// copy database
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
@ -70,8 +70,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
await backupDB.deleteDataForTwonlySafe();
await backupDB
.customStatement('VACUUM INTO ?', [backupDatabaseFileCleaned.path]);
await backupDB.customStatement('VACUUM INTO ?', [
backupDatabaseFileCleaned.path,
]);
await backupDB.printTableSizes();
@ -80,10 +81,11 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
// ignore: inference_failure_on_collection_literal
final secureStorageBackup = {};
const storage = FlutterSecureStorage();
secureStorageBackup[SecureStorageKeys.signalIdentity] =
await storage.read(key: SecureStorageKeys.signalIdentity);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
secureStorageBackup[SecureStorageKeys.signalIdentity] = await storage.read(
key: SecureStorageKeys.signalIdentity,
);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = await storage
.read(key: SecureStorageKeys.signalSignedPreKey);
final userBackup = await getUser();
if (userBackup == null) return;
@ -117,13 +119,15 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (gUser.twonlySafeBackup!.lastBackupDone == null ||
gUser.twonlySafeBackup!.lastBackupDone!
.isAfter(clock.now().subtract(const Duration(days: 90)))) {
gUser.twonlySafeBackup!.lastBackupDone!.isAfter(
clock.now().subtract(const Duration(days: 90)),
)) {
force = true;
}
final lastHash =
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash);
final lastHash = await storage.read(
key: SecureStorageKeys.twonlySafeLastBackupHash,
);
if (lastHash != null && !force) {
if (backupHash == lastHash) {
@ -155,8 +159,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Log.info('Backup files created.');
final encryptedBackupBytesFile =
File(join(backupDir.path, 'twonly_safe.backup'));
final encryptedBackupBytesFile = File(
join(backupDir.path, 'twonly_safe.backup'),
);
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);

View file

@ -8,7 +8,7 @@ import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:path_provider/path_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/model/protobuf/client/generated/backup.pb.dart';
@ -23,8 +23,10 @@ Future<void> recoverBackup(
) async {
final (backupId, encryptionKey) = await getMasterKey(password, username);
final backupServerUrl =
await getTwonlySafeBackupUrlFromServer(backupId, server);
final backupServerUrl = await getTwonlySafeBackupUrlFromServer(
backupId,
server,
);
if (backupServerUrl == null) {
Log.error('Could not create backup url');
@ -87,8 +89,9 @@ Future<void> handleBackupData(
plaintextBytes,
);
final baseDir = (await getApplicationSupportDirectory()).path;
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
final originalDatabase = File(
join(globalApplicationSupportDirectory, 'twonly.sqlite'),
);
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
const storage = FlutterSecureStorage();

View file

@ -1,12 +1,11 @@
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/log.dart';
class KeyValueStore {
static Future<File> _getFilePath(String key) async {
final directory = await getApplicationSupportDirectory();
return File('${directory.path}/keyvalue/$key.json');
return File('$globalApplicationSupportDirectory/keyvalue/$key.json');
}
static Future<void> delete(String key) async {

View file

@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
@ -11,7 +11,7 @@ void initLogger() {
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) async {
await _writeLogToFile(record);
unawaited(_writeLogToFile(record));
if (!kReleaseMode) {
// ignore: avoid_print
print(
@ -67,83 +67,123 @@ class Log {
}
Future<String> loadLogFile() async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');
return _protectFileAccess(() async {
final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) {
return logFile.readAsString();
} else {
return 'Log file does not exist.';
}
if (logFile.existsSync()) {
return logFile.readAsString();
} else {
return 'Log file does not exist.';
}
});
}
Future<String> readLast1000Lines() async {
final dir = await getApplicationSupportDirectory();
final file = File('${dir.path}/app.log');
if (!file.existsSync()) return '';
final all = await file.readAsLines();
final start = all.length > 1000 ? all.length - 1000 : 0;
return all.sublist(start).join('\n');
return _protectFileAccess(() async {
final file = File('$globalApplicationSupportDirectory/app.log');
if (!file.existsSync()) return '';
final all = await file.readAsLines();
final start = all.length > 1000 ? all.length - 1000 : 0;
return all.sublist(start).join('\n');
});
}
Mutex sameProcessProtection = Mutex();
final Mutex _logMutex = Mutex();
Future<T> _protectFileAccess<T>(Future<T> Function() action) async {
return _logMutex.protect(() async {
final lockFile = File('$globalApplicationSupportDirectory/app.log.lock');
var lockAcquired = false;
while (!lockAcquired) {
try {
lockFile.createSync(exclusive: true);
lockAcquired = true;
} on FileSystemException catch (e) {
final isExists = e is PathExistsException || e.osError?.errorCode == 17;
if (!isExists) {
break;
}
try {
final stat = lockFile.statSync();
if (stat.type != FileSystemEntityType.notFound) {
final age = DateTime.now().difference(stat.modified).inMilliseconds;
if (age > 1000) {
lockFile.deleteSync();
continue;
}
}
} catch (_) {}
await Future.delayed(const Duration(milliseconds: 50));
} catch (_) {
break;
}
}
try {
return await action();
} finally {
if (lockAcquired) {
try {
if (lockFile.existsSync()) {
lockFile.deleteSync();
}
} catch (_) {}
}
}
});
}
Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');
if (!logFile.existsSync()) {
logFile.createSync(recursive: true);
}
final logFile = File('$globalApplicationSupportDirectory/app.log');
final logMessage =
'${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n';
// > Note that this does not actually lock the file for access. Also note that advisory locks are on a process level.
// > This means that several isolates in the same process can obtain an exclusive lock on the same file.
return sameProcessProtection.protect(() async {
return _protectFileAccess(() async {
if (!logFile.existsSync()) {
logFile.createSync(recursive: true);
}
final raf = await logFile.open(mode: FileMode.writeOnlyAppend);
try {
// Use FileLock.blockingExclusive to wait until the lock is available
await raf.lock(FileLock.blockingExclusive);
await raf.writeString(logMessage);
await raf.flush();
} catch (e) {
// ignore: avoid_print
print('Error during file access: $e');
} finally {
await raf.unlock();
await raf.close();
}
});
}
Future<void> cleanLogFile() async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');
return _protectFileAccess(() async {
final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) {
final lines = await logFile.readAsLines();
if (logFile.existsSync()) {
final lines = await logFile.readAsLines();
if (lines.length <= 10000) return;
if (lines.length <= 10000) return;
final removeCount = lines.length - 10000;
final remaining = lines.sublist(removeCount, lines.length);
final removeCount = lines.length - 10000;
final remaining = lines.sublist(removeCount, lines.length);
final sink = logFile.openWrite()..writeAll(remaining, '\n');
await sink.close();
}
final sink = logFile.openWrite()..writeAll(remaining, '\n');
await sink.close();
}
});
}
Future<bool> deleteLogFile() async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');
return _protectFileAccess(() async {
final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) {
await logFile.delete();
return true;
}
return false;
if (logFile.existsSync()) {
await logFile.delete();
return true;
}
return false;
});
}
String _getCallerSourceCodeFilename() {
@ -159,8 +199,11 @@ String _getCallerSourceCodeFilename() {
lineNumber = parts.last.split(':')[1]; // Extract the line number
} else {
final firstLine = stackTraceString.split('\n')[0];
fileName =
firstLine.split('/').last.split(':').first; // Extract the file name
fileName = firstLine
.split('/')
.last
.split(':')
.first; // Extract the file name
lineNumber = firstLine.split(':')[1]; // Extract the line number
}
lineNumber = lineNumber.replaceAll(')', '');

View file

@ -88,9 +88,8 @@ class MainCameraController {
scannedUrl = null;
try {
await cameraController?.stopImageStream();
} catch (e) {
Log.warn(e);
}
// ignore: empty_catches
} catch (e) {}
final cameraControllerTemp = cameraController;
cameraController = null;
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
@ -166,7 +165,8 @@ class MainCameraController {
selectedCameraDetails.minAvailableZoom =
await cameraController?.getMinZoomLevel() ?? 1;
selectedCameraDetails
..isZoomAble = selectedCameraDetails.maxAvailableZoom !=
..isZoomAble =
selectedCameraDetails.maxAvailableZoom !=
selectedCameraDetails.minAvailableZoom
..cameraLoaded = true
..cameraId = cameraId;
@ -323,8 +323,9 @@ class MainCameraController {
customPaint = CustomPaint(painter: painter);
if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) {
if (timeSharedLinkWasSetWithQr!
.isAfter(DateTime.now().subtract(const Duration(seconds: 2)))) {
if (timeSharedLinkWasSetWithQr!.isAfter(
DateTime.now().subtract(const Duration(seconds: 2)),
)) {
setSharedLinkForPreview(null);
}
}
@ -376,8 +377,8 @@ class MainCameraController {
content: Text(
globalRootScaffoldMessengerKey.currentContext?.lang
.verifiedPublicKey(
getContactDisplayName(contact),
) ??
getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.0.97+97
version: 0.0.99+99
environment:
sdk: ^3.11.0

View file

@ -38,7 +38,7 @@ void main() {
image:
'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg',
vendor: Vendor.mastodonSocialMediaPosting,
shareAction: 90,
shareAction: 80,
likeAction: 290,
),
LinkParserTest(