mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-05 08:06:47 +00:00
Merge pull request #388 from twonlyapp/dev
- Adds link preview to images - Adds option to manual focus in the camera - Adds support to switch between front and back cameras during video recording - Adds basic face filters - Improves image editor, like emojis or text under a drawing can be moved - Improves speed after taking a picture - Fixes issue with emojis disappearing in the image editor
This commit is contained in:
commit
e8b98761a7
112 changed files with 11350 additions and 1550 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## 0.0.89
|
||||
|
||||
- Adds link preview to images
|
||||
- Adds option to manual focus in the camera
|
||||
- Adds support to switch between front and back cameras during video recording
|
||||
- Adds basic face filters
|
||||
- Improves image editor, like emojis or text under a drawing can be moved
|
||||
- Improves speed after taking a picture
|
||||
- Fixes issue with emojis disappearing in the image editor
|
||||
|
||||
## 0.0.86
|
||||
|
||||
- Allows to reopen send images (if send without time limit or enabled auth)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,21 @@
|
|||
<data android:scheme="http" android:host="me.twonly.eu" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<!-- Allow other apps to share links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
<!-- Allow other apps to share links via plain text -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
|
|||
BIN
assets/filters/beard_upper_lip.webp
Normal file
BIN
assets/filters/beard_upper_lip.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/filters/dog_brown_ear.webp
Normal file
BIN
assets/filters/dog_brown_ear.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/filters/dog_brown_nose.webp
Normal file
BIN
assets/filters/dog_brown_nose.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -109,10 +109,10 @@ func getPushNotificationData(pushData: String) -> (
|
|||
} else if pushUser != nil {
|
||||
return (
|
||||
pushUser!.displayName,
|
||||
getPushNotificationText(pushNotification: pushNotification).0, pushUser!.userID
|
||||
getPushNotificationText(pushNotification: pushNotification, userKnown: true).0, pushUser!.userID
|
||||
)
|
||||
} else {
|
||||
let content = getPushNotificationText(pushNotification: pushNotification)
|
||||
let content = getPushNotificationText(pushNotification: pushNotification, userKnown: false)
|
||||
return (
|
||||
content.1, content.0, 1
|
||||
)
|
||||
|
|
@ -205,57 +205,77 @@ func readFromKeychain(key: String) -> String? {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getPushNotificationText(pushNotification: PushNotification) -> (String, String) {
|
||||
func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) {
|
||||
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
||||
|
||||
var pushNotificationText: [PushKind: String] = [:]
|
||||
var title = "[Unknown]"
|
||||
var title = "You"
|
||||
var noTranslationFoundTitle = "You have a new message."
|
||||
var noTranslationFoundBody = "Open twonly to learn more."
|
||||
|
||||
// Define the messages based on the system language
|
||||
if systemLanguage.contains("de") { // German
|
||||
title = "[Unbekannt]"
|
||||
pushNotificationText = [
|
||||
.text: "hat eine Nachricht{inGroup} gesendet.",
|
||||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
||||
.video: "hat ein Video{inGroup} gesendet.",
|
||||
.image: "hat ein Bild{inGroup} gesendet.",
|
||||
.audio: "hat eine Sprachnachricht{inGroup} gesendet.",
|
||||
.contactRequest: "möchte sich mit dir vernetzen.",
|
||||
.acceptRequest: "ist jetzt mit dir vernetzt.",
|
||||
.storedMediaFile: "hat dein Bild gespeichert.",
|
||||
.reaction: "hat auf dein Bild reagiert.",
|
||||
.testNotification: "Das ist eine Testbenachrichtigung.",
|
||||
.reopenedMedia: "hat dein Bild erneut geöffnet.",
|
||||
.reactionToVideo: "hat mit {{content}} auf dein Video reagiert.",
|
||||
.reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
|
||||
.reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
|
||||
.reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.",
|
||||
.response: "hat dir{inGroup} geantwortet.",
|
||||
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
|
||||
]
|
||||
} else { // Default to English
|
||||
pushNotificationText = [
|
||||
.text: "sent a message{inGroup}.",
|
||||
.twonly: "sent a twonly{inGroup}.",
|
||||
.video: "sent a video{inGroup}.",
|
||||
.image: "sent an image{inGroup}.",
|
||||
.audio: "sent a voice message{inGroup}.",
|
||||
.contactRequest: "wants to connect with you.",
|
||||
.acceptRequest: "is now connected with you.",
|
||||
.storedMediaFile: "has stored your image.",
|
||||
.reaction: "has reacted to your image.",
|
||||
.testNotification: "This is a test notification.",
|
||||
.reopenedMedia: "has reopened your image.",
|
||||
.reactionToVideo: "has reacted with {{content}} to your video.",
|
||||
.reactionToText: "has reacted with {{content}} to your text.",
|
||||
.reactionToImage: "has reacted with {{content}} to your image.",
|
||||
.reactionToAudio: "has reacted with {{content}} to your voice message.",
|
||||
.response: "has responded{inGroup}.",
|
||||
.addedToGroup: "has added you to \"{{content}}\"",
|
||||
]
|
||||
title = "Du"
|
||||
noTranslationFoundTitle = "Du hast eine neue Nachricht."
|
||||
noTranslationFoundBody = "Öffne twonly um mehr zu erfahren."
|
||||
if (userKnown) {
|
||||
pushNotificationText = [
|
||||
.text: "hat eine Nachricht{inGroup} gesendet.",
|
||||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
||||
.video: "hat ein Video{inGroup} gesendet.",
|
||||
.image: "hat ein Bild{inGroup} gesendet.",
|
||||
.audio: "hat eine Sprachnachricht{inGroup} gesendet.",
|
||||
.contactRequest: "möchte sich mit dir vernetzen.",
|
||||
.acceptRequest: "ist jetzt mit dir vernetzt.",
|
||||
.storedMediaFile: "hat dein Bild gespeichert.",
|
||||
.reaction: "hat auf dein Bild reagiert.",
|
||||
.testNotification: "Das ist eine Testbenachrichtigung.",
|
||||
.reopenedMedia: "hat dein Bild erneut geöffnet.",
|
||||
.reactionToVideo: "hat mit {{content}} auf dein Video reagiert.",
|
||||
.reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
|
||||
.reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
|
||||
.reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.",
|
||||
.response: "hat dir{inGroup} geantwortet.",
|
||||
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
|
||||
]
|
||||
} else {
|
||||
pushNotificationText = [
|
||||
.contactRequest: "hast eine neue Kontaktanfrage erhalten.",
|
||||
]
|
||||
}
|
||||
} else {
|
||||
if (userKnown) {
|
||||
pushNotificationText = [
|
||||
.text: "sent a message{inGroup}.",
|
||||
.twonly: "sent a twonly{inGroup}.",
|
||||
.video: "sent a video{inGroup}.",
|
||||
.image: "sent an image{inGroup}.",
|
||||
.audio: "sent a voice message{inGroup}.",
|
||||
.contactRequest: "wants to connect with you.",
|
||||
.acceptRequest: "is now connected with you.",
|
||||
.storedMediaFile: "has stored your image.",
|
||||
.reaction: "has reacted to your image.",
|
||||
.testNotification: "This is a test notification.",
|
||||
.reopenedMedia: "has reopened your image.",
|
||||
.reactionToVideo: "has reacted with {{content}} to your video.",
|
||||
.reactionToText: "has reacted with {{content}} to your text.",
|
||||
.reactionToImage: "has reacted with {{content}} to your image.",
|
||||
.reactionToAudio: "has reacted with {{content}} to your voice message.",
|
||||
.response: "has responded{inGroup}.",
|
||||
.addedToGroup: "has added you to \"{{content}}\"",
|
||||
]
|
||||
} else {
|
||||
pushNotificationText = [
|
||||
.contactRequest: "have received a new contact request.",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var content = pushNotificationText[pushNotification.kind] ?? ""
|
||||
if (content == "") {
|
||||
title = noTranslationFoundTitle
|
||||
content = noTranslationFoundBody
|
||||
}
|
||||
|
||||
if pushNotification.hasAdditionalContent {
|
||||
content.replace("{{content}}", with: pushNotification.additionalContent)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ PODS:
|
|||
- google_mlkit_commons (0.11.0):
|
||||
- Flutter
|
||||
- MLKitVision
|
||||
- google_mlkit_face_detection (0.13.1):
|
||||
- Flutter
|
||||
- google_mlkit_commons
|
||||
- GoogleMLKit/FaceDetection (~> 7.0.0)
|
||||
- GoogleAdsOnDeviceConversion (3.2.0):
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
|
|
@ -169,6 +173,9 @@ PODS:
|
|||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 6.0.0)
|
||||
- GoogleMLKit/FaceDetection (7.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitFaceDetection (~> 6.0.0)
|
||||
- GoogleMLKit/MLKitCore (7.0.0):
|
||||
- MLKitCommon (~> 12.0.0)
|
||||
- GoogleToolboxForMac/Defines (4.2.1)
|
||||
|
|
@ -251,6 +258,9 @@ PODS:
|
|||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLKitFaceDetection (6.0.0):
|
||||
- MLKitCommon (~> 12.0)
|
||||
- MLKitVision (~> 8.0)
|
||||
- MLKitVision (8.0.0):
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
|
|
@ -357,6 +367,7 @@ DEPENDENCIES:
|
|||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
|
||||
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
|
||||
- GoogleUtilities
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
|
|
@ -398,6 +409,7 @@ SPEC REPOS:
|
|||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
- MLKitFaceDetection
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
|
|
@ -454,6 +466,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
|
||||
google_mlkit_commons:
|
||||
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
||||
google_mlkit_face_detection:
|
||||
:path: ".symlinks/plugins/google_mlkit_face_detection/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_purchase_storekit:
|
||||
|
|
@ -518,6 +532,7 @@ SPEC CHECKSUMS:
|
|||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
|
||||
google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab
|
||||
google_mlkit_face_detection: 754da2113a1952f063c7c5dc347ac6ae8934fb77
|
||||
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
||||
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
|
|
@ -533,6 +548,7 @@ SPEC CHECKSUMS:
|
|||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||
MLKitFaceDetection: 2a593db4837db503ad3426b565e7aab045cefea5
|
||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
String emoji,
|
||||
bool remove,
|
||||
) async {
|
||||
if (!isEmoji(emoji)) {
|
||||
if (!isOneEmoji(emoji)) {
|
||||
Log.error('Did not update reaction as it is not an emoji!');
|
||||
return;
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
String emoji,
|
||||
bool remove,
|
||||
) async {
|
||||
if (!isEmoji(emoji)) {
|
||||
if (!isOneEmoji(emoji)) {
|
||||
Log.error('Did not update reaction as it is not an emoji!');
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
1
lib/src/database/schemas/twonly_db/drift_schema_v7.json
Normal file
1
lib/src/database/schemas/twonly_db/drift_schema_v7.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,8 @@ class Messages extends Table {
|
|||
.nullable()
|
||||
.references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)();
|
||||
|
||||
BlobColumn get additionalMessageData => blob().nullable()();
|
||||
|
||||
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get mediaReopened =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
|
@ -56,7 +58,7 @@ class MessageActions extends Table {
|
|||
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get contactId =>
|
||||
integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)();
|
||||
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get type => textEnum<MessageActionType>()();
|
||||
|
||||
|
|
@ -75,7 +77,7 @@ class MessageHistories extends Table {
|
|||
|
||||
IntColumn get contactId => integer()
|
||||
.nullable()
|
||||
.references(Contacts, #contactId, onDelete: KeyAction.cascade)();
|
||||
.references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get content => text().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 7;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
|
|
@ -85,39 +85,49 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
await m.addColumn(schema.messages, schema.messages.mediaReopened);
|
||||
await m.dropColumn(schema.mediaFiles, 'reopen_by_contact');
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.groups, schema.groups.draftMessage);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.groupHistories,
|
||||
columnTransformer: {
|
||||
schema.groupHistories.affectedContactId:
|
||||
schema.groupHistories.affectedContactId,
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
await m.addColumn(schema.receipts, schema.receipts.markForRetry);
|
||||
await m.addColumn(
|
||||
schema.mediaFiles,
|
||||
schema.mediaFiles.storedFileHash,
|
||||
);
|
||||
},
|
||||
from5To6: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.receipts,
|
||||
schema.receipts.markForRetryAfterAccepted,
|
||||
);
|
||||
},
|
||||
),
|
||||
onUpgrade: (m, from, to) async {
|
||||
// disable foreign_keys before migrations
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
return stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
await m.addColumn(schema.messages, schema.messages.mediaReopened);
|
||||
await m.dropColumn(schema.mediaFiles, 'reopen_by_contact');
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.groups, schema.groups.draftMessage);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.groupHistories,
|
||||
columnTransformer: {
|
||||
schema.groupHistories.affectedContactId:
|
||||
schema.groupHistories.affectedContactId,
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
await m.addColumn(schema.receipts, schema.receipts.markForRetry);
|
||||
await m.addColumn(
|
||||
schema.mediaFiles,
|
||||
schema.mediaFiles.storedFileHash,
|
||||
);
|
||||
},
|
||||
from5To6: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.receipts,
|
||||
schema.receipts.markForRetryAfterAccepted,
|
||||
);
|
||||
},
|
||||
from6To7: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.messages,
|
||||
schema.messages.additionalMessageData,
|
||||
);
|
||||
},
|
||||
)(m, from, to);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2796,6 +2796,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES media_files (media_id) ON DELETE SET NULL'));
|
||||
static const VerificationMeta _additionalMessageDataMeta =
|
||||
const VerificationMeta('additionalMessageData');
|
||||
@override
|
||||
late final GeneratedColumn<Uint8List> additionalMessageData =
|
||||
GeneratedColumn<Uint8List>('additional_message_data', aliasedName, true,
|
||||
type: DriftSqlType.blob, requiredDuringInsert: false);
|
||||
static const VerificationMeta _mediaStoredMeta =
|
||||
const VerificationMeta('mediaStored');
|
||||
@override
|
||||
|
|
@ -2884,6 +2890,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
type,
|
||||
content,
|
||||
mediaId,
|
||||
additionalMessageData,
|
||||
mediaStored,
|
||||
mediaReopened,
|
||||
downloadToken,
|
||||
|
|
@ -2930,6 +2937,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
context.handle(_mediaIdMeta,
|
||||
mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta));
|
||||
}
|
||||
if (data.containsKey('additional_message_data')) {
|
||||
context.handle(
|
||||
_additionalMessageDataMeta,
|
||||
additionalMessageData.isAcceptableOrUnknown(
|
||||
data['additional_message_data']!, _additionalMessageDataMeta));
|
||||
}
|
||||
if (data.containsKey('media_stored')) {
|
||||
context.handle(
|
||||
_mediaStoredMeta,
|
||||
|
|
@ -3013,6 +3026,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
.read(DriftSqlType.string, data['${effectivePrefix}content']),
|
||||
mediaId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}media_id']),
|
||||
additionalMessageData: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.blob, data['${effectivePrefix}additional_message_data']),
|
||||
mediaStored: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!,
|
||||
mediaReopened: attachedDatabase.typeMapping
|
||||
|
|
@ -3054,6 +3069,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
final MessageType type;
|
||||
final String? content;
|
||||
final String? mediaId;
|
||||
final Uint8List? additionalMessageData;
|
||||
final bool mediaStored;
|
||||
final bool mediaReopened;
|
||||
final Uint8List? downloadToken;
|
||||
|
|
@ -3072,6 +3088,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
required this.type,
|
||||
this.content,
|
||||
this.mediaId,
|
||||
this.additionalMessageData,
|
||||
required this.mediaStored,
|
||||
required this.mediaReopened,
|
||||
this.downloadToken,
|
||||
|
|
@ -3100,6 +3117,10 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
if (!nullToAbsent || mediaId != null) {
|
||||
map['media_id'] = Variable<String>(mediaId);
|
||||
}
|
||||
if (!nullToAbsent || additionalMessageData != null) {
|
||||
map['additional_message_data'] =
|
||||
Variable<Uint8List>(additionalMessageData);
|
||||
}
|
||||
map['media_stored'] = Variable<bool>(mediaStored);
|
||||
map['media_reopened'] = Variable<bool>(mediaReopened);
|
||||
if (!nullToAbsent || downloadToken != null) {
|
||||
|
|
@ -3142,6 +3163,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
mediaId: mediaId == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(mediaId),
|
||||
additionalMessageData: additionalMessageData == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(additionalMessageData),
|
||||
mediaStored: Value(mediaStored),
|
||||
mediaReopened: Value(mediaReopened),
|
||||
downloadToken: downloadToken == null && nullToAbsent
|
||||
|
|
@ -3181,6 +3205,8 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
.fromJson(serializer.fromJson<String>(json['type'])),
|
||||
content: serializer.fromJson<String?>(json['content']),
|
||||
mediaId: serializer.fromJson<String?>(json['mediaId']),
|
||||
additionalMessageData:
|
||||
serializer.fromJson<Uint8List?>(json['additionalMessageData']),
|
||||
mediaStored: serializer.fromJson<bool>(json['mediaStored']),
|
||||
mediaReopened: serializer.fromJson<bool>(json['mediaReopened']),
|
||||
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
|
||||
|
|
@ -3206,6 +3232,8 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
serializer.toJson<String>($MessagesTable.$convertertype.toJson(type)),
|
||||
'content': serializer.toJson<String?>(content),
|
||||
'mediaId': serializer.toJson<String?>(mediaId),
|
||||
'additionalMessageData':
|
||||
serializer.toJson<Uint8List?>(additionalMessageData),
|
||||
'mediaStored': serializer.toJson<bool>(mediaStored),
|
||||
'mediaReopened': serializer.toJson<bool>(mediaReopened),
|
||||
'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
|
||||
|
|
@ -3227,6 +3255,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
MessageType? type,
|
||||
Value<String?> content = const Value.absent(),
|
||||
Value<String?> mediaId = const Value.absent(),
|
||||
Value<Uint8List?> additionalMessageData = const Value.absent(),
|
||||
bool? mediaStored,
|
||||
bool? mediaReopened,
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
|
|
@ -3245,6 +3274,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
type: type ?? this.type,
|
||||
content: content.present ? content.value : this.content,
|
||||
mediaId: mediaId.present ? mediaId.value : this.mediaId,
|
||||
additionalMessageData: additionalMessageData.present
|
||||
? additionalMessageData.value
|
||||
: this.additionalMessageData,
|
||||
mediaStored: mediaStored ?? this.mediaStored,
|
||||
mediaReopened: mediaReopened ?? this.mediaReopened,
|
||||
downloadToken:
|
||||
|
|
@ -3268,6 +3300,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
type: data.type.present ? data.type.value : this.type,
|
||||
content: data.content.present ? data.content.value : this.content,
|
||||
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
|
||||
additionalMessageData: data.additionalMessageData.present
|
||||
? data.additionalMessageData.value
|
||||
: this.additionalMessageData,
|
||||
mediaStored:
|
||||
data.mediaStored.present ? data.mediaStored.value : this.mediaStored,
|
||||
mediaReopened: data.mediaReopened.present
|
||||
|
|
@ -3303,6 +3338,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
..write('type: $type, ')
|
||||
..write('content: $content, ')
|
||||
..write('mediaId: $mediaId, ')
|
||||
..write('additionalMessageData: $additionalMessageData, ')
|
||||
..write('mediaStored: $mediaStored, ')
|
||||
..write('mediaReopened: $mediaReopened, ')
|
||||
..write('downloadToken: $downloadToken, ')
|
||||
|
|
@ -3326,6 +3362,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
type,
|
||||
content,
|
||||
mediaId,
|
||||
$driftBlobEquality.hash(additionalMessageData),
|
||||
mediaStored,
|
||||
mediaReopened,
|
||||
$driftBlobEquality.hash(downloadToken),
|
||||
|
|
@ -3347,6 +3384,8 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
other.type == this.type &&
|
||||
other.content == this.content &&
|
||||
other.mediaId == this.mediaId &&
|
||||
$driftBlobEquality.equals(
|
||||
other.additionalMessageData, this.additionalMessageData) &&
|
||||
other.mediaStored == this.mediaStored &&
|
||||
other.mediaReopened == this.mediaReopened &&
|
||||
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
|
||||
|
|
@ -3367,6 +3406,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
final Value<MessageType> type;
|
||||
final Value<String?> content;
|
||||
final Value<String?> mediaId;
|
||||
final Value<Uint8List?> additionalMessageData;
|
||||
final Value<bool> mediaStored;
|
||||
final Value<bool> mediaReopened;
|
||||
final Value<Uint8List?> downloadToken;
|
||||
|
|
@ -3386,6 +3426,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
this.type = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.mediaId = const Value.absent(),
|
||||
this.additionalMessageData = const Value.absent(),
|
||||
this.mediaStored = const Value.absent(),
|
||||
this.mediaReopened = const Value.absent(),
|
||||
this.downloadToken = const Value.absent(),
|
||||
|
|
@ -3406,6 +3447,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
required MessageType type,
|
||||
this.content = const Value.absent(),
|
||||
this.mediaId = const Value.absent(),
|
||||
this.additionalMessageData = const Value.absent(),
|
||||
this.mediaStored = const Value.absent(),
|
||||
this.mediaReopened = const Value.absent(),
|
||||
this.downloadToken = const Value.absent(),
|
||||
|
|
@ -3428,6 +3470,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
Expression<String>? type,
|
||||
Expression<String>? content,
|
||||
Expression<String>? mediaId,
|
||||
Expression<Uint8List>? additionalMessageData,
|
||||
Expression<bool>? mediaStored,
|
||||
Expression<bool>? mediaReopened,
|
||||
Expression<Uint8List>? downloadToken,
|
||||
|
|
@ -3448,6 +3491,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
if (type != null) 'type': type,
|
||||
if (content != null) 'content': content,
|
||||
if (mediaId != null) 'media_id': mediaId,
|
||||
if (additionalMessageData != null)
|
||||
'additional_message_data': additionalMessageData,
|
||||
if (mediaStored != null) 'media_stored': mediaStored,
|
||||
if (mediaReopened != null) 'media_reopened': mediaReopened,
|
||||
if (downloadToken != null) 'download_token': downloadToken,
|
||||
|
|
@ -3471,6 +3516,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
Value<MessageType>? type,
|
||||
Value<String?>? content,
|
||||
Value<String?>? mediaId,
|
||||
Value<Uint8List?>? additionalMessageData,
|
||||
Value<bool>? mediaStored,
|
||||
Value<bool>? mediaReopened,
|
||||
Value<Uint8List?>? downloadToken,
|
||||
|
|
@ -3490,6 +3536,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
type: type ?? this.type,
|
||||
content: content ?? this.content,
|
||||
mediaId: mediaId ?? this.mediaId,
|
||||
additionalMessageData:
|
||||
additionalMessageData ?? this.additionalMessageData,
|
||||
mediaStored: mediaStored ?? this.mediaStored,
|
||||
mediaReopened: mediaReopened ?? this.mediaReopened,
|
||||
downloadToken: downloadToken ?? this.downloadToken,
|
||||
|
|
@ -3527,6 +3575,10 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
if (mediaId.present) {
|
||||
map['media_id'] = Variable<String>(mediaId.value);
|
||||
}
|
||||
if (additionalMessageData.present) {
|
||||
map['additional_message_data'] =
|
||||
Variable<Uint8List>(additionalMessageData.value);
|
||||
}
|
||||
if (mediaStored.present) {
|
||||
map['media_stored'] = Variable<bool>(mediaStored.value);
|
||||
}
|
||||
|
|
@ -3575,6 +3627,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
..write('type: $type, ')
|
||||
..write('content: $content, ')
|
||||
..write('mediaId: $mediaId, ')
|
||||
..write('additionalMessageData: $additionalMessageData, ')
|
||||
..write('mediaStored: $mediaStored, ')
|
||||
..write('mediaReopened: $mediaReopened, ')
|
||||
..write('downloadToken: $downloadToken, ')
|
||||
|
|
@ -3621,7 +3674,10 @@ class $MessageHistoriesTable extends MessageHistories
|
|||
@override
|
||||
late final GeneratedColumn<int> contactId = GeneratedColumn<int>(
|
||||
'contact_id', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES contacts (user_id) ON DELETE CASCADE'));
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
|
|
@ -6964,7 +7020,10 @@ class $MessageActionsTable extends MessageActions
|
|||
@override
|
||||
late final GeneratedColumn<int> contactId = GeneratedColumn<int>(
|
||||
'contact_id', aliasedName, false,
|
||||
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES contacts (user_id) ON DELETE CASCADE'));
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<MessageActionType, String> type =
|
||||
GeneratedColumn<String>('type', aliasedName, false,
|
||||
|
|
@ -7837,6 +7896,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
TableUpdate('message_histories', kind: UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName('contacts',
|
||||
limitUpdateKind: UpdateKind.delete),
|
||||
result: [
|
||||
TableUpdate('message_histories', kind: UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName('messages',
|
||||
limitUpdateKind: UpdateKind.delete),
|
||||
|
|
@ -7894,6 +7960,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
TableUpdate('message_actions', kind: UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName('contacts',
|
||||
limitUpdateKind: UpdateKind.delete),
|
||||
result: [
|
||||
TableUpdate('message_actions', kind: UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName('groups',
|
||||
limitUpdateKind: UpdateKind.delete),
|
||||
|
|
@ -7955,6 +8028,23 @@ final class $$ContactsTableReferences
|
|||
manager.$state.copyWith(prefetchedData: cache));
|
||||
}
|
||||
|
||||
static MultiTypedResultKey<$MessageHistoriesTable, List<MessageHistory>>
|
||||
_messageHistoriesRefsTable(_$TwonlyDB db) =>
|
||||
MultiTypedResultKey.fromTable(db.messageHistories,
|
||||
aliasName: $_aliasNameGenerator(
|
||||
db.contacts.userId, db.messageHistories.contactId));
|
||||
|
||||
$$MessageHistoriesTableProcessedTableManager get messageHistoriesRefs {
|
||||
final manager =
|
||||
$$MessageHistoriesTableTableManager($_db, $_db.messageHistories).filter(
|
||||
(f) => f.contactId.userId.sqlEquals($_itemColumn<int>('user_id')!));
|
||||
|
||||
final cache =
|
||||
$_typedResult.readTableOrNull(_messageHistoriesRefsTable($_db));
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: cache));
|
||||
}
|
||||
|
||||
static MultiTypedResultKey<$ReactionsTable, List<Reaction>>
|
||||
_reactionsRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable(
|
||||
db.reactions,
|
||||
|
|
@ -8041,6 +8131,22 @@ final class $$ContactsTableReferences
|
|||
manager.$state.copyWith(prefetchedData: cache));
|
||||
}
|
||||
|
||||
static MultiTypedResultKey<$MessageActionsTable, List<MessageAction>>
|
||||
_messageActionsRefsTable(_$TwonlyDB db) =>
|
||||
MultiTypedResultKey.fromTable(db.messageActions,
|
||||
aliasName: $_aliasNameGenerator(
|
||||
db.contacts.userId, db.messageActions.contactId));
|
||||
|
||||
$$MessageActionsTableProcessedTableManager get messageActionsRefs {
|
||||
final manager = $$MessageActionsTableTableManager($_db, $_db.messageActions)
|
||||
.filter(
|
||||
(f) => f.contactId.userId.sqlEquals($_itemColumn<int>('user_id')!));
|
||||
|
||||
final cache = $_typedResult.readTableOrNull(_messageActionsRefsTable($_db));
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: cache));
|
||||
}
|
||||
|
||||
static MultiTypedResultKey<$GroupHistoriesTable, List<GroupHistory>>
|
||||
_groupHistoriesRefsTable(_$TwonlyDB db) =>
|
||||
MultiTypedResultKey.fromTable(db.groupHistories,
|
||||
|
|
@ -8130,6 +8236,27 @@ class $$ContactsTableFilterComposer
|
|||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<bool> messageHistoriesRefs(
|
||||
Expression<bool> Function($$MessageHistoriesTableFilterComposer f) f) {
|
||||
final $$MessageHistoriesTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.userId,
|
||||
referencedTable: $db.messageHistories,
|
||||
getReferencedColumn: (t) => t.contactId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$MessageHistoriesTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: $db.messageHistories,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<bool> reactionsRefs(
|
||||
Expression<bool> Function($$ReactionsTableFilterComposer f) f) {
|
||||
final $$ReactionsTableFilterComposer composer = $composerBuilder(
|
||||
|
|
@ -8239,6 +8366,27 @@ class $$ContactsTableFilterComposer
|
|||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<bool> messageActionsRefs(
|
||||
Expression<bool> Function($$MessageActionsTableFilterComposer f) f) {
|
||||
final $$MessageActionsTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.userId,
|
||||
referencedTable: $db.messageActions,
|
||||
getReferencedColumn: (t) => t.contactId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$MessageActionsTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: $db.messageActions,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<bool> groupHistoriesRefs(
|
||||
Expression<bool> Function($$GroupHistoriesTableFilterComposer f) f) {
|
||||
final $$GroupHistoriesTableFilterComposer composer = $composerBuilder(
|
||||
|
|
@ -8383,6 +8531,27 @@ class $$ContactsTableAnnotationComposer
|
|||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<T> messageHistoriesRefs<T extends Object>(
|
||||
Expression<T> Function($$MessageHistoriesTableAnnotationComposer a) f) {
|
||||
final $$MessageHistoriesTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.userId,
|
||||
referencedTable: $db.messageHistories,
|
||||
getReferencedColumn: (t) => t.contactId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$MessageHistoriesTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: $db.messageHistories,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<T> reactionsRefs<T extends Object>(
|
||||
Expression<T> Function($$ReactionsTableAnnotationComposer a) f) {
|
||||
final $$ReactionsTableAnnotationComposer composer = $composerBuilder(
|
||||
|
|
@ -8493,6 +8662,27 @@ class $$ContactsTableAnnotationComposer
|
|||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<T> messageActionsRefs<T extends Object>(
|
||||
Expression<T> Function($$MessageActionsTableAnnotationComposer a) f) {
|
||||
final $$MessageActionsTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.userId,
|
||||
referencedTable: $db.messageActions,
|
||||
getReferencedColumn: (t) => t.contactId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$MessageActionsTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: $db.messageActions,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return f(composer);
|
||||
}
|
||||
|
||||
Expression<T> groupHistoriesRefs<T extends Object>(
|
||||
Expression<T> Function($$GroupHistoriesTableAnnotationComposer a) f) {
|
||||
final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder(
|
||||
|
|
@ -8528,11 +8718,13 @@ class $$ContactsTableTableManager extends RootTableManager<
|
|||
Contact,
|
||||
PrefetchHooks Function(
|
||||
{bool messagesRefs,
|
||||
bool messageHistoriesRefs,
|
||||
bool reactionsRefs,
|
||||
bool groupMembersRefs,
|
||||
bool receiptsRefs,
|
||||
bool signalContactPreKeysRefs,
|
||||
bool signalContactSignedPreKeysRefs,
|
||||
bool messageActionsRefs,
|
||||
bool groupHistoriesRefs})> {
|
||||
$$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table)
|
||||
: super(TableManagerState(
|
||||
|
|
@ -8610,22 +8802,26 @@ class $$ContactsTableTableManager extends RootTableManager<
|
|||
.toList(),
|
||||
prefetchHooksCallback: (
|
||||
{messagesRefs = false,
|
||||
messageHistoriesRefs = false,
|
||||
reactionsRefs = false,
|
||||
groupMembersRefs = false,
|
||||
receiptsRefs = false,
|
||||
signalContactPreKeysRefs = false,
|
||||
signalContactSignedPreKeysRefs = false,
|
||||
messageActionsRefs = false,
|
||||
groupHistoriesRefs = false}) {
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [
|
||||
if (messagesRefs) db.messages,
|
||||
if (messageHistoriesRefs) db.messageHistories,
|
||||
if (reactionsRefs) db.reactions,
|
||||
if (groupMembersRefs) db.groupMembers,
|
||||
if (receiptsRefs) db.receipts,
|
||||
if (signalContactPreKeysRefs) db.signalContactPreKeys,
|
||||
if (signalContactSignedPreKeysRefs)
|
||||
db.signalContactSignedPreKeys,
|
||||
if (messageActionsRefs) db.messageActions,
|
||||
if (groupHistoriesRefs) db.groupHistories
|
||||
],
|
||||
addJoins: null,
|
||||
|
|
@ -8643,6 +8839,19 @@ class $$ContactsTableTableManager extends RootTableManager<
|
|||
(item, referencedItems) => referencedItems
|
||||
.where((e) => e.senderId == item.userId),
|
||||
typedResults: items),
|
||||
if (messageHistoriesRefs)
|
||||
await $_getPrefetchedData<Contact, $ContactsTable,
|
||||
MessageHistory>(
|
||||
currentTable: table,
|
||||
referencedTable: $$ContactsTableReferences
|
||||
._messageHistoriesRefsTable(db),
|
||||
managerFromTypedResult: (p0) =>
|
||||
$$ContactsTableReferences(db, table, p0)
|
||||
.messageHistoriesRefs,
|
||||
referencedItemsForCurrentItem:
|
||||
(item, referencedItems) => referencedItems
|
||||
.where((e) => e.contactId == item.userId),
|
||||
typedResults: items),
|
||||
if (reactionsRefs)
|
||||
await $_getPrefetchedData<Contact, $ContactsTable,
|
||||
Reaction>(
|
||||
|
|
@ -8707,6 +8916,19 @@ class $$ContactsTableTableManager extends RootTableManager<
|
|||
(item, referencedItems) => referencedItems
|
||||
.where((e) => e.contactId == item.userId),
|
||||
typedResults: items),
|
||||
if (messageActionsRefs)
|
||||
await $_getPrefetchedData<Contact, $ContactsTable,
|
||||
MessageAction>(
|
||||
currentTable: table,
|
||||
referencedTable: $$ContactsTableReferences
|
||||
._messageActionsRefsTable(db),
|
||||
managerFromTypedResult: (p0) =>
|
||||
$$ContactsTableReferences(db, table, p0)
|
||||
.messageActionsRefs,
|
||||
referencedItemsForCurrentItem:
|
||||
(item, referencedItems) => referencedItems
|
||||
.where((e) => e.contactId == item.userId),
|
||||
typedResults: items),
|
||||
if (groupHistoriesRefs)
|
||||
await $_getPrefetchedData<Contact, $ContactsTable,
|
||||
GroupHistory>(
|
||||
|
|
@ -8740,11 +8962,13 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager<
|
|||
Contact,
|
||||
PrefetchHooks Function(
|
||||
{bool messagesRefs,
|
||||
bool messageHistoriesRefs,
|
||||
bool reactionsRefs,
|
||||
bool groupMembersRefs,
|
||||
bool receiptsRefs,
|
||||
bool signalContactPreKeysRefs,
|
||||
bool signalContactSignedPreKeysRefs,
|
||||
bool messageActionsRefs,
|
||||
bool groupHistoriesRefs})>;
|
||||
typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({
|
||||
required String groupId,
|
||||
|
|
@ -9927,6 +10151,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
|
|||
required MessageType type,
|
||||
Value<String?> content,
|
||||
Value<String?> mediaId,
|
||||
Value<Uint8List?> additionalMessageData,
|
||||
Value<bool> mediaStored,
|
||||
Value<bool> mediaReopened,
|
||||
Value<Uint8List?> downloadToken,
|
||||
|
|
@ -9947,6 +10172,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
|||
Value<MessageType> type,
|
||||
Value<String?> content,
|
||||
Value<String?> mediaId,
|
||||
Value<Uint8List?> additionalMessageData,
|
||||
Value<bool> mediaStored,
|
||||
Value<bool> mediaReopened,
|
||||
Value<Uint8List?> downloadToken,
|
||||
|
|
@ -10096,6 +10322,10 @@ class $$MessagesTableFilterComposer
|
|||
ColumnFilters<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<Uint8List> get additionalMessageData => $composableBuilder(
|
||||
column: $table.additionalMessageData,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<bool> get mediaStored => $composableBuilder(
|
||||
column: $table.mediaStored, builder: (column) => ColumnFilters(column));
|
||||
|
||||
|
|
@ -10294,6 +10524,10 @@ class $$MessagesTableOrderingComposer
|
|||
ColumnOrderings<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<Uint8List> get additionalMessageData => $composableBuilder(
|
||||
column: $table.additionalMessageData,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<bool> get mediaStored => $composableBuilder(
|
||||
column: $table.mediaStored, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
|
|
@ -10410,6 +10644,9 @@ class $$MessagesTableAnnotationComposer
|
|||
GeneratedColumn<String> get content =>
|
||||
$composableBuilder(column: $table.content, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<Uint8List> get additionalMessageData => $composableBuilder(
|
||||
column: $table.additionalMessageData, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> get mediaStored => $composableBuilder(
|
||||
column: $table.mediaStored, builder: (column) => column);
|
||||
|
||||
|
|
@ -10624,6 +10861,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
Value<MessageType> type = const Value.absent(),
|
||||
Value<String?> content = const Value.absent(),
|
||||
Value<String?> mediaId = const Value.absent(),
|
||||
Value<Uint8List?> additionalMessageData = const Value.absent(),
|
||||
Value<bool> mediaStored = const Value.absent(),
|
||||
Value<bool> mediaReopened = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
|
|
@ -10644,6 +10882,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
type: type,
|
||||
content: content,
|
||||
mediaId: mediaId,
|
||||
additionalMessageData: additionalMessageData,
|
||||
mediaStored: mediaStored,
|
||||
mediaReopened: mediaReopened,
|
||||
downloadToken: downloadToken,
|
||||
|
|
@ -10664,6 +10903,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
required MessageType type,
|
||||
Value<String?> content = const Value.absent(),
|
||||
Value<String?> mediaId = const Value.absent(),
|
||||
Value<Uint8List?> additionalMessageData = const Value.absent(),
|
||||
Value<bool> mediaStored = const Value.absent(),
|
||||
Value<bool> mediaReopened = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
|
|
@ -10684,6 +10924,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
type: type,
|
||||
content: content,
|
||||
mediaId: mediaId,
|
||||
additionalMessageData: additionalMessageData,
|
||||
mediaStored: mediaStored,
|
||||
mediaReopened: mediaReopened,
|
||||
downloadToken: downloadToken,
|
||||
|
|
@ -10878,6 +11119,21 @@ final class $$MessageHistoriesTableReferences
|
|||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
|
||||
static $ContactsTable _contactIdTable(_$TwonlyDB db) =>
|
||||
db.contacts.createAlias($_aliasNameGenerator(
|
||||
db.messageHistories.contactId, db.contacts.userId));
|
||||
|
||||
$$ContactsTableProcessedTableManager? get contactId {
|
||||
final $_column = $_itemColumn<int>('contact_id');
|
||||
if ($_column == null) return null;
|
||||
final manager = $$ContactsTableTableManager($_db, $_db.contacts)
|
||||
.filter((f) => f.userId.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_contactIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageHistoriesTableFilterComposer
|
||||
|
|
@ -10892,9 +11148,6 @@ class $$MessageHistoriesTableFilterComposer
|
|||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get contactId => $composableBuilder(
|
||||
column: $table.contactId, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnFilters(column));
|
||||
|
||||
|
|
@ -10920,6 +11173,26 @@ class $$MessageHistoriesTableFilterComposer
|
|||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
$$ContactsTableFilterComposer get contactId {
|
||||
final $$ContactsTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.contactId,
|
||||
referencedTable: $db.contacts,
|
||||
getReferencedColumn: (t) => t.userId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$ContactsTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: $db.contacts,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageHistoriesTableOrderingComposer
|
||||
|
|
@ -10934,9 +11207,6 @@ class $$MessageHistoriesTableOrderingComposer
|
|||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get contactId => $composableBuilder(
|
||||
column: $table.contactId, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
|
|
@ -10962,6 +11232,26 @@ class $$MessageHistoriesTableOrderingComposer
|
|||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
$$ContactsTableOrderingComposer get contactId {
|
||||
final $$ContactsTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.contactId,
|
||||
referencedTable: $db.contacts,
|
||||
getReferencedColumn: (t) => t.userId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$ContactsTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: $db.contacts,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageHistoriesTableAnnotationComposer
|
||||
|
|
@ -10976,9 +11266,6 @@ class $$MessageHistoriesTableAnnotationComposer
|
|||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get contactId =>
|
||||
$composableBuilder(column: $table.contactId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get content =>
|
||||
$composableBuilder(column: $table.content, builder: (column) => column);
|
||||
|
||||
|
|
@ -11004,6 +11291,26 @@ class $$MessageHistoriesTableAnnotationComposer
|
|||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
$$ContactsTableAnnotationComposer get contactId {
|
||||
final $$ContactsTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.contactId,
|
||||
referencedTable: $db.contacts,
|
||||
getReferencedColumn: (t) => t.userId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$ContactsTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: $db.contacts,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageHistoriesTableTableManager extends RootTableManager<
|
||||
|
|
@ -11017,7 +11324,7 @@ class $$MessageHistoriesTableTableManager extends RootTableManager<
|
|||
$$MessageHistoriesTableUpdateCompanionBuilder,
|
||||
(MessageHistory, $$MessageHistoriesTableReferences),
|
||||
MessageHistory,
|
||||
PrefetchHooks Function({bool messageId})> {
|
||||
PrefetchHooks Function({bool messageId, bool contactId})> {
|
||||
$$MessageHistoriesTableTableManager(
|
||||
_$TwonlyDB db, $MessageHistoriesTable table)
|
||||
: super(TableManagerState(
|
||||
|
|
@ -11063,7 +11370,7 @@ class $$MessageHistoriesTableTableManager extends RootTableManager<
|
|||
$$MessageHistoriesTableReferences(db, table, e)
|
||||
))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({messageId = false}) {
|
||||
prefetchHooksCallback: ({messageId = false, contactId = false}) {
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
|
|
@ -11091,6 +11398,17 @@ class $$MessageHistoriesTableTableManager extends RootTableManager<
|
|||
.messageId,
|
||||
) as T;
|
||||
}
|
||||
if (contactId) {
|
||||
state = state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.contactId,
|
||||
referencedTable:
|
||||
$$MessageHistoriesTableReferences._contactIdTable(db),
|
||||
referencedColumn: $$MessageHistoriesTableReferences
|
||||
._contactIdTable(db)
|
||||
.userId,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
|
@ -11113,7 +11431,7 @@ typedef $$MessageHistoriesTableProcessedTableManager = ProcessedTableManager<
|
|||
$$MessageHistoriesTableUpdateCompanionBuilder,
|
||||
(MessageHistory, $$MessageHistoriesTableReferences),
|
||||
MessageHistory,
|
||||
PrefetchHooks Function({bool messageId})>;
|
||||
PrefetchHooks Function({bool messageId, bool contactId})>;
|
||||
typedef $$ReactionsTableCreateCompanionBuilder = ReactionsCompanion Function({
|
||||
required String messageId,
|
||||
required String emoji,
|
||||
|
|
@ -13581,6 +13899,21 @@ final class $$MessageActionsTableReferences
|
|||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
|
||||
static $ContactsTable _contactIdTable(_$TwonlyDB db) =>
|
||||
db.contacts.createAlias($_aliasNameGenerator(
|
||||
db.messageActions.contactId, db.contacts.userId));
|
||||
|
||||
$$ContactsTableProcessedTableManager get contactId {
|
||||
final $_column = $_itemColumn<int>('contact_id')!;
|
||||
|
||||
final manager = $$ContactsTableTableManager($_db, $_db.contacts)
|
||||
.filter((f) => f.userId.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_contactIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageActionsTableFilterComposer
|
||||
|
|
@ -13592,9 +13925,6 @@ class $$MessageActionsTableFilterComposer
|
|||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get contactId => $composableBuilder(
|
||||
column: $table.contactId, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<MessageActionType, MessageActionType, String>
|
||||
get type => $composableBuilder(
|
||||
column: $table.type,
|
||||
|
|
@ -13622,6 +13952,26 @@ class $$MessageActionsTableFilterComposer
|
|||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
$$ContactsTableFilterComposer get contactId {
|
||||
final $$ContactsTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.contactId,
|
||||
referencedTable: $db.contacts,
|
||||
getReferencedColumn: (t) => t.userId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$ContactsTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: $db.contacts,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageActionsTableOrderingComposer
|
||||
|
|
@ -13633,9 +13983,6 @@ class $$MessageActionsTableOrderingComposer
|
|||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get contactId => $composableBuilder(
|
||||
column: $table.contactId, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get type => $composableBuilder(
|
||||
column: $table.type, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
|
|
@ -13661,6 +14008,26 @@ class $$MessageActionsTableOrderingComposer
|
|||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
$$ContactsTableOrderingComposer get contactId {
|
||||
final $$ContactsTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.contactId,
|
||||
referencedTable: $db.contacts,
|
||||
getReferencedColumn: (t) => t.userId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$ContactsTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: $db.contacts,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageActionsTableAnnotationComposer
|
||||
|
|
@ -13672,9 +14039,6 @@ class $$MessageActionsTableAnnotationComposer
|
|||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get contactId =>
|
||||
$composableBuilder(column: $table.contactId, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<MessageActionType, String> get type =>
|
||||
$composableBuilder(column: $table.type, builder: (column) => column);
|
||||
|
||||
|
|
@ -13700,6 +14064,26 @@ class $$MessageActionsTableAnnotationComposer
|
|||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
$$ContactsTableAnnotationComposer get contactId {
|
||||
final $$ContactsTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.contactId,
|
||||
referencedTable: $db.contacts,
|
||||
getReferencedColumn: (t) => t.userId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
$$ContactsTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: $db.contacts,
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$MessageActionsTableTableManager extends RootTableManager<
|
||||
|
|
@ -13713,7 +14097,7 @@ class $$MessageActionsTableTableManager extends RootTableManager<
|
|||
$$MessageActionsTableUpdateCompanionBuilder,
|
||||
(MessageAction, $$MessageActionsTableReferences),
|
||||
MessageAction,
|
||||
PrefetchHooks Function({bool messageId})> {
|
||||
PrefetchHooks Function({bool messageId, bool contactId})> {
|
||||
$$MessageActionsTableTableManager(_$TwonlyDB db, $MessageActionsTable table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
|
|
@ -13758,7 +14142,7 @@ class $$MessageActionsTableTableManager extends RootTableManager<
|
|||
$$MessageActionsTableReferences(db, table, e)
|
||||
))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({messageId = false}) {
|
||||
prefetchHooksCallback: ({messageId = false, contactId = false}) {
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
|
|
@ -13786,6 +14170,17 @@ class $$MessageActionsTableTableManager extends RootTableManager<
|
|||
.messageId,
|
||||
) as T;
|
||||
}
|
||||
if (contactId) {
|
||||
state = state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.contactId,
|
||||
referencedTable:
|
||||
$$MessageActionsTableReferences._contactIdTable(db),
|
||||
referencedColumn: $$MessageActionsTableReferences
|
||||
._contactIdTable(db)
|
||||
.userId,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
|
@ -13808,7 +14203,7 @@ typedef $$MessageActionsTableProcessedTableManager = ProcessedTableManager<
|
|||
$$MessageActionsTableUpdateCompanionBuilder,
|
||||
(MessageAction, $$MessageActionsTableReferences),
|
||||
MessageAction,
|
||||
PrefetchHooks Function({bool messageId})>;
|
||||
PrefetchHooks Function({bool messageId, bool contactId})>;
|
||||
typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion
|
||||
Function({
|
||||
required String groupHistoryId,
|
||||
|
|
|
|||
|
|
@ -2804,12 +2804,439 @@ i1.GeneratedColumn<DateTime> _column_104(String aliasedName) =>
|
|||
i1.GeneratedColumn<DateTime>(
|
||||
'mark_for_retry_after_accepted', aliasedName, true,
|
||||
type: i1.DriftSqlType.dateTime);
|
||||
|
||||
final class Schema7 extends i0.VersionedSchema {
|
||||
Schema7({required super.database}) : super(version: 7);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
contacts,
|
||||
groups,
|
||||
mediaFiles,
|
||||
messages,
|
||||
messageHistories,
|
||||
reactions,
|
||||
groupMembers,
|
||||
receipts,
|
||||
receivedReceipts,
|
||||
signalIdentityKeyStores,
|
||||
signalPreKeyStores,
|
||||
signalSenderKeyStores,
|
||||
signalSessionStores,
|
||||
signalContactPreKeys,
|
||||
signalContactSignedPreKeys,
|
||||
messageActions,
|
||||
groupHistories,
|
||||
];
|
||||
late final Shape0 contacts = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'contacts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(user_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_4,
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape17 groups = Shape17(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'groups',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(group_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_22,
|
||||
_column_23,
|
||||
_column_24,
|
||||
_column_100,
|
||||
_column_25,
|
||||
_column_26,
|
||||
_column_27,
|
||||
_column_12,
|
||||
_column_28,
|
||||
_column_29,
|
||||
_column_30,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_33,
|
||||
_column_34,
|
||||
_column_35,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape18 mediaFiles = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'media_files',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(media_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_102,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape21 messages = Shape21(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'messages',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(message_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_37,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_105,
|
||||
_column_55,
|
||||
_column_56,
|
||||
_column_46,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
_column_60,
|
||||
_column_12,
|
||||
_column_61,
|
||||
_column_62,
|
||||
_column_63,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape4 messageHistories = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_53,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape5 reactions = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'reactions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(message_id, sender_id, emoji)',
|
||||
],
|
||||
columns: [
|
||||
_column_65,
|
||||
_column_67,
|
||||
_column_68,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape6 groupMembers = Shape6(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_members',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(group_id, contact_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_50,
|
||||
_column_69,
|
||||
_column_70,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape20 receipts = Shape20(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(receipt_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_73,
|
||||
_column_74,
|
||||
_column_75,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_103,
|
||||
_column_104,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape8 receivedReceipts = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'received_receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(receipt_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_73,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape9 signalIdentityKeyStores = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_identity_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(device_id, name)',
|
||||
],
|
||||
columns: [
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape10 signalPreKeyStores = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_pre_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(pre_key_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape11 signalSenderKeyStores = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_sender_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(sender_key_name)',
|
||||
],
|
||||
columns: [
|
||||
_column_86,
|
||||
_column_87,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape12 signalSessionStores = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_session_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(device_id, name)',
|
||||
],
|
||||
columns: [
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_88,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape13 signalContactPreKeys = Shape13(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_contact_pre_keys',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(contact_id, pre_key_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_74,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape14 signalContactSignedPreKeys = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_contact_signed_pre_keys',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(contact_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_74,
|
||||
_column_89,
|
||||
_column_90,
|
||||
_column_91,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape15 messageActions = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_actions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(message_id, contact_id, type)',
|
||||
],
|
||||
columns: [
|
||||
_column_65,
|
||||
_column_92,
|
||||
_column_37,
|
||||
_column_93,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape16 groupHistories = Shape16(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(group_history_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_94,
|
||||
_column_50,
|
||||
_column_95,
|
||||
_column_101,
|
||||
_column_97,
|
||||
_column_98,
|
||||
_column_99,
|
||||
_column_37,
|
||||
_column_93,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
}
|
||||
|
||||
class Shape21 extends i0.VersionedTable {
|
||||
Shape21({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get groupId =>
|
||||
columnsByName['group_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get messageId =>
|
||||
columnsByName['message_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get senderId =>
|
||||
columnsByName['sender_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get mediaId =>
|
||||
columnsByName['media_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<i2.Uint8List> get additionalMessageData =>
|
||||
columnsByName['additional_message_data']!
|
||||
as i1.GeneratedColumn<i2.Uint8List>;
|
||||
i1.GeneratedColumn<bool> get mediaStored =>
|
||||
columnsByName['media_stored']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get mediaReopened =>
|
||||
columnsByName['media_reopened']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<i2.Uint8List> get downloadToken =>
|
||||
columnsByName['download_token']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||
i1.GeneratedColumn<String> get quotesMessageId =>
|
||||
columnsByName['quotes_message_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isDeletedFromSender =>
|
||||
columnsByName['is_deleted_from_sender']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<DateTime> get openedAt =>
|
||||
columnsByName['opened_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get openedByAll =>
|
||||
columnsByName['opened_by_all']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get modifiedAt =>
|
||||
columnsByName['modified_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get ackByUser =>
|
||||
columnsByName['ack_by_user']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get ackByServer =>
|
||||
columnsByName['ack_by_server']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<i2.Uint8List> _column_105(String aliasedName) =>
|
||||
i1.GeneratedColumn<i2.Uint8List>(
|
||||
'additional_message_data', aliasedName, true,
|
||||
type: i1.DriftSqlType.blob);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -2838,6 +3265,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from5To6(migrator, schema);
|
||||
return 6;
|
||||
case 6:
|
||||
final schema = Schema7(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from6To7(migrator, schema);
|
||||
return 7;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
|
|
@ -2850,6 +3282,7 @@ i1.OnUpgrade stepByStep({
|
|||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
|
|
@ -2858,4 +3291,5 @@ i1.OnUpgrade stepByStep({
|
|||
from3To4: from3To4,
|
||||
from4To5: from4To5,
|
||||
from5To6: from5To6,
|
||||
from6To7: from6To7,
|
||||
));
|
||||
|
|
|
|||
|
|
@ -2518,6 +2518,12 @@ abstract class AppLocalizations {
|
|||
/// **'wants to connect with you.'**
|
||||
String get notificationContactRequest;
|
||||
|
||||
/// No description provided for @notificationContactRequestUnknownUser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'have received a new contact request.'**
|
||||
String get notificationContactRequestUnknownUser;
|
||||
|
||||
/// No description provided for @notificationAcceptRequest.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2572,11 +2578,17 @@ abstract class AppLocalizations {
|
|||
/// **'has responded{inGroup}.'**
|
||||
String notificationResponse(Object inGroup);
|
||||
|
||||
/// No description provided for @notificationTitleUnknownUser.
|
||||
/// No description provided for @notificationTitleUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'[Unknown]'**
|
||||
String get notificationTitleUnknownUser;
|
||||
/// **'You have a new message.'**
|
||||
String get notificationTitleUnknown;
|
||||
|
||||
/// No description provided for @notificationBodyUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open twonly to learn more.'**
|
||||
String get notificationBodyUnknown;
|
||||
|
||||
/// No description provided for @notificationCategoryMessageTitle.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1380,6 +1380,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get notificationContactRequest => 'möchte sich mit dir vernetzen.';
|
||||
|
||||
@override
|
||||
String get notificationContactRequestUnknownUser =>
|
||||
'hast eine neue Kontaktanfrage erhalten.';
|
||||
|
||||
@override
|
||||
String get notificationAcceptRequest => 'ist jetzt mit dir vernetzt.';
|
||||
|
||||
|
|
@ -1418,7 +1422,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get notificationTitleUnknownUser => '[Unbekannt]';
|
||||
String get notificationTitleUnknown => 'Du hast eine neue Nachricht.';
|
||||
|
||||
@override
|
||||
String get notificationBodyUnknown => 'Öffne twonly um mehr zu erfahren.';
|
||||
|
||||
@override
|
||||
String get notificationCategoryMessageTitle => 'Nachrichten';
|
||||
|
|
|
|||
|
|
@ -1372,6 +1372,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get notificationContactRequest => 'wants to connect with you.';
|
||||
|
||||
@override
|
||||
String get notificationContactRequestUnknownUser =>
|
||||
'have received a new contact request.';
|
||||
|
||||
@override
|
||||
String get notificationAcceptRequest => 'is now connected with you.';
|
||||
|
||||
|
|
@ -1410,7 +1414,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get notificationTitleUnknownUser => '[Unknown]';
|
||||
String get notificationTitleUnknown => 'You have a new message.';
|
||||
|
||||
@override
|
||||
String get notificationBodyUnknown => 'Open twonly to learn more.';
|
||||
|
||||
@override
|
||||
String get notificationCategoryMessageTitle => 'Messages';
|
||||
|
|
|
|||
|
|
@ -1372,6 +1372,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get notificationContactRequest => 'wants to connect with you.';
|
||||
|
||||
@override
|
||||
String get notificationContactRequestUnknownUser =>
|
||||
'have received a new contact request.';
|
||||
|
||||
@override
|
||||
String get notificationAcceptRequest => 'is now connected with you.';
|
||||
|
||||
|
|
@ -1410,7 +1414,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get notificationTitleUnknownUser => '[Unknown]';
|
||||
String get notificationTitleUnknown => 'You have a new message.';
|
||||
|
||||
@override
|
||||
String get notificationBodyUnknown => 'Open twonly to learn more.';
|
||||
|
||||
@override
|
||||
String get notificationCategoryMessageTitle => 'Messages';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 20f3c2f0a49e4c9be452ecbc84d98054c92974e1
|
||||
Subproject commit 9d04e9e1d0cdba8f1be4b0cbba341706c3cffac9
|
||||
|
|
@ -108,6 +108,19 @@ class UserData {
|
|||
DateTime? nextTimeToShowBackupNotice;
|
||||
BackupServer? backupServer;
|
||||
TwonlySafeBackup? twonlySafeBackup;
|
||||
|
||||
// For my master thesis I want to create a anonymous user study:
|
||||
// - users in the "Tester" Plan can, if they want, take part of the user study
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool askedForUserStudyPermission = false;
|
||||
|
||||
// So update data can be assigned. If set the user choose to participate.
|
||||
String? userStudyParticipantsToken;
|
||||
|
||||
// Once a day the anonymous data is collected and send to the server
|
||||
DateTime? lastUserStudyDataUpload;
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,14 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..twonlySafeBackup = json['twonlySafeBackup'] == null
|
||||
? null
|
||||
: TwonlySafeBackup.fromJson(
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>);
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>)
|
||||
..askedForUserStudyPermission =
|
||||
json['askedForUserStudyPermission'] as bool? ?? false
|
||||
..userStudyParticipantsToken =
|
||||
json['userStudyParticipantsToken'] as String?
|
||||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastUserStudyDataUpload'] as String);
|
||||
|
||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
|
|
@ -122,6 +129,10 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
instance.nextTimeToShowBackupNotice?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
'twonlySafeBackup': instance.twonlySafeBackup,
|
||||
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
|
||||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
||||
'lastUserStudyDataUpload':
|
||||
instance.lastUserStudyDataUpload?.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
|
|
|||
11
lib/src/model/protobuf/client/data.proto
Normal file
11
lib/src/model/protobuf/client/data.proto
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
syntax = "proto3";
|
||||
|
||||
|
||||
message AdditionalMessageData {
|
||||
enum Type {
|
||||
LINK = 0;
|
||||
}
|
||||
Type type = 1;
|
||||
|
||||
optional string link = 2;
|
||||
}
|
||||
99
lib/src/model/protobuf/client/generated/data.pb.dart
Normal file
99
lib/src/model/protobuf/client/generated/data.pb.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from data.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
import 'data.pbenum.dart';
|
||||
|
||||
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
||||
|
||||
export 'data.pbenum.dart';
|
||||
|
||||
class AdditionalMessageData extends $pb.GeneratedMessage {
|
||||
factory AdditionalMessageData({
|
||||
AdditionalMessageData_Type? type,
|
||||
$core.String? link,
|
||||
}) {
|
||||
final result = create();
|
||||
if (type != null) result.type = type;
|
||||
if (link != null) result.link = link;
|
||||
return result;
|
||||
}
|
||||
|
||||
AdditionalMessageData._();
|
||||
|
||||
factory AdditionalMessageData.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory AdditionalMessageData.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'AdditionalMessageData',
|
||||
createEmptyInstance: create)
|
||||
..e<AdditionalMessageData_Type>(
|
||||
1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE,
|
||||
defaultOrMaker: AdditionalMessageData_Type.LINK,
|
||||
valueOf: AdditionalMessageData_Type.valueOf,
|
||||
enumValues: AdditionalMessageData_Type.values)
|
||||
..aOS(2, _omitFieldNames ? '' : 'link')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
AdditionalMessageData clone() =>
|
||||
AdditionalMessageData()..mergeFromMessage(this);
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
AdditionalMessageData copyWith(
|
||||
void Function(AdditionalMessageData) updates) =>
|
||||
super.copyWith((message) => updates(message as AdditionalMessageData))
|
||||
as AdditionalMessageData;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static AdditionalMessageData create() => AdditionalMessageData._();
|
||||
@$core.override
|
||||
AdditionalMessageData createEmptyInstance() => create();
|
||||
static $pb.PbList<AdditionalMessageData> createRepeated() =>
|
||||
$pb.PbList<AdditionalMessageData>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static AdditionalMessageData getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<AdditionalMessageData>(create);
|
||||
static AdditionalMessageData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
AdditionalMessageData_Type get type => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set type(AdditionalMessageData_Type value) => $_setField(1, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasType() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearType() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get link => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set link($core.String value) => $_setString(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasLink() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearLink() => $_clearField(2);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const $core.bool _omitMessageNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||
35
lib/src/model/protobuf/client/generated/data.pbenum.dart
Normal file
35
lib/src/model/protobuf/client/generated/data.pbenum.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from data.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class AdditionalMessageData_Type extends $pb.ProtobufEnum {
|
||||
static const AdditionalMessageData_Type LINK =
|
||||
AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK');
|
||||
|
||||
static const $core.List<AdditionalMessageData_Type> values =
|
||||
<AdditionalMessageData_Type>[
|
||||
LINK,
|
||||
];
|
||||
|
||||
static final $core.List<AdditionalMessageData_Type?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 0);
|
||||
static AdditionalMessageData_Type? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const AdditionalMessageData_Type._(super.value, super.name);
|
||||
}
|
||||
|
||||
const $core.bool _omitEnumNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
49
lib/src/model/protobuf/client/generated/data.pbjson.dart
Normal file
49
lib/src/model/protobuf/client/generated/data.pbjson.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from data.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, unused_import
|
||||
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use additionalMessageDataDescriptor instead')
|
||||
const AdditionalMessageData$json = {
|
||||
'1': 'AdditionalMessageData',
|
||||
'2': [
|
||||
{
|
||||
'1': 'type',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.AdditionalMessageData.Type',
|
||||
'10': 'type'
|
||||
},
|
||||
{'1': 'link', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'link', '17': true},
|
||||
],
|
||||
'4': [AdditionalMessageData_Type$json],
|
||||
'8': [
|
||||
{'1': '_link'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use additionalMessageDataDescriptor instead')
|
||||
const AdditionalMessageData_Type$json = {
|
||||
'1': 'Type',
|
||||
'2': [
|
||||
{'1': 'LINK', '2': 0},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `AdditionalMessageData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode(
|
||||
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
|
||||
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBASIQCgRUeXBlEggKBExJ'
|
||||
'TksQAEIHCgVfbGluaw==');
|
||||
|
|
@ -969,6 +969,7 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? encryptionKey,
|
||||
$core.List<$core.int>? encryptionMac,
|
||||
$core.List<$core.int>? encryptionNonce,
|
||||
$core.List<$core.int>? additionalMessageData,
|
||||
}) {
|
||||
final result = create();
|
||||
if (senderMessageId != null) result.senderMessageId = senderMessageId;
|
||||
|
|
@ -983,6 +984,8 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
|||
if (encryptionKey != null) result.encryptionKey = encryptionKey;
|
||||
if (encryptionMac != null) result.encryptionMac = encryptionMac;
|
||||
if (encryptionNonce != null) result.encryptionNonce = encryptionNonce;
|
||||
if (additionalMessageData != null)
|
||||
result.additionalMessageData = additionalMessageData;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1024,6 +1027,8 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
|||
..a<$core.List<$core.int>>(
|
||||
10, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY,
|
||||
protoName: 'encryptionNonce')
|
||||
..a<$core.List<$core.int>>(
|
||||
11, _omitFieldNames ? '' : 'additionalMessageData', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -1138,6 +1143,16 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
|||
$core.bool hasEncryptionNonce() => $_has(9);
|
||||
@$pb.TagNumber(10)
|
||||
void clearEncryptionNonce() => $_clearField(10);
|
||||
|
||||
@$pb.TagNumber(11)
|
||||
$core.List<$core.int> get additionalMessageData => $_getN(10);
|
||||
@$pb.TagNumber(11)
|
||||
set additionalMessageData($core.List<$core.int> value) =>
|
||||
$_setBytes(10, value);
|
||||
@$pb.TagNumber(11)
|
||||
$core.bool hasAdditionalMessageData() => $_has(10);
|
||||
@$pb.TagNumber(11)
|
||||
void clearAdditionalMessageData() => $_clearField(11);
|
||||
}
|
||||
|
||||
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
||||
|
|
|
|||
|
|
@ -603,6 +603,15 @@ const EncryptedContent_Media$json = {
|
|||
'10': 'encryptionNonce',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'additional_message_data',
|
||||
'3': 11,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 6,
|
||||
'10': 'additionalMessageData',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'4': [EncryptedContent_Media_Type$json],
|
||||
'8': [
|
||||
|
|
@ -612,6 +621,7 @@ const EncryptedContent_Media$json = {
|
|||
{'1': '_encryptionKey'},
|
||||
{'1': '_encryptionMac'},
|
||||
{'1': '_encryptionNonce'},
|
||||
{'1': '_additional_message_data'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -840,7 +850,7 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'EjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYW'
|
||||
'dlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz'
|
||||
'dGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhIKEF'
|
||||
'9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQalwUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB'
|
||||
'9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB'
|
||||
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANIAFIa'
|
||||
'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdGlvbh'
|
||||
|
|
@ -849,29 +859,31 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktleRgI'
|
||||
'IAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW5jcn'
|
||||
'lwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNl'
|
||||
'iAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEA'
|
||||
'MSCQoFQVVESU8QBEIdChtfZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVz'
|
||||
'c2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW'
|
||||
'9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4y'
|
||||
'Ii5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2'
|
||||
'FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNU'
|
||||
'T1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGA'
|
||||
'EgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5'
|
||||
'cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYX'
|
||||
'RlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIE'
|
||||
'dHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZW'
|
||||
'SIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNlcm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCAB'
|
||||
'KAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFg'
|
||||
'oUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3VzZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoI'
|
||||
'UHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZV'
|
||||
'IEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEB'
|
||||
'EiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEA'
|
||||
'ASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVT'
|
||||
'eW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW'
|
||||
'50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgD'
|
||||
'IAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2'
|
||||
'dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21l'
|
||||
'c3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZU'
|
||||
'IRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0'
|
||||
'aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdXBDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3'
|
||||
'JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVibGljS2V5QhEKD19lcnJvcl9tZXNzYWdlcw==');
|
||||
'iAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZXNzYW'
|
||||
'dlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxACEgcK'
|
||||
'A0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD19xdW'
|
||||
'90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5fZW5j'
|
||||
'cnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZG'
|
||||
'F0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVk'
|
||||
'aWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3'
|
||||
'NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9F'
|
||||
'UlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW'
|
||||
'50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVK'
|
||||
'RUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3'
|
||||
'J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXBy'
|
||||
'ZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCU'
|
||||
'gBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIf'
|
||||
'CgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZE'
|
||||
'ILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgO'
|
||||
'Mh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSA'
|
||||
'BSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgDSAJS'
|
||||
'CWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2V5SW'
|
||||
'RCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVyGAEg'
|
||||
'ASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbGFzdE'
|
||||
'ZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAKC2Zv'
|
||||
'cmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaG'
|
||||
'F0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFC'
|
||||
'DgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCg'
|
||||
'pfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4K'
|
||||
'DF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3'
|
||||
'JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2Vz');
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ message EncryptedContent {
|
|||
optional bytes encryptionKey = 8;
|
||||
optional bytes encryptionMac = 9;
|
||||
optional bytes encryptionNonce = 10;
|
||||
|
||||
optional bytes additional_message_data = 11;
|
||||
}
|
||||
|
||||
message MediaUpdate {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ 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';
|
||||
|
|
@ -41,6 +42,7 @@ 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();
|
||||
|
|
@ -100,6 +102,11 @@ class ApiService {
|
|||
unawaited(fetchGroupStatesForUnjoinedGroups());
|
||||
unawaited(fetchMissingGroupPublicKey());
|
||||
unawaited(checkForDeletedUsernames());
|
||||
|
||||
if (gUser.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +320,12 @@ class ApiService {
|
|||
return user;
|
||||
});
|
||||
globalCallbackUpdatePlan(planFromString(authenticated.plan));
|
||||
|
||||
// this was triggered by apiService.ipaPurchase, so call the onAuthenticated again
|
||||
if (isAuthenticated) {
|
||||
// Trigger the re-upload from images, after Plan change, in case the limit was reached before...
|
||||
unawaited(finishStartedPreprocessing());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res.isError) {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ Future<void> handleMedia(
|
|||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaFile.mediaId),
|
||||
type: const Value(MessageType.media),
|
||||
additionalMessageData: Value.absentIfNull(
|
||||
media.hasAdditionalMessageData()
|
||||
? Uint8List.fromList(media.additionalMessageData)
|
||||
: null,
|
||||
),
|
||||
quotesMessageId: Value(
|
||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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';
|
||||
|
|
@ -88,8 +89,9 @@ Future<MediaFileService?> initializeMediaUpload(
|
|||
|
||||
Future<void> insertMediaFileInMessagesTable(
|
||||
MediaFileService mediaService,
|
||||
List<String> groupIds,
|
||||
) async {
|
||||
List<String> groupIds, {
|
||||
AdditionalMessageData? additionalData,
|
||||
}) async {
|
||||
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
|
||||
const MediaFilesCompanion(
|
||||
isDraftMedia: Value(false),
|
||||
|
|
@ -101,6 +103,8 @@ Future<void> insertMediaFileInMessagesTable(
|
|||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaService.mediaFile.mediaId),
|
||||
type: const Value(MessageType.media),
|
||||
additionalMessageData:
|
||||
Value.absentIfNull(additionalData?.writeToBuffer()),
|
||||
),
|
||||
);
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||
|
|
@ -245,6 +249,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
encryptionKey: media.mediaFile.encryptionKey,
|
||||
encryptionNonce: media.mediaFile.encryptionNonce,
|
||||
encryptionMac: media.mediaFile.encryptionMac,
|
||||
additionalMessageData: message.additionalMessageData,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
String? receiptId,
|
||||
Receipt? receipt,
|
||||
bool onlyReturnEncryptedData = false,
|
||||
bool blocking = true,
|
||||
}) async {
|
||||
try {
|
||||
if (receiptId == null && receipt == null) return null;
|
||||
|
|
@ -238,12 +239,11 @@ Future<void> sendCipherTextToGroup(
|
|||
encryptedContent.groupId = groupId;
|
||||
|
||||
for (final groupMember in groupMembers) {
|
||||
unawaited(
|
||||
sendCipherText(
|
||||
groupMember.contactId,
|
||||
encryptedContent,
|
||||
messageId: messageId,
|
||||
),
|
||||
await sendCipherText(
|
||||
groupMember.contactId,
|
||||
encryptedContent,
|
||||
messageId: messageId,
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -252,6 +252,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
int contactId,
|
||||
pb.EncryptedContent encryptedContent, {
|
||||
bool onlyReturnEncryptedData = false,
|
||||
bool blocking = true,
|
||||
String? messageId,
|
||||
}) async {
|
||||
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter);
|
||||
|
|
@ -270,10 +271,15 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
);
|
||||
|
||||
if (receipt != null) {
|
||||
return tryToSendCompleteMessage(
|
||||
final tmp = tryToSendCompleteMessage(
|
||||
receipt: receipt,
|
||||
onlyReturnEncryptedData: onlyReturnEncryptedData,
|
||||
blocking: blocking,
|
||||
);
|
||||
if (!blocking) {
|
||||
return null;
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -302,6 +308,7 @@ Future<void> notifyContactAboutOpeningMessage(
|
|||
timestamp: Int64(actionAt.millisecondsSinceEpoch),
|
||||
),
|
||||
),
|
||||
blocking: false,
|
||||
);
|
||||
for (final messageId in messageOtherIds) {
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.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/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/views/contact/contact.view.dart';
|
||||
|
|
@ -151,6 +151,7 @@ Future<void> handleIntentMediaFile(
|
|||
Future<void> handleIntentSharedFile(
|
||||
BuildContext context,
|
||||
List<SharedFile> files,
|
||||
void Function(Uri) onUrlCallBack,
|
||||
) async {
|
||||
for (final file in files) {
|
||||
if (file.value == null) {
|
||||
|
|
@ -163,7 +164,9 @@ Future<void> handleIntentSharedFile(
|
|||
|
||||
switch (file.type) {
|
||||
case SharedMediaType.URL:
|
||||
// await handleIntentUrl(context, Uri.parse(file.value!));
|
||||
if (file.value?.startsWith('http') ?? false) {
|
||||
onUrlCallBack(Uri.parse(file.value!));
|
||||
}
|
||||
case SharedMediaType.IMAGE:
|
||||
var type = MediaType.image;
|
||||
if (file.value!.endsWith('.gif')) {
|
||||
|
|
|
|||
|
|
@ -24,66 +24,71 @@ class MediaFileService {
|
|||
}
|
||||
|
||||
static Future<void> purgeTempFolder() async {
|
||||
final tempDirectory = MediaFileService.buildDirectoryPath(
|
||||
'tmp',
|
||||
globalApplicationSupportDirectory,
|
||||
);
|
||||
try {
|
||||
final tempDirectory = MediaFileService.buildDirectoryPath(
|
||||
'tmp',
|
||||
globalApplicationSupportDirectory,
|
||||
);
|
||||
|
||||
final files = tempDirectory.listSync();
|
||||
for (final file in files) {
|
||||
final mediaId = basename(file.path).split('.').first;
|
||||
final files = tempDirectory.listSync();
|
||||
for (final file in files) {
|
||||
final mediaId = basename(file.path).split('.').first;
|
||||
|
||||
// in case the mediaID is unknown the file will be deleted
|
||||
var delete = true;
|
||||
// in case the mediaID is unknown the file will be deleted
|
||||
var delete = true;
|
||||
|
||||
final service = await MediaFileService.fromMediaId(mediaId);
|
||||
final service = await MediaFileService.fromMediaId(mediaId);
|
||||
|
||||
if (service != null) {
|
||||
if (service.mediaFile.isDraftMedia) {
|
||||
delete = false;
|
||||
}
|
||||
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
||||
|
||||
// in case messages in empty the file will be deleted, as delete is true by default
|
||||
|
||||
for (final message in messages) {
|
||||
if (service.mediaFile.type == MediaType.audio) {
|
||||
delete = false; // do not delete voice messages
|
||||
}
|
||||
|
||||
if (message.openedAt == null) {
|
||||
// Message was not yet opened from all persons, so wait...
|
||||
if (service != null) {
|
||||
if (service.mediaFile.isDraftMedia) {
|
||||
delete = false;
|
||||
} else if (service.mediaFile.requiresAuthentication ||
|
||||
service.mediaFile.displayLimitInMilliseconds != null) {
|
||||
// Message was opened by all persons, and they can not reopen the image.
|
||||
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days
|
||||
// delete = true; // do not overwrite a previous delete = false
|
||||
// this is just to make it easier to understand :)
|
||||
} else if (message.openedAt!
|
||||
.isAfter(clock.now().subtract(const Duration(days: 2)))) {
|
||||
// In case the image was opened, but send with unlimited time or no authentication.
|
||||
if (message.senderId == null) {
|
||||
delete = false;
|
||||
} else {
|
||||
// Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image.
|
||||
// This also allows to reopen this image for two days.
|
||||
final group = await twonlyDB.groupsDao.getGroup(message.groupId);
|
||||
if (group != null && !group.isDirectChat) {
|
||||
delete = false;
|
||||
}
|
||||
}
|
||||
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
||||
|
||||
// in case messages in empty the file will be deleted, as delete is true by default
|
||||
|
||||
for (final message in messages) {
|
||||
if (service.mediaFile.type == MediaType.audio) {
|
||||
delete = false; // do not delete voice messages
|
||||
}
|
||||
|
||||
if (message.openedAt == null) {
|
||||
// Message was not yet opened from all persons, so wait...
|
||||
delete = false;
|
||||
} else if (service.mediaFile.requiresAuthentication ||
|
||||
service.mediaFile.displayLimitInMilliseconds != null) {
|
||||
// Message was opened by all persons, and they can not reopen the image.
|
||||
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days
|
||||
// delete = true; // do not overwrite a previous delete = false
|
||||
// this is just to make it easier to understand :)
|
||||
} else if (message.openedAt!
|
||||
.isAfter(clock.now().subtract(const Duration(days: 2)))) {
|
||||
// In case the image was opened, but send with unlimited time or no authentication.
|
||||
if (message.senderId == null) {
|
||||
delete = false;
|
||||
} else {
|
||||
// Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image.
|
||||
// This also allows to reopen this image for two days.
|
||||
final group =
|
||||
await twonlyDB.groupsDao.getGroup(message.groupId);
|
||||
if (group != null && !group.isDirectChat) {
|
||||
delete = false;
|
||||
}
|
||||
}
|
||||
// In case the app was send in a direct chat, then it can be deleted.
|
||||
}
|
||||
// In case the app was send in a direct chat, then it can be deleted.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delete) {
|
||||
Log.info('Purging media file $mediaId');
|
||||
file.deleteSync();
|
||||
if (delete) {
|
||||
Log.info('Purging media file $mediaId');
|
||||
file.deleteSync();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,9 +99,10 @@ Future<void> handlePushData(String pushDataB64) async {
|
|||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
final lang = getLocalizations();
|
||||
await customLocalPushNotification(
|
||||
'Du hast eine neue Nachricht.',
|
||||
'Öffne twonly um mehr zu erfahren.',
|
||||
lang.notificationTitleUnknown,
|
||||
lang.notificationBodyUnknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -186,16 +187,14 @@ Future<void> showLocalPushNotification(
|
|||
Future<void> showLocalPushNotificationWithoutUserId(
|
||||
PushNotification pushNotification,
|
||||
) async {
|
||||
String? body;
|
||||
|
||||
body = getPushNotificationText(pushNotification);
|
||||
|
||||
final lang = getLocalizations();
|
||||
|
||||
final title = lang.notificationTitleUnknownUser;
|
||||
var title = lang.notificationTitleUnknown;
|
||||
var body = lang.notificationBodyUnknown;
|
||||
|
||||
if (body == '') {
|
||||
Log.error('No push notification type defined!');
|
||||
if (pushNotification.kind == PushKind.contactRequest) {
|
||||
title = lang.you;
|
||||
body = lang.notificationContactRequestUnknownUser;
|
||||
}
|
||||
|
||||
final androidNotificationDetails = AndroidNotificationDetails(
|
||||
|
|
|
|||
|
|
@ -4,38 +4,41 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class KeyValueStore {
|
||||
static Future<String> _getFilePath(String key) async {
|
||||
static Future<File> _getFilePath(String key) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
return '${directory.path}/keyvalue/$key.json';
|
||||
return File('${directory.path}/keyvalue/$key.json');
|
||||
}
|
||||
|
||||
static Future<void> delete(String key) async {
|
||||
try {
|
||||
final file = await _getFilePath(key);
|
||||
if (file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error deleting file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> get(String key) async {
|
||||
try {
|
||||
final filePath = await _getFilePath(key);
|
||||
final file = File(filePath);
|
||||
|
||||
// Check if the file exists
|
||||
final file = await _getFilePath(key);
|
||||
if (file.existsSync()) {
|
||||
final contents = await file.readAsString();
|
||||
return jsonDecode(contents) as Map<String, dynamic>;
|
||||
} else {
|
||||
return null; // File does not exist
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error reading file: $e');
|
||||
Log.warn('Error reading file: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> put(String key, Map<String, dynamic> value) async {
|
||||
try {
|
||||
final filePath = await _getFilePath(key);
|
||||
final file = File(filePath);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
final file = await _getFilePath(key);
|
||||
await file.parent.create(recursive: true);
|
||||
|
||||
// Write the JSON data to the file
|
||||
await file.writeAsString(jsonEncode(value));
|
||||
} catch (e) {
|
||||
Log.error('Error writing file: $e');
|
||||
|
|
|
|||
|
|
@ -23,26 +23,78 @@ class MainCameraPreview extends StatelessWidget {
|
|||
requiredHeight: 0,
|
||||
additionalPadding: 59,
|
||||
bottomNavigation: Container(),
|
||||
child: Screenshot(
|
||||
controller: mainCameraController.screenshotController,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 9 / 16,
|
||||
child: ClipRect(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: mainCameraController
|
||||
.cameraController!.value.previewSize!.height,
|
||||
height: mainCameraController
|
||||
.cameraController!.value.previewSize!.width,
|
||||
child: CameraPreview(
|
||||
mainCameraController.cameraController!,
|
||||
child: mainCameraController.customPaint,
|
||||
child: Stack(
|
||||
children: [
|
||||
Screenshot(
|
||||
controller: mainCameraController.screenshotController,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 9 / 16,
|
||||
child: ClipRect(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: mainCameraController
|
||||
.cameraController!.value.previewSize!.height,
|
||||
height: mainCameraController
|
||||
.cameraController!.value.previewSize!.width,
|
||||
child: CameraPreview(
|
||||
key: mainCameraController.cameraPreviewKey,
|
||||
mainCameraController.cameraController!,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (mainCameraController.customPaint != null)
|
||||
Positioned.fill(
|
||||
child: mainCameraController.customPaint!,
|
||||
),
|
||||
if (mainCameraController.facePaint != null)
|
||||
Positioned.fill(
|
||||
child: mainCameraController.facePaint!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (mainCameraController.focusPointOffset != null &&
|
||||
!mainCameraController.isSharePreviewIsShown)
|
||||
AspectRatio(
|
||||
aspectRatio: 9 / 16,
|
||||
child: ClipRect(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: mainCameraController
|
||||
.cameraController!.value.previewSize!.height,
|
||||
height: mainCameraController
|
||||
.cameraController!.value.previewSize!.width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: mainCameraController.focusPointOffset!.dy - 40,
|
||||
left:
|
||||
mainCameraController.focusPointOffset!.dx - 40,
|
||||
child: Container(
|
||||
height: 80,
|
||||
width: 80,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white.withAlpha(150),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
|
@ -21,13 +22,14 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/qr.dart';
|
||||
import 'package:twonly/src/utils/screenshot.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||
|
|
@ -36,55 +38,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||
|
||||
int maxVideoRecordingTime = 60;
|
||||
|
||||
Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
|
||||
SelectedCameraDetails details,
|
||||
int sCameraId,
|
||||
bool init,
|
||||
) async {
|
||||
var cameraId = sCameraId;
|
||||
if (cameraId >= gCameras.length) return null;
|
||||
if (init) {
|
||||
for (; cameraId < gCameras.length; cameraId++) {
|
||||
if (gCameras[cameraId].lensDirection == CameraLensDirection.back) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
details.isZoomAble = false;
|
||||
if (details.cameraId != cameraId) {
|
||||
// switch between front and back
|
||||
details.scaleFactor = 1;
|
||||
}
|
||||
|
||||
final cameraController = CameraController(
|
||||
gCameras[cameraId],
|
||||
ResolutionPreset.high,
|
||||
enableAudio: await Permission.microphone.isGranted,
|
||||
imageFormatGroup:
|
||||
Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888,
|
||||
);
|
||||
|
||||
await cameraController.initialize().then((_) async {
|
||||
await cameraController.setZoomLevel(details.scaleFactor);
|
||||
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
||||
await cameraController
|
||||
.setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off);
|
||||
await cameraController
|
||||
.getMaxZoomLevel()
|
||||
.then((double value) => details.maxAvailableZoom = value);
|
||||
await cameraController
|
||||
.getMinZoomLevel()
|
||||
.then((double value) => details.minAvailableZoom = value);
|
||||
details
|
||||
..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom
|
||||
..cameraLoaded = true
|
||||
..cameraId = cameraId;
|
||||
}).catchError((Object e) {
|
||||
Log.error('$e');
|
||||
});
|
||||
return (details, cameraController);
|
||||
}
|
||||
|
||||
class SelectedCameraDetails {
|
||||
double maxAvailableZoom = 1;
|
||||
double minAvailableZoom = 1;
|
||||
|
|
@ -156,12 +109,10 @@ class CameraPreviewView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||
bool _sharePreviewIsShown = false;
|
||||
bool _galleryLoadedImageIsShown = false;
|
||||
bool _showSelfieFlash = false;
|
||||
double _basePanY = 0;
|
||||
double _baseScaleFactor = 0;
|
||||
bool _isVideoRecording = false;
|
||||
bool _hasAudioPermission = true;
|
||||
DateTime? _videoRecordingStarted;
|
||||
Timer? _videoRecordingTimer;
|
||||
|
|
@ -317,10 +268,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
|
||||
Future<void> takePicture() async {
|
||||
if (_sharePreviewIsShown || _isVideoRecording) return;
|
||||
if (mc.isSharePreviewIsShown || mc.isVideoRecording) return;
|
||||
|
||||
setState(() {
|
||||
_sharePreviewIsShown = true;
|
||||
mc.isSharePreviewIsShown = true;
|
||||
});
|
||||
if (mc.selectedCameraDetails.isFlashOn) {
|
||||
if (isFront) {
|
||||
|
|
@ -353,12 +304,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
return;
|
||||
}
|
||||
setState(() {
|
||||
_sharePreviewIsShown = false;
|
||||
mc.isSharePreviewIsShown = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> pushMediaEditor(
|
||||
ScreenshotImage? imageBytes,
|
||||
ScreenshotImage? screenshotImage,
|
||||
File? videoFilePath, {
|
||||
bool sharedFromGallery = false,
|
||||
MediaType? mediaType,
|
||||
|
|
@ -394,11 +345,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
pageBuilder: (context, a1, a2) => ShareImageEditorView(
|
||||
imageBytesFuture: imageBytes,
|
||||
screenshotImage: screenshotImage,
|
||||
sharedFromGallery: sharedFromGallery,
|
||||
sendToGroup: widget.sendToGroup,
|
||||
mediaFileService: mediaFileService,
|
||||
mainCameraController: mc,
|
||||
previewLink: mc.sharedLinkForPreview,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return child;
|
||||
|
|
@ -409,7 +361,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
) as bool?;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sharePreviewIsShown = false;
|
||||
mc.isSharePreviewIsShown = false;
|
||||
_showSelfieFlash = false;
|
||||
});
|
||||
}
|
||||
|
|
@ -459,7 +411,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Future<void> pickImageFromGallery() async {
|
||||
setState(() {
|
||||
_galleryLoadedImageIsShown = true;
|
||||
_sharePreviewIsShown = true;
|
||||
mc.isSharePreviewIsShown = true;
|
||||
});
|
||||
final picker = ImagePicker();
|
||||
final pickedFile = await picker.pickMedia();
|
||||
|
|
@ -502,17 +454,47 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
setState(() {
|
||||
_galleryLoadedImageIsShown = false;
|
||||
_sharePreviewIsShown = false;
|
||||
mc.isSharePreviewIsShown = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> pressSideButtonLeft() async {
|
||||
if (!mc.isSelectingFaceFilters) {
|
||||
return pickImageFromGallery();
|
||||
}
|
||||
if (mc.currentFilterType.index == 1) {
|
||||
mc.setFilter(FaceFilterType.none);
|
||||
setState(() {
|
||||
mc.isSelectingFaceFilters = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
mc.setFilter(mc.currentFilterType.goLeft());
|
||||
}
|
||||
|
||||
Future<void> pressSideButtonRight() async {
|
||||
if (!mc.isSelectingFaceFilters) {
|
||||
setState(() {
|
||||
mc.isSelectingFaceFilters = true;
|
||||
});
|
||||
}
|
||||
if (mc.currentFilterType.index == FaceFilterType.values.length - 1) {
|
||||
mc.setFilter(FaceFilterType.none);
|
||||
setState(() {
|
||||
mc.isSelectingFaceFilters = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
mc.setFilter(mc.currentFilterType.goRight());
|
||||
}
|
||||
|
||||
Future<void> startVideoRecording() async {
|
||||
if (mc.cameraController != null &&
|
||||
mc.cameraController!.value.isRecordingVideo) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isVideoRecording = true;
|
||||
mc.isVideoRecording = true;
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -532,11 +514,11 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
});
|
||||
setState(() {
|
||||
_videoRecordingStarted = clock.now();
|
||||
_isVideoRecording = true;
|
||||
mc.isVideoRecording = true;
|
||||
});
|
||||
} on CameraException catch (e) {
|
||||
setState(() {
|
||||
_isVideoRecording = false;
|
||||
mc.isVideoRecording = false;
|
||||
});
|
||||
_showCameraException(e);
|
||||
return;
|
||||
|
|
@ -551,7 +533,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
|
||||
setState(() {
|
||||
_videoRecordingStarted = null;
|
||||
_isVideoRecording = false;
|
||||
mc.isVideoRecording = false;
|
||||
});
|
||||
|
||||
if (mc.cameraController == null ||
|
||||
|
|
@ -560,7 +542,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
|
||||
setState(() {
|
||||
_sharePreviewIsShown = true;
|
||||
mc.isSharePreviewIsShown = true;
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -646,12 +628,23 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!_sharePreviewIsShown &&
|
||||
if (!mc.isSharePreviewIsShown &&
|
||||
widget.sendToGroup != null &&
|
||||
!_isVideoRecording)
|
||||
SendToWidget(sendTo: widget.sendToGroup!.groupName),
|
||||
if (!_sharePreviewIsShown &&
|
||||
!_isVideoRecording &&
|
||||
!mc.isVideoRecording)
|
||||
ShowTitleText(
|
||||
title: widget.sendToGroup!.groupName,
|
||||
desc: context.lang.cameraPreviewSendTo,
|
||||
),
|
||||
if (!mc.isSharePreviewIsShown &&
|
||||
mc.sharedLinkForPreview != null &&
|
||||
!mc.isVideoRecording)
|
||||
ShowTitleText(
|
||||
title: mc.sharedLinkForPreview?.host ?? '',
|
||||
desc: 'Link',
|
||||
isLink: true,
|
||||
),
|
||||
if (!mc.isSharePreviewIsShown &&
|
||||
!mc.isVideoRecording &&
|
||||
!widget.hideControllers)
|
||||
Positioned(
|
||||
right: 5,
|
||||
|
|
@ -707,7 +700,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!_sharePreviewIsShown && !widget.hideControllers)
|
||||
if (!mc.isSharePreviewIsShown && !widget.hideControllers)
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
|
|
@ -718,7 +711,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
children: [
|
||||
if (mc.cameraController!.value.isInitialized &&
|
||||
mc.selectedCameraDetails.isZoomAble &&
|
||||
!_isVideoRecording)
|
||||
!mc.isVideoRecording)
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: CameraZoomButtons(
|
||||
|
|
@ -734,17 +727,21 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!_isVideoRecording)
|
||||
if (!mc.isVideoRecording)
|
||||
GestureDetector(
|
||||
onTap: pickImageFromGallery,
|
||||
onTap: pressSideButtonLeft,
|
||||
child: Align(
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 80,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.photoFilm,
|
||||
mc.isSelectingFaceFilters
|
||||
? mc.currentFilterType.index == 1
|
||||
? FontAwesomeIcons.xmark
|
||||
: FontAwesomeIcons.arrowLeft
|
||||
: FontAwesomeIcons.photoFilm,
|
||||
color: Colors.white,
|
||||
size: 25,
|
||||
),
|
||||
|
|
@ -766,15 +763,44 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
width: 7,
|
||||
color: _isVideoRecording
|
||||
color: mc.isVideoRecording
|
||||
? Colors.red
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
child: mc.currentFilterType.preview,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_isVideoRecording) const SizedBox(width: 80),
|
||||
if (!mc.isVideoRecording)
|
||||
if (isFront)
|
||||
GestureDetector(
|
||||
onTap: pressSideButtonRight,
|
||||
child: Align(
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 80,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Center(
|
||||
child: FaIcon(
|
||||
mc.isSelectingFaceFilters
|
||||
? mc.currentFilterType.index ==
|
||||
FaceFilterType
|
||||
.values.length -
|
||||
1
|
||||
? FontAwesomeIcons.xmark
|
||||
: FontAwesomeIcons.arrowRight
|
||||
: FontAwesomeIcons
|
||||
.faceGrinTongueSquint,
|
||||
color: Colors.white,
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 80),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -785,7 +811,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
videoRecordingStarted: _videoRecordingStarted,
|
||||
maxVideoRecordingTime: maxVideoRecordingTime,
|
||||
),
|
||||
if (!_sharePreviewIsShown && widget.sendToGroup != null ||
|
||||
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null ||
|
||||
widget.hideControllers)
|
||||
Positioned(
|
||||
left: 5,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart';
|
||||
|
||||
enum FaceFilterType {
|
||||
none,
|
||||
dogBrown,
|
||||
beardUpperLip,
|
||||
}
|
||||
|
||||
extension FaceFilterTypeExtension on FaceFilterType {
|
||||
FaceFilterType goRight() {
|
||||
final nextIndex = (index + 1) % FaceFilterType.values.length;
|
||||
return FaceFilterType.values[nextIndex];
|
||||
}
|
||||
|
||||
FaceFilterType goLeft() {
|
||||
final prevIndex = (index - 1 + FaceFilterType.values.length) %
|
||||
FaceFilterType.values.length;
|
||||
return FaceFilterType.values[prevIndex];
|
||||
}
|
||||
|
||||
Widget get preview {
|
||||
switch (this) {
|
||||
case FaceFilterType.none:
|
||||
return Container();
|
||||
case FaceFilterType.dogBrown:
|
||||
return DogFilterPainter.getPreview();
|
||||
case FaceFilterType.beardUpperLip:
|
||||
return BeardFilterPainter.getPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -5,6 +6,8 @@ import 'package:drift/drift.dart' show Value;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
|
||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -15,7 +18,11 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/qr.dart';
|
||||
import 'package:twonly/src/utils/screenshot.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
||||
import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/barcode_detector_painter.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
|
||||
|
||||
class ScannedVerifiedContact {
|
||||
ScannedVerifiedContact({
|
||||
|
|
@ -45,6 +52,34 @@ class MainCameraController {
|
|||
Map<int, ScannedNewProfile> scannedNewProfiles = {};
|
||||
String? scannedUrl;
|
||||
GlobalKey zoomButtonKey = GlobalKey();
|
||||
GlobalKey cameraPreviewKey = GlobalKey();
|
||||
bool isSelectingFaceFilters = false;
|
||||
|
||||
bool isSharePreviewIsShown = false;
|
||||
bool isVideoRecording = false;
|
||||
|
||||
Uri? sharedLinkForPreview;
|
||||
|
||||
void setSharedLinkForPreview(Uri url) {
|
||||
sharedLinkForPreview = url;
|
||||
setState();
|
||||
}
|
||||
|
||||
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
||||
final FaceDetector _faceDetector = FaceDetector(
|
||||
options: FaceDetectorOptions(
|
||||
enableContours: true,
|
||||
enableLandmarks: true,
|
||||
),
|
||||
);
|
||||
bool _isBusy = false;
|
||||
bool _isBusyFaces = false;
|
||||
CustomPaint? customPaint;
|
||||
CustomPaint? facePaint;
|
||||
Offset? focusPointOffset;
|
||||
|
||||
FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip;
|
||||
FaceFilterType get currentFilterType => _currentFilterType;
|
||||
|
||||
Future<void> closeCamera() async {
|
||||
contactsVerified = {};
|
||||
|
|
@ -57,51 +92,131 @@ class MainCameraController {
|
|||
}
|
||||
final cameraControllerTemp = cameraController;
|
||||
cameraController = null;
|
||||
await cameraControllerTemp?.dispose();
|
||||
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
|
||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||
await cameraControllerTemp?.dispose();
|
||||
});
|
||||
initCameraStarted = false;
|
||||
selectedCameraDetails = SelectedCameraDetails();
|
||||
}
|
||||
|
||||
Future<CameraController?> selectCamera(int sCameraId, bool init) async {
|
||||
Future<void> selectCamera(int sCameraId, bool init) async {
|
||||
initCameraStarted = true;
|
||||
final opts = await initializeCameraController(
|
||||
selectedCameraDetails,
|
||||
sCameraId,
|
||||
init,
|
||||
);
|
||||
if (opts != null) {
|
||||
selectedCameraDetails = opts.$1;
|
||||
cameraController = opts.$2;
|
||||
}
|
||||
if (cameraController?.description.lensDirection ==
|
||||
CameraLensDirection.back) {
|
||||
await cameraController?.startImageStream(_processCameraImage);
|
||||
}
|
||||
zoomButtonKey = GlobalKey();
|
||||
setState();
|
||||
return cameraController;
|
||||
}
|
||||
|
||||
Future<void> toggleSelectedCamera() async {
|
||||
if (cameraController == null) return;
|
||||
// do not allow switching camera when recording
|
||||
if (cameraController!.value.isRecordingVideo) {
|
||||
var cameraId = sCameraId;
|
||||
if (cameraId >= gCameras.length) {
|
||||
Log.warn(
|
||||
'Trying to select a non existing camera $cameraId >= ${gCameras.length}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await cameraController!.stopImageStream();
|
||||
} catch (e) {
|
||||
// Log.warn(e);
|
||||
|
||||
if (init) {
|
||||
for (; cameraId < gCameras.length; cameraId++) {
|
||||
if (gCameras[cameraId].lensDirection == CameraLensDirection.back) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final tmp = cameraController;
|
||||
cameraController = null;
|
||||
await tmp!.dispose();
|
||||
|
||||
selectedCameraDetails.isZoomAble = false;
|
||||
|
||||
if (cameraController == null) {
|
||||
cameraController = CameraController(
|
||||
gCameras[cameraId],
|
||||
ResolutionPreset.high,
|
||||
enableAudio: await Permission.microphone.isGranted,
|
||||
imageFormatGroup: Platform.isAndroid
|
||||
? ImageFormatGroup.nv21
|
||||
: ImageFormatGroup.bgra8888,
|
||||
);
|
||||
await cameraController?.initialize();
|
||||
await cameraController?.startImageStream(_processCameraImage);
|
||||
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
|
||||
} else {
|
||||
try {
|
||||
if (!isVideoRecording) {
|
||||
await cameraController?.stopImageStream();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.info(e);
|
||||
}
|
||||
selectedCameraDetails.scaleFactor = 1;
|
||||
|
||||
await cameraController?.setZoomLevel(1);
|
||||
await cameraController?.setDescription(gCameras[cameraId]);
|
||||
try {
|
||||
if (!isVideoRecording) {
|
||||
await cameraController?.startImageStream(_processCameraImage);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.info(e);
|
||||
}
|
||||
}
|
||||
|
||||
await cameraController
|
||||
?.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
||||
await cameraController?.setFlashMode(
|
||||
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
|
||||
);
|
||||
selectedCameraDetails.maxAvailableZoom =
|
||||
await cameraController?.getMaxZoomLevel() ?? 1;
|
||||
selectedCameraDetails.minAvailableZoom =
|
||||
await cameraController?.getMinZoomLevel() ?? 1;
|
||||
selectedCameraDetails
|
||||
..isZoomAble = selectedCameraDetails.maxAvailableZoom !=
|
||||
selectedCameraDetails.minAvailableZoom
|
||||
..cameraLoaded = true
|
||||
..cameraId = cameraId;
|
||||
|
||||
facePaint = null;
|
||||
customPaint = null;
|
||||
isSelectingFaceFilters = false;
|
||||
setFilter(FaceFilterType.none);
|
||||
zoomButtonKey = GlobalKey();
|
||||
setState();
|
||||
}
|
||||
|
||||
Future<void> onDoubleTap() async {
|
||||
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
|
||||
}
|
||||
|
||||
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
||||
bool _isBusy = false;
|
||||
CustomPaint? customPaint;
|
||||
Future<void> onTapDown(TapDownDetails details) async {
|
||||
final box =
|
||||
cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
final localPosition = box.globalToLocal(details.globalPosition);
|
||||
|
||||
focusPointOffset = Offset(localPosition.dx, localPosition.dy);
|
||||
|
||||
final dx = localPosition.dx / box.size.width;
|
||||
final dy = localPosition.dy / box.size.height;
|
||||
|
||||
setState();
|
||||
|
||||
await HapticFeedback.lightImpact();
|
||||
try {
|
||||
await cameraController?.setFocusPoint(Offset(dx, dy));
|
||||
await cameraController?.setFocusMode(FocusMode.auto);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
|
||||
focusPointOffset = null;
|
||||
setState();
|
||||
}
|
||||
|
||||
void setFilter(FaceFilterType type) {
|
||||
_currentFilterType = type;
|
||||
if (_currentFilterType == FaceFilterType.none) {
|
||||
faceFilterPainter = null;
|
||||
facePaint = null;
|
||||
_isBusyFaces = false;
|
||||
}
|
||||
setState();
|
||||
}
|
||||
|
||||
FaceFilterPainter? faceFilterPainter;
|
||||
|
||||
final Map<DeviceOrientation, int> _orientations = {
|
||||
DeviceOrientation.portraitUp: 0,
|
||||
|
|
@ -111,9 +226,21 @@ class MainCameraController {
|
|||
};
|
||||
|
||||
void _processCameraImage(CameraImage image) {
|
||||
if (isVideoRecording || isSharePreviewIsShown) {
|
||||
return;
|
||||
}
|
||||
final inputImage = _inputImageFromCameraImage(image);
|
||||
if (inputImage == null) return;
|
||||
_processImage(inputImage);
|
||||
_processBarcode(inputImage);
|
||||
// check if front camera is selected
|
||||
if (cameraController?.description.lensDirection ==
|
||||
CameraLensDirection.front) {
|
||||
if (_currentFilterType != FaceFilterType.none) {
|
||||
_processFaces(inputImage);
|
||||
}
|
||||
} else {
|
||||
_processBarcode(inputImage);
|
||||
}
|
||||
}
|
||||
|
||||
InputImage? _inputImageFromCameraImage(CameraImage image) {
|
||||
|
|
@ -175,7 +302,7 @@ class MainCameraController {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _processImage(InputImage inputImage) async {
|
||||
Future<void> _processBarcode(InputImage inputImage) async {
|
||||
if (_isBusy) return;
|
||||
_isBusy = true;
|
||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||
|
|
@ -255,4 +382,48 @@ class MainCameraController {
|
|||
_isBusy = false;
|
||||
setState();
|
||||
}
|
||||
|
||||
Future<void> _processFaces(InputImage inputImage) async {
|
||||
if (_isBusyFaces) return;
|
||||
_isBusyFaces = true;
|
||||
final faces = await _faceDetector.processImage(inputImage);
|
||||
if (inputImage.metadata?.size != null &&
|
||||
inputImage.metadata?.rotation != null &&
|
||||
cameraController != null) {
|
||||
if (faces.isNotEmpty) {
|
||||
CustomPainter? painter;
|
||||
if (_currentFilterType == FaceFilterType.dogBrown) {
|
||||
painter = DogFilterPainter(
|
||||
faces,
|
||||
inputImage.metadata!.size,
|
||||
inputImage.metadata!.rotation,
|
||||
cameraController!.description.lensDirection,
|
||||
);
|
||||
} else if (_currentFilterType == FaceFilterType.beardUpperLip) {
|
||||
painter = BeardFilterPainter(
|
||||
faces,
|
||||
inputImage.metadata!.size,
|
||||
inputImage.metadata!.rotation,
|
||||
cameraController!.description.lensDirection,
|
||||
);
|
||||
}
|
||||
|
||||
if (painter != null) {
|
||||
facePaint = CustomPaint(painter: painter);
|
||||
// Also set the correct FaceFilterPainter reference if needed for other logic,
|
||||
// though currently facePaint is what's used for display.
|
||||
if (painter is FaceFilterPainter) {
|
||||
faceFilterPainter = painter;
|
||||
}
|
||||
} else {
|
||||
facePaint = null;
|
||||
faceFilterPainter = null;
|
||||
}
|
||||
} else {
|
||||
facePaint = null;
|
||||
}
|
||||
}
|
||||
_isBusyFaces = false;
|
||||
setState();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
|
||||
|
||||
class BeardFilterPainter extends FaceFilterPainter {
|
||||
BeardFilterPainter(
|
||||
super.faces,
|
||||
super.imageSize,
|
||||
super.rotation,
|
||||
super.cameraLensDirection,
|
||||
) {
|
||||
_loadAssets();
|
||||
}
|
||||
|
||||
static ui.Image? _beardImage;
|
||||
static bool _loading = false;
|
||||
|
||||
static Future<void> _loadAssets() async {
|
||||
if (_loading || _beardImage != null) return;
|
||||
_loading = true;
|
||||
try {
|
||||
_beardImage = await _loadImage('assets/filters/beard_upper_lip.webp');
|
||||
} catch (e) {
|
||||
Log.error('Failed to load filter assets: $e');
|
||||
} finally {
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ui.Image> _loadImage(String assetPath) async {
|
||||
final data = await rootBundle.load(assetPath);
|
||||
final list = Uint8List.view(data.buffer);
|
||||
final completer = Completer<ui.Image>();
|
||||
ui.decodeImageFromList(list, completer.complete);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (_beardImage == null) return;
|
||||
|
||||
for (final face in faces) {
|
||||
final noseBase = face.landmarks[FaceLandmarkType.noseBase];
|
||||
final mouthLeft = face.landmarks[FaceLandmarkType.leftMouth];
|
||||
final mouthRight = face.landmarks[FaceLandmarkType.rightMouth];
|
||||
final bottomMouth = face.landmarks[FaceLandmarkType.bottomMouth];
|
||||
|
||||
if (noseBase != null &&
|
||||
mouthLeft != null &&
|
||||
mouthRight != null &&
|
||||
bottomMouth != null) {
|
||||
final noseX = translateX(
|
||||
noseBase.position.x.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
final noseY = translateY(
|
||||
noseBase.position.y.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
|
||||
final mouthLeftX = translateX(
|
||||
mouthLeft.position.x.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
final mouthLeftY = translateY(
|
||||
mouthLeft.position.y.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
|
||||
final mouthRightX = translateX(
|
||||
mouthRight.position.x.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
final mouthRightY = translateY(
|
||||
mouthRight.position.y.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
|
||||
final mouthCenterX = (mouthLeftX + mouthRightX) / 2;
|
||||
final mouthCenterY = (mouthLeftY + mouthRightY) / 2;
|
||||
|
||||
final beardCenterX = (noseX + mouthCenterX) / 2;
|
||||
final beardCenterY = (noseY + mouthCenterY) / 2;
|
||||
|
||||
final dx = mouthRightX - mouthLeftX;
|
||||
final dy = mouthRightY - mouthLeftY;
|
||||
final angle = atan2(dy, dx);
|
||||
|
||||
final mouthWidth = sqrt(dx * dx + dy * dy);
|
||||
final beardWidth = mouthWidth * 1.5;
|
||||
|
||||
final yaw = face.headEulerAngleY ?? 0;
|
||||
final scaleX = cos(yaw * pi / 180).abs();
|
||||
|
||||
_drawImage(
|
||||
canvas,
|
||||
_beardImage!,
|
||||
Offset(beardCenterX, beardCenterY),
|
||||
beardWidth,
|
||||
angle,
|
||||
scaleX,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawImage(
|
||||
Canvas canvas,
|
||||
ui.Image image,
|
||||
Offset position,
|
||||
double width,
|
||||
double rotation,
|
||||
double scaleX,
|
||||
) {
|
||||
canvas
|
||||
..save()
|
||||
..translate(position.dx, position.dy)
|
||||
..rotate(rotation)
|
||||
..scale(scaleX, Platform.isAndroid ? -1 : 1);
|
||||
|
||||
final srcRect =
|
||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
||||
|
||||
final aspectRatio = image.width / image.height;
|
||||
final dstWidth = width;
|
||||
final dstHeight = width / aspectRatio;
|
||||
|
||||
final dstRect = Rect.fromCenter(
|
||||
center: Offset.zero,
|
||||
width: dstWidth,
|
||||
height: dstHeight,
|
||||
);
|
||||
|
||||
canvas
|
||||
..drawImageRect(image, srcRect, dstRect, Paint())
|
||||
..restore();
|
||||
}
|
||||
|
||||
static Widget getPreview() {
|
||||
return Preview(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Image.asset(
|
||||
'assets/filters/beard_upper_lip.webp',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
|
||||
|
||||
class DogFilterPainter extends FaceFilterPainter {
|
||||
DogFilterPainter(
|
||||
super.faces,
|
||||
super.imageSize,
|
||||
super.rotation,
|
||||
super.cameraLensDirection,
|
||||
) {
|
||||
_loadAssets();
|
||||
}
|
||||
|
||||
static ui.Image? _earImage;
|
||||
static ui.Image? _noseImage;
|
||||
static bool _loading = false;
|
||||
|
||||
static Future<void> _loadAssets() async {
|
||||
if (_loading || (_earImage != null && _noseImage != null)) return;
|
||||
_loading = true;
|
||||
try {
|
||||
_earImage = await _loadImage('assets/filters/dog_brown_ear.webp');
|
||||
_noseImage = await _loadImage('assets/filters/dog_brown_nose.webp');
|
||||
} catch (e) {
|
||||
Log.error('Failed to load filter assets: $e');
|
||||
} finally {
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ui.Image> _loadImage(String assetPath) async {
|
||||
final data = await rootBundle.load(assetPath);
|
||||
final list = Uint8List.view(data.buffer);
|
||||
final completer = Completer<ui.Image>();
|
||||
ui.decodeImageFromList(list, completer.complete);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (_earImage == null || _noseImage == null) return;
|
||||
|
||||
for (final face in faces) {
|
||||
final faceContour = face.contours[FaceContourType.face];
|
||||
final noseBase = face.landmarks[FaceLandmarkType.noseBase];
|
||||
|
||||
if (faceContour != null && noseBase != null) {
|
||||
final points = faceContour.points;
|
||||
if (points.isEmpty) continue;
|
||||
|
||||
final upperPoints =
|
||||
points.where((p) => p.y < noseBase.position.y).toList();
|
||||
|
||||
if (upperPoints.isEmpty) continue;
|
||||
|
||||
Point<int>? leftMost;
|
||||
Point<int>? rightMost;
|
||||
Point<int>? topMost;
|
||||
|
||||
for (final point in upperPoints) {
|
||||
if (leftMost == null || point.x < leftMost.x) {
|
||||
leftMost = point;
|
||||
}
|
||||
if (rightMost == null || point.x > rightMost.x) {
|
||||
rightMost = point;
|
||||
}
|
||||
if (topMost == null || point.y < topMost.y) {
|
||||
topMost = point;
|
||||
}
|
||||
}
|
||||
|
||||
if (leftMost == null || rightMost == null || topMost == null) continue;
|
||||
|
||||
final leftEarX = translateX(
|
||||
leftMost.x.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
final leftEarY = translateY(
|
||||
topMost.y.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
|
||||
final rightEarX = translateX(
|
||||
rightMost.x.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
final rightEarY = translateY(
|
||||
topMost.y.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
|
||||
final noseX = translateX(
|
||||
noseBase.position.x.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
final noseY = translateY(
|
||||
noseBase.position.y.toDouble(),
|
||||
size,
|
||||
imageSize,
|
||||
rotation,
|
||||
cameraLensDirection,
|
||||
);
|
||||
|
||||
final dx = rightEarX - leftEarX;
|
||||
final dy = rightEarY - leftEarY;
|
||||
|
||||
final faceWidth = sqrt(dx * dx + dy * dy) * 1.5;
|
||||
final angle = atan2(dy, dx);
|
||||
|
||||
final yaw = face.headEulerAngleY ?? 0;
|
||||
final scaleX = cos(yaw * pi / 180).abs();
|
||||
|
||||
final earSize = faceWidth / 2.5;
|
||||
|
||||
_drawImage(
|
||||
canvas,
|
||||
_earImage!,
|
||||
Offset(leftEarX, leftEarY + earSize * 0.3),
|
||||
earSize,
|
||||
angle,
|
||||
scaleX,
|
||||
);
|
||||
|
||||
_drawImage(
|
||||
canvas,
|
||||
_earImage!,
|
||||
Offset(rightEarX, rightEarY + earSize * 0.3),
|
||||
earSize,
|
||||
angle,
|
||||
scaleX,
|
||||
isFlipped: true,
|
||||
);
|
||||
|
||||
final noseSize = faceWidth * 0.4;
|
||||
_drawImage(
|
||||
canvas,
|
||||
_noseImage!,
|
||||
Offset(noseX, noseY + noseSize * 0.1),
|
||||
noseSize,
|
||||
angle,
|
||||
scaleX,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawImage(
|
||||
Canvas canvas,
|
||||
ui.Image image,
|
||||
Offset position,
|
||||
double size,
|
||||
double rotation,
|
||||
double scaleX, {
|
||||
bool isFlipped = false,
|
||||
}) {
|
||||
canvas
|
||||
..save()
|
||||
..translate(position.dx, position.dy)
|
||||
..rotate(rotation);
|
||||
if (isFlipped) {
|
||||
canvas.scale(-scaleX, Platform.isAndroid ? -1 : 1);
|
||||
} else {
|
||||
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
|
||||
}
|
||||
|
||||
final srcRect =
|
||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
||||
final aspectRatio = image.width / image.height;
|
||||
final dstWidth = size;
|
||||
final dstHeight = size / aspectRatio;
|
||||
|
||||
final dstRect = Rect.fromCenter(
|
||||
center: Offset.zero,
|
||||
width: dstWidth,
|
||||
height: dstHeight,
|
||||
);
|
||||
|
||||
canvas
|
||||
..drawImageRect(image, srcRect, dstRect, Paint())
|
||||
..restore();
|
||||
}
|
||||
|
||||
static Widget getPreview() {
|
||||
return Preview(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 25),
|
||||
child: Image.asset(
|
||||
'assets/filters/dog_brown_nose.webp',
|
||||
width: 25,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/filters/dog_brown_ear.webp',
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Transform.scale(
|
||||
scaleX: -1,
|
||||
child: Image.asset(
|
||||
'assets/filters/dog_brown_ear.webp',
|
||||
width: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
|
||||
|
||||
abstract class FaceFilterPainter extends CustomPainter {
|
||||
FaceFilterPainter(
|
||||
this.faces,
|
||||
this.imageSize,
|
||||
this.rotation,
|
||||
this.cameraLensDirection,
|
||||
);
|
||||
|
||||
final List<Face> faces;
|
||||
final Size imageSize;
|
||||
final InputImageRotation rotation;
|
||||
final CameraLensDirection cameraLensDirection;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant FaceFilterPainter oldDelegate) {
|
||||
return oldDelegate.imageSize != imageSize ||
|
||||
oldDelegate.faces != faces ||
|
||||
oldDelegate.rotation != rotation ||
|
||||
oldDelegate.cameraLensDirection != cameraLensDirection;
|
||||
}
|
||||
}
|
||||
|
||||
class Preview extends StatelessWidget {
|
||||
const Preview({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
child: Center(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -8,6 +7,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/screenshot.dart';
|
||||
|
||||
class SaveToGalleryButton extends StatefulWidget {
|
||||
const SaveToGalleryButton({
|
||||
|
|
@ -17,7 +17,7 @@ class SaveToGalleryButton extends StatefulWidget {
|
|||
this.storeImageAsOriginal,
|
||||
super.key,
|
||||
});
|
||||
final Future<Uint8List?> Function()? storeImageAsOriginal;
|
||||
final Future<ScreenshotImage?> Function()? storeImageAsOriginal;
|
||||
final bool displayButtonLabel;
|
||||
final MediaFileService mediaService;
|
||||
final bool isLoading;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class SendToWidget extends StatelessWidget {
|
||||
const SendToWidget({
|
||||
required this.sendTo,
|
||||
class ShowTitleText extends StatelessWidget {
|
||||
const ShowTitleText({
|
||||
required this.desc,
|
||||
required this.title,
|
||||
this.isLink = false,
|
||||
super.key,
|
||||
});
|
||||
final String sendTo;
|
||||
final String title;
|
||||
final String desc;
|
||||
final bool isLink;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const textStyle = TextStyle(
|
||||
final textStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
fontSize: isLink ? 14 : 24,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: [
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
|
|
@ -26,7 +29,7 @@ class SendToWidget extends StatelessWidget {
|
|||
|
||||
final boldTextStyle = textStyle.copyWith(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 28,
|
||||
fontSize: isLink ? 17 : 28,
|
||||
);
|
||||
|
||||
return Positioned(
|
||||
|
|
@ -36,12 +39,12 @@ class SendToWidget extends StatelessWidget {
|
|||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
context.lang.cameraPreviewSendTo,
|
||||
desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: textStyle,
|
||||
),
|
||||
Text(
|
||||
substringBy(sendTo, 20),
|
||||
substringBy(title, isLink ? 30 : 20),
|
||||
textAlign: TextAlign.center,
|
||||
style: boldTextStyle, // Use the bold text style here
|
||||
),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ class QrCodeScannerState extends State<QrCodeScanner> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onDoubleTap: _mainCameraController.toggleSelectedCamera,
|
||||
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||
onTapDown: _mainCameraController.onTapDown,
|
||||
child: Stack(
|
||||
children: [
|
||||
MainCameraPreview(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onDoubleTap: _mainCameraController.toggleSelectedCamera,
|
||||
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||
onTapDown: _mainCameraController.onTapDown,
|
||||
child: Stack(
|
||||
children: [
|
||||
MainCameraPreview(
|
||||
|
|
@ -1,710 +0,0 @@
|
|||
Map<String, String> emojiWeights = {};
|
||||
|
||||
List<String> emojis = [
|
||||
'😀',
|
||||
'😁',
|
||||
'😂',
|
||||
'🤣',
|
||||
'😃',
|
||||
'😄',
|
||||
'😅',
|
||||
'😆',
|
||||
'😉',
|
||||
'😊',
|
||||
'😋',
|
||||
'😎',
|
||||
'😍',
|
||||
'😘',
|
||||
'🥰',
|
||||
'😗',
|
||||
'😙',
|
||||
'😚',
|
||||
'🙂️',
|
||||
'🤗',
|
||||
'🤩',
|
||||
'🤔',
|
||||
'🤔',
|
||||
'🤨',
|
||||
'😐',
|
||||
'😑',
|
||||
'😶',
|
||||
'🙄',
|
||||
'😏',
|
||||
'😣',
|
||||
'😥',
|
||||
'😮',
|
||||
'🤐',
|
||||
'😯',
|
||||
'😪',
|
||||
'😫',
|
||||
'😴',
|
||||
'😌',
|
||||
'😛',
|
||||
'😜',
|
||||
'😝',
|
||||
'🤤',
|
||||
'😒',
|
||||
'😓',
|
||||
'😔',
|
||||
'😕',
|
||||
'🙃',
|
||||
'🤑',
|
||||
'😲',
|
||||
'🙁',
|
||||
'😖',
|
||||
'😞',
|
||||
'😟',
|
||||
'😤',
|
||||
'😢',
|
||||
'😭',
|
||||
'😦',
|
||||
'😧',
|
||||
'😨',
|
||||
'😩',
|
||||
'🤯',
|
||||
'😬',
|
||||
'😰',
|
||||
'😱',
|
||||
'🥵',
|
||||
'🥶',
|
||||
'😳',
|
||||
'🤪',
|
||||
'😵',
|
||||
'😡',
|
||||
'😠',
|
||||
'🤬',
|
||||
'😷',
|
||||
'🤒',
|
||||
'🤕',
|
||||
'🤢',
|
||||
'🤮',
|
||||
'🤧',
|
||||
'😇',
|
||||
'🤠',
|
||||
'🤡',
|
||||
'🥳',
|
||||
'🥴',
|
||||
'🤥',
|
||||
'🤫',
|
||||
'🤭',
|
||||
'🤭',
|
||||
'🧐',
|
||||
'🤓',
|
||||
'😈',
|
||||
'👿',
|
||||
'👹',
|
||||
'👺',
|
||||
'💀',
|
||||
'👻',
|
||||
'👽',
|
||||
'🤖',
|
||||
'💩',
|
||||
'😺',
|
||||
'😸',
|
||||
'😹',
|
||||
'😻',
|
||||
'😼',
|
||||
'😽',
|
||||
'🙀',
|
||||
'😿',
|
||||
'😾',
|
||||
'😾',
|
||||
|
||||
/// People and Fantasy
|
||||
'👶',
|
||||
'👧',
|
||||
'🧒',
|
||||
'👩',
|
||||
'🧑',
|
||||
'👨',
|
||||
'👵',
|
||||
'👴',
|
||||
'👲',
|
||||
'👳♀️️',
|
||||
'👳♂️️️',
|
||||
'🧕️️️',
|
||||
'🧔',
|
||||
'👱♂️️',
|
||||
'👱♀️️️',
|
||||
'👨🦰️️️',
|
||||
'👩🦰',
|
||||
'👨🦱',
|
||||
'👨🦲',
|
||||
'👩🦲',
|
||||
'👨🦳',
|
||||
'👩🦳',
|
||||
'🦸♀️',
|
||||
'🦸♂️️',
|
||||
'🦹♀️️️',
|
||||
'🦹♂️️️️',
|
||||
'👮♀️',
|
||||
'👮♂️️',
|
||||
'👷♀️️️',
|
||||
'👷♂️️️️',
|
||||
'💂♀️️️️️',
|
||||
'💂♂️️️️️️',
|
||||
'🕵️♀️️️️️️️',
|
||||
'🕵️♂️️️️️️️️',
|
||||
'👩⚕️️️️️️️️️',
|
||||
'👨⚕️️️️️️️️️️',
|
||||
'👩🌾️️️️️️️️️️',
|
||||
'👨🌾',
|
||||
'👩🍳',
|
||||
'👨🍳',
|
||||
'👩🎓',
|
||||
'👨🎓',
|
||||
'👩🎤',
|
||||
'👨🎤',
|
||||
'👩🏫',
|
||||
'👨🏫',
|
||||
'👩🏭',
|
||||
'👨🏭',
|
||||
'👩💻',
|
||||
'👨💻',
|
||||
'👩💼',
|
||||
'👨💼',
|
||||
'👩🔧',
|
||||
'👨🔧',
|
||||
'👩🔬',
|
||||
'👨🔬',
|
||||
'👩🎨',
|
||||
'👨🎨',
|
||||
'👩🚒',
|
||||
'👨🚒',
|
||||
'👩✈️',
|
||||
'👨✈️️',
|
||||
'👩🚀',
|
||||
'👨🚀',
|
||||
'👩⚖️',
|
||||
'👨⚖️️',
|
||||
'👰',
|
||||
'🤵',
|
||||
'👸',
|
||||
'🤴',
|
||||
'🤶',
|
||||
'🎅',
|
||||
'🧙♀️',
|
||||
'🧙♂️️',
|
||||
'🧝♀️️️',
|
||||
'🧝♂️',
|
||||
'🧛♀️️',
|
||||
'🧛♂️️️',
|
||||
'🧟♀️️️️',
|
||||
'🧟♂️️️️️',
|
||||
'🧞♀️️️️️️',
|
||||
'🧞♂️️️️️️️',
|
||||
'🧜♀️️️️️️️️',
|
||||
'🧜♂️️️️️️️️️',
|
||||
'🧚♀️️️️️️️️️️',
|
||||
'🧚♂️️️️️️️️️️️',
|
||||
'👼️️️️️️️️️️️',
|
||||
'🤰',
|
||||
'🤱',
|
||||
'🙇♀️',
|
||||
'🙇♂️',
|
||||
'💁♀️️',
|
||||
'💁♂️️️',
|
||||
'🙅♀️️️️',
|
||||
'🙅♂️',
|
||||
'🙆♀️️',
|
||||
'🙆♂️️️',
|
||||
'🙋♀️️️️',
|
||||
'🙋♂️',
|
||||
'🤦♀️️',
|
||||
'🤦♂️️️',
|
||||
'🤷♀️️️️',
|
||||
'🤷♂️️️️️',
|
||||
'🙎♀️️️️️️',
|
||||
'🙎♂️️️️️️️',
|
||||
'🙍♀️️️️️️️️',
|
||||
'🙍♂️️️️️️️️️',
|
||||
'💇♀️️️️️️️️️️',
|
||||
'💇♂️️️️️️️️️️️',
|
||||
'💆♀️️️️️️️️️️️️',
|
||||
'💆♂️️️️️️️️️️️️️',
|
||||
'🧖♀️️️️️️️️️️️️️️',
|
||||
'🧖♂️️️️️️️️️️️️️️️',
|
||||
'💅️️️️️️️️️️️️️️️',
|
||||
'🤳️️️️️️️️️️️️️️',
|
||||
'💃️️️️️️️️️️️️️',
|
||||
'🕺️️️️️️️️️️️️',
|
||||
'👯♀️',
|
||||
'👯♂️️',
|
||||
'🕴️️',
|
||||
'🚶♀️️',
|
||||
'🚶♂️️️',
|
||||
'🏃♀️️️️',
|
||||
'🏃♂️',
|
||||
'👫️',
|
||||
'👭',
|
||||
'👬',
|
||||
'💑',
|
||||
'👩❤️👩',
|
||||
'👨❤️👨',
|
||||
'💏',
|
||||
'👩❤️💋👩',
|
||||
'👨❤️💋👨',
|
||||
'👪',
|
||||
'👨👩👧',
|
||||
'👨👩👧👦',
|
||||
'👨👩👦👦',
|
||||
'👨👩👧👧',
|
||||
'👩👩👦',
|
||||
'👩👩👧',
|
||||
'👩👩👧👦',
|
||||
'👩👩👦👦',
|
||||
'👩👩👧👧',
|
||||
'👨👨👦',
|
||||
'👨👨👧',
|
||||
'👨👨👧👦',
|
||||
'👨👨👦👦',
|
||||
'👨👨👧👧',
|
||||
'👩👦',
|
||||
'👩👧',
|
||||
'👩👧👦',
|
||||
'👩👦👦',
|
||||
'👩👧👧',
|
||||
'👨👦',
|
||||
'👨👧',
|
||||
'👨👧👦',
|
||||
'👨👦👦',
|
||||
'👨👧👧',
|
||||
'🤲',
|
||||
'👐',
|
||||
'🙌',
|
||||
'👏',
|
||||
'🤝',
|
||||
'👍',
|
||||
'👎',
|
||||
'👊',
|
||||
'✊',
|
||||
'🤛',
|
||||
'🤜',
|
||||
'🤞',
|
||||
'✌️',
|
||||
'🤟️',
|
||||
'🤘',
|
||||
'👌',
|
||||
'👈',
|
||||
'👉',
|
||||
'👆',
|
||||
'👇',
|
||||
'☝️',
|
||||
'✋️',
|
||||
'🤚️',
|
||||
'🤚️',
|
||||
'🖐',
|
||||
'🖖',
|
||||
'👋',
|
||||
'🤙',
|
||||
'💪',
|
||||
'🦵',
|
||||
'🦶',
|
||||
'🖕',
|
||||
'✍️',
|
||||
'🙏️',
|
||||
'💍',
|
||||
'💄',
|
||||
'💋',
|
||||
'👄',
|
||||
'👅',
|
||||
'👂',
|
||||
'👃',
|
||||
'👣',
|
||||
'👁',
|
||||
'👀',
|
||||
'🧠',
|
||||
'🦴',
|
||||
'🦷',
|
||||
'🗣',
|
||||
'👤',
|
||||
'👥',
|
||||
'🧥',
|
||||
'👚',
|
||||
'👕',
|
||||
'👖',
|
||||
'👔',
|
||||
'👗',
|
||||
'👙',
|
||||
'👘',
|
||||
'👠',
|
||||
'👡',
|
||||
'👢',
|
||||
'👞',
|
||||
'👟',
|
||||
'🥾',
|
||||
'🥿',
|
||||
'🧦',
|
||||
'🧤',
|
||||
'🧣',
|
||||
'🎩',
|
||||
'🧢',
|
||||
'👒',
|
||||
'🎓',
|
||||
'⛑',
|
||||
'👑',
|
||||
'👝',
|
||||
'👛',
|
||||
'👜',
|
||||
'💼',
|
||||
'🎒',
|
||||
'👓',
|
||||
'🕶',
|
||||
'🥽',
|
||||
'🥼',
|
||||
'🌂',
|
||||
'🧵',
|
||||
'🧶',
|
||||
|
||||
/// Animals
|
||||
'🐶',
|
||||
'🐱',
|
||||
'🐭',
|
||||
'🐰',
|
||||
'🦊',
|
||||
'🦝',
|
||||
'🐻',
|
||||
'🦘',
|
||||
'🦡',
|
||||
'🐨',
|
||||
'🐯',
|
||||
'🦁',
|
||||
'🐼',
|
||||
'🐼',
|
||||
'🐮',
|
||||
'🐷',
|
||||
'🐽',
|
||||
'🐸',
|
||||
'🐵',
|
||||
'🙈',
|
||||
'🙉',
|
||||
'🙊',
|
||||
'🐒',
|
||||
'🐔',
|
||||
'🐧',
|
||||
'🐦',
|
||||
'🐤',
|
||||
'🐣',
|
||||
'🐥',
|
||||
'🦆',
|
||||
'🦢',
|
||||
'🦅',
|
||||
'🦉',
|
||||
'🦚',
|
||||
'🦜',
|
||||
'🦇',
|
||||
'🐺',
|
||||
'🐗',
|
||||
'🐴',
|
||||
'🦄',
|
||||
'🐝',
|
||||
'🐛',
|
||||
'🦋',
|
||||
'🐌',
|
||||
'🐚',
|
||||
'🐞',
|
||||
'🐜',
|
||||
'🦗',
|
||||
'🕷',
|
||||
'🕸',
|
||||
'🦂',
|
||||
'🦟',
|
||||
'🦠',
|
||||
'🐢',
|
||||
'🐍',
|
||||
'🦎',
|
||||
'🦖',
|
||||
'🦕',
|
||||
'🐙',
|
||||
'🦑',
|
||||
'🦐',
|
||||
'🦀',
|
||||
'🐡',
|
||||
'🐠',
|
||||
'🐟',
|
||||
'🐬',
|
||||
'🐳',
|
||||
'🐋',
|
||||
'🦈',
|
||||
'🐊',
|
||||
'🐅',
|
||||
'🐆',
|
||||
'🦓',
|
||||
'🦍',
|
||||
'🐘',
|
||||
'🦏',
|
||||
'🦛',
|
||||
'🐪',
|
||||
'🐫',
|
||||
'🦙',
|
||||
'🦒',
|
||||
'🐃',
|
||||
'🐂',
|
||||
'🐄',
|
||||
'🐎',
|
||||
'🐖',
|
||||
'🐏',
|
||||
'🐐',
|
||||
'🦌',
|
||||
'🐕',
|
||||
'🐩',
|
||||
'🐈',
|
||||
'🐓',
|
||||
'🦃',
|
||||
'🕊',
|
||||
'🐇',
|
||||
'🐁',
|
||||
'🐀',
|
||||
'🐿',
|
||||
'🦔',
|
||||
'🐾',
|
||||
'🐉',
|
||||
'🐲',
|
||||
'🌵',
|
||||
'🎄',
|
||||
'🌲',
|
||||
'🌳',
|
||||
'🌴',
|
||||
'🌱',
|
||||
'🌿',
|
||||
'☘️',
|
||||
'🎍️',
|
||||
'🎋️',
|
||||
'🍃',
|
||||
'🍂',
|
||||
'🍁',
|
||||
'🍄',
|
||||
'🌾️',
|
||||
'💐️',
|
||||
'🌷️',
|
||||
'🌹',
|
||||
'🥀',
|
||||
'🌺',
|
||||
'🌸',
|
||||
'🌼',
|
||||
'🌻️',
|
||||
'🌞',
|
||||
'🌝',
|
||||
'🌛',
|
||||
'🌜',
|
||||
'🌚',
|
||||
'🌕',
|
||||
'🌖',
|
||||
'🌗',
|
||||
'🌘',
|
||||
'🌑',
|
||||
'🌒',
|
||||
'🌔',
|
||||
'🌙',
|
||||
'🌎',
|
||||
'🌍',
|
||||
'🌏',
|
||||
'💫',
|
||||
'⭐️',
|
||||
'🌟️',
|
||||
'✨️',
|
||||
'⚡️️',
|
||||
'☄️️️',
|
||||
'💥️️️',
|
||||
'🔥',
|
||||
'🌪',
|
||||
'🌈',
|
||||
'☀️',
|
||||
'🌤️',
|
||||
'⛅️️',
|
||||
'🌥️️',
|
||||
'☁️️',
|
||||
'🌦️️',
|
||||
'🌧️',
|
||||
'⛈',
|
||||
'🌩',
|
||||
'🌨',
|
||||
'❄️',
|
||||
'☃️️',
|
||||
'⛄️️️',
|
||||
'🌬️️️',
|
||||
'💨️️️',
|
||||
'💧️️️',
|
||||
'💦️️️',
|
||||
'☔️️️️',
|
||||
'☂️️️️️',
|
||||
'🌊️️️️️',
|
||||
'🌫️️️️',
|
||||
|
||||
/// Foods
|
||||
'🍏',
|
||||
'🍎',
|
||||
'🍐',
|
||||
'🍊',
|
||||
'🍋',
|
||||
'🍌',
|
||||
'🍉',
|
||||
'🍇',
|
||||
'🍓',
|
||||
'🍈',
|
||||
'🍒',
|
||||
'🍑',
|
||||
'🍍',
|
||||
'🥭',
|
||||
'🥥',
|
||||
'🥝',
|
||||
'🍅',
|
||||
'🍆',
|
||||
'🥑',
|
||||
'🥦',
|
||||
'🥒',
|
||||
'🥬',
|
||||
'🌶',
|
||||
'🌽',
|
||||
'🥕',
|
||||
'🥔',
|
||||
'🍠',
|
||||
'🥐',
|
||||
'🍞',
|
||||
'🥖',
|
||||
'🥨',
|
||||
'🥯',
|
||||
'🧀',
|
||||
'🥚',
|
||||
'🍳',
|
||||
'🥞',
|
||||
'🥓',
|
||||
'🥩',
|
||||
'🍗',
|
||||
'🍖',
|
||||
'🌭',
|
||||
'🍔',
|
||||
'🍟',
|
||||
'🍕',
|
||||
'🥪',
|
||||
'🥙',
|
||||
'🌮',
|
||||
'🌯',
|
||||
'🥗',
|
||||
'🥘',
|
||||
'🥫',
|
||||
'🍝',
|
||||
'🍜',
|
||||
'🍲',
|
||||
'🍛',
|
||||
'🍣',
|
||||
'🍱',
|
||||
'🥟',
|
||||
'🍤',
|
||||
'🍙',
|
||||
'🍚',
|
||||
'🍘',
|
||||
'🍥',
|
||||
'🥮',
|
||||
'🥠',
|
||||
'🍢',
|
||||
'🍧',
|
||||
'🍨',
|
||||
'🍦',
|
||||
'🥧',
|
||||
'🍰',
|
||||
'🎂',
|
||||
'🍮',
|
||||
'🍭',
|
||||
'🍬',
|
||||
'🍫',
|
||||
'🍿',
|
||||
'🧂',
|
||||
'🍩',
|
||||
'🍪',
|
||||
'🌰',
|
||||
'🥜',
|
||||
'🍯',
|
||||
'🥛',
|
||||
'🍼',
|
||||
'☕️',
|
||||
'🍵️',
|
||||
'🥤️',
|
||||
'🍶',
|
||||
'🍺',
|
||||
'🍻',
|
||||
'🥂',
|
||||
'🍷',
|
||||
'🍸',
|
||||
'🍹',
|
||||
'🍾',
|
||||
'🥄',
|
||||
'🍴',
|
||||
'🍽',
|
||||
'🥣',
|
||||
'🥡',
|
||||
'🥢',
|
||||
|
||||
/// Activity and Sports
|
||||
'⚽️',
|
||||
'🏀️',
|
||||
'🏈',
|
||||
'⚾️',
|
||||
'🥎️',
|
||||
'🏐️',
|
||||
'🏉',
|
||||
'🎾',
|
||||
'🥏',
|
||||
'🎱',
|
||||
'🏓',
|
||||
'🏸',
|
||||
'🥅',
|
||||
'🏒',
|
||||
'🏑',
|
||||
'🥍',
|
||||
'🏏',
|
||||
'⛳️',
|
||||
'🏹️',
|
||||
'🎣️',
|
||||
'🥊',
|
||||
'🥋',
|
||||
'🎽',
|
||||
'⛸',
|
||||
'🥌',
|
||||
'🛷',
|
||||
'🛹',
|
||||
'🎿',
|
||||
'⛷',
|
||||
'🏂',
|
||||
'🏋️♀️',
|
||||
'🏋🏼♀️',
|
||||
'🏋🏽♀️️',
|
||||
'🏋🏾♀️️️',
|
||||
'🏋🏿♀️️️️',
|
||||
'🏋️♂️️️️',
|
||||
'🏋🏻♂️️️️',
|
||||
'🏋🏼♂️️️️',
|
||||
'🏋🏽♂️️️️',
|
||||
'🏋🏾♂️️️️',
|
||||
'🏋🏿♂️️️️',
|
||||
'🤼♀️️️️',
|
||||
'🤼♂️️️️',
|
||||
'🤸♀️️️️',
|
||||
'🤸🏻♀️️️️',
|
||||
'🤸🏼♀️️️️',
|
||||
'🤸🏽♀️️️️',
|
||||
'🤸🏿♀️️️️️',
|
||||
'🤸♂️️️️',
|
||||
'🤸🏻♂️️️️',
|
||||
'🤸🏼♂️️️️️',
|
||||
'🤸🏽♂️️️️️️',
|
||||
'🤸🏾♂️️️️️️',
|
||||
'🤸🏿♂️️️️️️',
|
||||
'⛹️♀️️️️️️',
|
||||
'⛹🏻♀️️️️️️️',
|
||||
'⛹🏼♀️️️️️️️️',
|
||||
'⛹🏽♀️️️️️️️️️',
|
||||
'⛹🏾♀️️️️️️️️️️',
|
||||
'⛹🏿♀️️️️️️️️️️️',
|
||||
'⛹️♂️️️️️️️️️️️️',
|
||||
'⛹🏻♂️️️️️️️️️️️️️',
|
||||
'⛹🏼♂️️️️️️️️️️️️️️',
|
||||
'⛹🏽♂️️️️️️️️️️️️️️️',
|
||||
'⛹🏾♂️️️️️️️️️️️️️️️️',
|
||||
'⛹🏿♂️',
|
||||
'🤺️',
|
||||
'🤾♀️',
|
||||
'🤾🏻♀️️',
|
||||
'🤾🏼♀️️️',
|
||||
'🤾🏾♀️️️️',
|
||||
];
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageItem {
|
||||
ImageItem([dynamic image]) {
|
||||
if (image != null) unawaited(load(image));
|
||||
}
|
||||
int width = 1;
|
||||
int height = 1;
|
||||
Uint8List bytes = Uint8List.fromList([]);
|
||||
Completer<bool> loader = Completer<bool>();
|
||||
|
||||
Future<void> load(dynamic image) async {
|
||||
loader = Completer<bool>();
|
||||
|
||||
if (image is ImageItem) {
|
||||
bytes = image.bytes;
|
||||
|
||||
height = image.height;
|
||||
width = image.width;
|
||||
|
||||
return loader.complete(true);
|
||||
} else if (image is Uint8List) {
|
||||
bytes = image;
|
||||
final decodedImage = await decodeImageFromList(bytes);
|
||||
|
||||
height = decodedImage.height;
|
||||
width = decodedImage.width;
|
||||
|
||||
return loader.complete(true);
|
||||
} else {
|
||||
return loader.complete(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
|
||||
class BackgroundLayer extends StatefulWidget {
|
||||
const BackgroundLayer({
|
||||
required this.layerData,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final BackgroundLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
|
||||
@override
|
||||
State<BackgroundLayer> createState() => _BackgroundLayerState();
|
||||
}
|
||||
|
||||
class _BackgroundLayerState extends State<BackgroundLayer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: widget.layerData.image.width.toDouble(),
|
||||
height: widget.layerData.image.height.toDouble(),
|
||||
// color: Theme.of(context).colorScheme.surface,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Image.memory(
|
||||
widget.layerData.image.bytes,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.layerData.imageLoaded = true;
|
||||
});
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
|
||||
/// Emoji layer
|
||||
class EmojiLayer extends StatefulWidget {
|
||||
const EmojiLayer({
|
||||
required this.layerData,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final EmojiLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
|
||||
@override
|
||||
State<EmojiLayer> createState() => _EmojiLayerState();
|
||||
}
|
||||
|
||||
class _EmojiLayerState extends State<EmojiLayer> {
|
||||
double initialRotation = 0;
|
||||
Offset initialOffset = Offset.zero;
|
||||
Offset initialFocalPoint = Offset.zero;
|
||||
double initialScale = 1;
|
||||
bool deleteLayer = false;
|
||||
bool twoPointerWhereDown = false;
|
||||
final GlobalKey outlineKey = GlobalKey();
|
||||
final GlobalKey emojiKey = GlobalKey();
|
||||
int pointers = 0;
|
||||
bool display = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.layerData.offset.dy == 0) {
|
||||
// Set the initial offset to the center of the screen
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
widget.layerData.offset = Offset(
|
||||
MediaQuery.of(context).size.width / 2 - (153 / 2),
|
||||
MediaQuery.of(context).size.height / 2 - (153 / 2) - 100,
|
||||
);
|
||||
});
|
||||
display = true;
|
||||
});
|
||||
} else {
|
||||
display = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!display) return Container();
|
||||
if (widget.layerData.isDeleted) return Container();
|
||||
return Stack(
|
||||
key: outlineKey,
|
||||
children: [
|
||||
Positioned(
|
||||
left: widget.layerData.offset.dx,
|
||||
top: widget.layerData.offset.dy,
|
||||
child: Listener(
|
||||
onPointerUp: (details) {
|
||||
setState(() {
|
||||
pointers--;
|
||||
if (pointers == 0) {
|
||||
twoPointerWhereDown = false;
|
||||
}
|
||||
if (deleteLayer) {
|
||||
widget.layerData.isDeleted = true;
|
||||
widget.onUpdate!();
|
||||
}
|
||||
});
|
||||
},
|
||||
onPointerDown: (details) {
|
||||
setState(() {
|
||||
pointers++;
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onScaleStart: (details) {
|
||||
initialScale = widget.layerData.size;
|
||||
initialRotation = widget.layerData.rotation;
|
||||
initialOffset = widget.layerData.offset;
|
||||
initialFocalPoint =
|
||||
Offset(details.focalPoint.dx, details.focalPoint.dy);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
onScaleUpdate: (details) async {
|
||||
if (twoPointerWhereDown && details.pointerCount != 2) {
|
||||
return;
|
||||
}
|
||||
final outlineBox =
|
||||
outlineKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
final emojiBox =
|
||||
emojiKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
final isAtTheBottom =
|
||||
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
|
||||
outlineBox.size.height - 80;
|
||||
final isInTheCenter = MediaQuery.of(context).size.width / 2 -
|
||||
30 <
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2) &&
|
||||
MediaQuery.of(context).size.width / 2 + 20 >
|
||||
(widget.layerData.offset.dx + emojiBox.size.width / 2);
|
||||
|
||||
if (isAtTheBottom && isInTheCenter) {
|
||||
if (!deleteLayer) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
deleteLayer = true;
|
||||
} else {
|
||||
deleteLayer = false;
|
||||
}
|
||||
setState(() {
|
||||
twoPointerWhereDown = details.pointerCount >= 2;
|
||||
widget.layerData.size = initialScale * details.scale;
|
||||
if (widget.layerData.size > 96) {
|
||||
// https://github.com/twonlyapp/twonly-app/issues/349
|
||||
widget.layerData.size = 96;
|
||||
}
|
||||
// print(widget.layerData.size);
|
||||
widget.layerData.rotation =
|
||||
initialRotation + details.rotation;
|
||||
|
||||
// Update the position based on the translation
|
||||
final dx = (initialOffset.dx) +
|
||||
(details.focalPoint.dx - initialFocalPoint.dx);
|
||||
final dy = (initialOffset.dy) +
|
||||
(details.focalPoint.dy - initialFocalPoint.dy);
|
||||
widget.layerData.offset = Offset(dx, dy);
|
||||
});
|
||||
},
|
||||
child: Transform.rotate(
|
||||
angle: widget.layerData.rotation,
|
||||
key: emojiKey,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(44),
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
widget.layerData.text,
|
||||
style: TextStyle(
|
||||
fontSize: widget.layerData.size,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pointers > 0)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
child: ActionButton(
|
||||
FontAwesomeIcons.trashCan,
|
||||
tooltipText: '',
|
||||
color: deleteLayer ? Colors.red : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,18 +2,20 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.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/data.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';
|
||||
import 'package:twonly/src/utils/screenshot.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_contact_selection/best_friends_selector.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/background.layer.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/headline.dart';
|
||||
|
|
@ -24,12 +26,14 @@ class ShareImageView extends StatefulWidget {
|
|||
required this.updateSelectedGroupIds,
|
||||
required this.mediaStoreFuture,
|
||||
required this.mediaFileService,
|
||||
required this.additionalData,
|
||||
super.key,
|
||||
});
|
||||
final HashSet<String> selectedGroupIds;
|
||||
final void Function(String, bool) updateSelectedGroupIds;
|
||||
final Future<Uint8List?>? mediaStoreFuture;
|
||||
final Future<ScreenshotImage?>? mediaStoreFuture;
|
||||
final MediaFileService mediaFileService;
|
||||
final AdditionalMessageData? additionalData;
|
||||
|
||||
@override
|
||||
State<ShareImageView> createState() => _ShareImageView();
|
||||
|
|
@ -43,7 +47,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
bool sendingImage = false;
|
||||
bool mediaStoreFutureReady = false;
|
||||
Uint8List? _imageBytes;
|
||||
ScreenshotImage? _screenshotImage;
|
||||
bool hideArchivedUsers = true;
|
||||
final TextEditingController searchUserName = TextEditingController();
|
||||
late StreamSubscription<List<Group>> allGroupSub;
|
||||
|
|
@ -66,7 +70,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
|
||||
Future<void> initAsync() async {
|
||||
if (widget.mediaStoreFuture != null) {
|
||||
_imageBytes = await widget.mediaStoreFuture;
|
||||
_screenshotImage = await widget.mediaStoreFuture;
|
||||
}
|
||||
mediaStoreFutureReady = true;
|
||||
if (!mounted) return;
|
||||
|
|
@ -244,10 +248,11 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
|
||||
_imageBytes != null &&
|
||||
_screenshotImage?.image != null &&
|
||||
gUser.showShowImagePreviewWhenSending)
|
||||
SizedBox(
|
||||
height: 100,
|
||||
width: 100 * 9 / 16,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -258,7 +263,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(_imageBytes!),
|
||||
child: CustomPaint(
|
||||
painter: UiImagePainter(_screenshotImage!.image!),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -283,19 +290,15 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
sendingImage = true;
|
||||
});
|
||||
|
||||
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
|
||||
await insertMediaFileInMessagesTable(
|
||||
widget.mediaFileService,
|
||||
widget.selectedGroupIds.toList(),
|
||||
additionalData: widget.additionalData,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
// if (widget.preselectedUser != null) {
|
||||
// Navigator.pop(context, true);
|
||||
// } else {
|
||||
// Navigator.popUntil(context, (route) => route.isFirst, true);
|
||||
// globalUpdateOfHomeViewPageIndex(1);
|
||||
// }
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
|
@ -2,14 +2,15 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.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/data.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -18,13 +19,13 @@ import 'package:twonly/src/utils/screenshot.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_components/select_show_time.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_contact_selection.view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_contact_selection/select_show_time.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/image_item.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers_viewer.dart';
|
||||
import 'package:twonly/src/views/components/emoji_picker.bottom.dart';
|
||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||
import 'package:twonly/src/views/components/notification_badge.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
|
@ -37,16 +38,18 @@ class ShareImageEditorView extends StatefulWidget {
|
|||
const ShareImageEditorView({
|
||||
required this.sharedFromGallery,
|
||||
required this.mediaFileService,
|
||||
this.screenshotImage,
|
||||
this.previewLink,
|
||||
super.key,
|
||||
this.imageBytesFuture,
|
||||
this.sendToGroup,
|
||||
this.mainCameraController,
|
||||
});
|
||||
final ScreenshotImage? imageBytesFuture;
|
||||
final ScreenshotImage? screenshotImage;
|
||||
final Group? sendToGroup;
|
||||
final bool sharedFromGallery;
|
||||
final MediaFileService mediaFileService;
|
||||
final MainCameraController? mainCameraController;
|
||||
final Uri? previewLink;
|
||||
@override
|
||||
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
||||
}
|
||||
|
|
@ -60,7 +63,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
double widthRatio = 1;
|
||||
double heightRatio = 1;
|
||||
double pixelRatio = 1;
|
||||
Uint8List? imageBytes;
|
||||
VideoPlayerController? videoController;
|
||||
ImageItem currentImage = ImageItem();
|
||||
ScreenshotController screenshotController = ScreenshotController();
|
||||
|
|
@ -77,14 +79,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
layers.add(FilterLayerData(key: GlobalKey()));
|
||||
}
|
||||
|
||||
if (widget.previewLink != null) {
|
||||
layers.add(
|
||||
LinkPreviewLayerData(key: GlobalKey(), link: widget.previewLink!),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.sendToGroup != null) {
|
||||
selectedGroupIds.add(widget.sendToGroup!.groupId);
|
||||
}
|
||||
|
||||
if (widget.mediaFileService.mediaFile.type == MediaType.image ||
|
||||
widget.mediaFileService.mediaFile.type == MediaType.gif) {
|
||||
if (widget.imageBytesFuture != null) {
|
||||
loadImage(widget.imageBytesFuture!);
|
||||
if (widget.screenshotImage != null) {
|
||||
loadImage(widget.screenshotImage!);
|
||||
} else {
|
||||
if (widget.mediaFileService.tempPath.existsSync()) {
|
||||
loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath));
|
||||
|
|
@ -411,6 +419,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
mediaStoreFuture: mediaStoreFuture,
|
||||
mediaFileService: mediaService,
|
||||
additionalData: getAdditionalData(),
|
||||
),
|
||||
),
|
||||
) as bool?;
|
||||
|
|
@ -424,8 +433,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Future<ScreenshotImage?> getEditedImageBytes() async {
|
||||
if (layers.length == 1) {
|
||||
if (layers.first is BackgroundLayerData) {
|
||||
final image = (layers.first as BackgroundLayerData).image.bytes;
|
||||
return ScreenshotImage(imageBytes: image);
|
||||
return (layers.first as BackgroundLayerData).image.image;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -454,7 +462,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
return image;
|
||||
}
|
||||
|
||||
Future<Uint8List?> storeImageAsOriginal() async {
|
||||
Future<ScreenshotImage?> storeImageAsOriginal() async {
|
||||
if (mediaService.overlayImagePath.existsSync()) {
|
||||
mediaService.overlayImagePath.deleteSync();
|
||||
}
|
||||
|
|
@ -466,11 +474,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
mediaService.originalPath.deleteSync();
|
||||
}
|
||||
}
|
||||
var bytes = imageBytes;
|
||||
ScreenshotImage? image;
|
||||
var bytes = await widget.screenshotImage?.getBytes();
|
||||
if (media.type == MediaType.gif) {
|
||||
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
|
||||
if (bytes != null) {
|
||||
mediaService.originalPath.writeAsBytesSync(bytes.toList());
|
||||
} else {
|
||||
Log.error('Could not load image bytes for gif!');
|
||||
}
|
||||
} else {
|
||||
final image = await getEditedImageBytes();
|
||||
image = await getEditedImageBytes();
|
||||
if (image == null) return null;
|
||||
bytes = await image.getBytes();
|
||||
if (bytes == null) {
|
||||
|
|
@ -485,16 +498,38 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Log.error('MediaType not supported: ${media.type}');
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
return image;
|
||||
}
|
||||
|
||||
Future<void> loadImage(ScreenshotImage imageBytesFuture) async {
|
||||
imageBytes = await imageBytesFuture.getBytes();
|
||||
// store this image so it can be used as a draft in case the app is restarted
|
||||
Future<void> storeIoImageAsDraft(ScreenshotImage screenshotImage) async {
|
||||
final imageBytes = await screenshotImage.getBytes();
|
||||
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
|
||||
}
|
||||
|
||||
Future<void> loadImage(ScreenshotImage screenshotImage) async {
|
||||
if (screenshotImage.image == null &&
|
||||
screenshotImage.imageBytes == null &&
|
||||
screenshotImage.imageBytesFuture != null) {
|
||||
// this ensures that the imageBytes are defined
|
||||
await storeIoImageAsDraft(screenshotImage);
|
||||
} else {
|
||||
// store this image so it can be used as a draft in case the app is restarted
|
||||
unawaited(storeIoImageAsDraft(screenshotImage));
|
||||
}
|
||||
|
||||
if (screenshotImage.image == null) {
|
||||
final imageBytes = await screenshotImage.getBytes();
|
||||
if (imageBytes != null) {
|
||||
screenshotImage.image = await decodeImageFromList(imageBytes);
|
||||
}
|
||||
}
|
||||
if (screenshotImage.image == null) {
|
||||
Log.error('Could not load screenshotImage.image');
|
||||
return;
|
||||
}
|
||||
|
||||
currentImage.load(screenshotImage);
|
||||
|
||||
await currentImage.load(imageBytes);
|
||||
if (isDisposed) return;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
|
@ -536,6 +571,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
});
|
||||
}
|
||||
|
||||
AdditionalMessageData? getAdditionalData() {
|
||||
AdditionalMessageData? additionalData;
|
||||
|
||||
if (widget.previewLink != null) {
|
||||
additionalData = AdditionalMessageData(
|
||||
type: AdditionalMessageData_Type.LINK,
|
||||
link: widget.previewLink.toString(),
|
||||
);
|
||||
}
|
||||
return additionalData;
|
||||
}
|
||||
|
||||
Future<void> sendImageToSinglePerson() async {
|
||||
if (sendingOrLoadingImage) return;
|
||||
setState(() {
|
||||
|
|
@ -551,6 +598,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
await insertMediaFileInMessagesTable(
|
||||
mediaService,
|
||||
[widget.sendToGroup!.groupId],
|
||||
additionalData: getAdditionalData(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
18
lib/src/views/camera/share_image_editor/image_item.dart
Executable file
18
lib/src/views/camera/share_image_editor/image_item.dart
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
import 'dart:async';
|
||||
import 'package:twonly/src/utils/screenshot.dart';
|
||||
|
||||
class ImageItem {
|
||||
ImageItem();
|
||||
int width = 1;
|
||||
int height = 1;
|
||||
ScreenshotImage? image;
|
||||
Completer<bool> loader = Completer<bool>();
|
||||
|
||||
void load(ScreenshotImage img) {
|
||||
image = img;
|
||||
if (image?.image != null) {
|
||||
height = image!.image!.height;
|
||||
width = image!.image!.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
// ignore_for_file: comment_references
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hand_signature/signature.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/image_item.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
|
||||
/// Layer class with some common properties
|
||||
class Layer {
|
||||
|
|
@ -28,7 +27,6 @@ class Layer {
|
|||
bool showCustomButtons;
|
||||
}
|
||||
|
||||
/// Attributes used by [BackgroundLayer]
|
||||
class BackgroundLayerData extends Layer {
|
||||
BackgroundLayerData({
|
||||
required super.key,
|
||||
|
|
@ -38,6 +36,16 @@ class BackgroundLayerData extends Layer {
|
|||
bool imageLoaded = false;
|
||||
}
|
||||
|
||||
class LinkPreviewLayerData extends Layer {
|
||||
LinkPreviewLayerData({
|
||||
required super.key,
|
||||
required this.link,
|
||||
});
|
||||
Uri link;
|
||||
Metadata? metadata;
|
||||
bool error = false;
|
||||
}
|
||||
|
||||
class FilterLayerData extends Layer {
|
||||
FilterLayerData({
|
||||
required super.key,
|
||||
|
|
@ -46,12 +54,11 @@ class FilterLayerData extends Layer {
|
|||
int page = 1;
|
||||
}
|
||||
|
||||
/// Attributes used by [EmojiLayer]
|
||||
class EmojiLayerData extends Layer {
|
||||
EmojiLayerData({
|
||||
required super.key,
|
||||
this.text = '',
|
||||
this.size = 64,
|
||||
this.size = 94,
|
||||
super.offset,
|
||||
super.opacity,
|
||||
super.rotation,
|
||||
|
|
@ -62,7 +69,6 @@ class EmojiLayerData extends Layer {
|
|||
double size;
|
||||
}
|
||||
|
||||
/// Attributes used by [TextLayer]
|
||||
class TextLayerData extends Layer {
|
||||
TextLayerData({
|
||||
required super.key,
|
||||
|
|
@ -78,9 +84,7 @@ class TextLayerData extends Layer {
|
|||
int textLayersBefore;
|
||||
}
|
||||
|
||||
/// Attributes used by [DrawLayer]
|
||||
class DrawLayerData extends Layer {
|
||||
// String text;
|
||||
DrawLayerData({
|
||||
required super.key,
|
||||
super.offset,
|
||||
62
lib/src/views/camera/share_image_editor/layers/background.layer.dart
Executable file
62
lib/src/views/camera/share_image_editor/layers/background.layer.dart
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
|
||||
class BackgroundLayer extends StatefulWidget {
|
||||
const BackgroundLayer({
|
||||
required this.layerData,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final BackgroundLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
|
||||
@override
|
||||
State<BackgroundLayer> createState() => _BackgroundLayerState();
|
||||
}
|
||||
|
||||
class _BackgroundLayerState extends State<BackgroundLayer> {
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.layerData.imageLoaded = true;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scImage = widget.layerData.image.image;
|
||||
if (scImage == null || scImage.image == null) return Container();
|
||||
return Container(
|
||||
width: widget.layerData.image.width.toDouble(),
|
||||
height: widget.layerData.image.height.toDouble(),
|
||||
padding: EdgeInsets.zero,
|
||||
color: Colors.green,
|
||||
child: CustomPaint(
|
||||
painter: UiImagePainter(scImage.image!),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UiImagePainter extends CustomPainter {
|
||||
UiImagePainter(this.image);
|
||||
final ui.Image image;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.drawImageRect(
|
||||
image,
|
||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Paint(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hand_signature/signature.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:hand_signature/src/utils.dart';
|
||||
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/screenshot.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/draw/custom_hand_signature.dart';
|
||||
|
||||
class DrawLayer extends StatefulWidget {
|
||||
const DrawLayer({
|
||||
|
|
@ -23,8 +22,6 @@ class DrawLayer extends StatefulWidget {
|
|||
class _DrawLayerState extends State<DrawLayer> {
|
||||
Color currentColor = Colors.red;
|
||||
|
||||
ScreenshotController screenshotController = ScreenshotController();
|
||||
|
||||
List<CubicPath> undoList = [];
|
||||
bool skipNextEvent = false;
|
||||
bool showMagnifyingGlass = false;
|
||||
|
|
@ -85,17 +82,11 @@ class _DrawLayerState extends State<DrawLayer> {
|
|||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Screenshot(
|
||||
controller: screenshotController,
|
||||
child: HandSignature(
|
||||
control: widget.layerData.control,
|
||||
drawer: CustomSignatureDrawer(color: currentColor, width: 7),
|
||||
),
|
||||
),
|
||||
child: CustomHandSignature(
|
||||
control: widget.layerData.control,
|
||||
isModificationEnabled: widget.layerData.isEditing,
|
||||
currentColor: currentColor,
|
||||
width: 7,
|
||||
),
|
||||
),
|
||||
if (widget.layerData.isEditing && widget.layerData.showCustomButtons)
|
||||
|
|
@ -211,12 +202,12 @@ class _DrawLayerState extends State<DrawLayer> {
|
|||
top: 50 + (185 * _sliderValue),
|
||||
child: MagnifyingGlass(color: currentColor),
|
||||
),
|
||||
if (!widget.layerData.isEditing)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
// if (!widget.layerData.isEditing)
|
||||
// Positioned.fill(
|
||||
// child: Container(
|
||||
// color: Colors.transparent,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -244,33 +235,3 @@ class MagnifyingGlass extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSignatureDrawer extends HandSignatureDrawer {
|
||||
const CustomSignatureDrawer({
|
||||
this.width = 1.0,
|
||||
this.color = Colors.black,
|
||||
});
|
||||
final Color color;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, List<CubicPath> paths) {
|
||||
for (final path in paths) {
|
||||
var lineColor = color;
|
||||
if (path.setup.args!['color'] != null) {
|
||||
lineColor = path.setup.args!['color'] as Color;
|
||||
} else {
|
||||
path.setup.args!['color'] = color;
|
||||
}
|
||||
final paint = Paint()
|
||||
..color = lineColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..strokeWidth = width;
|
||||
if (path.isFilled) {
|
||||
canvas.drawPath(PathUtil.toLinePath(path.lines), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hand_signature/signature.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:hand_signature/src/utils.dart';
|
||||
|
||||
class CustomHandSignature extends StatelessWidget {
|
||||
const CustomHandSignature({
|
||||
required this.control,
|
||||
required this.isModificationEnabled,
|
||||
required this.currentColor,
|
||||
required this.width,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The controller that manages the creation and manipulation of signature paths.
|
||||
final HandSignatureControl control;
|
||||
final bool isModificationEnabled;
|
||||
final Color currentColor;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
control.params = SignaturePaintParams(
|
||||
color: currentColor,
|
||||
strokeWidth: 7,
|
||||
);
|
||||
|
||||
final drawer = CustomSignatureDrawer(color: currentColor, width: width);
|
||||
|
||||
if (isModificationEnabled) {
|
||||
return HandSignature(
|
||||
control: control,
|
||||
drawer: drawer,
|
||||
);
|
||||
}
|
||||
|
||||
return IgnorePointer(
|
||||
child: ClipRRect(
|
||||
child: HandSignaturePaint(
|
||||
control: control,
|
||||
drawer: drawer,
|
||||
onSize: control.notifyDimension,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSignatureDrawer extends HandSignatureDrawer {
|
||||
const CustomSignatureDrawer({
|
||||
this.width = 1.0,
|
||||
this.color = Colors.black,
|
||||
});
|
||||
final Color color;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, List<CubicPath> paths) {
|
||||
for (final path in paths) {
|
||||
var lineColor = color;
|
||||
if (path.setup.args!['color'] != null) {
|
||||
lineColor = path.setup.args!['color'] as Color;
|
||||
} else {
|
||||
path.setup.args!['color'] = color;
|
||||
}
|
||||
final paint = Paint()
|
||||
..color = lineColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..strokeWidth = width;
|
||||
if (path.isFilled) {
|
||||
canvas.drawPath(PathUtil.toLinePath(path.lines), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
248
lib/src/views/camera/share_image_editor/layers/emoji.layer.dart
Executable file
248
lib/src/views/camera/share_image_editor/layers/emoji.layer.dart
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
|
||||
/// Emoji layer
|
||||
class EmojiLayer extends StatefulWidget {
|
||||
const EmojiLayer({
|
||||
required this.layerData,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final EmojiLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
|
||||
@override
|
||||
State<EmojiLayer> createState() => _EmojiLayerState();
|
||||
}
|
||||
|
||||
class _EmojiLayerState extends State<EmojiLayer> {
|
||||
double initialRotation = 0;
|
||||
Offset initialOffset = Offset.zero;
|
||||
Offset initialFocalPoint = Offset.zero;
|
||||
double initialScale = 1;
|
||||
bool deleteLayer = false;
|
||||
bool twoPointerWhereDown = false;
|
||||
final GlobalKey outlineKey = GlobalKey();
|
||||
final GlobalKey emojiKey = GlobalKey();
|
||||
int pointers = 0;
|
||||
bool display = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.layerData.offset.dy == 0) {
|
||||
// Set the initial offset to the center of the screen
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
widget.layerData.offset = Offset(
|
||||
MediaQuery.of(context).size.width / 2 - (153 / 2),
|
||||
MediaQuery.of(context).size.height / 2 - (153 / 2) - 100,
|
||||
);
|
||||
});
|
||||
display = true;
|
||||
});
|
||||
} else {
|
||||
display = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!display) return Container();
|
||||
if (widget.layerData.isDeleted) return Container();
|
||||
return Stack(
|
||||
key: outlineKey,
|
||||
children: [
|
||||
Positioned(
|
||||
left: widget.layerData.offset.dx,
|
||||
top: widget.layerData.offset.dy,
|
||||
child: PhysicalModel(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(180),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Listener(
|
||||
onPointerUp: (details) {
|
||||
setState(() {
|
||||
pointers--;
|
||||
if (pointers == 0) {
|
||||
twoPointerWhereDown = false;
|
||||
}
|
||||
if (deleteLayer) {
|
||||
widget.layerData.isDeleted = true;
|
||||
widget.onUpdate!();
|
||||
}
|
||||
});
|
||||
},
|
||||
onPointerDown: (details) {
|
||||
setState(() {
|
||||
pointers++;
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onScaleStart: (details) {
|
||||
initialScale = widget.layerData.size;
|
||||
initialRotation = widget.layerData.rotation;
|
||||
initialOffset = widget.layerData.offset;
|
||||
initialFocalPoint =
|
||||
Offset(details.focalPoint.dx, details.focalPoint.dy);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
onScaleUpdate: (details) async {
|
||||
if (twoPointerWhereDown && details.pointerCount != 2) {
|
||||
return;
|
||||
}
|
||||
final outlineBox = outlineKey.currentContext!
|
||||
.findRenderObject()! as RenderBox;
|
||||
|
||||
final emojiBox =
|
||||
emojiKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
final isAtTheBottom =
|
||||
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
|
||||
outlineBox.size.height - 80;
|
||||
final isInTheCenter =
|
||||
MediaQuery.of(context).size.width / 2 - 30 <
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2) &&
|
||||
MediaQuery.of(context).size.width / 2 + 20 >
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2);
|
||||
|
||||
if (isAtTheBottom && isInTheCenter) {
|
||||
if (!deleteLayer) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
deleteLayer = true;
|
||||
} else {
|
||||
deleteLayer = false;
|
||||
}
|
||||
setState(() {
|
||||
twoPointerWhereDown = details.pointerCount >= 2;
|
||||
widget.layerData.size = initialScale * details.scale;
|
||||
// print(widget.layerData.size);
|
||||
widget.layerData.rotation =
|
||||
initialRotation + details.rotation;
|
||||
|
||||
// Update the position based on the translation
|
||||
final dx = (initialOffset.dx) +
|
||||
(details.focalPoint.dx - initialFocalPoint.dx);
|
||||
final dy = (initialOffset.dy) +
|
||||
(details.focalPoint.dy - initialFocalPoint.dy);
|
||||
widget.layerData.offset = Offset(dx, dy);
|
||||
});
|
||||
},
|
||||
child: Transform.rotate(
|
||||
angle: widget.layerData.rotation,
|
||||
key: emojiKey,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(44),
|
||||
color: Colors.transparent,
|
||||
child: ScreenshotEmoji(
|
||||
emoji: widget.layerData.text,
|
||||
displaySize: widget.layerData.size,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pointers > 0)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
child: ActionButton(
|
||||
FontAwesomeIcons.trashCan,
|
||||
tooltipText: '',
|
||||
color: deleteLayer ? Colors.red : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround: https://github.com/twonlyapp/twonly-app/issues/349
|
||||
class ScreenshotEmoji extends StatefulWidget {
|
||||
const ScreenshotEmoji({
|
||||
required this.emoji,
|
||||
required this.displaySize,
|
||||
super.key,
|
||||
});
|
||||
final String emoji;
|
||||
final double displaySize;
|
||||
|
||||
@override
|
||||
State<ScreenshotEmoji> createState() => _ScreenshotEmojiState();
|
||||
}
|
||||
|
||||
class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
|
||||
final GlobalKey _boundaryKey = GlobalKey();
|
||||
ui.Image? _capturedImage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Capture the emoji immediately after the first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _captureEmoji());
|
||||
}
|
||||
|
||||
Future<void> _captureEmoji() async {
|
||||
try {
|
||||
final boundary = _boundaryKey.currentContext?.findRenderObject()
|
||||
as RenderRepaintBoundary?;
|
||||
if (boundary == null) return;
|
||||
|
||||
final image = await boundary.toImage(pixelRatio: 4);
|
||||
setState(() {
|
||||
_capturedImage = image;
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error('Error capturing emoji: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_capturedImage != null) {
|
||||
return SizedBox(
|
||||
width: widget.displaySize,
|
||||
height: widget.displaySize,
|
||||
child: RawImage(
|
||||
image: _capturedImage,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -200, // hide from the user as the size changes with the image
|
||||
child: RepaintBoundary(
|
||||
key: _boundaryKey,
|
||||
child: Text(
|
||||
widget.emoji,
|
||||
style: const TextStyle(fontSize: 94),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: widget.displaySize, height: widget.displaySize),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@ import 'dart:async';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filters/datetime_filter.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filters/image_filter.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filters/location_filter.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filters/image_filter.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filters/location_filter.dart';
|
||||
|
||||
/// Main layer
|
||||
class FilterLayer extends StatefulWidget {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart';
|
||||
|
||||
class DateTimeFilter extends StatelessWidget {
|
||||
const DateTimeFilter({super.key, this.color = Colors.white});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart';
|
||||
|
||||
class ImageFilter extends StatelessWidget {
|
||||
const ImageFilter({required this.imagePath, super.key});
|
||||
|
|
@ -11,8 +11,8 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filters/datetime_filter.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filters/datetime_filter.dart';
|
||||
|
||||
class LocationFilter extends StatefulWidget {
|
||||
const LocationFilter({super.key});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/custom.card.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
|
||||
class LinkPreviewLayer extends StatefulWidget {
|
||||
const LinkPreviewLayer({
|
||||
required this.layerData,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final LinkPreviewLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
|
||||
@override
|
||||
State<LinkPreviewLayer> createState() => _LinkPreviewLayerState();
|
||||
}
|
||||
|
||||
class _LinkPreviewLayerState extends State<LinkPreviewLayer> {
|
||||
Metadata? metadata;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
if (widget.layerData.metadata == null) {
|
||||
widget.layerData.metadata =
|
||||
await getMetadata(widget.layerData.link.toString());
|
||||
if (widget.layerData.metadata == null) {
|
||||
widget.layerData.error = true;
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.layerData.error) {
|
||||
return Container();
|
||||
}
|
||||
final meta = widget.layerData.metadata;
|
||||
late Widget child;
|
||||
if (meta == null) {
|
||||
child = const ThreeRotatingDots(size: 30);
|
||||
} else if (meta.title == null) {
|
||||
return Container();
|
||||
} else if (meta.vendor == Vendor.mastodonSocialMediaPosting) {
|
||||
child = MastodonPostCard(info: meta);
|
||||
} else if (meta.vendor == Vendor.twitterPosting) {
|
||||
child = TwitterPostCard(info: meta);
|
||||
} else if (meta.vendor == Vendor.youtubeVideo) {
|
||||
child = YouTubePostCard(info: meta);
|
||||
} else {
|
||||
child = CustomLinkCard(info: meta);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
|
||||
class CustomLinkCard extends StatelessWidget {
|
||||
const CustomLinkCard({required this.info, super.key});
|
||||
final Metadata info;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E1E1E),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Uri.parse(info.url).host.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: context.color.primary,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
substringBy(info.title ?? 'Link Preview', 35),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (info.desc != null && info.desc != 'null') ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
substringBy(info.desc!, 500),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFB0B0B0),
|
||||
fontSize: 13,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (info.image != null && info.image != 'null')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: info.image!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
|
||||
class MastodonPostCard extends StatelessWidget {
|
||||
const MastodonPostCard({required this.info, super.key});
|
||||
final Metadata info;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const backgroundColor = Color(0xFF282C37);
|
||||
const secondaryTextColor = Color(0xFF9BA3AF);
|
||||
const accentColor = Color(0xFF6364FF);
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.mastodon,
|
||||
color: accentColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
substringBy(info.title ?? 'Mastodon User', 37),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (info.desc != null && info.desc != 'null')
|
||||
Text(
|
||||
substringBy(info.desc!, 1000),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
if (info.image != null && info.image != 'null')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 250),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: info.image!,
|
||||
fit: BoxFit.contain,
|
||||
width: double.infinity,
|
||||
placeholder: (context, url) => Container(
|
||||
height: 150,
|
||||
color: Colors.black12,
|
||||
child: const Center(
|
||||
child: ThreeRotatingDots(size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildAction(
|
||||
Icons.repeat,
|
||||
'${info.shareAction ?? 0}',
|
||||
secondaryTextColor,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
_buildAction(
|
||||
Icons.star_border,
|
||||
'${info.likeAction ?? 0}',
|
||||
secondaryTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAction(IconData icon, String count, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
if (count.isNotEmpty && count != '0') ...[
|
||||
const SizedBox(width: 5),
|
||||
Text(count, style: TextStyle(color: color, fontSize: 13)),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
// Assuming the same Metadata import structure
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
|
||||
class TwitterPostCard extends StatelessWidget {
|
||||
const TwitterPostCard({required this.info, super.key});
|
||||
final Metadata info;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Classic Twitter Brand Colors
|
||||
const twitterBlue = Color(0xFF1DA1F2);
|
||||
const backgroundWhite = Colors.white;
|
||||
const primaryText = Color(0xFF14171A);
|
||||
const borderColor = Color(0xFFE1E8ED);
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.9, // Twitter cards often feel a bit wider
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundWhite,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.twitter,
|
||||
color: twitterBlue,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
substringBy(info.title ?? 'Twitter User', 37),
|
||||
style: const TextStyle(
|
||||
color: primaryText,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (info.desc != null && info.desc != 'null')
|
||||
Text(
|
||||
substringBy(info.desc!, 1000),
|
||||
style: const TextStyle(
|
||||
color: primaryText,
|
||||
fontSize: 15,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
if (info.image != null && info.image != 'null')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: borderColor),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: info.image!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
placeholder: (context, url) => Container(
|
||||
height: 150,
|
||||
color: const Color(0xFFF5F8FA),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(twitterBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
|
||||
class YouTubePostCard extends StatelessWidget {
|
||||
const YouTubePostCard({required this.info, super.key});
|
||||
final Metadata info;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const ytBlack = Color(0xFF0F0F0F);
|
||||
const ytWhite = Colors.white;
|
||||
const ytRed = Color.fromARGB(255, 255, 1, 51);
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ytBlack,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: info.image ?? '',
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) =>
|
||||
Container(color: Colors.white10),
|
||||
errorWidget: (context, url, error) => const ColoredBox(
|
||||
color: Colors.white10,
|
||||
child: Icon(
|
||||
Icons.play_circle_outline,
|
||||
color: ytWhite,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.youtube,
|
||||
color: ytRed,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
substringBy(info.title ?? 'Video Title', 600),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: ytWhite,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
// Based on: https://github.com/sur950/any_link_preview
|
||||
// Copyright (c) 2020-2024 Konakanchi Venkata Suresh Babu
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:html/dom.dart' show Document;
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/html.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/other.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/twitter.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/utils.dart';
|
||||
|
||||
Future<Metadata?> getMetadata(String link) async {
|
||||
const userAgent = 'WhatsApp/2.21.12.21 A';
|
||||
try {
|
||||
final linkToFetch = link.trim();
|
||||
final info = await getInfo(linkToFetch, userAgent);
|
||||
|
||||
final img = info?.image ?? '';
|
||||
if (img.isNotEmpty) {
|
||||
info?.image = resolveImageUrl(link, img);
|
||||
}
|
||||
|
||||
return info;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String resolveImageUrl(String baseUrl, String imageUrl) {
|
||||
try {
|
||||
final baseUri = Uri.parse(baseUrl);
|
||||
return baseUri.resolve(imageUrl).toString();
|
||||
} catch (e) {
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Metadata?> getInfo(
|
||||
String url,
|
||||
String userAgent,
|
||||
) async {
|
||||
Metadata? info;
|
||||
|
||||
info = Metadata()
|
||||
..title = getDomain(url)
|
||||
..desc = url
|
||||
..siteName = getDomain(url)
|
||||
..url = url;
|
||||
|
||||
try {
|
||||
final videoId = getYouTubeVideoId(url);
|
||||
final response = videoId == null
|
||||
? await fetchWithRedirects(
|
||||
Uri.parse(url),
|
||||
userAgent: userAgent,
|
||||
)
|
||||
: await getYoutubeData(
|
||||
videoId,
|
||||
userAgent,
|
||||
);
|
||||
|
||||
final headerContentType = response.headers['content-type'];
|
||||
|
||||
if (headerContentType != null && headerContentType.startsWith('image/')) {
|
||||
info
|
||||
..title = ''
|
||||
..desc = ''
|
||||
..siteName = ''
|
||||
..image = url;
|
||||
return info;
|
||||
}
|
||||
|
||||
final document = responseToDocument(response);
|
||||
if (document == null) return info;
|
||||
|
||||
final data_ = _parse(document, url);
|
||||
|
||||
return data_;
|
||||
} catch (error) {
|
||||
Log.warn('Error in $url response ($error)');
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
Document? responseToDocument(http.Response response) {
|
||||
if (response.statusCode != 200) return null;
|
||||
|
||||
Document? document;
|
||||
try {
|
||||
document = parse(utf8.decode(response.bodyBytes));
|
||||
} catch (err) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
Metadata _parse(Document? document, String url) {
|
||||
final output = Metadata()..url = url;
|
||||
|
||||
final allParsers = [
|
||||
// start with vendor specific to parse the vendor type
|
||||
MastodonParser(document),
|
||||
YoutubeParser(document, url),
|
||||
TwitterParser(document, url),
|
||||
|
||||
JsonLdParser(document),
|
||||
OpenGraphParser(document),
|
||||
HtmlMetaParser(document),
|
||||
OtherParser(document),
|
||||
];
|
||||
|
||||
for (final parser in allParsers) {
|
||||
try {
|
||||
output.vendor ??= parser.vendor;
|
||||
output.title ??= parser.title;
|
||||
output.desc ??= parser.desc;
|
||||
if (output.vendor == Vendor.twitterPosting) {
|
||||
if (output.image == null) {
|
||||
if (parser.image?.contains('/media/') ?? false) {
|
||||
output.image ??= parser.image;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.image ??= parser.image;
|
||||
}
|
||||
output.siteName ??= parser.siteName;
|
||||
output.publishDate ??= parser.publishDate;
|
||||
output.likeAction ??= parser.likeAction;
|
||||
output.shareAction ??= parser.shareAction;
|
||||
if (output.hasAllMetadata) break;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
enum Vendor { mastodonSocialMediaPosting, youtubeVideo, twitterPosting }
|
||||
|
||||
mixin BaseMetaInfo {
|
||||
late String url;
|
||||
String? title;
|
||||
String? desc;
|
||||
String? image;
|
||||
String? siteName;
|
||||
|
||||
Vendor? vendor;
|
||||
|
||||
DateTime? publishDate;
|
||||
int? likeAction; // https://schema.org/LikeAction
|
||||
int? shareAction; // https://schema.org/ShareAction
|
||||
|
||||
/// Returns `true` if any parameter other than [url] is filled.
|
||||
bool get hasData =>
|
||||
((title?.isNotEmpty ?? false) && title != 'null') ||
|
||||
((desc?.isNotEmpty ?? false) && desc != 'null') ||
|
||||
((image?.isNotEmpty ?? false) && image != 'null');
|
||||
}
|
||||
|
||||
/// Container class for Metadata.
|
||||
class Metadata with BaseMetaInfo {
|
||||
Metadata();
|
||||
bool get hasAllMetadata {
|
||||
return title != null && desc != null && image != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:html/dom.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'util.dart';
|
||||
|
||||
/// Parses [Metadata] from `<meta>`, `<title>`, and `<img>` tags.
|
||||
class HtmlMetaParser with BaseMetaInfo {
|
||||
HtmlMetaParser(this._document);
|
||||
|
||||
/// The [Document] to parse.
|
||||
final Document? _document;
|
||||
|
||||
/// Get the [Metadata.title] from the <title> tag.
|
||||
@override
|
||||
String? get title => _document?.head?.querySelector('title')?.text;
|
||||
|
||||
/// Get the [Metadata.desc] from the content of the
|
||||
/// <meta name="description"> tag.
|
||||
@override
|
||||
String? get desc => _document?.head
|
||||
?.querySelector("meta[name='description']")
|
||||
?.attributes
|
||||
.get('content');
|
||||
|
||||
/// Get the [Metadata.image] from the first <img> tag in the body.
|
||||
@override
|
||||
String? get image =>
|
||||
_document?.body?.querySelector('img')?.attributes.get('src');
|
||||
|
||||
/// Get the [Metadata.siteName] from the content of the
|
||||
/// <meta name="site_name"> meta tag.
|
||||
@override
|
||||
String? get siteName => _document?.head
|
||||
?.querySelector("meta[name='site_name']")
|
||||
?.attributes
|
||||
.get('content');
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
||||
|
||||
/// Parses [Metadata] from `json-ld` data in `<script>` tags.
|
||||
class JsonLdParser with BaseMetaInfo {
|
||||
JsonLdParser(this.document) {
|
||||
_parseToJson(document);
|
||||
}
|
||||
|
||||
Document? document;
|
||||
Map<String, dynamic>? _jsonData;
|
||||
|
||||
void _parseToJson(Document? document) {
|
||||
try {
|
||||
final data = document?.head
|
||||
?.querySelector("script[type='application/ld+json']")
|
||||
?.innerHtml;
|
||||
if (data == null) return;
|
||||
// For multiline json file
|
||||
// Replacing all new line characters with empty space
|
||||
// before performing json decode on data
|
||||
_jsonData =
|
||||
jsonDecode(data.replaceAll('\n', ' ')) as Map<String, dynamic>;
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/// Get the [Metadata.title] from the <title> tag.
|
||||
@override
|
||||
String? get title {
|
||||
final data = _jsonData;
|
||||
if (data is Map<String, dynamic>) {
|
||||
return data['name'] as String? ?? data['headline'] as String?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
int? get shareAction {
|
||||
final statistics = _jsonData?['interactionStatistic'] as List<dynamic>?;
|
||||
if (statistics != null) {
|
||||
for (final statsDy in statistics) {
|
||||
final stats = statsDy as Map<String, dynamic>?;
|
||||
if (stats != null) {
|
||||
if (stats['interactionType'] == 'https://schema.org/ShareAction') {
|
||||
return stats['userInteractionCount'] as int?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
int? get likeAction {
|
||||
final statistics = _jsonData?['interactionStatistic'] as List<dynamic>?;
|
||||
if (statistics != null) {
|
||||
for (final statsDy in statistics) {
|
||||
final stats = statsDy as Map<String, dynamic>?;
|
||||
if (stats != null) {
|
||||
if (stats['interactionType'] == 'https://schema.org/LikeAction') {
|
||||
return stats['userInteractionCount'] as int?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String? get desc {
|
||||
return _jsonData?['description'] as String?;
|
||||
}
|
||||
|
||||
/// Get the [Metadata.image] from the first <img> tag in the body.
|
||||
@override
|
||||
String? get image {
|
||||
final data = _jsonData;
|
||||
return _imgResultToStr(
|
||||
data?.getDynamic('logo') ?? data?.getDynamic('image'),
|
||||
);
|
||||
}
|
||||
|
||||
/// JSON LD does not have a siteName property, so we get it from
|
||||
/// [og:site_name] if available.
|
||||
@override
|
||||
String? get siteName => OpenGraphParser(document).siteName;
|
||||
|
||||
String? _imgResultToStr(dynamic result) {
|
||||
if (result is List && result.isNotEmpty) {
|
||||
final tmp = result.first;
|
||||
if (tmp is String) return tmp;
|
||||
}
|
||||
if (result is String) return result;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import 'package:html/dom.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
|
||||
class MastodonParser with BaseMetaInfo {
|
||||
MastodonParser(this._document);
|
||||
final Document? _document;
|
||||
|
||||
@override
|
||||
Vendor? get vendor => ((_document?.head?.innerHtml
|
||||
.contains('"repository":"mastodon/mastodon"') ??
|
||||
false) &&
|
||||
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
|
||||
? Vendor.mastodonSocialMediaPosting
|
||||
: null;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:html/dom.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'util.dart';
|
||||
|
||||
/// Parses [Metadata] from `<meta property='og:*'>` tags.
|
||||
class OpenGraphParser with BaseMetaInfo {
|
||||
OpenGraphParser(this._document);
|
||||
|
||||
/// The [Document] to parse.
|
||||
final Document? _document;
|
||||
|
||||
/// Get [Metadata.title] from 'og:title'.
|
||||
@override
|
||||
String? get title => getProperty(_document, property: 'og:title');
|
||||
|
||||
/// Get [Metadata.desc] from 'og:description'.
|
||||
@override
|
||||
String? get desc => getProperty(_document, property: 'og:description');
|
||||
|
||||
/// Get [Metadata.image] from 'og:image'.
|
||||
@override
|
||||
String? get image => getProperty(_document, property: 'og:image');
|
||||
|
||||
/// Get [Metadata.siteName] from 'og:site_name'.
|
||||
@override
|
||||
String? get siteName => getProperty(_document, property: 'og:site_name');
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:html/dom.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'util.dart';
|
||||
|
||||
/// Parses [Metadata] from `<meta attribute: 'name' property='*'>` tags.
|
||||
class OtherParser with BaseMetaInfo {
|
||||
OtherParser(this._document);
|
||||
|
||||
final Document? _document;
|
||||
|
||||
@override
|
||||
String? get title =>
|
||||
getProperty(_document, attribute: 'name', property: 'title');
|
||||
|
||||
@override
|
||||
String? get desc =>
|
||||
getProperty(_document, attribute: 'name', property: 'description');
|
||||
|
||||
@override
|
||||
String? get image =>
|
||||
getProperty(_document, attribute: 'name', property: 'image');
|
||||
|
||||
@override
|
||||
String? get siteName =>
|
||||
getProperty(_document, attribute: 'name', property: 'site_name');
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:html/dom.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
||||
|
||||
/// Parses [Metadata] from `<meta property='twitter:*'>` tags.
|
||||
class TwitterParser with BaseMetaInfo {
|
||||
TwitterParser(this._document, this._url);
|
||||
final Document? _document;
|
||||
final String _url;
|
||||
|
||||
@override
|
||||
String? get title =>
|
||||
getProperty(_document, attribute: 'name', property: 'twitter:title') ??
|
||||
getProperty(_document, property: 'twitter:title');
|
||||
|
||||
@override
|
||||
String? get desc =>
|
||||
getProperty(
|
||||
_document,
|
||||
attribute: 'name',
|
||||
property: 'twitter:description',
|
||||
) ??
|
||||
getProperty(_document, property: 'twitter:description');
|
||||
|
||||
@override
|
||||
String? get image =>
|
||||
getProperty(_document, attribute: 'name', property: 'twitter:image') ??
|
||||
getProperty(_document, property: 'twitter:image');
|
||||
|
||||
@override
|
||||
Vendor? get vendor =>
|
||||
_url.startsWith('https://x.com/') && _url.contains('/status/')
|
||||
? Vendor.twitterPosting
|
||||
: null;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:html/dom.dart';
|
||||
|
||||
// ignore: strict_raw_type
|
||||
extension GetMethod on Map {
|
||||
String? get(dynamic key) {
|
||||
final value = this[key];
|
||||
if (value is List<String>) return value.first;
|
||||
return value?.toString();
|
||||
}
|
||||
|
||||
dynamic getDynamic(dynamic key) {
|
||||
return this[key];
|
||||
}
|
||||
}
|
||||
|
||||
String? getDomain(String url) {
|
||||
return Uri.parse(url).host.split('.')[0];
|
||||
}
|
||||
|
||||
String? getProperty(
|
||||
Document? document, {
|
||||
String tag = 'meta',
|
||||
String attribute = 'property',
|
||||
String? property,
|
||||
String key = 'content',
|
||||
}) {
|
||||
final value_ = document
|
||||
?.getElementsByTagName(tag)
|
||||
.cast<Element?>()
|
||||
.firstWhere(
|
||||
(element) => element?.attributes[attribute] == property,
|
||||
orElse: () => null,
|
||||
)
|
||||
?.attributes
|
||||
.get(key);
|
||||
|
||||
return value_;
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import 'dart:convert';
|
||||
import 'package:html/dom.dart';
|
||||
import 'base.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class YoutubeParser with BaseMetaInfo {
|
||||
YoutubeParser(this.document, this._url) {
|
||||
_jsonData = _parseToJson(document);
|
||||
}
|
||||
|
||||
final String _url;
|
||||
|
||||
Document? document;
|
||||
dynamic _jsonData;
|
||||
|
||||
dynamic _parseToJson(Document? document) {
|
||||
try {
|
||||
final data = document?.outerHtml
|
||||
.replaceAll('<html><head></head><body>', '')
|
||||
.replaceAll('</body></html>', '');
|
||||
if (data == null) return null;
|
||||
/* For multiline json file */
|
||||
// Replacing all new line characters with empty space
|
||||
// before performing json decode on data
|
||||
final d = jsonDecode(data.replaceAll('\n', ' '));
|
||||
return d;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? get title {
|
||||
final data = _jsonData;
|
||||
if (data is List<Map<String, dynamic>>) {
|
||||
return data.first['title'] as String?;
|
||||
} else if (data is Map) {
|
||||
return data.get('title');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String? get image {
|
||||
final data = _jsonData;
|
||||
if (data is List<Map<String, dynamic>> && data.isNotEmpty) {
|
||||
return _imgResultToStr(data.first['thumbnail_url']);
|
||||
} else if (data is Map) {
|
||||
return _imgResultToStr(data.getDynamic('thumbnail_url'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String? get siteName {
|
||||
final data = _jsonData;
|
||||
if (data is List<Map<String, dynamic>>) {
|
||||
return data.first['provider_name'] as String?;
|
||||
} else if (data is Map) {
|
||||
return data.get('provider_name');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Vendor? get vendor => (Uri.parse(_url).host.contains('youtube.com'))
|
||||
? Vendor.youtubeVideo
|
||||
: null;
|
||||
|
||||
String? _imgResultToStr(dynamic result) {
|
||||
if (result is List && result.isNotEmpty) {
|
||||
final tmp = result.first;
|
||||
if (tmp is String) return tmp;
|
||||
}
|
||||
if (result is String) return result;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:http/http.dart' as http;
|
||||
|
||||
Future<http.Response> fetchWithRedirects(
|
||||
Uri uri, {
|
||||
int maxRedirects = 7,
|
||||
Map<String, String> headers = const {},
|
||||
String? userAgent,
|
||||
}) async {
|
||||
const userAgentFallback = 'WhatsApp/2.21.12.21 A';
|
||||
final allHeaders = <String, String>{
|
||||
...headers,
|
||||
'User-Agent': userAgent ?? userAgentFallback,
|
||||
};
|
||||
var response = await http.get(uri, headers: allHeaders);
|
||||
var redirectCount = 0;
|
||||
|
||||
while (_isRedirect(response) && redirectCount < maxRedirects) {
|
||||
final location = response.headers['location'];
|
||||
if (location == null) {
|
||||
throw Exception('HTTP redirect without Location header');
|
||||
}
|
||||
|
||||
response = await http.get(Uri.parse(location), headers: allHeaders);
|
||||
redirectCount++;
|
||||
}
|
||||
|
||||
if (redirectCount >= maxRedirects) {
|
||||
throw Exception('Maximum redirect limit reached');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
bool _isRedirect(http.Response response) {
|
||||
return [301, 302, 303, 307, 308].contains(response.statusCode);
|
||||
}
|
||||
|
||||
Future<http.Response> getYoutubeData(String videoId, String userAgent) async {
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
'https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=$videoId&format=json',
|
||||
),
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
String? getYouTubeVideoId(String url) {
|
||||
// Regular expression pattern to detect YouTube URLs
|
||||
// with or without a proxy prefix
|
||||
final regExp = RegExp(
|
||||
r'(?:https?:\/\/)?(?:[^\/]+\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|v\/|.+\?v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})',
|
||||
);
|
||||
|
||||
// Apply the regex to the URL
|
||||
final match = regExp.firstMatch(url);
|
||||
|
||||
// If a match is found, return the first capture group, which is the video ID
|
||||
return match?.group(1);
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import 'package:flutter/services.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
|
||||
/// Text layer
|
||||
class TextLayer extends StatefulWidget {
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/background_layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/draw_layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/emoji_layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/layers/text_layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/background.layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/draw.layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/emoji.layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/filter.layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview.layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/text.layer.dart';
|
||||
|
||||
/// View stacked layers (unbounded height, width)
|
||||
class LayersViewer extends StatelessWidget {
|
||||
|
|
@ -37,7 +38,10 @@ class LayersViewer extends StatelessWidget {
|
|||
...layers
|
||||
.where(
|
||||
(layerItem) =>
|
||||
layerItem is EmojiLayerData || layerItem is DrawLayerData,
|
||||
layerItem is EmojiLayerData ||
|
||||
layerItem is DrawLayerData ||
|
||||
layerItem is LinkPreviewLayerData ||
|
||||
layerItem is TextLayerData,
|
||||
)
|
||||
.map((layerItem) {
|
||||
if (layerItem is EmojiLayerData) {
|
||||
|
|
@ -52,16 +56,21 @@ class LayersViewer extends StatelessWidget {
|
|||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is TextLayerData) {
|
||||
return TextLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is LinkPreviewLayerData) {
|
||||
return LinkPreviewLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
...layers.whereType<TextLayerData>().map((layerItem) {
|
||||
return TextLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import 'package:twonly/src/views/settings/help/changelog.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/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/user_study/user_study_welcome.view.dart';
|
||||
|
||||
class ChatListView extends StatefulWidget {
|
||||
const ChatListView({super.key});
|
||||
|
|
@ -58,31 +59,49 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
});
|
||||
});
|
||||
|
||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||
final changeLogHash =
|
||||
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
|
||||
if (!gUser.hideChangeLog &&
|
||||
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||
await updateUserdata((u) {
|
||||
u.lastChangeLogHash = changeLogHash;
|
||||
return u;
|
||||
});
|
||||
if (!mounted) return;
|
||||
// only show changelog to people who already have contacts
|
||||
// this prevents that this is shown directly after the user registered
|
||||
if (_groupsNotPinned.isNotEmpty) {
|
||||
// In case the user is already a Tester, ask him for permission.
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (gUser.subscriptionPlan == SubscriptionPlan.Tester.name &&
|
||||
!gUser.askedForUserStudyPermission) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChangeLogView(
|
||||
changeLog: changeLog,
|
||||
return const UserStudyWelcomeView(
|
||||
wasOpenedAutomatic: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||
final changeLogHash =
|
||||
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
|
||||
if (!gUser.hideChangeLog &&
|
||||
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||
await updateUserdata((u) {
|
||||
u.lastChangeLogHash = changeLogHash;
|
||||
return u;
|
||||
});
|
||||
if (!mounted) return;
|
||||
// only show changelog to people who already have contacts
|
||||
// this prevents that this is shown directly after the user registered
|
||||
if (_groupsNotPinned.isNotEmpty) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChangeLogView(
|
||||
changeLog: changeLog,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -295,21 +314,32 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton.small(
|
||||
backgroundColor: context.color.primary,
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const PublicProfileView();
|
||||
},
|
||||
Material(
|
||||
elevation: 3,
|
||||
shape: const CircleBorder(),
|
||||
color: context.color.primary,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const PublicProfileView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.qrcode,
|
||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.qrcode,
|
||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:twonly/src/database/tables/messages.table.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
|
||||
|
|
@ -135,6 +135,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
_previewMessages.where((x) => x.type == MessageType.media).toList();
|
||||
if (msgs.isNotEmpty &&
|
||||
msgs.first.type == MessageType.media &&
|
||||
!msgs.first.isDeletedFromSender &&
|
||||
msgs.first.senderId != null &&
|
||||
msgs.first.openedAt == null) {
|
||||
_hasNonOpenedMediaFile = true;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class FriendlyMessageTime extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (message.modifiedAt != null)
|
||||
if (message.modifiedAt != null && !message.isDeletedFromSender)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: SizedBox(
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dar
|
|||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/chats/message_info.view.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/context_menu.component.dart';
|
||||
import 'package:twonly/src/views/components/emoji_picker.bottom.dart';
|
||||
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
||||
|
||||
class MessageInput extends StatefulWidget {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
case MessageSendState.receivedOpened:
|
||||
icon = Icon(Icons.crop_square, size: 14, color: color);
|
||||
if (message.content != null) {
|
||||
if (isEmoji(message.content!)) {
|
||||
if (isOneEmoji(message.content!)) {
|
||||
icon = Text(
|
||||
message.content!,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
|||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to.view.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer_components/additional_message_content.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
|
|
@ -91,8 +92,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
_noScreenshot.screenshotOn();
|
||||
_subscription.cancel();
|
||||
downloadStateListener?.cancel();
|
||||
videoController?.dispose();
|
||||
final tmp = videoController;
|
||||
videoController = null;
|
||||
tmp?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -493,6 +495,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _loader() {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: ThreeRotatingDots(
|
||||
size: 40,
|
||||
color: context.color.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -500,17 +515,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (_showDownloadingLoader)
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: ThreeRotatingDots(
|
||||
size: 40,
|
||||
color: context.color.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_showDownloadingLoader) _loader(),
|
||||
if ((currentMedia != null || videoController != null) &&
|
||||
(canBeSeenUntil == null || progress >= 0))
|
||||
GestureDetector(
|
||||
|
|
@ -549,9 +554,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if (displayTwonlyPresent)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
loadCurrentMediaFile(showTwonly: true);
|
||||
},
|
||||
onTap: () => loadCurrentMediaFile(showTwonly: true),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -575,26 +578,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 30),
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (currentMedia != null &&
|
||||
currentMedia?.mediaFile.downloadState != DownloadState.ready)
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: _loader()),
|
||||
if (canBeSeenUntil != null || progress >= 0)
|
||||
Positioned(
|
||||
right: 20,
|
||||
|
|
@ -700,6 +691,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (currentMessage != null)
|
||||
AdditionalMessageContent(currentMessage!),
|
||||
if (currentMedia != null)
|
||||
ReactionButtons(
|
||||
show: showShortReactions,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AdditionalMessageContent extends StatelessWidget {
|
||||
const AdditionalMessageContent(this.message, {super.key});
|
||||
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (message.additionalMessageData == null) return Container();
|
||||
try {
|
||||
final data =
|
||||
AdditionalMessageData.fromBuffer(message.additionalMessageData!);
|
||||
|
||||
switch (data.type) {
|
||||
case AdditionalMessageData_Type.LINK:
|
||||
if (!data.link.startsWith('http://') &&
|
||||
!data.link.startsWith('https://')) {
|
||||
return Container();
|
||||
}
|
||||
return Positioned(
|
||||
bottom: 150,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const FaIcon(FontAwesomeIcons.shareFromSquare),
|
||||
onPressed: () => launchUrlString(data.link),
|
||||
label: Text(
|
||||
substringBy(
|
||||
data.link
|
||||
.replaceAll('http://', '')
|
||||
.replaceAll('https://', ''),
|
||||
30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
default:
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/emoji_picker.bottom.dart';
|
||||
|
||||
class ReactionButtons extends StatefulWidget {
|
||||
const ReactionButtons({
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,8 +6,8 @@ class BlinkWidget extends StatefulWidget {
|
|||
required this.child,
|
||||
required this.enabled,
|
||||
super.key,
|
||||
this.blinkDuration = const Duration(milliseconds: 2500),
|
||||
this.interval = const Duration(milliseconds: 100),
|
||||
this.blinkDuration = const Duration(milliseconds: 2000),
|
||||
this.interval = const Duration(milliseconds: 300),
|
||||
this.visibleOpacity = 1.0,
|
||||
this.hiddenOpacity = 0.05,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
|
||||
class EmojiPickerBottom extends StatelessWidget {
|
||||
const EmojiPickerBottom({super.key});
|
||||
|
|
@ -14,7 +14,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list.view.dart';
|
||||
import 'package:twonly/src/views/memories/memories.view.dart';
|
||||
|
||||
|
|
@ -120,7 +120,13 @@ class HomeViewState extends State<HomeView> {
|
|||
|
||||
_intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen(
|
||||
(f) {
|
||||
if (mounted) handleIntentSharedFile(context, f);
|
||||
if (mounted) {
|
||||
handleIntentSharedFile(
|
||||
context,
|
||||
f,
|
||||
_mainCameraController.setSharedLinkForPreview,
|
||||
);
|
||||
}
|
||||
},
|
||||
// ignore: inference_failure_on_untyped_parameter
|
||||
onError: (err) {
|
||||
|
|
@ -129,7 +135,13 @@ class HomeViewState extends State<HomeView> {
|
|||
);
|
||||
|
||||
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
||||
if (mounted) handleIntentSharedFile(context, f);
|
||||
if (mounted) {
|
||||
handleIntentSharedFile(
|
||||
context,
|
||||
f,
|
||||
_mainCameraController.setSharedLinkForPreview,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -172,9 +184,9 @@ class HomeViewState extends State<HomeView> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onDoubleTap: offsetRatio == 0
|
||||
? _mainCameraController.toggleSelectedCamera
|
||||
: null,
|
||||
onDoubleTap:
|
||||
offsetRatio == 0 ? _mainCameraController.onDoubleTap : null,
|
||||
onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
MainCameraPreview(mainCameraController: _mainCameraController),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||
import 'package:twonly/src/views/components/video_player_wrapper.dart';
|
||||
|
|
@ -105,7 +105,11 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
return;
|
||||
}
|
||||
|
||||
orgMediaService.storedPath.copySync(newMediaService.originalPath.path);
|
||||
if (orgMediaService.storedPath.existsSync()) {
|
||||
orgMediaService.storedPath.copySync(newMediaService.originalPath.path);
|
||||
} else if (orgMediaService.tempPath.existsSync()) {
|
||||
orgMediaService.tempPath.copySync(newMediaService.originalPath.path);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue