mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
fix smaller layout issues and add sticker creation button to stored images
This commit is contained in:
parent
76018bd780
commit
4337db58be
19 changed files with 798 additions and 143 deletions
|
|
@ -1906,6 +1906,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Group name'**
|
/// **'Group name'**
|
||||||
String get groupNameInput;
|
String get groupNameInput;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupNamePlaceholder.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enter name here'**
|
||||||
|
String get contactGroupNamePlaceholder;
|
||||||
|
|
||||||
/// No description provided for @groupMembers.
|
/// No description provided for @groupMembers.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -4774,6 +4780,54 @@ abstract class AppLocalizations {
|
||||||
/// **'Home screen widget'**
|
/// **'Home screen widget'**
|
||||||
String get contactGroupUsedByWidget;
|
String get contactGroupUsedByWidget;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupAddWidgetSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add a widget for this contact group.'**
|
||||||
|
String get contactGroupAddWidgetSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add a home screen widget'**
|
||||||
|
String get contactGroupWidgetTutorialTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialAndroidStep1.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Touch and hold an empty area on your home screen, then tap Widgets.'**
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep1;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialAndroidStep2.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Find twonly, then drag its widget onto your home screen.'**
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep2;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialAndroidStep3.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select this contact group in the setup screen, then tap Save.'**
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep3;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialIosStep1.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Touch and hold an empty area on your Home Screen, then tap Edit and Add Widget.'**
|
||||||
|
String get contactGroupWidgetTutorialIosStep1;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialIosStep2.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Find twonly, choose a size, then tap Add Widget.'**
|
||||||
|
String get contactGroupWidgetTutorialIosStep2;
|
||||||
|
|
||||||
|
/// No description provided for @contactGroupWidgetTutorialIosStep3.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Touch and hold the widget, tap Edit Widget, then select this contact group.'**
|
||||||
|
String get contactGroupWidgetTutorialIosStep3;
|
||||||
|
|
||||||
/// No description provided for @contactGroupUsedByWidgetSubtitle.
|
/// No description provided for @contactGroupUsedByWidgetSubtitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -5014,6 +5068,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Create sticker'**
|
/// **'Create sticker'**
|
||||||
String get createSticker;
|
String get createSticker;
|
||||||
|
|
||||||
|
/// No description provided for @stickerPickFromGallery.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Gallery'**
|
||||||
|
String get stickerPickFromGallery;
|
||||||
|
|
||||||
/// No description provided for @noStickersYet.
|
/// No description provided for @noStickersYet.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -1033,6 +1033,9 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get groupNameInput => 'اسم المجموعة';
|
String get groupNameInput => 'اسم المجموعة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupNamePlaceholder => 'أدخل الاسم هنا';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get groupMembers => 'الأعضاء';
|
String get groupMembers => 'الأعضاء';
|
||||||
|
|
||||||
|
|
@ -2779,6 +2782,38 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get contactGroupUsedByWidget => 'أداة الشاشة الرئيسية';
|
String get contactGroupUsedByWidget => 'أداة الشاشة الرئيسية';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupAddWidgetSubtitle =>
|
||||||
|
'أضف أداة لمجموعة جهات الاتصال هذه.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialTitle =>
|
||||||
|
'إضافة أداة إلى الشاشة الرئيسية';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep1 =>
|
||||||
|
'المس مطولاً مساحة فارغة على شاشتك الرئيسية، ثم اضغط على «الأدوات».';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep2 =>
|
||||||
|
'ابحث عن twonly، ثم اسحب أداته إلى شاشتك الرئيسية.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep3 =>
|
||||||
|
'اختر مجموعة جهات الاتصال هذه في شاشة الإعداد، ثم اضغط على «حفظ».';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep1 =>
|
||||||
|
'المس مطولاً مساحة فارغة على شاشتك الرئيسية، ثم اضغط على «تحرير» و«إضافة أداة».';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep2 =>
|
||||||
|
'ابحث عن twonly، واختر حجماً، ثم اضغط على «إضافة أداة».';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep3 =>
|
||||||
|
'المس الأداة مطولاً، واضغط على «تحرير الأداة»، ثم اختر مجموعة جهات الاتصال هذه.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactGroupUsedByWidgetSubtitle(int count) {
|
String contactGroupUsedByWidgetSubtitle(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
|
@ -2954,6 +2989,9 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get createSticker => 'إنشاء ملصق';
|
String get createSticker => 'إنشاء ملصق';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stickerPickFromGallery => 'المعرض';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noStickersYet => 'أنشئ ملصقك الأول من صورة.';
|
String get noStickersYet => 'أنشئ ملصقك الأول من صورة.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1028,6 +1028,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get groupNameInput => 'Gruppennamen';
|
String get groupNameInput => 'Gruppennamen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupNamePlaceholder => 'Namen hier eingeben';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get groupMembers => 'Mitglieder';
|
String get groupMembers => 'Mitglieder';
|
||||||
|
|
||||||
|
|
@ -2785,6 +2788,38 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get contactGroupUsedByWidget => 'Homebildschirm-Widget';
|
String get contactGroupUsedByWidget => 'Homebildschirm-Widget';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupAddWidgetSubtitle =>
|
||||||
|
'Füge ein Widget für diese Kontaktgruppe hinzu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialTitle =>
|
||||||
|
'Homebildschirm-Widget hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep1 =>
|
||||||
|
'Halte eine freie Stelle auf deinem Homebildschirm gedrückt und tippe dann auf „Widgets“.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep2 =>
|
||||||
|
'Suche nach twonly und ziehe das Widget auf deinen Homebildschirm.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep3 =>
|
||||||
|
'Wähle diese Kontaktgruppe in der Einrichtung aus und tippe dann auf „Speichern“.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep1 =>
|
||||||
|
'Halte eine freie Stelle auf deinem Homebildschirm gedrückt und tippe dann auf „Bearbeiten“ und „Widget hinzufügen“.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep2 =>
|
||||||
|
'Suche nach twonly, wähle eine Größe und tippe dann auf „Widget hinzufügen“.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep3 =>
|
||||||
|
'Halte das Widget gedrückt, tippe auf „Widget bearbeiten“ und wähle dann diese Kontaktgruppe aus.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactGroupUsedByWidgetSubtitle(int count) {
|
String contactGroupUsedByWidgetSubtitle(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
|
@ -2955,6 +2990,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get createSticker => 'Sticker erstellen';
|
String get createSticker => 'Sticker erstellen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stickerPickFromGallery => 'Galerie';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noStickersYet => 'Erstelle deinen ersten Sticker aus einem Foto.';
|
String get noStickersYet => 'Erstelle deinen ersten Sticker aus einem Foto.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1022,6 +1022,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get groupNameInput => 'Group name';
|
String get groupNameInput => 'Group name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupNamePlaceholder => 'Enter name here';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get groupMembers => 'Members';
|
String get groupMembers => 'Members';
|
||||||
|
|
||||||
|
|
@ -2760,6 +2763,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get contactGroupUsedByWidget => 'Home screen widget';
|
String get contactGroupUsedByWidget => 'Home screen widget';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupAddWidgetSubtitle =>
|
||||||
|
'Add a widget for this contact group.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialTitle => 'Add a home screen widget';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep1 =>
|
||||||
|
'Touch and hold an empty area on your home screen, then tap Widgets.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep2 =>
|
||||||
|
'Find twonly, then drag its widget onto your home screen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialAndroidStep3 =>
|
||||||
|
'Select this contact group in the setup screen, then tap Save.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep1 =>
|
||||||
|
'Touch and hold an empty area on your Home Screen, then tap Edit and Add Widget.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep2 =>
|
||||||
|
'Find twonly, choose a size, then tap Add Widget.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactGroupWidgetTutorialIosStep3 =>
|
||||||
|
'Touch and hold the widget, tap Edit Widget, then select this contact group.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactGroupUsedByWidgetSubtitle(int count) {
|
String contactGroupUsedByWidgetSubtitle(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
|
@ -2926,6 +2960,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get createSticker => 'Create sticker';
|
String get createSticker => 'Create sticker';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stickerPickFromGallery => 'Gallery';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noStickersYet => 'Create your first sticker from a photo.';
|
String get noStickersYet => 'Create your first sticker from a photo.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,7 @@
|
||||||
"selectMembers": "اختر الأعضاء",
|
"selectMembers": "اختر الأعضاء",
|
||||||
"selectGroupName": "اختر اسم المجموعة",
|
"selectGroupName": "اختر اسم المجموعة",
|
||||||
"groupNameInput": "اسم المجموعة",
|
"groupNameInput": "اسم المجموعة",
|
||||||
|
"contactGroupNamePlaceholder": "أدخل الاسم هنا",
|
||||||
"groupMembers": "الأعضاء",
|
"groupMembers": "الأعضاء",
|
||||||
"addMember": "إضافة عضو",
|
"addMember": "إضافة عضو",
|
||||||
"createGroup": "إنشاء مجموعة",
|
"createGroup": "إنشاء مجموعة",
|
||||||
|
|
@ -779,6 +780,14 @@
|
||||||
"fileLimitReachedHintFree": "سجّل فيديو أقصر، أو قم بترقية خطتك لإرسال ملفات أكبر.",
|
"fileLimitReachedHintFree": "سجّل فيديو أقصر، أو قم بترقية خطتك لإرسال ملفات أكبر.",
|
||||||
"fileLimitReachedUpgrade": "ترقية الخطة",
|
"fileLimitReachedUpgrade": "ترقية الخطة",
|
||||||
"contactGroupUsedByWidget": "أداة الشاشة الرئيسية",
|
"contactGroupUsedByWidget": "أداة الشاشة الرئيسية",
|
||||||
|
"contactGroupAddWidgetSubtitle": "أضف أداة لمجموعة جهات الاتصال هذه.",
|
||||||
|
"contactGroupWidgetTutorialTitle": "إضافة أداة إلى الشاشة الرئيسية",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep1": "المس مطولاً مساحة فارغة على شاشتك الرئيسية، ثم اضغط على «الأدوات».",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep2": "ابحث عن twonly، ثم اسحب أداته إلى شاشتك الرئيسية.",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep3": "اختر مجموعة جهات الاتصال هذه في شاشة الإعداد، ثم اضغط على «حفظ».",
|
||||||
|
"contactGroupWidgetTutorialIosStep1": "المس مطولاً مساحة فارغة على شاشتك الرئيسية، ثم اضغط على «تحرير» و«إضافة أداة».",
|
||||||
|
"contactGroupWidgetTutorialIosStep2": "ابحث عن twonly، واختر حجماً، ثم اضغط على «إضافة أداة».",
|
||||||
|
"contactGroupWidgetTutorialIosStep3": "المس الأداة مطولاً، واضغط على «تحرير الأداة»، ثم اختر مجموعة جهات الاتصال هذه.",
|
||||||
"contactGroupUsedByWidgetSubtitle": "{count, plural, =1{تعرض أداة على شاشتك الرئيسية صورًا من هذه المجموعة.} =2{تعرض أداتان على شاشتك الرئيسية صورًا من هذه المجموعة.} few{تعرض {count} أدوات على شاشتك الرئيسية صورًا من هذه المجموعة.} many{تعرض {count} أداةً على شاشتك الرئيسية صورًا من هذه المجموعة.} other{تعرض {count} أداة على شاشتك الرئيسية صورًا من هذه المجموعة.}}",
|
"contactGroupUsedByWidgetSubtitle": "{count, plural, =1{تعرض أداة على شاشتك الرئيسية صورًا من هذه المجموعة.} =2{تعرض أداتان على شاشتك الرئيسية صورًا من هذه المجموعة.} few{تعرض {count} أدوات على شاشتك الرئيسية صورًا من هذه المجموعة.} many{تعرض {count} أداةً على شاشتك الرئيسية صورًا من هذه المجموعة.} other{تعرض {count} أداة على شاشتك الرئيسية صورًا من هذه المجموعة.}}",
|
||||||
"contactGroupDeleteBlockedByWidget": "{count, plural, =1{تستخدم أداةٌ على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأداة أولًا، ثم يمكنك حذف المجموعة.} =2{تستخدم أداتان على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل هاتين الأداتين أولًا، ثم يمكنك حذف المجموعة.} few{تستخدم {count} أدوات على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأدوات أولًا، ثم يمكنك حذف المجموعة.} many{تستخدم {count} أداةً على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأدوات أولًا، ثم يمكنك حذف المجموعة.} other{تستخدم {count} أداة على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأدوات أولًا، ثم يمكنك حذف المجموعة.}}",
|
"contactGroupDeleteBlockedByWidget": "{count, plural, =1{تستخدم أداةٌ على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأداة أولًا، ثم يمكنك حذف المجموعة.} =2{تستخدم أداتان على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل هاتين الأداتين أولًا، ثم يمكنك حذف المجموعة.} few{تستخدم {count} أدوات على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأدوات أولًا، ثم يمكنك حذف المجموعة.} many{تستخدم {count} أداةً على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأدوات أولًا، ثم يمكنك حذف المجموعة.} other{تستخدم {count} أداة على شاشتك الرئيسية مجموعة جهات الاتصال هذه. أزل تلك الأدوات أولًا، ثم يمكنك حذف المجموعة.}}",
|
||||||
"webxdcStoreMenu": "تطبيق",
|
"webxdcStoreMenu": "تطبيق",
|
||||||
|
|
@ -840,6 +849,7 @@
|
||||||
"sticker": "ملصق",
|
"sticker": "ملصق",
|
||||||
"stickers": "الملصقات",
|
"stickers": "الملصقات",
|
||||||
"createSticker": "إنشاء ملصق",
|
"createSticker": "إنشاء ملصق",
|
||||||
|
"stickerPickFromGallery": "المعرض",
|
||||||
"noStickersYet": "أنشئ ملصقك الأول من صورة.",
|
"noStickersYet": "أنشئ ملصقك الأول من صورة.",
|
||||||
"stickerAdded": "تم حفظ الملصق",
|
"stickerAdded": "تم حفظ الملصق",
|
||||||
"stickerAlreadySaved": "هذا الملصق محفوظ بالفعل",
|
"stickerAlreadySaved": "هذا الملصق محفوظ بالفعل",
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,7 @@
|
||||||
"selectMembers": "Mitglieder auswählen",
|
"selectMembers": "Mitglieder auswählen",
|
||||||
"selectGroupName": "Gruppennamen wählen",
|
"selectGroupName": "Gruppennamen wählen",
|
||||||
"groupNameInput": "Gruppennamen",
|
"groupNameInput": "Gruppennamen",
|
||||||
|
"contactGroupNamePlaceholder": "Namen hier eingeben",
|
||||||
"groupMembers": "Mitglieder",
|
"groupMembers": "Mitglieder",
|
||||||
"createGroup": "Gruppe erstellen",
|
"createGroup": "Gruppe erstellen",
|
||||||
"addMember": "Mitglied hinzufügen",
|
"addMember": "Mitglied hinzufügen",
|
||||||
|
|
@ -1078,6 +1079,14 @@
|
||||||
"fileLimitReachedHintFree": "Nimm ein kürzeres Video auf oder wechsle den Tarif, um größere Dateien zu senden.",
|
"fileLimitReachedHintFree": "Nimm ein kürzeres Video auf oder wechsle den Tarif, um größere Dateien zu senden.",
|
||||||
"fileLimitReachedUpgrade": "Tarif wechseln",
|
"fileLimitReachedUpgrade": "Tarif wechseln",
|
||||||
"contactGroupUsedByWidget": "Homebildschirm-Widget",
|
"contactGroupUsedByWidget": "Homebildschirm-Widget",
|
||||||
|
"contactGroupAddWidgetSubtitle": "Füge ein Widget für diese Kontaktgruppe hinzu.",
|
||||||
|
"contactGroupWidgetTutorialTitle": "Homebildschirm-Widget hinzufügen",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep1": "Halte eine freie Stelle auf deinem Homebildschirm gedrückt und tippe dann auf „Widgets“.",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep2": "Suche nach twonly und ziehe das Widget auf deinen Homebildschirm.",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep3": "Wähle diese Kontaktgruppe in der Einrichtung aus und tippe dann auf „Speichern“.",
|
||||||
|
"contactGroupWidgetTutorialIosStep1": "Halte eine freie Stelle auf deinem Homebildschirm gedrückt und tippe dann auf „Bearbeiten“ und „Widget hinzufügen“.",
|
||||||
|
"contactGroupWidgetTutorialIosStep2": "Suche nach twonly, wähle eine Größe und tippe dann auf „Widget hinzufügen“.",
|
||||||
|
"contactGroupWidgetTutorialIosStep3": "Halte das Widget gedrückt, tippe auf „Widget bearbeiten“ und wähle dann diese Kontaktgruppe aus.",
|
||||||
"contactGroupUsedByWidgetSubtitle": "{count, plural, =1{Ein Widget auf deinem Homebildschirm zeigt Fotos aus dieser Gruppe.} other{{count} Widgets auf deinem Homebildschirm zeigen Fotos aus dieser Gruppe.}}",
|
"contactGroupUsedByWidgetSubtitle": "{count, plural, =1{Ein Widget auf deinem Homebildschirm zeigt Fotos aus dieser Gruppe.} other{{count} Widgets auf deinem Homebildschirm zeigen Fotos aus dieser Gruppe.}}",
|
||||||
"@contactGroupUsedByWidgetSubtitle": {
|
"@contactGroupUsedByWidgetSubtitle": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -1153,6 +1162,7 @@
|
||||||
"sticker": "Sticker",
|
"sticker": "Sticker",
|
||||||
"stickers": "Sticker",
|
"stickers": "Sticker",
|
||||||
"createSticker": "Sticker erstellen",
|
"createSticker": "Sticker erstellen",
|
||||||
|
"stickerPickFromGallery": "Galerie",
|
||||||
"noStickersYet": "Erstelle deinen ersten Sticker aus einem Foto.",
|
"noStickersYet": "Erstelle deinen ersten Sticker aus einem Foto.",
|
||||||
"stickerAdded": "Sticker gespeichert",
|
"stickerAdded": "Sticker gespeichert",
|
||||||
"stickerAlreadySaved": "Dieser Sticker ist bereits gespeichert",
|
"stickerAlreadySaved": "Dieser Sticker ist bereits gespeichert",
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@
|
||||||
"selectMembers": "Select members",
|
"selectMembers": "Select members",
|
||||||
"selectGroupName": "Select group name",
|
"selectGroupName": "Select group name",
|
||||||
"groupNameInput": "Group name",
|
"groupNameInput": "Group name",
|
||||||
|
"contactGroupNamePlaceholder": "Enter name here",
|
||||||
"groupMembers": "Members",
|
"groupMembers": "Members",
|
||||||
"addMember": "Add member",
|
"addMember": "Add member",
|
||||||
"createGroup": "Create group",
|
"createGroup": "Create group",
|
||||||
|
|
@ -1088,6 +1089,14 @@
|
||||||
"fileLimitReachedHintFree": "Record a shorter video, or upgrade your plan to send larger files.",
|
"fileLimitReachedHintFree": "Record a shorter video, or upgrade your plan to send larger files.",
|
||||||
"fileLimitReachedUpgrade": "Upgrade plan",
|
"fileLimitReachedUpgrade": "Upgrade plan",
|
||||||
"contactGroupUsedByWidget": "Home screen widget",
|
"contactGroupUsedByWidget": "Home screen widget",
|
||||||
|
"contactGroupAddWidgetSubtitle": "Add a widget for this contact group.",
|
||||||
|
"contactGroupWidgetTutorialTitle": "Add a home screen widget",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep1": "Touch and hold an empty area on your home screen, then tap Widgets.",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep2": "Find twonly, then drag its widget onto your home screen.",
|
||||||
|
"contactGroupWidgetTutorialAndroidStep3": "Select this contact group in the setup screen, then tap Save.",
|
||||||
|
"contactGroupWidgetTutorialIosStep1": "Touch and hold an empty area on your Home Screen, then tap Edit and Add Widget.",
|
||||||
|
"contactGroupWidgetTutorialIosStep2": "Find twonly, choose a size, then tap Add Widget.",
|
||||||
|
"contactGroupWidgetTutorialIosStep3": "Touch and hold the widget, tap Edit Widget, then select this contact group.",
|
||||||
"contactGroupUsedByWidgetSubtitle": "{count, plural, =1{A widget on your home screen shows photos from this group.} other{{count} widgets on your home screen show photos from this group.}}",
|
"contactGroupUsedByWidgetSubtitle": "{count, plural, =1{A widget on your home screen shows photos from this group.} other{{count} widgets on your home screen show photos from this group.}}",
|
||||||
"@contactGroupUsedByWidgetSubtitle": {
|
"@contactGroupUsedByWidgetSubtitle": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -1163,6 +1172,7 @@
|
||||||
"sticker": "Sticker",
|
"sticker": "Sticker",
|
||||||
"stickers": "Stickers",
|
"stickers": "Stickers",
|
||||||
"createSticker": "Create sticker",
|
"createSticker": "Create sticker",
|
||||||
|
"stickerPickFromGallery": "Gallery",
|
||||||
"noStickersYet": "Create your first sticker from a photo.",
|
"noStickersYet": "Create your first sticker from a photo.",
|
||||||
"stickerAdded": "Sticker saved",
|
"stickerAdded": "Sticker saved",
|
||||||
"stickerAlreadySaved": "This sticker is already saved",
|
"stickerAlreadySaved": "This sticker is already saved",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:convert/convert.dart';
|
import 'package:convert/convert.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
|
||||||
|
|
||||||
class StickerService {
|
class StickerService {
|
||||||
const StickerService._();
|
const StickerService._();
|
||||||
|
|
@ -73,10 +76,40 @@ class StickerService {
|
||||||
return sticker;
|
return sticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<LocalSticker?> createFromGallery() async {
|
/// Finds the best full-resolution local source for a stored memory. A
|
||||||
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
|
/// cloud-only memory is downloaded before its path is returned.
|
||||||
if (image == null) return null;
|
static Future<String?> resolveMediaPath(MediaFileService media) async {
|
||||||
return createFromPath(image.path);
|
File? readableSource() {
|
||||||
|
for (final file in [
|
||||||
|
media.storedPath,
|
||||||
|
media.tempPath,
|
||||||
|
media.originalPath,
|
||||||
|
]) {
|
||||||
|
final stat = file.statSync();
|
||||||
|
if (stat.type != FileSystemEntityType.notFound && stat.size > 0) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = readableSource();
|
||||||
|
if (source == null && media.mediaFile.cloudState == CloudState.uploaded) {
|
||||||
|
final downloaded = await MemoriesCloudService.downloadFromCloud(
|
||||||
|
media,
|
||||||
|
isThumbnail: false,
|
||||||
|
);
|
||||||
|
if (downloaded) source = readableSource();
|
||||||
|
}
|
||||||
|
return source?.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LocalSticker> createFromMedia(MediaFileService media) async {
|
||||||
|
final path = await resolveMediaPath(media);
|
||||||
|
if (path == null) {
|
||||||
|
throw StateError('sticker source media is unavailable');
|
||||||
|
}
|
||||||
|
return createFromPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> saveReceived(StickerData sticker) {
|
static Future<bool> saveReceived(StickerData sticker) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/emoji_picker/bottom_action_bar.dart';
|
import 'package:twonly/src/visual/components/emoji_picker/bottom_action_bar.dart';
|
||||||
import 'package:twonly/src/visual/components/sticker_preview.dialog.dart';
|
import 'package:twonly/src/visual/components/sticker_preview.dialog.dart';
|
||||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/sticker_source_picker.view.dart';
|
||||||
|
|
||||||
class StickerPicker extends StatefulWidget {
|
class StickerPicker extends StatefulWidget {
|
||||||
const StickerPicker({
|
const StickerPicker({
|
||||||
|
|
@ -32,8 +33,14 @@ class _StickerPickerState extends State<StickerPicker> {
|
||||||
if (_creating) return;
|
if (_creating) return;
|
||||||
setState(() => _creating = true);
|
setState(() => _creating = true);
|
||||||
try {
|
try {
|
||||||
final sticker = await StickerService.createFromGallery();
|
final imagePath = await Navigator.of(context).push<String>(
|
||||||
if (!mounted || sticker == null) return;
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const StickerSourcePickerView(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!mounted || imagePath == null) return;
|
||||||
|
final sticker = await StickerService.createFromPath(imagePath);
|
||||||
|
if (!mounted) return;
|
||||||
if (widget.selectCreatedSticker) widget.onStickerSelected(sticker);
|
if (widget.selectCreatedSticker) widget.onStickerSelected(sticker);
|
||||||
} on Object {
|
} on Object {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
@ -50,86 +57,103 @@ class _StickerPickerState extends State<StickerPicker> {
|
||||||
final color = context.color;
|
final color = context.color;
|
||||||
return ColoredBox(
|
return ColoredBox(
|
||||||
color: color.surfaceContainer,
|
color: color.surfaceContainer,
|
||||||
child: Column(
|
child: StreamBuilder<List<LocalSticker>>(
|
||||||
children: [
|
stream: _stickers,
|
||||||
PickerBottomBar(
|
builder: (context, snapshot) {
|
||||||
backgroundColor: color.surfaceContainer,
|
final isLoading = !snapshot.hasData && !snapshot.hasError;
|
||||||
buttonIconColor: color.secondary,
|
final stickers = snapshot.data ?? const <LocalSticker>[];
|
||||||
emojiButtonLabel: 'Emoji',
|
final hasStickers = stickers.isNotEmpty;
|
||||||
stickerButtonLabel: context.lang.sticker,
|
return Column(
|
||||||
showStickerButton: true,
|
children: [
|
||||||
stickerSelected: true,
|
PickerBottomBar(
|
||||||
onEmojiButtonPressed: widget.onEmojiPressed,
|
backgroundColor: color.surfaceContainer,
|
||||||
),
|
buttonIconColor: color.secondary,
|
||||||
Expanded(
|
emojiButtonLabel: 'Emoji',
|
||||||
child: StreamBuilder<List<LocalSticker>>(
|
stickerButtonLabel: context.lang.sticker,
|
||||||
stream: _stickers,
|
showStickerButton: true,
|
||||||
builder: (context, snapshot) {
|
stickerSelected: true,
|
||||||
if (!snapshot.hasData && !snapshot.hasError) {
|
onEmojiButtonPressed: widget.onEmojiPressed,
|
||||||
return const Center(child: ThreeRotatingDots(size: 26));
|
trailing: hasStickers
|
||||||
}
|
? IconButton(
|
||||||
final stickers = snapshot.data ?? const <LocalSticker>[];
|
key: const Key('stickerPickerHeaderAdd'),
|
||||||
return GridView.builder(
|
tooltip: context.lang.createSticker,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
onPressed: _creating ? null : _createSticker,
|
||||||
maxCrossAxisExtent: 104,
|
icon: _creating
|
||||||
mainAxisSpacing: 8,
|
? const ThreeRotatingDots(size: 22)
|
||||||
crossAxisSpacing: 8,
|
: Icon(
|
||||||
),
|
Icons.add_photo_alternate_outlined,
|
||||||
itemCount: stickers.length + 1,
|
color: color.secondary,
|
||||||
itemBuilder: (context, index) {
|
),
|
||||||
if (index == 0) {
|
)
|
||||||
return Semantics(
|
: null,
|
||||||
button: true,
|
),
|
||||||
label: context.lang.createSticker,
|
Expanded(
|
||||||
child: Material(
|
child: isLoading
|
||||||
color: color.surfaceContainerHigh,
|
? const Center(child: ThreeRotatingDots(size: 26))
|
||||||
borderRadius: BorderRadius.circular(16),
|
: GridView.builder(
|
||||||
clipBehavior: Clip.antiAlias,
|
padding: const EdgeInsets.all(12),
|
||||||
child: InkWell(
|
gridDelegate:
|
||||||
onTap: _creating ? null : _createSticker,
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
child: Center(
|
maxCrossAxisExtent: 104,
|
||||||
child: _creating
|
mainAxisSpacing: 8,
|
||||||
? const ThreeRotatingDots(size: 26)
|
crossAxisSpacing: 8,
|
||||||
: Icon(
|
|
||||||
Icons.add_photo_alternate_outlined,
|
|
||||||
color: color.primary,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
itemCount: stickers.length + (hasStickers ? 0 : 1),
|
||||||
),
|
itemBuilder: (context, index) {
|
||||||
);
|
if (!hasStickers) {
|
||||||
}
|
return Semantics(
|
||||||
final sticker = stickers[index - 1];
|
key: const Key('stickerPickerEmptyAdd'),
|
||||||
return Semantics(
|
button: true,
|
||||||
button: true,
|
label: context.lang.createSticker,
|
||||||
label: context.lang.sticker,
|
child: Material(
|
||||||
child: InkWell(
|
color: color.surfaceContainerHigh,
|
||||||
key: ValueKey(sticker.contentHash),
|
borderRadius: BorderRadius.circular(16),
|
||||||
borderRadius: BorderRadius.circular(12),
|
clipBehavior: Clip.antiAlias,
|
||||||
onTap: () => widget.onStickerSelected(sticker),
|
child: InkWell(
|
||||||
onLongPress: () => showStickerPreview(
|
onTap: _creating ? null : _createSticker,
|
||||||
context,
|
child: Center(
|
||||||
StickerService.fromLocal(sticker),
|
child: _creating
|
||||||
collectionOnly: true,
|
? const ThreeRotatingDots(size: 26)
|
||||||
),
|
: Icon(
|
||||||
child: Padding(
|
Icons.add_photo_alternate_outlined,
|
||||||
padding: const EdgeInsets.all(5),
|
color: color.primary,
|
||||||
child: Image.memory(
|
size: 32,
|
||||||
sticker.webp,
|
),
|
||||||
fit: BoxFit.contain,
|
),
|
||||||
gaplessPlayback: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
final sticker = stickers[index];
|
||||||
|
return Semantics(
|
||||||
|
button: true,
|
||||||
|
label: context.lang.sticker,
|
||||||
|
child: InkWell(
|
||||||
|
key: ValueKey(sticker.contentHash),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
onTap: () => widget.onStickerSelected(sticker),
|
||||||
|
onLongPress: () => showStickerPreview(
|
||||||
|
context,
|
||||||
|
StickerService.fromLocal(sticker),
|
||||||
|
collectionOnly: true,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: Image.memory(
|
||||||
|
sticker.webp,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -671,7 +671,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
}
|
}
|
||||||
final group = _group!;
|
final group = _group!;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => FocusScope.of(context).unfocus(),
|
onTap: () {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
if (_emojiPickerShowing) {
|
||||||
|
setState(() => _emojiPickerShowing = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
resizeToAvoidBottomInset: !_emojiPickerShowing,
|
resizeToAvoidBottomInset: !_emojiPickerShowing,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -869,6 +874,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
quotesMessage: quotesMessage,
|
quotesMessage: quotesMessage,
|
||||||
textFieldFocus: textFieldFocus!,
|
textFieldFocus: textFieldFocus!,
|
||||||
composing: _composing,
|
composing: _composing,
|
||||||
|
emojiPickerShowing: _emojiPickerShowing,
|
||||||
onMessageSend: () {
|
onMessageSend: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
quotesMessage = null;
|
quotesMessage = null;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class MessageInput extends StatefulWidget {
|
||||||
required this.textFieldFocus,
|
required this.textFieldFocus,
|
||||||
required this.onMessageSend,
|
required this.onMessageSend,
|
||||||
required this.onEmojiVisibilityChanged,
|
required this.onEmojiVisibilityChanged,
|
||||||
|
required this.emojiPickerShowing,
|
||||||
required this.composing,
|
required this.composing,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
@ -42,6 +43,7 @@ class MessageInput extends StatefulWidget {
|
||||||
final Message? quotesMessage;
|
final Message? quotesMessage;
|
||||||
final VoidCallback onMessageSend;
|
final VoidCallback onMessageSend;
|
||||||
final ValueChanged<bool> onEmojiVisibilityChanged;
|
final ValueChanged<bool> onEmojiVisibilityChanged;
|
||||||
|
final bool emojiPickerShowing;
|
||||||
|
|
||||||
/// Published so the chat-open heartbeat can stay quiet while the typing
|
/// Published so the chat-open heartbeat can stay quiet while the typing
|
||||||
/// announcement is already covering this conversation.
|
/// announcement is already covering this conversation.
|
||||||
|
|
@ -71,7 +73,6 @@ class _MessageInputState extends State<MessageInput>
|
||||||
late final TextEditingController _textFieldController;
|
late final TextEditingController _textFieldController;
|
||||||
late final RecorderController recorderController;
|
late final RecorderController recorderController;
|
||||||
final bool isApple = Platform.isIOS;
|
final bool isApple = Platform.isIOS;
|
||||||
bool _emojiShowing = false;
|
|
||||||
bool _stickerShowing = false;
|
bool _stickerShowing = false;
|
||||||
bool _switchingToSystemKeyboard = false;
|
bool _switchingToSystemKeyboard = false;
|
||||||
double _emojiPickerHeight = 300;
|
double _emojiPickerHeight = 300;
|
||||||
|
|
@ -226,7 +227,7 @@ class _MessageInputState extends State<MessageInput>
|
||||||
|
|
||||||
void _handleTextFocusChange() {
|
void _handleTextFocusChange() {
|
||||||
if (widget.textFieldFocus.hasFocus &&
|
if (widget.textFieldFocus.hasFocus &&
|
||||||
_emojiShowing &&
|
widget.emojiPickerShowing &&
|
||||||
!_switchingToSystemKeyboard) {
|
!_switchingToSystemKeyboard) {
|
||||||
_showSystemKeyboard();
|
_showSystemKeyboard();
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +247,7 @@ class _MessageInputState extends State<MessageInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleKeyboard() {
|
void _toggleKeyboard() {
|
||||||
if (_emojiShowing) {
|
if (widget.emojiPickerShowing) {
|
||||||
_showSystemKeyboard();
|
_showSystemKeyboard();
|
||||||
} else {
|
} else {
|
||||||
_showEmojiPicker();
|
_showEmojiPicker();
|
||||||
|
|
@ -261,7 +262,6 @@ class _MessageInputState extends State<MessageInput>
|
||||||
if (keyboardHeight > 0) {
|
if (keyboardHeight > 0) {
|
||||||
_emojiPickerHeight = keyboardHeight;
|
_emojiPickerHeight = keyboardHeight;
|
||||||
}
|
}
|
||||||
_emojiShowing = true;
|
|
||||||
_stickerShowing = false;
|
_stickerShowing = false;
|
||||||
});
|
});
|
||||||
widget.onEmojiVisibilityChanged(true);
|
widget.onEmojiVisibilityChanged(true);
|
||||||
|
|
@ -295,7 +295,6 @@ class _MessageInputState extends State<MessageInput>
|
||||||
_keyboardTransitionTimer?.cancel();
|
_keyboardTransitionTimer?.cancel();
|
||||||
setState(() {
|
setState(() {
|
||||||
_switchingToSystemKeyboard = false;
|
_switchingToSystemKeyboard = false;
|
||||||
_emojiShowing = false;
|
|
||||||
});
|
});
|
||||||
widget.onEmojiVisibilityChanged(false);
|
widget.onEmojiVisibilityChanged(false);
|
||||||
}
|
}
|
||||||
|
|
@ -490,8 +489,8 @@ class _MessageInputState extends State<MessageInput>
|
||||||
right: 8,
|
right: 8,
|
||||||
),
|
),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
size: _emojiShowing ? 18 : 20,
|
size: widget.emojiPickerShowing ? 18 : 20,
|
||||||
_emojiShowing
|
widget.emojiPickerShowing
|
||||||
? FontAwesomeIcons.keyboard
|
? FontAwesomeIcons.keyboard
|
||||||
: FontAwesomeIcons.faceSmile,
|
: FontAwesomeIcons.faceSmile,
|
||||||
),
|
),
|
||||||
|
|
@ -771,7 +770,7 @@ class _MessageInputState extends State<MessageInput>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !_emojiShowing,
|
offstage: !widget.emojiPickerShowing,
|
||||||
child: EmojiPicker(
|
child: EmojiPicker(
|
||||||
alternateView: _stickerShowing
|
alternateView: _stickerShowing
|
||||||
? StickerPicker(
|
? StickerPicker(
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import 'package:twonly/src/visual/components/custom_color_picker_dialog.comp.dar
|
||||||
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
|
||||||
|
|
||||||
class ContactGroupSettingsView extends StatefulWidget {
|
class ContactGroupSettingsView extends StatefulWidget {
|
||||||
|
|
@ -67,6 +67,7 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
];
|
];
|
||||||
|
|
||||||
late final TextEditingController _nameController;
|
late final TextEditingController _nameController;
|
||||||
|
final TextEditingController _memberFilterController = TextEditingController();
|
||||||
late int _backgroundColor;
|
late int _backgroundColor;
|
||||||
late int _textColor;
|
late int _textColor;
|
||||||
late bool _showAsShortcut;
|
late bool _showAsShortcut;
|
||||||
|
|
@ -86,6 +87,7 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
/// members are what makes those widgets able to receive anything, so the
|
/// members are what makes those widgets able to receive anything, so the
|
||||||
/// group cannot be deleted while any of them is still placed.
|
/// group cannot be deleted while any of them is still placed.
|
||||||
int _widgetCount = 0;
|
int _widgetCount = 0;
|
||||||
|
bool _widgetUsageLoaded = false;
|
||||||
|
|
||||||
bool get _isEditing => widget.contactGroup != null;
|
bool get _isEditing => widget.contactGroup != null;
|
||||||
|
|
||||||
|
|
@ -161,6 +163,7 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
unawaited(subscription.cancel());
|
unawaited(subscription.cancel());
|
||||||
}
|
}
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
|
_memberFilterController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +268,70 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
.where((placed) => placed.contactGroupIds.contains(contactGroupId))
|
.where((placed) => placed.contactGroupIds.contains(contactGroupId))
|
||||||
.length;
|
.length;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _widgetCount = count);
|
setState(() {
|
||||||
|
_widgetCount = count;
|
||||||
|
_widgetUsageLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showWidgetTutorial() async {
|
||||||
|
final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(context.lang.contactGroupWidgetTutorialTitle),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_widgetTutorialStep(
|
||||||
|
1,
|
||||||
|
isIOS
|
||||||
|
? context.lang.contactGroupWidgetTutorialIosStep1
|
||||||
|
: context.lang.contactGroupWidgetTutorialAndroidStep1,
|
||||||
|
),
|
||||||
|
_widgetTutorialStep(
|
||||||
|
2,
|
||||||
|
isIOS
|
||||||
|
? context.lang.contactGroupWidgetTutorialIosStep2
|
||||||
|
: context.lang.contactGroupWidgetTutorialAndroidStep2,
|
||||||
|
),
|
||||||
|
_widgetTutorialStep(
|
||||||
|
3,
|
||||||
|
isIOS
|
||||||
|
? context.lang.contactGroupWidgetTutorialIosStep3
|
||||||
|
: context.lang.contactGroupWidgetTutorialAndroidStep3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(context.lang.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _widgetTutorialStep(int number, String text) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: context.color.primaryContainer,
|
||||||
|
foregroundColor: context.color.onPrimaryContainer,
|
||||||
|
child: Text('$number'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(text)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _delete() async {
|
Future<void> _delete() async {
|
||||||
|
|
@ -365,6 +431,7 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
Widget _nameEditor() {
|
Widget _nameEditor() {
|
||||||
final hasBackground = contactGroupHasBackground(_backgroundColor);
|
final hasBackground = contactGroupHasBackground(_backgroundColor);
|
||||||
final fontSize = contactGroupFontSize(13, _backgroundColor);
|
final fontSize = contactGroupFontSize(13, _backgroundColor);
|
||||||
|
final placeholder = context.lang.contactGroupNamePlaceholder;
|
||||||
final style = TextStyle(
|
final style = TextStyle(
|
||||||
color: Color(_textColor),
|
color: Color(_textColor),
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
|
|
@ -373,7 +440,10 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
// The field has no intrinsic width, so measure the text to let the badge
|
// The field has no intrinsic width, so measure the text to let the badge
|
||||||
// hug its content just like the rendered label does.
|
// hug its content just like the rendered label does.
|
||||||
final painter = TextPainter(
|
final painter = TextPainter(
|
||||||
text: TextSpan(text: _nameController.text, style: style),
|
text: TextSpan(
|
||||||
|
text: _nameController.text.isEmpty ? placeholder : _nameController.text,
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
textScaler: MediaQuery.textScalerOf(context),
|
textScaler: MediaQuery.textScalerOf(context),
|
||||||
)..layout();
|
)..layout();
|
||||||
|
|
@ -400,7 +470,11 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
style: style,
|
style: style,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
hintText: placeholder,
|
||||||
|
hintStyle: style.copyWith(
|
||||||
|
color: style.color?.withValues(alpha: 0.65),
|
||||||
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
|
|
@ -532,6 +606,25 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
value: _showAsShortcut,
|
value: _showAsShortcut,
|
||||||
onChanged: (value) => setState(() => _showAsShortcut = value),
|
onChanged: (value) => setState(() => _showAsShortcut = value),
|
||||||
),
|
),
|
||||||
|
if (_showAsShortcut)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(left: 16, bottom: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: const Icon(Icons.emoji_emotions_outlined),
|
||||||
|
title: Text(context.lang.selectEmoji),
|
||||||
|
trailing: Text(
|
||||||
|
_emoji ?? '+',
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
),
|
||||||
|
onTap: _selectEmoji,
|
||||||
|
),
|
||||||
|
),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(context.lang.contactGroupShareStories),
|
title: Text(context.lang.contactGroupShareStories),
|
||||||
|
|
@ -539,30 +632,25 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
value: _shareStories,
|
value: _shareStories,
|
||||||
onChanged: (value) => setState(() => _shareStories = value),
|
onChanged: (value) => setState(() => _shareStories = value),
|
||||||
),
|
),
|
||||||
if (_showAsShortcut)
|
if (_isEditing && _widgetUsageLoaded)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(context.lang.selectEmoji),
|
leading: Icon(
|
||||||
trailing: Text(
|
Icons.widgets_outlined,
|
||||||
_emoji ?? '+',
|
|
||||||
style: const TextStyle(fontSize: 24),
|
|
||||||
),
|
|
||||||
onTap: _selectEmoji,
|
|
||||||
),
|
|
||||||
if (_widgetCount > 0)
|
|
||||||
// Stated rather than offered as a switch: which groups a widget
|
|
||||||
// draws from is chosen on the home screen, not here.
|
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: Text(context.lang.contactGroupUsedByWidget),
|
|
||||||
subtitle: Text(
|
|
||||||
context.lang.contactGroupUsedByWidgetSubtitle(_widgetCount),
|
|
||||||
),
|
|
||||||
trailing: FaIcon(
|
|
||||||
FontAwesomeIcons.image,
|
|
||||||
size: 18,
|
|
||||||
color: context.color.primary,
|
color: context.color.primary,
|
||||||
),
|
),
|
||||||
|
title: Text(context.lang.contactGroupUsedByWidget),
|
||||||
|
subtitle: Text(
|
||||||
|
_widgetCount > 0
|
||||||
|
? context.lang.contactGroupUsedByWidgetSubtitle(
|
||||||
|
_widgetCount,
|
||||||
|
)
|
||||||
|
: context.lang.contactGroupAddWidgetSubtitle,
|
||||||
|
),
|
||||||
|
trailing: _widgetCount == 0
|
||||||
|
? const Icon(Icons.chevron_right)
|
||||||
|
: null,
|
||||||
|
onTap: _widgetCount == 0 ? _showWidgetTutorial : null,
|
||||||
),
|
),
|
||||||
const Divider(height: 40),
|
const Divider(height: 40),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -570,12 +658,13 @@ class _ContactGroupSettingsViewState extends State<ContactGroupSettingsView> {
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextField(
|
MyInput(
|
||||||
|
controller: _memberFilterController,
|
||||||
|
dense: true,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
hintText: context.lang.shareImageSearchAllContacts,
|
||||||
|
prefixIcon: const Icon(Icons.search, size: 20),
|
||||||
onChanged: (value) => setState(() => _memberFilter = value),
|
onChanged: (value) => setState(() => _memberFilter = value),
|
||||||
decoration: getInputDecoration(
|
|
||||||
context,
|
|
||||||
context.lang.shareImageSearchAllContacts,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
for (final entry in members) _memberTile(entry),
|
for (final entry in members) _memberTile(entry),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import 'package:twonly/src/visual/components/contact_groups.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
|
|
||||||
class GroupCreateSelectGroupNameView extends StatefulWidget {
|
class GroupCreateSelectGroupNameView extends StatefulWidget {
|
||||||
const GroupCreateSelectGroupNameView({
|
const GroupCreateSelectGroupNameView({
|
||||||
|
|
@ -101,16 +101,11 @@ class _GroupCreateSelectGroupNameViewState
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: TextField(
|
child: MyInput(
|
||||||
onChanged: (_) async {
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
autofocus: true,
|
|
||||||
controller: textFieldGroupName,
|
controller: textFieldGroupName,
|
||||||
decoration: getInputDecoration(
|
autofocus: true,
|
||||||
context,
|
hintText: context.lang.groupNameInput,
|
||||||
context.lang.groupNameInput,
|
onChanged: (_) => setState(() {}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/contact_chip.element.dart';
|
import 'package:twonly/src/visual/elements/contact_chip.element.dart';
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||||
import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart';
|
import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart';
|
||||||
|
|
||||||
class GroupCreateSelectMembersView extends StatefulWidget {
|
class GroupCreateSelectMembersView extends StatefulWidget {
|
||||||
|
|
@ -162,15 +162,13 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: TextField(
|
child: MyInput(
|
||||||
onChanged: (_) async {
|
|
||||||
await filterUsers();
|
|
||||||
},
|
|
||||||
controller: searchUserName,
|
controller: searchUserName,
|
||||||
decoration: getInputDecoration(
|
dense: true,
|
||||||
context,
|
fontWeight: FontWeight.normal,
|
||||||
context.lang.shareImageSearchAllContacts,
|
hintText: context.lang.shareImageSearchAllContacts,
|
||||||
),
|
prefixIcon: const Icon(Icons.search, size: 20),
|
||||||
|
onChanged: (_) => unawaited(filterUsers()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ class SynchronizedViewerActionsToolbarComp extends StatelessWidget {
|
||||||
required this.onExport,
|
required this.onExport,
|
||||||
required this.onToggleFavorite,
|
required this.onToggleFavorite,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
|
required this.showCreateStickerButton,
|
||||||
|
required this.onCreateSticker,
|
||||||
this.showStoreButton = false,
|
this.showStoreButton = false,
|
||||||
this.onStore,
|
this.onStore,
|
||||||
this.isImageSaving = false,
|
this.isImageSaving = false,
|
||||||
|
this.isCreatingSticker = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -20,9 +23,12 @@ class SynchronizedViewerActionsToolbarComp extends StatelessWidget {
|
||||||
final VoidCallback onExport;
|
final VoidCallback onExport;
|
||||||
final VoidCallback onToggleFavorite;
|
final VoidCallback onToggleFavorite;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
|
final bool showCreateStickerButton;
|
||||||
|
final VoidCallback onCreateSticker;
|
||||||
final bool showStoreButton;
|
final bool showStoreButton;
|
||||||
final VoidCallback? onStore;
|
final VoidCallback? onStore;
|
||||||
final bool isImageSaving;
|
final bool isImageSaving;
|
||||||
|
final bool isCreatingSticker;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -80,6 +86,27 @@ class SynchronizedViewerActionsToolbarComp extends StatelessWidget {
|
||||||
: context.lang.galleryActionFavorite,
|
: context.lang.galleryActionFavorite,
|
||||||
onTap: onToggleFavorite,
|
onTap: onToggleFavorite,
|
||||||
),
|
),
|
||||||
|
if (showCreateStickerButton)
|
||||||
|
_ToolbarAction(
|
||||||
|
icon: isCreatingSticker
|
||||||
|
? SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation(
|
||||||
|
context.appColor(AppColor.mediaForeground),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: FaIcon(
|
||||||
|
FontAwesomeIcons.wandMagicSparkles,
|
||||||
|
color: context.appColor(AppColor.mediaForeground),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
label: context.lang.createSticker,
|
||||||
|
onTap: isCreatingSticker ? null : onCreateSticker,
|
||||||
|
),
|
||||||
_ToolbarAction(
|
_ToolbarAction(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.delete,
|
Icons.delete,
|
||||||
|
|
|
||||||
136
lib/src/visual/views/memories/sticker_source_picker.view.dart
Normal file
136
lib/src/visual/views/memories/sticker_source_picker.view.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
|
import 'package:twonly/src/services/memories/memories.service.dart';
|
||||||
|
import 'package:twonly/src/services/stickers/sticker.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart';
|
||||||
|
|
||||||
|
/// Selects a full-resolution image from twonly Memories, with an explicit
|
||||||
|
/// escape hatch to the device gallery in the top app bar.
|
||||||
|
class StickerSourcePickerView extends StatefulWidget {
|
||||||
|
const StickerSourcePickerView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StickerSourcePickerView> createState() =>
|
||||||
|
_StickerSourcePickerViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerSourcePickerViewState extends State<StickerSourcePickerView> {
|
||||||
|
late final MemoriesService _memories = MemoriesService();
|
||||||
|
String? _resolvingMediaId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_memories.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFromGallery() async {
|
||||||
|
if (_resolvingMediaId != null) return;
|
||||||
|
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
|
if (!mounted || image == null) return;
|
||||||
|
Navigator.pop(context, image.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectMemory(MemoryItem item) async {
|
||||||
|
if (_resolvingMediaId != null) return;
|
||||||
|
final mediaId = item.mediaService.mediaFile.mediaId;
|
||||||
|
setState(() => _resolvingMediaId = mediaId);
|
||||||
|
try {
|
||||||
|
final path = await StickerService.resolveMediaPath(item.mediaService);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (path == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.lang.stickerCreateFailed)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.pop(context, path);
|
||||||
|
} on Object {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.lang.stickerCreateFailed)),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _resolvingMediaId = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.createSticker),
|
||||||
|
actions: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: _resolvingMediaId == null ? _pickFromGallery : null,
|
||||||
|
icon: const Icon(Icons.photo_library_outlined),
|
||||||
|
label: Text(context.lang.stickerPickFromGallery),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: StreamBuilder<MemoriesState>(
|
||||||
|
stream: _memories.watchState,
|
||||||
|
initialData: _memories.currentState,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final state = snapshot.data ?? _memories.currentState;
|
||||||
|
final images = state.galleryItems
|
||||||
|
.where(
|
||||||
|
(item) => item.mediaService.mediaFile.type == MediaType.image,
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
if (images.isEmpty && state.isLoading) {
|
||||||
|
return const Center(child: ThreeRotatingDots(size: 32));
|
||||||
|
}
|
||||||
|
if (images.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Text(
|
||||||
|
context.lang.memoriesEmpty,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: context.color.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
mainAxisSpacing: 2,
|
||||||
|
crossAxisSpacing: 2,
|
||||||
|
childAspectRatio: 9 / 16,
|
||||||
|
),
|
||||||
|
itemCount: images.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = images[index];
|
||||||
|
return MemoriesThumbnailComp(
|
||||||
|
galleryItem: item,
|
||||||
|
index: index,
|
||||||
|
onTap: () => _selectMemory(item),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_resolvingMediaId != null)
|
||||||
|
const Positioned.fill(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Color(0x66000000),
|
||||||
|
child: Center(child: ThreeRotatingDots(size: 32)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/services/stickers/sticker.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart';
|
import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart';
|
||||||
|
|
@ -41,6 +42,7 @@ class _SynchronizedImageViewerScreenState
|
||||||
|
|
||||||
final Set<String> _favoritedMediaIds = {};
|
final Set<String> _favoritedMediaIds = {};
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
bool _isCreatingSticker = false;
|
||||||
final Set<String> _storedMediaIds = {};
|
final Set<String> _storedMediaIds = {};
|
||||||
final Set<String> _precachedMediaIds = {};
|
final Set<String> _precachedMediaIds = {};
|
||||||
|
|
||||||
|
|
@ -95,6 +97,26 @@ class _SynchronizedImageViewerScreenState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _createSticker() async {
|
||||||
|
if (_isCreatingSticker) return;
|
||||||
|
final media = widget.galleryItems[_currentIndex].mediaService;
|
||||||
|
setState(() => _isCreatingSticker = true);
|
||||||
|
try {
|
||||||
|
await StickerService.createFromMedia(media);
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.stickerAdded,
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
} on Object {
|
||||||
|
if (!mounted) return;
|
||||||
|
showSnackbar(context, context.lang.stickerCreateFailed);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isCreatingSticker = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _toggleFavorite(String mediaId) async {
|
Future<void> _toggleFavorite(String mediaId) async {
|
||||||
final wasFavorite = _favoritedMediaIds.contains(mediaId);
|
final wasFavorite = _favoritedMediaIds.contains(mediaId);
|
||||||
final isFavoriteNow = !wasFavorite;
|
final isFavoriteNow = !wasFavorite;
|
||||||
|
|
@ -365,6 +387,11 @@ class _SynchronizedImageViewerScreenState
|
||||||
onExport: _exportFile,
|
onExport: _exportFile,
|
||||||
onToggleFavorite: () =>
|
onToggleFavorite: () =>
|
||||||
_toggleFavorite(currentMediaId),
|
_toggleFavorite(currentMediaId),
|
||||||
|
showCreateStickerButton:
|
||||||
|
orgMediaService.mediaFile.type ==
|
||||||
|
MediaType.image,
|
||||||
|
onCreateSticker: _createSticker,
|
||||||
|
isCreatingSticker: _isCreatingSticker,
|
||||||
onDelete: _deleteFile,
|
onDelete: _deleteFile,
|
||||||
showStoreButton: !_storedMediaIds.contains(
|
showStoreButton: !_storedMediaIds.contains(
|
||||||
currentMediaId,
|
currentMediaId,
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:drift/drift.dart' show DatabaseConnection;
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||||
|
|
@ -16,6 +18,7 @@ import 'package:twonly/src/visual/components/sticker_picker.dart';
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/sticker_source_picker.view.dart';
|
||||||
|
|
||||||
// A small valid image for exercising image widgets; protocol codec validation
|
// A small valid image for exercising image widgets; protocol codec validation
|
||||||
// and real transparent WebP inference are covered by the Rust sticker tests.
|
// and real transparent WebP inference are covered by the Rust sticker tests.
|
||||||
|
|
@ -50,10 +53,17 @@ Future<void> _settle(WidgetTester tester) async {
|
||||||
void main() {
|
void main() {
|
||||||
late TwonlyDB database;
|
late TwonlyDB database;
|
||||||
|
|
||||||
|
setUpAll(AppEnvironment.initTesting);
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
await locator.reset();
|
await locator.reset();
|
||||||
database = TwonlyDB(NativeDatabase.memory());
|
database = TwonlyDB.forTesting(
|
||||||
|
DatabaseConnection(
|
||||||
|
NativeDatabase.memory(),
|
||||||
|
closeStreamsSynchronously: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
locator.registerSingleton<TwonlyDB>(database);
|
locator.registerSingleton<TwonlyDB>(database);
|
||||||
await database.customSelect('SELECT 1').get();
|
await database.customSelect('SELECT 1').get();
|
||||||
});
|
});
|
||||||
|
|
@ -115,6 +125,61 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('new sticker opens Memories with a Gallery option', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_app(
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: StickerPicker(
|
||||||
|
onEmojiPressed: () {},
|
||||||
|
onStickerSelected: (_) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await _settle(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.add_photo_alternate_outlined));
|
||||||
|
await _settle(tester);
|
||||||
|
|
||||||
|
expect(find.byType(StickerSourcePickerView), findsOneWidget);
|
||||||
|
expect(find.text('Gallery'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox.shrink());
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(milliseconds: 100)),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('moves add action to the header once a sticker exists', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_app(
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: StickerPicker(
|
||||||
|
onEmojiPressed: () {},
|
||||||
|
onStickerSelected: (_) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await _settle(tester);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('stickerPickerEmptyAdd')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('stickerPickerHeaderAdd')), findsNothing);
|
||||||
|
|
||||||
|
await tester.runAsync(() => StickerService.saveReceived(_sticker()));
|
||||||
|
await _settle(tester);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('stickerPickerEmptyAdd')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('stickerPickerHeaderAdd')), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('chat tap previews without saving; buttons save then remove', (
|
testWidgets('chat tap previews without saving; buttons save then remove', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
53
test/visual/memories/sticker_viewer_action_test.dart
Normal file
53
test/visual/memories/sticker_viewer_action_test.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart';
|
||||||
|
|
||||||
|
Widget _app({required bool showCreateSticker, VoidCallback? onCreateSticker}) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: lightTheme,
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 700,
|
||||||
|
child: SynchronizedViewerActionsToolbarComp(
|
||||||
|
isFavorite: false,
|
||||||
|
onShare: () {},
|
||||||
|
onExport: () {},
|
||||||
|
onToggleFavorite: () {},
|
||||||
|
onDelete: () {},
|
||||||
|
showCreateStickerButton: showCreateSticker,
|
||||||
|
onCreateSticker: onCreateSticker ?? () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('shows and invokes sticker creation for image media', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
var invoked = false;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_app(
|
||||||
|
showCreateSticker: true,
|
||||||
|
onCreateSticker: () => invoked = true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Create sticker'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Create sticker'));
|
||||||
|
expect(invoked, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('hides sticker creation for unsupported media', (tester) async {
|
||||||
|
await tester.pumpWidget(_app(showCreateSticker: false));
|
||||||
|
|
||||||
|
expect(find.text('Create sticker'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue