Merge branch 'main' into clip
This commit is contained in:
commit
6b7579da26
77 changed files with 3164 additions and 679 deletions
|
@ -53,7 +53,7 @@ const double restrictedMaxWidth = 430;
|
|||
const double mobileSmallThreshold = 336;
|
||||
|
||||
// Note: 0 indicates no device limit
|
||||
const publicLinkDeviceLimits = [0,50, 25, 10, 5, 2, 1];
|
||||
const publicLinkDeviceLimits = [0, 50, 25, 10, 5, 2, 1];
|
||||
|
||||
const kilometersPerDegree = 111.16;
|
||||
|
||||
|
@ -62,3 +62,5 @@ const defaultRadiusValues = <double>[1, 2, 10, 20, 40, 80, 200, 400, 1200];
|
|||
const defaultRadiusValue = 40.0;
|
||||
|
||||
const galleryGridSpacing = 2.0;
|
||||
|
||||
const searchSectionLimit = 7;
|
||||
|
|
|
@ -1,3 +1,27 @@
|
|||
const List<String> connectWords = [
|
||||
'a', 'an', 'the', // Articles
|
||||
|
||||
'about', 'above', 'across', 'after', 'against', 'along', 'amid', 'among',
|
||||
'around', 'as', 'at', 'before', 'behind', 'below', 'beneath', 'beside',
|
||||
'between', 'beyond', 'by', 'concerning', 'considering', 'despite', 'down',
|
||||
'during', 'except', 'for', 'from', 'in', 'inside', 'into', 'like', 'near',
|
||||
'of', 'off', 'on', 'onto', 'out', 'outside', 'over', 'past', 'regarding',
|
||||
'round', 'since', 'through', 'to', 'toward', 'under', 'underneath', 'until',
|
||||
'unto', 'up', 'upon', 'with', 'within', 'without', // Prepositions
|
||||
|
||||
'and', 'as', 'because', 'but', 'for', 'if', 'nor', 'or', 'since', 'so',
|
||||
'that', 'though', 'unless', 'until', 'when', 'whenever', 'where', 'whereas',
|
||||
'wherever', 'while', 'yet', // Conjunctions
|
||||
|
||||
'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them',
|
||||
'my', 'your', 'his', 'its', 'our', 'their', 'mine', 'yours', 'hers', 'ours',
|
||||
'theirs', 'who', 'whom', 'whose', 'which', 'what', // Pronouns
|
||||
|
||||
'am', 'is', 'are', 'was', 'were', 'be', 'being', 'been', 'have', 'has', 'had',
|
||||
'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'can', 'could',
|
||||
'may', 'might', 'must', // Auxiliary Verbs
|
||||
];
|
||||
|
||||
extension StringExtensionsNullSafe on String? {
|
||||
int get sumAsciiValues {
|
||||
if (this == null) {
|
||||
|
@ -10,3 +34,26 @@ extension StringExtensionsNullSafe on String? {
|
|||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
extension DescriptionString on String? {
|
||||
bool get isAllConnectWords {
|
||||
if (this == null) {
|
||||
throw AssertionError("String cannot be null");
|
||||
}
|
||||
final subDescWords = this!.split(" ");
|
||||
return subDescWords.every(
|
||||
(subDescWord) => connectWords.any(
|
||||
(connectWord) => subDescWord.toLowerCase() == connectWord,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isLastWordConnectWord {
|
||||
if (this == null) {
|
||||
throw AssertionError("String cannot be null");
|
||||
}
|
||||
final subDescWords = this!.split(" ");
|
||||
return connectWords
|
||||
.any((element) => element == subDescWords.last.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
|
2
lib/generated/intl/messages_de.dart
generated
2
lib/generated/intl/messages_de.dart
generated
|
@ -56,7 +56,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Bitte kontaktieren Sie uns über support@ente.io, um Ihr ${provider} Abo zu verwalten.";
|
||||
|
||||
static String m11(count) =>
|
||||
"${Intl.plural(count, one: 'Lösche ${count} Element', other: 'Lösche ${count} Elemente')}";
|
||||
"${Intl.plural(count, one: 'Lösche 1 Element', other: 'Lösche ${count} Elemente')}";
|
||||
|
||||
static String m12(currentlyDeleting, totalCount) =>
|
||||
"Lösche ${currentlyDeleting} / ${totalCount}";
|
||||
|
|
24
lib/generated/intl/messages_en.dart
generated
24
lib/generated/intl/messages_en.dart
generated
|
@ -347,6 +347,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"backupSettings":
|
||||
MessageLookupByLibrary.simpleMessage("Backup settings"),
|
||||
"backupVideos": MessageLookupByLibrary.simpleMessage("Backup videos"),
|
||||
"blackFridaySale":
|
||||
MessageLookupByLibrary.simpleMessage("Black Friday Sale"),
|
||||
"blog": MessageLookupByLibrary.simpleMessage("Blog"),
|
||||
"cachedData": MessageLookupByLibrary.simpleMessage("Cached data"),
|
||||
"calculating": MessageLookupByLibrary.simpleMessage("Calculating..."),
|
||||
|
@ -662,6 +664,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("Add a description..."),
|
||||
"fileSavedToGallery":
|
||||
MessageLookupByLibrary.simpleMessage("File saved to gallery"),
|
||||
"fileTypesAndNames":
|
||||
MessageLookupByLibrary.simpleMessage("File types and names"),
|
||||
"filesBackedUpFromDevice": m19,
|
||||
"filesBackedUpInAlbum": m20,
|
||||
"filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"),
|
||||
|
@ -841,6 +845,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"mobileWebDesktop":
|
||||
MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"),
|
||||
"moderateStrength": MessageLookupByLibrary.simpleMessage("Moderate"),
|
||||
"moments": MessageLookupByLibrary.simpleMessage("Moments"),
|
||||
"monthly": MessageLookupByLibrary.simpleMessage("Monthly"),
|
||||
"moveItem": m30,
|
||||
"moveToAlbum": MessageLookupByLibrary.simpleMessage("Move to album"),
|
||||
|
@ -915,6 +920,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"paymentFailedWithReason": m34,
|
||||
"pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"),
|
||||
"pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"),
|
||||
"people": MessageLookupByLibrary.simpleMessage("People"),
|
||||
"peopleUsingYourCode":
|
||||
MessageLookupByLibrary.simpleMessage("People using your code"),
|
||||
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
|
||||
|
@ -923,6 +929,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("Permanently delete"),
|
||||
"permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage(
|
||||
"Permanently delete from device?"),
|
||||
"photoDescriptions":
|
||||
MessageLookupByLibrary.simpleMessage("Photo descriptions"),
|
||||
"photoGridSize":
|
||||
MessageLookupByLibrary.simpleMessage("Photo grid size"),
|
||||
"photoSmallCase": MessageLookupByLibrary.simpleMessage("photo"),
|
||||
|
@ -1083,12 +1091,26 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scan this barcode with\nyour authenticator app"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
MessageLookupByLibrary.simpleMessage("Album name"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
"• Album names (e.g. \"Camera\")\n• Types of files (e.g. \"Videos\", \".gif\")\n• Years and months (e.g. \"2022\", \"January\")\n• Holidays (e.g. \"Christmas\")\n• Photo descriptions (e.g. “#fun”)"),
|
||||
"searchCaptionEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||
"Add descriptions like \"#trip\" in photo info to quickly find them here"),
|
||||
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||
"Search by a date, month or year"),
|
||||
"searchFaceEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Find all photos of a person"),
|
||||
"searchFileTypesAndNamesEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("File types and names"),
|
||||
"searchHintText": MessageLookupByLibrary.simpleMessage(
|
||||
"Albums, months, days, years, ..."),
|
||||
"searchLocationEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||
"Group photos that are taken within some radius of a photo"),
|
||||
"searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||
"Invite people, and you\'ll see all photos shared by them here"),
|
||||
"security": MessageLookupByLibrary.simpleMessage("Security"),
|
||||
"selectAlbum": MessageLookupByLibrary.simpleMessage("Select album"),
|
||||
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
|
||||
|
@ -1326,6 +1348,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"),
|
||||
"uploadingFilesToAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Uploading files to album..."),
|
||||
"upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage(
|
||||
"Upto 50% off, until 4th Dec."),
|
||||
"usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage(
|
||||
"Usable storage is limited by your current plan. Excess claimed storage will automatically become usable when you upgrade your plan."),
|
||||
"usePublicLinksForPeopleNotOnEnte":
|
||||
|
|
170
lib/generated/l10n.dart
generated
170
lib/generated/l10n.dart
generated
|
@ -6859,6 +6859,126 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Photo descriptions`
|
||||
String get photoDescriptions {
|
||||
return Intl.message(
|
||||
'Photo descriptions',
|
||||
name: 'photoDescriptions',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `File types and names`
|
||||
String get fileTypes {
|
||||
return Intl.message(
|
||||
'File types',
|
||||
name: 'fileTypes',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Location`
|
||||
String get location {
|
||||
return Intl.message(
|
||||
'Location',
|
||||
name: 'location',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `People`
|
||||
String get people {
|
||||
return Intl.message(
|
||||
'People',
|
||||
name: 'people',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Moments`
|
||||
String get moments {
|
||||
return Intl.message(
|
||||
'Moments',
|
||||
name: 'moments',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Find all photos of a person`
|
||||
String get searchFaceEmptySection {
|
||||
return Intl.message(
|
||||
'Find all photos of a person',
|
||||
name: 'searchFaceEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Search by a date, month or year`
|
||||
String get searchDatesEmptySection {
|
||||
return Intl.message(
|
||||
'Search by a date, month or year',
|
||||
name: 'searchDatesEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Group photos that are taken within some radius of a photo`
|
||||
String get searchLocationEmptySection {
|
||||
return Intl.message(
|
||||
'Group photos that are taken within some radius of a photo',
|
||||
name: 'searchLocationEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Invite people, and you'll see all photos shared by them here`
|
||||
String get searchPeopleEmptySection {
|
||||
return Intl.message(
|
||||
'Invite people, and you\'ll see all photos shared by them here',
|
||||
name: 'searchPeopleEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Albums`
|
||||
String get searchAlbumsEmptySection {
|
||||
return Intl.message(
|
||||
'Albums',
|
||||
name: 'searchAlbumsEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `File types and names`
|
||||
String get searchFileTypesAndNamesEmptySection {
|
||||
return Intl.message(
|
||||
'File types and names',
|
||||
name: 'searchFileTypesAndNamesEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Add descriptions like "#trip" in photo info to quickly find them here`
|
||||
String get searchCaptionEmptySection {
|
||||
return Intl.message(
|
||||
'Add descriptions like "#trip" in photo info to quickly find them here',
|
||||
name: 'searchCaptionEmptySection',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Language`
|
||||
String get language {
|
||||
return Intl.message(
|
||||
|
@ -6909,16 +7029,6 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Location`
|
||||
String get location {
|
||||
return Intl.message(
|
||||
'Location',
|
||||
name: 'location',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `km`
|
||||
String get kiloMeterUnit {
|
||||
return Intl.message(
|
||||
|
@ -7834,6 +7944,46 @@ class S {
|
|||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Your map`
|
||||
String get yourMap {
|
||||
return Intl.message(
|
||||
'Your map',
|
||||
name: 'yourMap',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Black Friday Sale`
|
||||
String get blackFridaySale {
|
||||
return Intl.message(
|
||||
'Black Friday Sale',
|
||||
name: 'blackFridaySale',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Modify your query, or try searching for`
|
||||
String get modifyYourQueryOrTrySearchingFor {
|
||||
return Intl.message(
|
||||
'Modify your query, or try searching for',
|
||||
name: 'modifyYourQueryOrTrySearchingFor',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Upto 50% off, until 4th Dec.`
|
||||
String get upto50OffUntil4thDec {
|
||||
return Intl.message(
|
||||
'Upto 50% off, until 4th Dec.',
|
||||
name: 'upto50OffUntil4thDec',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1102,5 +1102,8 @@
|
|||
"crashReporting": "Absturzbericht",
|
||||
"addToHiddenAlbum": "Zum versteckten Album hinzufügen",
|
||||
"moveToHiddenAlbum": "Zu verstecktem Album verschieben",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -962,12 +962,23 @@
|
|||
"loadMessage7": "Our mobile apps run in the background to encrypt and backup any new photos you click",
|
||||
"loadMessage8": "web.ente.io has a slick uploader",
|
||||
"loadMessage9": "We use Xchacha20Poly1305 to safely encrypt your data",
|
||||
"photoDescriptions": "Photo descriptions",
|
||||
"fileTypesAndNames": "File types and names",
|
||||
"location": "Location",
|
||||
"people": "People",
|
||||
"moments": "Moments",
|
||||
"searchFaceEmptySection": "Find all photos of a person",
|
||||
"searchDatesEmptySection": "Search by a date, month or year",
|
||||
"searchLocationEmptySection": "Group photos that are taken within some radius of a photo",
|
||||
"searchPeopleEmptySection": "Invite people, and you'll see all photos shared by them here",
|
||||
"searchAlbumsEmptySection": "Albums",
|
||||
"searchFileTypesAndNamesEmptySection": "File types and names",
|
||||
"searchCaptionEmptySection": "Add descriptions like \"#trip\" in photo info to quickly find them here",
|
||||
"language": "Language",
|
||||
"selectLanguage": "Select Language",
|
||||
"locationName": "Location name",
|
||||
"addLocation": "Add location",
|
||||
"groupNearbyPhotos": "Group nearby photos",
|
||||
"location": "Location",
|
||||
"kiloMeterUnit": "km",
|
||||
"addLocationButton": "Add",
|
||||
"radius": "Radius",
|
||||
|
@ -1109,10 +1120,15 @@
|
|||
"crashReporting": "Crash reporting",
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
|
||||
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
|
||||
"viewAddOnButton": "View add-ons",
|
||||
"addOns": "Add-ons",
|
||||
"addOnPageSubtitle": "Details of add-ons"
|
||||
"addOnPageSubtitle": "Details of add-ons",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for",
|
||||
"blackFridaySale": "Black Friday Sale",
|
||||
"upto50OffUntil4thDec": "Upto 50% off, until 4th Dec."
|
||||
}
|
||||
|
|
|
@ -964,5 +964,8 @@
|
|||
"familyPlanOverview": "Añada 5 familiares a su plan existente sin pagar más.\n\nCada miembro tiene su propio espacio privado y no puede ver los archivos del otro a menos que sean compartidos.\n\nLos planes familiares están disponibles para los clientes que tienen una suscripción de ente pagada.\n\n¡Suscríbete ahora para empezar!",
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1102,5 +1102,8 @@
|
|||
"crashReporting": "Rapports d'erreurs",
|
||||
"addToHiddenAlbum": "Ajouter à un album masqué",
|
||||
"moveToHiddenAlbum": "Déplacer vers un album masqué",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1108,5 +1108,7 @@
|
|||
"hearUsExplanation": "Non teniamo traccia del numero di installazioni dell'app. Sarebbe utile se ci dicesse dove ci ha trovato!",
|
||||
"viewAddOnButton": "Visualizza componenti aggiuntivi",
|
||||
"addOns": "Componenti aggiuntivi",
|
||||
"addOnPageSubtitle": "Dettagli dei componenti aggiuntivi"
|
||||
"addOnPageSubtitle": "Dettagli dei componenti aggiuntivi",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1102,5 +1102,8 @@
|
|||
"crashReporting": "Crash rapportering",
|
||||
"addToHiddenAlbum": "Toevoegen aan verborgen album",
|
||||
"moveToHiddenAlbum": "Verplaatsen naar verborgen album",
|
||||
"deleteConfirmDialogBody": "Dit account is gekoppeld aan andere ente apps, als je er gebruik van maakt.\\n\\nJe geüploade gegevens worden in alle ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle ente diensten."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "Dit account is gekoppeld aan andere ente apps, als je er gebruik van maakt.\\n\\nJe geüploade gegevens worden in alle ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle ente diensten.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -15,5 +15,8 @@
|
|||
"confirmAccountDeletion": "Bekreft sletting av konto",
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -102,5 +102,8 @@
|
|||
"tryAgain": "Spróbuj ponownie",
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -268,5 +268,8 @@
|
|||
"updateAvailable": "Atualização disponível",
|
||||
"addToHiddenAlbum": "Add to hidden album",
|
||||
"moveToHiddenAlbum": "Move to hidden album",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"fileTypes": "File types",
|
||||
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1108,5 +1108,7 @@
|
|||
"hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
|
||||
"viewAddOnButton": "查看附加组件",
|
||||
"addOns": "附加组件",
|
||||
"addOnPageSubtitle": "附加组件详情"
|
||||
"addOnPageSubtitle": "附加组件详情",
|
||||
"yourMap": "Your map",
|
||||
"modifyYourQueryOrTrySearchingFor": "Modify your query, or try searching for"
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
|
||||
class AlbumSearchResult extends SearchResult {
|
||||
final CollectionWithThumbnail collectionWithThumbnail;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
|
||||
class FileSearchResult extends SearchResult {
|
||||
final EnteFile file;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import "package:flutter/cupertino.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
|
||||
class GenericSearchResult extends SearchResult {
|
||||
final String _name;
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
class LocationApiResponse {
|
||||
final List<LocationDataFromResponse> results;
|
||||
LocationApiResponse({
|
||||
required this.results,
|
||||
});
|
||||
|
||||
LocationApiResponse copyWith({
|
||||
required List<LocationDataFromResponse> results,
|
||||
}) {
|
||||
return LocationApiResponse(
|
||||
results: results,
|
||||
);
|
||||
}
|
||||
|
||||
factory LocationApiResponse.fromMap(Map<String, dynamic> map) {
|
||||
return LocationApiResponse(
|
||||
results: (map['results']) == null
|
||||
? []
|
||||
: List<LocationDataFromResponse>.from(
|
||||
(map['results']).map(
|
||||
(x) =>
|
||||
LocationDataFromResponse.fromMap(x as Map<String, dynamic>),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LocationDataFromResponse {
|
||||
final String place;
|
||||
final List<double> bbox;
|
||||
LocationDataFromResponse({
|
||||
required this.place,
|
||||
required this.bbox,
|
||||
});
|
||||
|
||||
factory LocationDataFromResponse.fromMap(Map<String, dynamic> map) {
|
||||
return LocationDataFromResponse(
|
||||
place: map['place'] as String,
|
||||
bbox: List<double>.from(
|
||||
(map['bbox']),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
24
lib/models/search/recent_searches.dart
Normal file
24
lib/models/search/recent_searches.dart
Normal file
|
@ -0,0 +1,24 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
|
||||
class RecentSearches with ChangeNotifier {
|
||||
static RecentSearches? _instance;
|
||||
|
||||
RecentSearches._();
|
||||
|
||||
factory RecentSearches() => _instance ??= RecentSearches._();
|
||||
|
||||
final searches = <String>{};
|
||||
|
||||
void add(String query) {
|
||||
searches.add(query);
|
||||
while (searches.length > searchSectionLimit) {
|
||||
searches.remove(searches.first);
|
||||
}
|
||||
//buffer for not surfacing a new recent search before going to the next
|
||||
//screen
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
|
||||
abstract class SearchResult {
|
||||
ResultType type();
|
||||
|
@ -13,16 +14,3 @@ abstract class SearchResult {
|
|||
|
||||
List<EnteFile> resultFiles();
|
||||
}
|
||||
|
||||
enum ResultType {
|
||||
collection,
|
||||
file,
|
||||
location,
|
||||
month,
|
||||
year,
|
||||
fileType,
|
||||
fileExtension,
|
||||
fileCaption,
|
||||
event,
|
||||
magic,
|
||||
}
|
||||
|
|
287
lib/models/search/search_types.dart
Normal file
287
lib/models/search/search_types.dart
Normal file
|
@ -0,0 +1,287 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/collection_updated_event.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/collection/collection.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/typedefs.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/ui/viewer/location/add_location_sheet.dart";
|
||||
import "package:photos/ui/viewer/location/pick_center_point_widget.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/share_util.dart";
|
||||
|
||||
enum ResultType {
|
||||
collection,
|
||||
file,
|
||||
location,
|
||||
month,
|
||||
year,
|
||||
fileType,
|
||||
fileExtension,
|
||||
fileCaption,
|
||||
event,
|
||||
shared,
|
||||
magic,
|
||||
}
|
||||
|
||||
enum SectionType {
|
||||
face,
|
||||
location,
|
||||
// Grouping based on ML or manual tagging
|
||||
content,
|
||||
// includes year, month , day, event ResultType
|
||||
moment,
|
||||
// People section shows the files shared by other persons
|
||||
people,
|
||||
fileCaption,
|
||||
album,
|
||||
fileTypesAndExtension,
|
||||
}
|
||||
|
||||
extension SectionTypeExtensions on SectionType {
|
||||
// passing context for internalization in the future
|
||||
String sectionTitle(BuildContext context) {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return "Faces";
|
||||
case SectionType.content:
|
||||
return "Contents";
|
||||
case SectionType.moment:
|
||||
return S.of(context).moments;
|
||||
case SectionType.location:
|
||||
return S.of(context).location;
|
||||
case SectionType.people:
|
||||
return S.of(context).people;
|
||||
case SectionType.album:
|
||||
return S.of(context).albums;
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return S.of(context).fileTypes;
|
||||
case SectionType.fileCaption:
|
||||
return S.of(context).photoDescriptions;
|
||||
}
|
||||
}
|
||||
|
||||
String getEmptyStateText(BuildContext context) {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return S.of(context).searchFaceEmptySection;
|
||||
case SectionType.content:
|
||||
return "Contents";
|
||||
case SectionType.moment:
|
||||
return S.of(context).searchDatesEmptySection;
|
||||
case SectionType.location:
|
||||
return S.of(context).searchLocationEmptySection;
|
||||
case SectionType.people:
|
||||
return S.of(context).searchPeopleEmptySection;
|
||||
case SectionType.album:
|
||||
return S.of(context).searchAlbumsEmptySection;
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return S.of(context).searchFileTypesAndNamesEmptySection;
|
||||
case SectionType.fileCaption:
|
||||
return S.of(context).searchCaptionEmptySection;
|
||||
}
|
||||
}
|
||||
|
||||
// isCTAVisible is used to show/hide the CTA button in the empty state
|
||||
// Disable the CTA for face, content, moment, fileTypesAndExtension, fileCaption
|
||||
bool get isCTAVisible {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return false;
|
||||
case SectionType.content:
|
||||
return false;
|
||||
case SectionType.moment:
|
||||
return false;
|
||||
case SectionType.location:
|
||||
return true;
|
||||
case SectionType.people:
|
||||
return true;
|
||||
case SectionType.album:
|
||||
return true;
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return false;
|
||||
case SectionType.fileCaption:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isEmptyCTAVisible {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return true;
|
||||
case SectionType.content:
|
||||
return false;
|
||||
case SectionType.moment:
|
||||
return false;
|
||||
case SectionType.location:
|
||||
return true;
|
||||
case SectionType.people:
|
||||
return true;
|
||||
case SectionType.album:
|
||||
return true;
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return false;
|
||||
case SectionType.fileCaption:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String getCTAText(BuildContext context) {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return "Setup";
|
||||
case SectionType.content:
|
||||
return "Add tags";
|
||||
case SectionType.moment:
|
||||
return "Add new";
|
||||
case SectionType.location:
|
||||
return "Add new";
|
||||
case SectionType.people:
|
||||
return "Invite";
|
||||
case SectionType.album:
|
||||
return "Add new";
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return "";
|
||||
case SectionType.fileCaption:
|
||||
return "Add new";
|
||||
}
|
||||
}
|
||||
|
||||
IconData? getCTAIcon() {
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return Icons.adaptive.arrow_forward_outlined;
|
||||
case SectionType.content:
|
||||
return null;
|
||||
case SectionType.moment:
|
||||
return null;
|
||||
case SectionType.location:
|
||||
return Icons.add_location_alt_outlined;
|
||||
case SectionType.people:
|
||||
return Icons.adaptive.share;
|
||||
case SectionType.album:
|
||||
return Icons.add;
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return null;
|
||||
case SectionType.fileCaption:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
FutureVoidCallback ctaOnTap(BuildContext context) {
|
||||
switch (this) {
|
||||
case SectionType.people:
|
||||
return () async {
|
||||
shareText(
|
||||
S.of(context).shareTextRecommendUsingEnte,
|
||||
);
|
||||
};
|
||||
case SectionType.location:
|
||||
return () async {
|
||||
final centerPoint = await showPickCenterPointSheet(context);
|
||||
if (centerPoint != null) {
|
||||
showAddLocationSheet(context, centerPoint);
|
||||
}
|
||||
};
|
||||
case SectionType.album:
|
||||
return () async {
|
||||
final result = await showTextInputDialog(
|
||||
context,
|
||||
title: S.of(context).newAlbum,
|
||||
submitButtonLabel: S.of(context).create,
|
||||
hintText: S.of(context).enterAlbumName,
|
||||
alwaysShowSuccessState: false,
|
||||
initialValue: "",
|
||||
textCapitalization: TextCapitalization.words,
|
||||
onSubmit: (String text) async {
|
||||
// indicates user cancelled the rename request
|
||||
if (text.trim() == "") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Collection c =
|
||||
await CollectionsService.instance.createAlbum(text);
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(CollectionWithThumbnail(c, null)),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("CreateNewAlbumIcon")
|
||||
.severe("Failed to create a new album", e, s);
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (result is Exception) {
|
||||
showGenericErrorDialog(context: context);
|
||||
}
|
||||
};
|
||||
default:
|
||||
{
|
||||
return () async {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SearchResult>> getData({int? limit, BuildContext? context}) {
|
||||
if (this == SectionType.moment && context == null) {
|
||||
AssertionError("context cannot be null for SectionType.moment");
|
||||
}
|
||||
switch (this) {
|
||||
case SectionType.face:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
|
||||
case SectionType.content:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
|
||||
case SectionType.moment:
|
||||
return SearchService.instance.getRandomMomentsSearchResults(context!);
|
||||
|
||||
case SectionType.location:
|
||||
return SearchService.instance.getAllLocationTags(limit);
|
||||
|
||||
case SectionType.people:
|
||||
return SearchService.instance.getAllPeopleSearchResults(limit);
|
||||
|
||||
case SectionType.album:
|
||||
return SearchService.instance.getAllCollectionSearchResults(limit);
|
||||
|
||||
case SectionType.fileTypesAndExtension:
|
||||
return SearchService.instance
|
||||
.getAllFileTypesAndExtensionsResults(limit);
|
||||
|
||||
case SectionType.fileCaption:
|
||||
return SearchService.instance.getAllDescriptionSearchResults(limit);
|
||||
}
|
||||
}
|
||||
|
||||
List<Stream<Event>> viewAllUpdateEvents() {
|
||||
switch (this) {
|
||||
case SectionType.location:
|
||||
return [Bus.instance.on<LocationTagUpdatedEvent>()];
|
||||
case SectionType.album:
|
||||
return [Bus.instance.on<CollectionUpdatedEvent>()];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
///Events to listen to for different search sections, different from common
|
||||
///events listened to in AllSectionsExampleState.
|
||||
List<Stream<Event>> sectionUpdateEvents() {
|
||||
switch (this) {
|
||||
case SectionType.location:
|
||||
return [Bus.instance.on<LocationTagUpdatedEvent>()];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
|
||||
typedef BoolCallBack = bool Function();
|
||||
|
||||
|
@ -10,6 +11,7 @@ typedef VoidCallbackParamDouble = Function(double);
|
|||
typedef VoidCallbackParamBool = void Function(bool);
|
||||
typedef VoidCallbackParamListDouble = void Function(List<double>);
|
||||
typedef VoidCallbackParamLocation = void Function(Location);
|
||||
typedef VoidCallbackParamSearchResults = void Function(List<SearchResult>);
|
||||
|
||||
typedef FutureVoidCallback = Future<void> Function();
|
||||
typedef FutureOrVoidCallback = FutureOr<void> Function();
|
||||
|
|
|
@ -33,6 +33,10 @@ class UserDetails {
|
|||
return familyData?.members?.isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
bool hasPaidAddon() {
|
||||
return bonusData?.getAddOnBonuses().isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
bool isFamilyAdmin() {
|
||||
assert(isPartOfFamily(), "verify user is part of family before calling");
|
||||
final FamilyMember currentUserMember = familyData!.members!
|
||||
|
|
|
@ -166,7 +166,8 @@ class BillingService {
|
|||
BuildContext context,
|
||||
UserDetails userDetails,
|
||||
) async {
|
||||
if (userDetails.subscription.productID == freeProductID) {
|
||||
if (userDetails.subscription.productID == freeProductID &&
|
||||
!userDetails.hasPaidAddon()) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
S.of(context).familyPlans,
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/data/holidays.dart';
|
||||
|
@ -6,15 +9,18 @@ import 'package:photos/data/months.dart';
|
|||
import 'package:photos/data/years.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/extensions/string_ext.dart";
|
||||
import "package:photos/models/api/collection/user.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import 'package:photos/models/search/album_search_result.dart';
|
||||
import 'package:photos/models/search/generic_search_result.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/services/semantic_search/semantic_search_service.dart';
|
||||
|
@ -89,6 +95,36 @@ class SearchService {
|
|||
return collectionSearchResults;
|
||||
}
|
||||
|
||||
Future<List<AlbumSearchResult>> getAllCollectionSearchResults(
|
||||
int? limit,
|
||||
) async {
|
||||
try {
|
||||
final List<Collection> collections =
|
||||
_collectionService.getCollectionsForUI(
|
||||
includedShared: true,
|
||||
);
|
||||
|
||||
final List<AlbumSearchResult> collectionSearchResults = [];
|
||||
|
||||
for (var c in collections) {
|
||||
if (limit != null && collectionSearchResults.length >= limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!c.isHidden() && c.type != CollectionType.uncategorized) {
|
||||
final EnteFile? thumbnail = await _collectionService.getCover(c);
|
||||
collectionSearchResults
|
||||
.add(AlbumSearchResult(CollectionWithThumbnail(c, thumbnail)));
|
||||
}
|
||||
}
|
||||
|
||||
return collectionSearchResults;
|
||||
} catch (e) {
|
||||
_logger.severe("error gettin allCollectionSearchResults", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getYearSearchResults(
|
||||
String yearFromQuery,
|
||||
) async {
|
||||
|
@ -111,6 +147,96 @@ class SearchService {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getRandomMomentsSearchResults(
|
||||
BuildContext context,
|
||||
) async {
|
||||
try {
|
||||
final nonNullSearchResults = <GenericSearchResult>[];
|
||||
final randomYear = getRadomYearSearchResult();
|
||||
final randomMonth = getRandomMonthSearchResult(context);
|
||||
final randomDate = getRandomDateResults(context);
|
||||
final randomHoliday = getRandomHolidaySearchResult(context);
|
||||
|
||||
final searchResults = await Future.wait(
|
||||
[randomYear, randomMonth, randomDate, randomHoliday],
|
||||
);
|
||||
|
||||
for (GenericSearchResult? searchResult in searchResults) {
|
||||
if (searchResult != null) {
|
||||
nonNullSearchResults.add(searchResult);
|
||||
}
|
||||
}
|
||||
|
||||
return nonNullSearchResults;
|
||||
} catch (e) {
|
||||
_logger.severe("Error getting RandomMomentsSearchResult", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<GenericSearchResult?> getRadomYearSearchResult() async {
|
||||
for (var yearData in YearsData.instance.yearsData..shuffle()) {
|
||||
final List<EnteFile> filesInYear =
|
||||
await _getFilesInYear(yearData.duration);
|
||||
if (filesInYear.isNotEmpty) {
|
||||
return GenericSearchResult(
|
||||
ResultType.year,
|
||||
yearData.year,
|
||||
filesInYear,
|
||||
);
|
||||
}
|
||||
}
|
||||
//todo this throws error
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getMonthSearchResults(
|
||||
BuildContext context,
|
||||
String query,
|
||||
) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
for (var month in _getMatchingMonths(context, query)) {
|
||||
final matchedFiles =
|
||||
await FilesDB.instance.getFilesCreatedWithinDurations(
|
||||
_getDurationsOfMonthInEveryYear(month.monthNumber),
|
||||
ignoreCollections(),
|
||||
order: 'DESC',
|
||||
);
|
||||
if (matchedFiles.isNotEmpty) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.month,
|
||||
month.name,
|
||||
matchedFiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
Future<GenericSearchResult?> getRandomMonthSearchResult(
|
||||
BuildContext context,
|
||||
) async {
|
||||
final months = getMonthData(context)..shuffle();
|
||||
for (MonthData month in months) {
|
||||
final matchedFiles =
|
||||
await FilesDB.instance.getFilesCreatedWithinDurations(
|
||||
_getDurationsOfMonthInEveryYear(month.monthNumber),
|
||||
ignoreCollections(),
|
||||
order: 'DESC',
|
||||
);
|
||||
if (matchedFiles.isNotEmpty) {
|
||||
return GenericSearchResult(
|
||||
ResultType.month,
|
||||
month.name,
|
||||
matchedFiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getHolidaySearchResults(
|
||||
BuildContext context,
|
||||
String query,
|
||||
|
@ -139,6 +265,28 @@ class SearchService {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
Future<GenericSearchResult?> getRandomHolidaySearchResult(
|
||||
BuildContext context,
|
||||
) async {
|
||||
final holidays = getHolidays(context)..shuffle();
|
||||
for (var holiday in holidays) {
|
||||
final matchedFiles =
|
||||
await FilesDB.instance.getFilesCreatedWithinDurations(
|
||||
_getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
|
||||
ignoreCollections(),
|
||||
order: 'DESC',
|
||||
);
|
||||
if (matchedFiles.isNotEmpty) {
|
||||
return GenericSearchResult(
|
||||
ResultType.event,
|
||||
holiday.name,
|
||||
matchedFiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getFileTypeResults(
|
||||
String query,
|
||||
) async {
|
||||
|
@ -163,6 +311,203 @@ class SearchService {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getAllFileTypesAndExtensionsResults(
|
||||
int? limit,
|
||||
) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
final List<EnteFile> allFiles = await getAllFiles();
|
||||
final fileTypesAndMatchingFiles = <FileType, List<EnteFile>>{};
|
||||
final extensionsAndMatchingFiles = <String, List<EnteFile>>{};
|
||||
try {
|
||||
for (EnteFile file in allFiles) {
|
||||
if (!fileTypesAndMatchingFiles.containsKey(file.fileType)) {
|
||||
fileTypesAndMatchingFiles[file.fileType] = <EnteFile>[];
|
||||
}
|
||||
fileTypesAndMatchingFiles[file.fileType]!.add(file);
|
||||
|
||||
final String fileName = file.displayName;
|
||||
late final String ext;
|
||||
//Noticed that some old edited files do not have extensions and a '.'
|
||||
ext = fileName.contains(".")
|
||||
? fileName.split(".").last.toUpperCase()
|
||||
: "";
|
||||
|
||||
if (ext != "") {
|
||||
if (!extensionsAndMatchingFiles.containsKey(ext)) {
|
||||
extensionsAndMatchingFiles[ext] = <EnteFile>[];
|
||||
}
|
||||
extensionsAndMatchingFiles[ext]!.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
fileTypesAndMatchingFiles.forEach((key, value) {
|
||||
searchResults
|
||||
.add(GenericSearchResult(ResultType.fileType, key.name, value));
|
||||
});
|
||||
|
||||
extensionsAndMatchingFiles.forEach((key, value) {
|
||||
searchResults
|
||||
.add(GenericSearchResult(ResultType.fileExtension, key, value));
|
||||
});
|
||||
|
||||
if (limit != null) {
|
||||
return searchResults.sublist(0, min(limit, searchResults.length));
|
||||
} else {
|
||||
return searchResults;
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe("Error getting allFileTypesAndExtensionsResults", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
///Todo: Optimise + make this function more readable
|
||||
//This can be furthur optimized by not just limiting keys to 0 and 1. Use key
|
||||
//0 for single word, 1 for 2 word, 2 for 3 ..... and only check the substrings
|
||||
//in higher key if there are matches in the lower key.
|
||||
Future<List<GenericSearchResult>> getAllDescriptionSearchResults(
|
||||
//todo: use limit
|
||||
int? limit,
|
||||
) async {
|
||||
try {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
final List<EnteFile> allFiles = await getAllFiles();
|
||||
|
||||
//each list element will be substrings from a description mapped by
|
||||
//word count = 1 and word count > 1
|
||||
//New items will be added to [orderedSubDescriptions] list for every
|
||||
//distinct description.
|
||||
//[orderedSubDescriptions[x]] has two keys, 0 & 1. Value of key 0 will be single
|
||||
//word substrings. Value of key 1 will be multi word subStrings. When
|
||||
//iterating through [allFiles], we check for matching substrings from
|
||||
//[orderedSubDescriptions[x]] with the file's description. Starts from value
|
||||
//of key 0 (x=0). If there are no substring matches from key 0, there will
|
||||
//be none from key 1 as well. So these two keys are for avoiding unnecessary
|
||||
//checking of all subDescriptions with file description.
|
||||
final orderedSubDescs = <Map<int, List<String>>>[];
|
||||
final descAndMatchingFiles = <String, Set<EnteFile>>{};
|
||||
int distinctFullDescCount = 0;
|
||||
final allDistinctFullDescs = <String>[];
|
||||
|
||||
for (EnteFile file in allFiles) {
|
||||
if (file.caption != null && file.caption!.isNotEmpty) {
|
||||
//This limit doesn't necessarily have to be the limit parameter of the
|
||||
//method. Using the same variable to avoid unwanted iterations when
|
||||
//iterating over [orderedSubDescriptions] in case there is a limit
|
||||
//passed. Using the limit passed here so that there will be almost
|
||||
//always be more than 7 descriptionAndMatchingFiles and can shuffle
|
||||
//and choose only limited elements from it. Without shuffling,
|
||||
//result will be ["hello", "world", "hello world"] for the string
|
||||
//"hello world"
|
||||
|
||||
if (limit == null || distinctFullDescCount < limit) {
|
||||
final descAlreadyRecorded = allDistinctFullDescs
|
||||
.any((element) => element.contains(file.caption!.trim()));
|
||||
|
||||
if (!descAlreadyRecorded) {
|
||||
distinctFullDescCount++;
|
||||
allDistinctFullDescs.add(file.caption!.trim());
|
||||
final words = file.caption!.trim().split(" ");
|
||||
orderedSubDescs.add({0: <String>[], 1: <String>[]});
|
||||
|
||||
for (int i = 1; i <= words.length; i++) {
|
||||
for (int j = 0; j <= words.length - i; j++) {
|
||||
final subList = words.sublist(j, j + i);
|
||||
final substring = subList.join(" ").toLowerCase();
|
||||
if (i == 1) {
|
||||
orderedSubDescs.last[0]!.add(substring);
|
||||
} else {
|
||||
orderedSubDescs.last[1]!.add(substring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Map<int, List<String>> orderedSubDescription
|
||||
in orderedSubDescs) {
|
||||
bool matchesSingleWordSubString = false;
|
||||
for (String subDescription in orderedSubDescription[0]!) {
|
||||
if (file.caption!.toLowerCase().contains(subDescription)) {
|
||||
matchesSingleWordSubString = true;
|
||||
|
||||
//continue only after setting [matchesSingleWordSubString] to true
|
||||
if (subDescription.isAllConnectWords ||
|
||||
subDescription.isLastWordConnectWord) continue;
|
||||
|
||||
if (descAndMatchingFiles.containsKey(subDescription)) {
|
||||
descAndMatchingFiles[subDescription]!.add(file);
|
||||
} else {
|
||||
descAndMatchingFiles[subDescription] = {file};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchesSingleWordSubString) {
|
||||
for (String subDescription in orderedSubDescription[1]!) {
|
||||
if (subDescription.isAllConnectWords ||
|
||||
subDescription.isLastWordConnectWord) continue;
|
||||
|
||||
if (file.caption!.toLowerCase().contains(subDescription)) {
|
||||
if (descAndMatchingFiles.containsKey(subDescription)) {
|
||||
descAndMatchingFiles[subDescription]!.add(file);
|
||||
} else {
|
||||
descAndMatchingFiles[subDescription] = {file};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///[relevantDescAndFiles] will be a filterd version of [descriptionAndMatchingFiles]
|
||||
///In [descriptionAndMatchingFiles], there will be descriptions with the same
|
||||
///set of matching files. These descriptions will be substrings of a full
|
||||
///description. [relevantDescAndFiles] will keep only the entry which has the
|
||||
///longest description among enties with matching set of files.
|
||||
final relevantDescAndFiles = <String, Set<EnteFile>>{};
|
||||
while (descAndMatchingFiles.isNotEmpty) {
|
||||
final baseEntry = descAndMatchingFiles.entries.first;
|
||||
final descsWithSameFiles = <String, Set<EnteFile>>{};
|
||||
final baseUploadedFileIDs =
|
||||
baseEntry.value.map((e) => e.uploadedFileID).toSet();
|
||||
|
||||
descAndMatchingFiles.forEach((desc, files) {
|
||||
final uploadedFileIDs = files.map((e) => e.uploadedFileID).toSet();
|
||||
|
||||
final hasSameFiles =
|
||||
uploadedFileIDs.containsAll(baseUploadedFileIDs) &&
|
||||
baseUploadedFileIDs.containsAll(uploadedFileIDs);
|
||||
if (hasSameFiles) {
|
||||
descsWithSameFiles.addAll({desc: files});
|
||||
}
|
||||
});
|
||||
descAndMatchingFiles
|
||||
.removeWhere((desc, files) => descsWithSameFiles.containsKey(desc));
|
||||
final longestDescription = descsWithSameFiles.keys.reduce(
|
||||
(desc1, desc2) => desc1.length > desc2.length ? desc1 : desc2,
|
||||
);
|
||||
relevantDescAndFiles.addAll(
|
||||
{longestDescription: descsWithSameFiles[longestDescription]!},
|
||||
);
|
||||
}
|
||||
|
||||
relevantDescAndFiles.forEach((key, value) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(ResultType.fileCaption, key, value.toList()),
|
||||
);
|
||||
});
|
||||
if (limit != null) {
|
||||
return searchResults.sublist(0, min(limit, searchResults.length));
|
||||
} else {
|
||||
return searchResults;
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe("Error in getAllDescriptionSearchResults", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getCaptionAndNameResults(
|
||||
String query,
|
||||
) async {
|
||||
|
@ -322,29 +667,63 @@ class SearchService {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getMonthSearchResults(
|
||||
BuildContext context,
|
||||
String query,
|
||||
) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
for (var month in _getMatchingMonths(context, query)) {
|
||||
final matchedFiles =
|
||||
await FilesDB.instance.getFilesCreatedWithinDurations(
|
||||
_getDurationsOfMonthInEveryYear(month.monthNumber),
|
||||
ignoreCollections(),
|
||||
order: 'DESC',
|
||||
);
|
||||
if (matchedFiles.isNotEmpty) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.month,
|
||||
month.name,
|
||||
matchedFiles,
|
||||
),
|
||||
);
|
||||
Future<List<GenericSearchResult>> getAllLocationTags(int? limit) async {
|
||||
try {
|
||||
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
||||
final List<GenericSearchResult> tagSearchResults = [];
|
||||
final locationTagEntities =
|
||||
(await LocationService.instance.getLocationTags());
|
||||
final allFiles = await getAllFiles();
|
||||
|
||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||
if (limit != null && i >= limit) break;
|
||||
tagToItemsMap[locationTagEntities.elementAt(i)] = [];
|
||||
}
|
||||
|
||||
for (EnteFile file in allFiles) {
|
||||
if (file.hasLocation) {
|
||||
for (LocalEntity<LocationTag> tag in tagToItemsMap.keys) {
|
||||
if (LocationService.instance.isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
file.location!,
|
||||
tag.item.radius,
|
||||
)) {
|
||||
tagToItemsMap[tag]!.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (MapEntry<LocalEntity<LocationTag>, List<EnteFile>> entry
|
||||
in tagToItemsMap.entries) {
|
||||
if (entry.value.isNotEmpty) {
|
||||
tagSearchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
entry.key.item.name,
|
||||
entry.value,
|
||||
onResultTap: (ctx) {
|
||||
routeToPage(
|
||||
ctx,
|
||||
LocationScreenStateProvider(
|
||||
entry.key,
|
||||
LocationScreen(
|
||||
//this is SearchResult.heroTag()
|
||||
tagPrefix:
|
||||
"${ResultType.location.toString()}_${entry.key.item.name}",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return tagSearchResults;
|
||||
} catch (e) {
|
||||
_logger.severe("Error in getAllLocationTags", e);
|
||||
return [];
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getDateResults(
|
||||
|
@ -389,6 +768,124 @@ class SearchService {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
Future<GenericSearchResult?> getRandomDateResults(
|
||||
BuildContext context,
|
||||
) async {
|
||||
final allFiles = await getAllFiles();
|
||||
if (allFiles.isEmpty) return null;
|
||||
|
||||
final length = allFiles.length;
|
||||
final randomFile = allFiles[Random().nextInt(length)];
|
||||
final creationTime = randomFile.creationTime!;
|
||||
|
||||
final originalDateTime = DateTime.fromMicrosecondsSinceEpoch(creationTime);
|
||||
final startOfDay = DateTime(
|
||||
originalDateTime.year,
|
||||
originalDateTime.month,
|
||||
originalDateTime.day,
|
||||
);
|
||||
|
||||
final endOfDay = DateTime(
|
||||
originalDateTime.year,
|
||||
originalDateTime.month,
|
||||
originalDateTime.day + 1,
|
||||
);
|
||||
|
||||
final durationOfDay = [
|
||||
startOfDay.microsecondsSinceEpoch,
|
||||
endOfDay.microsecondsSinceEpoch,
|
||||
];
|
||||
|
||||
final matchedFiles = await FilesDB.instance.getFilesCreatedWithinDurations(
|
||||
[durationOfDay],
|
||||
ignoreCollections(),
|
||||
order: 'DESC',
|
||||
);
|
||||
|
||||
return GenericSearchResult(
|
||||
ResultType.event,
|
||||
DateFormat.yMMMd(Localizations.localeOf(context).languageCode).format(
|
||||
DateTime.fromMicrosecondsSinceEpoch(creationTime).toLocal(),
|
||||
),
|
||||
matchedFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getPeopleSearchResults(
|
||||
String query,
|
||||
) async {
|
||||
final lowerCaseQuery = query.toLowerCase();
|
||||
final searchResults = <GenericSearchResult>[];
|
||||
final allFiles = await getAllFiles();
|
||||
final peopleToSharedFiles = <User, List<EnteFile>>{};
|
||||
for (EnteFile file in allFiles) {
|
||||
if (file.isOwner) continue;
|
||||
|
||||
final fileOwner = CollectionsService.instance
|
||||
.getFileOwner(file.ownerID!, file.collectionID);
|
||||
|
||||
if (fileOwner.email.toLowerCase().contains(lowerCaseQuery) ||
|
||||
((fileOwner.name?.toLowerCase().contains(lowerCaseQuery)) ?? false)) {
|
||||
if (peopleToSharedFiles.containsKey(fileOwner)) {
|
||||
peopleToSharedFiles[fileOwner]!.add(file);
|
||||
} else {
|
||||
peopleToSharedFiles[fileOwner] = [file];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peopleToSharedFiles.forEach((key, value) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.shared,
|
||||
key.name != null && key.name!.isNotEmpty ? key.name! : key.email,
|
||||
value,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getAllPeopleSearchResults(
|
||||
int? limit,
|
||||
) async {
|
||||
try {
|
||||
final searchResults = <GenericSearchResult>[];
|
||||
final allFiles = await getAllFiles();
|
||||
final peopleToSharedFiles = <User, List<EnteFile>>{};
|
||||
int peopleCount = 0;
|
||||
for (EnteFile file in allFiles) {
|
||||
if (file.isOwner) continue;
|
||||
|
||||
final fileOwner = CollectionsService.instance
|
||||
.getFileOwner(file.ownerID!, file.collectionID);
|
||||
if (peopleToSharedFiles.containsKey(fileOwner)) {
|
||||
peopleToSharedFiles[fileOwner]!.add(file);
|
||||
} else {
|
||||
if (limit != null && limit <= peopleCount) continue;
|
||||
peopleToSharedFiles[fileOwner] = [file];
|
||||
peopleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
peopleToSharedFiles.forEach((key, value) {
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.shared,
|
||||
key.name != null && key.name!.isNotEmpty ? key.name! : key.email,
|
||||
value,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return searchResults;
|
||||
} catch (e) {
|
||||
_logger.severe("Error in getAllLocationTags", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
List<MonthData> _getMatchingMonths(BuildContext context, String query) {
|
||||
return getMonthData(context)
|
||||
.where(
|
||||
|
|
108
lib/states/all_sections_examples_state.dart
Normal file
108
lib/states/all_sections_examples_state.dart
Normal file
|
@ -0,0 +1,108 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/scheduler.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/files_updated_event.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
|
||||
class AllSectionsExamplesProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
const AllSectionsExamplesProvider({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<AllSectionsExamplesProvider> createState() =>
|
||||
_AllSectionsExamplesProviderState();
|
||||
}
|
||||
|
||||
class _AllSectionsExamplesProviderState
|
||||
extends State<AllSectionsExamplesProvider> {
|
||||
//Some section results in [allSectionsExamplesFuture] can be out of sync
|
||||
//with what is displayed on UI. This happens when some section is
|
||||
//independently listening to some set of events and is rebuilt. Sections
|
||||
//can listen to a list of events and rebuild (see sectionUpdateEvents()
|
||||
//in search_types.dart) and new results will not reflect in
|
||||
//[allSectionsExamplesFuture] unless reloadAllSections() is called.
|
||||
Future<List<List<SearchResult>>> allSectionsExamplesFuture = Future.value([]);
|
||||
|
||||
late StreamSubscription<FilesUpdatedEvent> _filesUpdatedEvent;
|
||||
final _logger = Logger("AllSectionsExamplesProvider");
|
||||
|
||||
final _debouncer =
|
||||
Debouncer(const Duration(seconds: 3), executionInterval: 6000);
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
//add all common events for all search sections to reload to here.
|
||||
_filesUpdatedEvent = Bus.instance.on<FilesUpdatedEvent>().listen((event) {
|
||||
reloadAllSections();
|
||||
});
|
||||
reloadAllSections();
|
||||
});
|
||||
}
|
||||
|
||||
void reloadAllSections() {
|
||||
_debouncer.run(() async {
|
||||
setState(() {
|
||||
_logger.info("reloading all sections in search tab");
|
||||
final allSectionsExamples = <Future<List<SearchResult>>>[];
|
||||
for (SectionType sectionType in SectionType.values) {
|
||||
if (sectionType == SectionType.face ||
|
||||
sectionType == SectionType.content) {
|
||||
continue;
|
||||
}
|
||||
allSectionsExamples.add(
|
||||
sectionType.getData(limit: searchSectionLimit, context: context),
|
||||
);
|
||||
}
|
||||
allSectionsExamplesFuture =
|
||||
Future.wait<List<SearchResult>>(allSectionsExamples);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_filesUpdatedEvent.cancel();
|
||||
_debouncer.cancelDebounce();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedAllSectionsExamples(
|
||||
allSectionsExamplesFuture,
|
||||
_debouncer.debounceActiveNotifier,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedAllSectionsExamples extends InheritedWidget {
|
||||
final Future<List<List<SearchResult>>> allSectionsExamplesFuture;
|
||||
final ValueNotifier<bool> isDebouncingNotifier;
|
||||
const InheritedAllSectionsExamples(
|
||||
this.allSectionsExamplesFuture,
|
||||
this.isDebouncingNotifier, {
|
||||
super.key,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static InheritedAllSectionsExamples of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedAllSectionsExamples>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant InheritedAllSectionsExamples oldWidget) {
|
||||
return !identical(
|
||||
oldWidget.allSectionsExamplesFuture,
|
||||
allSectionsExamplesFuture,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -168,8 +168,6 @@ class InheritedLocationTagData extends InheritedWidget {
|
|||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedLocationTagData oldWidget) {
|
||||
print(selectedRadius);
|
||||
print(oldWidget.selectedRadius != selectedRadius);
|
||||
return oldWidget.selectedRadius != selectedRadius ||
|
||||
!oldWidget.radiusValues.equals(radiusValues) ||
|
||||
oldWidget.centerPoint != centerPoint ||
|
||||
|
|
53
lib/states/search_results_state.dart
Normal file
53
lib/states/search_results_state.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/typedefs.dart";
|
||||
|
||||
class SearchResultsProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
const SearchResultsProvider({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SearchResultsProvider> createState() => _SearchResultsProviderState();
|
||||
}
|
||||
|
||||
class _SearchResultsProviderState extends State<SearchResultsProvider> {
|
||||
var searchResults = <SearchResult>[];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedSearchResults(
|
||||
searchResults,
|
||||
updateSearchResults,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void updateSearchResults(List<SearchResult> newResult) {
|
||||
setState(() {
|
||||
searchResults = newResult;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedSearchResults extends InheritedWidget {
|
||||
final List<SearchResult> results;
|
||||
final VoidCallbackParamSearchResults updateResults;
|
||||
const InheritedSearchResults(
|
||||
this.results,
|
||||
this.updateResults, {
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static InheritedSearchResults of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedSearchResults>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant InheritedSearchResults oldWidget) {
|
||||
return results != oldWidget.results;
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
|
|||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: ScrollController(
|
||||
initialScrollOffset: widget.initialScrollOffset ?? 0,
|
||||
),
|
||||
|
|
|
@ -23,6 +23,7 @@ class DeviceFolderVerticalGridView extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
elevation: 0,
|
||||
|
|
|
@ -10,13 +10,21 @@ import "package:photos/utils/dialog_util.dart";
|
|||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class NewAlbumIcon extends StatelessWidget {
|
||||
const NewAlbumIcon({Key? key}) : super(key: key);
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
final IconButtonType iconButtonType;
|
||||
const NewAlbumIcon({
|
||||
required this.icon,
|
||||
required this.iconButtonType,
|
||||
this.color,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButtonWidget(
|
||||
icon: Icons.add_rounded,
|
||||
iconButtonType: IconButtonType.secondary,
|
||||
icon: icon,
|
||||
iconButtonType: iconButtonType,
|
||||
onTap: () async {
|
||||
final result = await showTextInputDialog(
|
||||
context,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import "package:photos/models/search/button_result.dart";
|
||||
import 'package:photos/models/button_result.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/effects.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import "package:photos/models/button_result.dart";
|
||||
import 'package:photos/models/execution_states.dart';
|
||||
import "package:photos/models/search/button_result.dart";
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import "package:flutter/services.dart";
|
||||
import 'package:photos/core/constants.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/search/button_result.dart";
|
||||
import 'package:photos/models/button_result.dart';
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/effects.dart';
|
||||
|
@ -213,6 +213,7 @@ class _TextInputDialogState extends State<TextInputDialog> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController =
|
||||
widget.textEditingController ?? TextEditingController();
|
||||
_inputIsEmptyNotifier = widget.initialValue?.isEmpty ?? true
|
||||
|
@ -223,7 +224,6 @@ class _TextInputDialogState extends State<TextInputDialog> {
|
|||
_inputIsEmptyNotifier.value = _textEditingController.text.isEmpty;
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -235,7 +235,7 @@ class _TextInputDialogState extends State<TextInputDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final widthOfScreen = MediaQuery.of(context).size.width;
|
||||
final widthOfScreen = MediaQuery.sizeOf(context).width;
|
||||
final isMobileSmall = widthOfScreen <= mobileSmallThreshold;
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Container(
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/services/local_sync_service.dart";
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import 'package:photos/ui/viewer/search/search_widget.dart';
|
||||
import "package:photos/ui/settings/backup/backup_folder_selection_page.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class HomeHeaderWidget extends StatefulWidget {
|
||||
final Widget centerWidget;
|
||||
|
@ -33,7 +42,56 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
|
|||
duration: const Duration(milliseconds: 250),
|
||||
child: widget.centerWidget,
|
||||
),
|
||||
const SearchIconWidget(),
|
||||
IconButtonWidget(
|
||||
icon: Icons.add_photo_alternate_outlined,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
onTap: () async {
|
||||
try {
|
||||
final PermissionState state =
|
||||
await PhotoManager.requestPermissionExtend();
|
||||
await LocalSyncService.instance.onUpdatePermission(state);
|
||||
} on Exception catch (e) {
|
||||
Logger("HomeHeaderWidget").severe(
|
||||
"Failed to request permission: ${e.toString()}",
|
||||
e,
|
||||
);
|
||||
}
|
||||
if (!LocalSyncService.instance.hasGrantedFullPermission()) {
|
||||
if (Platform.isAndroid) {
|
||||
await PhotoManager.openSetting();
|
||||
} else {
|
||||
final bool hasGrantedLimit =
|
||||
LocalSyncService.instance.hasGrantedLimitedPermissions();
|
||||
showChoiceActionSheet(
|
||||
context,
|
||||
title: S.of(context).preserveMore,
|
||||
body: S.of(context).grantFullAccessPrompt,
|
||||
firstButtonLabel: S.of(context).openSettings,
|
||||
firstButtonOnTap: () async {
|
||||
await PhotoManager.openSetting();
|
||||
},
|
||||
secondButtonLabel: hasGrantedLimit
|
||||
? S.of(context).selectMorePhotos
|
||||
: S.of(context).cancel,
|
||||
secondButtonOnTap: () async {
|
||||
if (hasGrantedLimit) {
|
||||
await PhotoManager.presentLimited();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
BackupFolderSelectionPage(
|
||||
buttonText: S.of(context).backup,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import "package:flutter_animate/flutter_animate.dart";
|
||||
import "package:photos/ente_theme_data.dart";
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
@ -20,6 +21,7 @@ class NotificationWidget extends StatelessWidget {
|
|||
final String? subText;
|
||||
final GestureTapCallback onTap;
|
||||
final NotificationType type;
|
||||
final bool isBlackFriday;
|
||||
|
||||
const NotificationWidget({
|
||||
Key? key,
|
||||
|
@ -27,13 +29,14 @@ class NotificationWidget extends StatelessWidget {
|
|||
required this.actionIcon,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.isBlackFriday = false,
|
||||
this.subText,
|
||||
this.type = NotificationType.warning,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EnteColorScheme colorScheme = getEnteColorScheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
EnteTextTheme textTheme = getEnteTextTheme(context);
|
||||
TextStyle mainTextStyle = darkTextTheme.bodyBold;
|
||||
TextStyle subTextStyle = darkTextTheme.miniMuted;
|
||||
|
@ -46,7 +49,6 @@ class NotificationWidget extends StatelessWidget {
|
|||
backgroundColor = warning500;
|
||||
break;
|
||||
case NotificationType.banner:
|
||||
colorScheme = getEnteColorScheme(context);
|
||||
textTheme = getEnteTextTheme(context);
|
||||
backgroundColor = colorScheme.backgroundElevated2;
|
||||
mainTextStyle = textTheme.bodyBold;
|
||||
|
@ -90,11 +92,34 @@ class NotificationWidget extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(
|
||||
startIcon,
|
||||
size: 36,
|
||||
color: strokeColorScheme.strokeBase,
|
||||
),
|
||||
isBlackFriday
|
||||
? Icon(
|
||||
startIcon,
|
||||
size: 36,
|
||||
color: strokeColorScheme.strokeBase,
|
||||
)
|
||||
.animate(
|
||||
onPlay: (controller) =>
|
||||
controller.repeat(reverse: true),
|
||||
delay: 2000.ms,
|
||||
)
|
||||
.shake(
|
||||
duration: 500.ms,
|
||||
hz: 6,
|
||||
delay: 1600.ms,
|
||||
)
|
||||
.scale(
|
||||
duration: 500.ms,
|
||||
begin: const Offset(0.9, 0.9),
|
||||
end: const Offset(1.1, 1.1),
|
||||
delay: 1600.ms,
|
||||
// curve: Curves.easeInOut,
|
||||
)
|
||||
: Icon(
|
||||
startIcon,
|
||||
size: 36,
|
||||
color: strokeColorScheme.strokeBase,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
@ -132,10 +157,47 @@ class NotificationWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class NotificationTipWidget extends StatelessWidget {
|
||||
final String name;
|
||||
const NotificationTipWidget(this.name, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: colorScheme.strokeFaint),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 12,
|
||||
child: Text(
|
||||
name,
|
||||
style: textTheme.miniFaint,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Icon(
|
||||
Icons.tips_and_updates_outlined,
|
||||
color: colorScheme.strokeFaint,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationNoteWidget extends StatelessWidget {
|
||||
final String note;
|
||||
const NotificationNoteWidget(this.note, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
|
|
|
@ -90,6 +90,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.submitNotifier?.addListener(_onSubmit);
|
||||
widget.cancelNotifier?.addListener(_onCancel);
|
||||
_textController = widget.textEditingController ?? TextEditingController();
|
||||
|
@ -109,7 +110,6 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
widget.isEmptyNotifier!.value = _textController.text.isEmpty;
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -147,6 +147,20 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
|
|||
// of occasional missing events
|
||||
},
|
||||
),
|
||||
GButton(
|
||||
margin: const EdgeInsets.fromLTRB(10, 6, 8, 6),
|
||||
icon: Icons.search_outlined,
|
||||
iconColor: enteColorScheme.tabIcon,
|
||||
iconActiveColor: strokeBaseLight,
|
||||
text: '',
|
||||
onPressed: () {
|
||||
_onTabChange(
|
||||
3,
|
||||
mode: "OnPressed",
|
||||
); // To take care
|
||||
// of occasional missing events
|
||||
},
|
||||
),
|
||||
],
|
||||
selectedIndex: currentTabIndex,
|
||||
onTabChange: _onTabChange,
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
import 'dart:async';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import 'package:photos/ui/common/gradient_button.dart';
|
||||
import 'package:photos/ui/settings/backup/backup_folder_selection_page.dart';
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
|
||||
class PreserveFooterWidget extends StatelessWidget {
|
||||
const PreserveFooterWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 100),
|
||||
child: GradientButton(
|
||||
onTap: () async {
|
||||
try {
|
||||
final PermissionState state =
|
||||
await PhotoManager.requestPermissionExtend();
|
||||
await LocalSyncService.instance.onUpdatePermission(state);
|
||||
} on Exception catch (e) {
|
||||
Logger("PreserveFooterWidget").severe(
|
||||
"Failed to request permission: ${e.toString()}",
|
||||
e,
|
||||
);
|
||||
}
|
||||
if (!LocalSyncService.instance.hasGrantedFullPermission()) {
|
||||
if (Platform.isAndroid) {
|
||||
await PhotoManager.openSetting();
|
||||
} else {
|
||||
final bool hasGrantedLimit =
|
||||
LocalSyncService.instance.hasGrantedLimitedPermissions();
|
||||
showChoiceActionSheet(
|
||||
context,
|
||||
title: S.of(context).preserveMore,
|
||||
body: S.of(context).grantFullAccessPrompt,
|
||||
firstButtonLabel: S.of(context).openSettings,
|
||||
firstButtonOnTap: () async {
|
||||
await PhotoManager.openSetting();
|
||||
},
|
||||
secondButtonLabel: hasGrantedLimit
|
||||
? S.of(context).selectMorePhotos
|
||||
: S.of(context).cancel,
|
||||
secondButtonOnTap: () async {
|
||||
if (hasGrantedLimit) {
|
||||
await PhotoManager.presentLimited();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
BackupFolderSelectionPage(
|
||||
buttonText: S.of(context).backup,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: S.of(context).preserveMore,
|
||||
iconData: Icons.cloud_upload_outlined,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -170,7 +170,7 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
|
|||
child: ScrollablePositionedList.builder(
|
||||
physics: widget.disableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
: const BouncingScrollPhysics(),
|
||||
itemScrollController: widget.controller,
|
||||
itemPositionsListener: listener,
|
||||
initialScrollIndex: widget.startIndex,
|
||||
|
@ -183,6 +183,7 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
|
|||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: max(widget.totalCount, 0),
|
||||
itemBuilder: (context, index) {
|
||||
return ExcludeSemantics(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/search/button_result.dart";
|
||||
import 'package:photos/models/button_result.dart';
|
||||
import "package:photos/services/user_remote_flag_service.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/dialog_widget.dart";
|
||||
|
|
|
@ -240,7 +240,12 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
|
|||
}
|
||||
|
||||
if (_hasActiveSubscription) {
|
||||
widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
|
||||
widgets.add(
|
||||
ValidityWidget(
|
||||
currentSubscription: _currentSubscription,
|
||||
bonusData: _userDetails.bonusData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_currentSubscription!.productID == freeProductID) {
|
||||
|
|
|
@ -211,7 +211,12 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
widgets.add(_showSubscriptionToggle());
|
||||
|
||||
if (_hasActiveSubscription) {
|
||||
widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
|
||||
widgets.add(
|
||||
ValidityWidget(
|
||||
currentSubscription: _currentSubscription,
|
||||
bonusData: _userDetails.bonusData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_currentSubscription!.productID == freeProductID) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import "package:intl/intl.dart";
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/api/storage_bonus/bonus.dart";
|
||||
import 'package:photos/models/subscription.dart';
|
||||
import "package:photos/services/update_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
@ -87,21 +88,27 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
|
|||
|
||||
class ValidityWidget extends StatelessWidget {
|
||||
final Subscription? currentSubscription;
|
||||
final BonusData? bonusData;
|
||||
|
||||
const ValidityWidget({Key? key, this.currentSubscription}) : super(key: key);
|
||||
const ValidityWidget({Key? key, this.currentSubscription, this.bonusData})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (currentSubscription == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final bool isFreeTrialSub = currentSubscription!.productID == freeProductID;
|
||||
if (isFreeTrialSub && (bonusData?.getAddOnBonuses().isNotEmpty ?? false)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final endDate =
|
||||
DateFormat.yMMMd(Localizations.localeOf(context).languageCode).format(
|
||||
DateTime.fromMicrosecondsSinceEpoch(currentSubscription!.expiryTime),
|
||||
);
|
||||
|
||||
var message = S.of(context).renewsOn(endDate);
|
||||
if (currentSubscription!.productID == freeProductID) {
|
||||
if (isFreeTrialSub) {
|
||||
message = UpdateService.instance.isPlayStoreFlavor()
|
||||
? S.of(context).playStoreFreeTrialValidTill(endDate)
|
||||
: S.of(context).freeTrialValidTill(endDate);
|
||||
|
|
128
lib/ui/search_tab.dart
Normal file
128
lib/ui/search_tab.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
import "package:fade_indexed_stack/fade_indexed_stack.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/states/all_sections_examples_state.dart";
|
||||
import "package:photos/states/search_results_state.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/viewer/search/result/no_result_widget.dart";
|
||||
import "package:photos/ui/viewer/search/search_section.dart";
|
||||
import "package:photos/ui/viewer/search/search_suggestions.dart";
|
||||
import 'package:photos/ui/viewer/search/search_widget.dart';
|
||||
import "package:photos/ui/viewer/search/tab_empty_state.dart";
|
||||
|
||||
class SearchTab extends StatefulWidget {
|
||||
const SearchTab({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SearchTab> createState() => _SearchTabState();
|
||||
}
|
||||
|
||||
class _SearchTabState extends State<SearchTab> {
|
||||
var _searchResults = <SearchResult>[];
|
||||
int index = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_searchResults = InheritedSearchResults.of(context).results;
|
||||
if (_searchResults.isEmpty) {
|
||||
if (isSearchQueryEmpty) {
|
||||
index = 0;
|
||||
} else {
|
||||
index = 2;
|
||||
}
|
||||
} else {
|
||||
index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: AllSectionsExamplesProvider(
|
||||
child: FadeIndexedStack(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
index: index,
|
||||
children: [
|
||||
const AllSearchSections(),
|
||||
SearchSuggestionsWidget(_searchResults),
|
||||
const NoResultWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllSearchSections extends StatefulWidget {
|
||||
const AllSearchSections({super.key});
|
||||
|
||||
@override
|
||||
State<AllSearchSections> createState() => _AllSearchSectionsState();
|
||||
}
|
||||
|
||||
class _AllSearchSectionsState extends State<AllSearchSections> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchTypes = SectionType.values.toList(growable: true);
|
||||
// remove face and content sectionType
|
||||
searchTypes.remove(SectionType.face);
|
||||
searchTypes.remove(SectionType.content);
|
||||
return Stack(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: InheritedAllSectionsExamples.of(context)
|
||||
.allSectionsExamplesFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (snapshot.data!.every((element) => element.isEmpty)) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 72),
|
||||
child: SearchTabEmptyState(),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 180),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: searchTypes.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SearchSection(
|
||||
sectionType: searchTypes[index],
|
||||
examples: snapshot.data!.elementAt(index),
|
||||
limit: searchSectionLimit,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
//Errors are handled and this else if condition will be false always
|
||||
//is the understanding.
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 72),
|
||||
child: EnteLoadingWidget(),
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 72),
|
||||
child: EnteLoadingWidget(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable:
|
||||
InheritedAllSectionsExamples.of(context).isDebouncingNotifier,
|
||||
builder: (context, value, _) {
|
||||
return value
|
||||
? const EnteLoadingWidget(
|
||||
alignment: Alignment.topRight,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,9 @@ import 'package:photos/services/deduplication_service.dart';
|
|||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/captioned_text_widget.dart";
|
||||
import "package:photos/ui/components/dialog_widget.dart";
|
||||
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
|
@ -16,9 +18,10 @@ import 'package:photos/ui/settings/backup/backup_folder_selection_page.dart';
|
|||
import 'package:photos/ui/settings/backup/backup_settings_screen.dart';
|
||||
import 'package:photos/ui/settings/common_settings.dart';
|
||||
import 'package:photos/ui/tools/deduplicate_page.dart';
|
||||
import 'package:photos/ui/tools/free_space_page.dart';
|
||||
import "package:photos/ui/tools/free_space_page.dart";
|
||||
import 'package:photos/utils/data_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
|
@ -153,25 +156,55 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
|
|||
}
|
||||
|
||||
void _showSpaceFreedDialog(BackupStatus status) {
|
||||
showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).success,
|
||||
body: S.of(context).youHaveSuccessfullyFreedUp(formatBytes(status.size)),
|
||||
firstButtonLabel: S.of(context).rateUs,
|
||||
firstButtonOnTap: () async {
|
||||
UpdateService.instance.launchReviewUrl();
|
||||
},
|
||||
firstButtonType: ButtonType.primary,
|
||||
secondButtonLabel: S.of(context).ok,
|
||||
secondButtonOnTap: () async {
|
||||
if (Platform.isIOS) {
|
||||
showToast(
|
||||
context,
|
||||
S.of(context).remindToEmptyDeviceTrash,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (LocalSettings.instance.shouldPromptToRateUs()) {
|
||||
LocalSettings.instance.setRateUsShownCount(
|
||||
LocalSettings.instance.getRateUsShownCount() + 1,
|
||||
);
|
||||
showChoiceDialog(
|
||||
context,
|
||||
title: S.of(context).success,
|
||||
body:
|
||||
S.of(context).youHaveSuccessfullyFreedUp(formatBytes(status.size)),
|
||||
firstButtonLabel: S.of(context).rateUs,
|
||||
firstButtonOnTap: () async {
|
||||
UpdateService.instance.launchReviewUrl();
|
||||
},
|
||||
firstButtonType: ButtonType.primary,
|
||||
secondButtonLabel: S.of(context).ok,
|
||||
secondButtonOnTap: () async {
|
||||
if (Platform.isIOS) {
|
||||
showToast(
|
||||
context,
|
||||
S.of(context).remindToEmptyDeviceTrash,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showDialogWidget(
|
||||
context: context,
|
||||
title: S.of(context).success,
|
||||
body:
|
||||
S.of(context).youHaveSuccessfullyFreedUp(formatBytes(status.size)),
|
||||
icon: Icons.download_done_rounded,
|
||||
isDismissible: true,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.neutral,
|
||||
labelText: S.of(context).ok,
|
||||
isInAlert: true,
|
||||
onTap: () async {
|
||||
if (Platform.isIOS) {
|
||||
showToast(
|
||||
context,
|
||||
S.of(context).remindToEmptyDeviceTrash,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDuplicateFilesDeletedDialog(DeduplicationResult result) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:photos/events/opened_settings_event.dart';
|
|||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/feature_flag_service.dart';
|
||||
import "package:photos/services/storage_bonus_service.dart";
|
||||
import "package:photos/services/user_service.dart";
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import "package:photos/ui/components/notification_widget.dart";
|
||||
|
@ -28,6 +29,7 @@ import 'package:photos/ui/settings/support_section_widget.dart';
|
|||
import 'package:photos/ui/settings/theme_switch_widget.dart';
|
||||
import "package:photos/ui/sharing/verify_identity_dialog.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:url_launcher/url_launcher_string.dart";
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
final ValueNotifier<String?> emailNotifier;
|
||||
|
@ -84,23 +86,42 @@ class SettingsPage extends StatelessWidget {
|
|||
const sectionSpacing = SizedBox(height: 8);
|
||||
contents.add(const SizedBox(height: 8));
|
||||
if (hasLoggedIn) {
|
||||
final shouldShowBFBanner = shouldShowBfBanner();
|
||||
final showStorageBonusBanner =
|
||||
StorageBonusService.instance.shouldShowStorageBonus();
|
||||
contents.addAll([
|
||||
const StorageCardWidget(),
|
||||
StorageBonusService.instance.shouldShowStorageBonus()
|
||||
(shouldShowBFBanner || showStorageBonusBanner)
|
||||
? RepaintBoundary(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: NotificationWidget(
|
||||
startIcon: Icons.auto_awesome,
|
||||
actionIcon: Icons.arrow_forward_outlined,
|
||||
text: S.of(context).doubleYourStorage,
|
||||
subText: S.of(context).referFriendsAnd2xYourPlan,
|
||||
type: NotificationType.goldenBanner,
|
||||
onTap: () async {
|
||||
StorageBonusService.instance.markStorageBonusAsDone();
|
||||
routeToPage(context, const ReferralScreen());
|
||||
},
|
||||
),
|
||||
child: shouldShowBFBanner
|
||||
? NotificationWidget(
|
||||
isBlackFriday: true,
|
||||
startIcon: Icons.celebration,
|
||||
actionIcon: Icons.arrow_forward_outlined,
|
||||
text: S.of(context).blackFridaySale,
|
||||
subText: S.of(context).upto50OffUntil4thDec,
|
||||
type: NotificationType.goldenBanner,
|
||||
onTap: () async {
|
||||
launchUrlString(
|
||||
"https://ente.io/blackfriday",
|
||||
mode: LaunchMode.platformDefault,
|
||||
);
|
||||
},
|
||||
)
|
||||
: NotificationWidget(
|
||||
startIcon: Icons.auto_awesome,
|
||||
actionIcon: Icons.arrow_forward_outlined,
|
||||
text: S.of(context).doubleYourStorage,
|
||||
subText: S.of(context).referFriendsAnd2xYourPlan,
|
||||
type: NotificationType.goldenBanner,
|
||||
onTap: () async {
|
||||
StorageBonusService.instance
|
||||
.markStorageBonusAsDone();
|
||||
routeToPage(context, const ReferralScreen());
|
||||
},
|
||||
),
|
||||
).animate(onPlay: (controller) => controller.repeat()).shimmer(
|
||||
duration: 1000.ms,
|
||||
delay: 3200.ms,
|
||||
|
@ -150,6 +171,7 @@ class SettingsPage extends StatelessWidget {
|
|||
return SafeArea(
|
||||
bottom: false,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -166,6 +188,23 @@ class SettingsPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
bool shouldShowBfBanner() {
|
||||
if (!Platform.isAndroid && !kDebugMode) {
|
||||
return false;
|
||||
}
|
||||
// if date is after 5th of December 2023, 00:00:00, hide banner
|
||||
if (DateTime.now().isAfter(DateTime(2023, 12, 5))) {
|
||||
return false;
|
||||
}
|
||||
// if coupon is already applied, can hide the banner
|
||||
return (UserService.instance
|
||||
.getCachedUserDetails()
|
||||
?.bonusData
|
||||
?.getAddOnBonuses()
|
||||
.isEmpty ??
|
||||
true);
|
||||
}
|
||||
|
||||
Future<void> _showVerifyIdentityDialog(BuildContext context) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import "package:flutter_animate/flutter_animate.dart";
|
||||
import "package:flutter_local_notifications/flutter_local_notifications.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:media_extension/media_extension_action_types.dart';
|
||||
|
@ -32,8 +33,10 @@ import 'package:photos/services/local_sync_service.dart';
|
|||
import "package:photos/services/notification_service.dart";
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import "package:photos/states/search_results_state.dart";
|
||||
import 'package:photos/states/user_details_state.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import "package:photos/theme/effects.dart";
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/collections/collection_action_sheet.dart';
|
||||
import 'package:photos/ui/extents_page_view.dart';
|
||||
|
@ -43,14 +46,15 @@ import 'package:photos/ui/home/home_bottom_nav_bar.dart';
|
|||
import 'package:photos/ui/home/home_gallery_widget.dart';
|
||||
import 'package:photos/ui/home/landing_page_widget.dart';
|
||||
import "package:photos/ui/home/loading_photos_widget.dart";
|
||||
import 'package:photos/ui/home/preserve_footer_widget.dart';
|
||||
import 'package:photos/ui/home/start_backup_hook_widget.dart';
|
||||
import 'package:photos/ui/notification/update/change_log_page.dart';
|
||||
import "package:photos/ui/search_tab.dart";
|
||||
import 'package:photos/ui/settings/app_update_dialog.dart';
|
||||
import 'package:photos/ui/settings_page.dart';
|
||||
import "package:photos/ui/settings_page.dart";
|
||||
import "package:photos/ui/tabs/shared_collections_tab.dart";
|
||||
import "package:photos/ui/tabs/user_collections_tab.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import 'package:photos/ui/viewer/search/search_widget.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
@ -69,6 +73,7 @@ class HomeWidget extends StatefulWidget {
|
|||
class _HomeWidgetState extends State<HomeWidget> {
|
||||
static const _userCollectionsTab = UserCollectionsTab();
|
||||
static const _sharedCollectionTab = SharedCollectionsTab();
|
||||
static const _searchTab = SearchTab();
|
||||
static final _settingsPage = SettingsPage(
|
||||
emailNotifier: UserService.instance.emailValueNotifier,
|
||||
);
|
||||
|
@ -87,6 +92,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
List<SharedMediaFile>? _sharedFiles;
|
||||
bool _shouldRenderCreateCollectionSheet = false;
|
||||
bool _showShowBackupHook = false;
|
||||
final isOnSearchTabNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
|
||||
late StreamSubscription<SubscriptionPurchasedEvent>
|
||||
|
@ -104,12 +110,17 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
_logger.info("Building initstate");
|
||||
_tabChangedEventSubscription =
|
||||
Bus.instance.on<TabChangedEvent>().listen((event) {
|
||||
_selectedTabIndex = event.selectedIndex;
|
||||
|
||||
if (event.selectedIndex == 3) {
|
||||
isOnSearchTabNotifier.value = true;
|
||||
} else {
|
||||
isOnSearchTabNotifier.value = false;
|
||||
}
|
||||
if (event.source != TabChangedEventSource.pageView) {
|
||||
debugPrint(
|
||||
"TabChange going from $_selectedTabIndex to ${event.selectedIndex} souce: ${event.source}",
|
||||
);
|
||||
_selectedTabIndex = event.selectedIndex;
|
||||
// _pageController.jumpToPage(_selectedTabIndex);
|
||||
_pageController.animateToPage(
|
||||
event.selectedIndex,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
|
@ -263,6 +274,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
_accountConfiguredEvent.cancel();
|
||||
_intentDataStreamSubscription?.cancel();
|
||||
_collectionUpdatedEvent.cancel();
|
||||
isOnSearchTabNotifier.dispose();
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -381,44 +393,90 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
!LocalSyncService.instance.hasGrantedLimitedPermissions() &&
|
||||
CollectionsService.instance.getActiveCollections().isEmpty;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return ExtentsPageView(
|
||||
onPageChanged: (page) {
|
||||
Bus.instance.fire(
|
||||
TabChangedEvent(
|
||||
page,
|
||||
TabChangedEventSource.pageView,
|
||||
return SearchResultsProvider(
|
||||
child: Stack(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return ExtentsPageView(
|
||||
onPageChanged: (page) {
|
||||
Bus.instance.fire(
|
||||
TabChangedEvent(
|
||||
page,
|
||||
TabChangedEventSource.pageView,
|
||||
),
|
||||
);
|
||||
},
|
||||
controller: _pageController,
|
||||
openDrawer: Scaffold.of(context).openDrawer,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
_showShowBackupHook
|
||||
? const StartBackupHookWidget(headerWidget: _headerWidget)
|
||||
: HomeGalleryWidget(
|
||||
header: _headerWidget,
|
||||
footer: const SizedBox(
|
||||
height: 160,
|
||||
),
|
||||
selectedFiles: _selectedFiles,
|
||||
),
|
||||
_userCollectionsTab,
|
||||
_sharedCollectionTab,
|
||||
_searchTab,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: isOnSearchTabNotifier,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
decoration: value
|
||||
? BoxDecoration(
|
||||
color: getEnteColorScheme(context).backgroundElevated,
|
||||
boxShadow: shadowFloatFaintLight,
|
||||
)
|
||||
: null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
value
|
||||
? const SearchWidget()
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 225),
|
||||
curve: Curves.easeInOutSine,
|
||||
)
|
||||
.scale(
|
||||
begin: const Offset(0.8, 0.8),
|
||||
end: const Offset(1, 1),
|
||||
duration: const Duration(
|
||||
milliseconds: 225,
|
||||
),
|
||||
curve: Curves.easeInOutSine,
|
||||
)
|
||||
.slide(
|
||||
begin: const Offset(0, 0.4),
|
||||
curve: Curves.easeInOutSine,
|
||||
duration: const Duration(
|
||||
milliseconds: 225,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
HomeBottomNavigationBar(
|
||||
_selectedFiles,
|
||||
selectedTabIndex: _selectedTabIndex,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
controller: _pageController,
|
||||
openDrawer: Scaffold.of(context).openDrawer,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
_showShowBackupHook
|
||||
? const StartBackupHookWidget(headerWidget: _headerWidget)
|
||||
: HomeGalleryWidget(
|
||||
header: _headerWidget,
|
||||
footer: const PreserveFooterWidget(),
|
||||
selectedFiles: _selectedFiles,
|
||||
),
|
||||
_userCollectionsTab,
|
||||
_sharedCollectionTab,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: HomeBottomNavigationBar(
|
||||
_selectedFiles,
|
||||
selectedTabIndex: _selectedTabIndex,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
|
|||
final SectionTitle sharedByYou =
|
||||
SectionTitle(title: S.of(context).sharedByYou);
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 50),
|
||||
child: Column(
|
||||
|
|
|
@ -95,6 +95,7 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
|
|||
);
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
|
@ -224,7 +225,10 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const NewAlbumIcon(),
|
||||
const NewAlbumIcon(
|
||||
icon: Icons.add_rounded,
|
||||
iconButtonType: IconButtonType.secondary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTapDown: (TapDownDetails details) async {
|
||||
final int? selectedValue = await showMenu<int>(
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import "package:photo_view/photo_view_gallery.dart";
|
||||
import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
|
@ -88,21 +89,23 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
Widget content;
|
||||
|
||||
if (_imageProvider != null) {
|
||||
content = PhotoViewGestureDetectorScope(
|
||||
axis: Axis.vertical,
|
||||
child: PhotoView(
|
||||
imageProvider: _imageProvider,
|
||||
controller: _photoViewController,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
: PhotoViewComputedScale.contained,
|
||||
gaplessPlayback: true,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
),
|
||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||
),
|
||||
content = PhotoViewGallery.builder(
|
||||
gaplessPlayback: true,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
backgroundDecoration: widget.backgroundDecoration as BoxDecoration?,
|
||||
builder: (context, index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: _imageProvider!,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.tagPrefix! + _photo.tag,
|
||||
),
|
||||
controller: _photoViewController,
|
||||
);
|
||||
},
|
||||
itemCount: 1,
|
||||
);
|
||||
} else {
|
||||
content = const EnteLoadingWidget();
|
||||
|
|
|
@ -113,15 +113,14 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
: AppBar(
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
title: TextButton(
|
||||
child: Text(
|
||||
_appBarTitle!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall!
|
||||
.copyWith(fontSize: 16),
|
||||
),
|
||||
onPressed: () => _renameAlbum(context),
|
||||
title: Text(
|
||||
_appBarTitle!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall!
|
||||
.copyWith(fontSize: 16),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: _getDefaultActions(context),
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/location_state.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
@ -50,13 +50,16 @@ class EditCenterPointTileWidget extends StatelessWidget {
|
|||
),
|
||||
IconButtonWidget(
|
||||
onTap: () async {
|
||||
final EnteFile? centerPointFile = await showPickCenterPointSheet(
|
||||
final Location? centerPoint = await showPickCenterPointSheet(
|
||||
context,
|
||||
InheritedLocationTagData.of(context).locationTagEntity!,
|
||||
locationTagName: InheritedLocationTagData.of(context)
|
||||
.locationTagEntity!
|
||||
.item
|
||||
.name,
|
||||
);
|
||||
if (centerPointFile != null) {
|
||||
if (centerPoint != null) {
|
||||
InheritedLocationTagData.of(context)
|
||||
.updateCenterPoint(centerPointFile.location!);
|
||||
.updateCenterPoint(centerPoint);
|
||||
}
|
||||
},
|
||||
icon: Icons.edit,
|
||||
|
|
|
@ -28,7 +28,8 @@ import "package:photos/ui/viewer/location/edit_location_sheet.dart";
|
|||
import "package:photos/utils/dialog_util.dart";
|
||||
|
||||
class LocationScreen extends StatelessWidget {
|
||||
const LocationScreen({super.key});
|
||||
final String tagPrefix;
|
||||
const LocationScreen({this.tagPrefix = "", super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -49,7 +50,9 @@ class LocationScreen extends StatelessWidget {
|
|||
height: MediaQuery.of(context).size.height -
|
||||
(heightOfAppBar + heightOfStatusBar),
|
||||
width: double.infinity,
|
||||
child: const LocationGalleryWidget(),
|
||||
child: LocationGalleryWidget(
|
||||
tagPrefix: tagPrefix,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -126,7 +129,8 @@ class LocationScreenPopUpMenu extends StatelessWidget {
|
|||
}
|
||||
|
||||
class LocationGalleryWidget extends StatefulWidget {
|
||||
const LocationGalleryWidget({super.key});
|
||||
final String tagPrefix;
|
||||
const LocationGalleryWidget({required this.tagPrefix, super.key});
|
||||
|
||||
@override
|
||||
State<LocationGalleryWidget> createState() => _LocationGalleryWidgetState();
|
||||
|
@ -229,7 +233,7 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
|
|||
EventType.deletedFromEverywhere,
|
||||
},
|
||||
selectedFiles: _selectedFiles,
|
||||
tagPrefix: "location_gallery",
|
||||
tagPrefix: widget.tagPrefix,
|
||||
),
|
||||
FileSelectionOverlayBar(
|
||||
GalleryType.locationTag,
|
||||
|
|
|
@ -7,10 +7,8 @@ import "package:photos/core/event_bus.dart";
|
|||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/events/local_photos_updated_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location_tag/location_tag.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/filter/db_filters.dart";
|
||||
|
@ -19,17 +17,18 @@ import "package:photos/theme/ente_theme.dart";
|
|||
import "package:photos/ui/components/bottom_of_title_bar_widget.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/components/notification_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
|
||||
Future<EnteFile?> showPickCenterPointSheet(
|
||||
BuildContext context,
|
||||
LocalEntity<LocationTag> locationTagEntity,
|
||||
) async {
|
||||
Future<Location?> showPickCenterPointSheet(
|
||||
BuildContext context, {
|
||||
String? locationTagName,
|
||||
}) async {
|
||||
return await showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PickCenterPointWidget(locationTagEntity);
|
||||
return PickCenterPointWidget(locationTagName);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0),
|
||||
|
@ -45,10 +44,10 @@ Future<EnteFile?> showPickCenterPointSheet(
|
|||
}
|
||||
|
||||
class PickCenterPointWidget extends StatelessWidget {
|
||||
final LocalEntity<LocationTag> locationTagEntity;
|
||||
final String? locationTagName;
|
||||
|
||||
const PickCenterPointWidget(
|
||||
this.locationTagEntity, {
|
||||
this.locationTagName, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -81,7 +80,7 @@ class PickCenterPointWidget extends StatelessWidget {
|
|||
title: TitleBarTitleWidget(
|
||||
title: S.of(context).pickCenterPoint,
|
||||
),
|
||||
caption: locationTagEntity.item.name,
|
||||
caption: locationTagName ?? "New location",
|
||||
),
|
||||
Expanded(
|
||||
child: Gallery(
|
||||
|
@ -114,6 +113,12 @@ class PickCenterPointWidget extends StatelessWidget {
|
|||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: true,
|
||||
showSelectAllByDefault: false,
|
||||
header: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: NotificationTipWidget(
|
||||
"You can also add a location centered on a photo from the photo's info screen",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -145,9 +150,9 @@ class PickCenterPointWidget extends StatelessWidget {
|
|||
buttonType: ButtonType.neutral,
|
||||
labelText: S.of(context).useSelectedPhoto,
|
||||
onTap: () async {
|
||||
final selectedFile =
|
||||
selectedFiles.files.first;
|
||||
Navigator.pop(context, selectedFile);
|
||||
final selectedLocation =
|
||||
selectedFiles.files.first.location;
|
||||
Navigator.pop(context, selectedLocation);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
68
lib/ui/viewer/search/result/go_to_map_widget.dart
Normal file
68
lib/ui/viewer/search/result/go_to_map_widget.dart
Normal file
|
@ -0,0 +1,68 @@
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/map/enable_map.dart";
|
||||
import "package:photos/ui/map/map_screen.dart";
|
||||
|
||||
class GoToMapWidget extends StatelessWidget {
|
||||
const GoToMapWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||
late final double width;
|
||||
if (textScaleFactor <= 1.0) {
|
||||
width = 85.0;
|
||||
} else {
|
||||
width = 85.0 + ((textScaleFactor - 1.0) * 64);
|
||||
}
|
||||
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final bool result = await requestForMapEnable(context);
|
||||
if (result) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapScreen(
|
||||
filesFutureFn: SearchService.instance.getAllFiles,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: Icon(
|
||||
CupertinoIcons.map_fill,
|
||||
color: colorScheme.strokeFaint,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
S.of(context).yourMap,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context).mini,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,74 +1,118 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/states/all_sections_examples_state.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
||||
class NoResultWidget extends StatelessWidget {
|
||||
class NoResultWidget extends StatefulWidget {
|
||||
const NoResultWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NoResultWidget> createState() => _NoResultWidgetState();
|
||||
}
|
||||
|
||||
class _NoResultWidgetState extends State<NoResultWidget> {
|
||||
late final List<SectionType> searchTypes;
|
||||
final searchTypeToQuerySuggestion = <String, List<String>>{};
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
searchTypes = SectionType.values.toList(growable: true);
|
||||
// remove face and content sectionType
|
||||
searchTypes.remove(SectionType.face);
|
||||
searchTypes.remove(SectionType.content);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
InheritedAllSectionsExamples.of(context)
|
||||
.allSectionsExamplesFuture
|
||||
.then((value) {
|
||||
for (int i = 0; i < searchTypes.length; i++) {
|
||||
final querySuggestions = <String>[];
|
||||
for (int j = 0; j < 2 && j < value[i].length; j++) {
|
||||
querySuggestions.add(value[i][j].name());
|
||||
}
|
||||
if (querySuggestions.isNotEmpty) {
|
||||
searchTypeToQuerySuggestion.addAll({
|
||||
searchTypes[i].sectionTitle(context): querySuggestions,
|
||||
});
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.searchResultsColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
spreadRadius: -3,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 8),
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final searchTypeAndSuggestion = <Widget>[];
|
||||
searchTypeToQuerySuggestion.forEach(
|
||||
(key, value) {
|
||||
searchTypeAndSuggestion.add(
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
key,
|
||||
style: textTheme.bodyMuted,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
formatList(value),
|
||||
style: textTheme.miniMuted,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).noResultsFound,
|
||||
style: textTheme.largeBold,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
searchTypeToQuerySuggestion.isNotEmpty
|
||||
? Text(
|
||||
S.of(context).modifyYourQueryOrTrySearchingFor,
|
||||
style: textTheme.smallMuted,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
return searchTypeAndSuggestion[index];
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 12);
|
||||
},
|
||||
itemCount: searchTypeToQuerySuggestion.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
S.of(context).noResultsFound,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
S.of(context).youCanTrySearchingForADifferentQuery,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.5),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 20, top: 12),
|
||||
child: Text(
|
||||
S.of(context).searchByExamples,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.defaultTextColor
|
||||
.withOpacity(0.5),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Join the strings with ', ' and wrap each element with double quotes
|
||||
String formatList(List<String> strings) {
|
||||
return strings.map((str) => '"$str"').join(', ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
|
|||
class SearchResultPage extends StatelessWidget {
|
||||
final SearchResult searchResult;
|
||||
final bool enableGrouping;
|
||||
final String tagPrefix;
|
||||
|
||||
final _selectedFiles = SelectedFiles();
|
||||
static const GalleryType appBarType = GalleryType.searchResults;
|
||||
|
@ -22,6 +23,7 @@ class SearchResultPage extends StatelessWidget {
|
|||
SearchResultPage(
|
||||
this.searchResult, {
|
||||
this.enableGrouping = true,
|
||||
this.tagPrefix = "",
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -49,10 +51,10 @@ class SearchResultPage extends StatelessWidget {
|
|||
EventType.deletedFromRemote,
|
||||
EventType.deletedFromEverywhere,
|
||||
},
|
||||
tagPrefix: searchResult.heroTag(),
|
||||
tagPrefix: tagPrefix + searchResult.heroTag(),
|
||||
selectedFiles: _selectedFiles,
|
||||
initialFiles: const [],
|
||||
enableFileGrouping: enableGrouping,
|
||||
initialFiles: [searchResult.resultFiles().first],
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/models/search/recent_searches.dart";
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/viewer/search/result/search_result_page.dart';
|
||||
import 'package:photos/ui/viewer/search/result/search_thumbnail_widget.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
|
@ -20,81 +23,81 @@ class SearchResultWidget extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final heroTagPrefix = searchResult.heroTag();
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.searchResultsColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SearchThumbnailWidget(
|
||||
searchResult.previewThumbnail(),
|
||||
heroTagPrefix,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
border: Border.all(
|
||||
color: getEnteColorScheme(context).strokeFainter,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SearchThumbnailWidget(
|
||||
searchResult.previewThumbnail(),
|
||||
heroTagPrefix,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_resultTypeName(searchResult.type()),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.subTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: Text(
|
||||
searchResult.name(),
|
||||
style: const TextStyle(fontSize: 18),
|
||||
style: textTheme.body,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
FutureBuilder<int>(
|
||||
future: resultCount ??
|
||||
Future.value(searchResult.resultFiles().length),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data! > 0) {
|
||||
final noOfMemories = snapshot.data;
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.searchResultsCountTextColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: noOfMemories.toString()),
|
||||
TextSpan(
|
||||
text:
|
||||
noOfMemories != 1 ? ' memories' : ' memory',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
_resultTypeName(searchResult.type()),
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: resultCount ??
|
||||
Future.value(searchResult.resultFiles().length),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data! > 0) {
|
||||
final noOfMemories = snapshot.data;
|
||||
|
||||
return Text(
|
||||
" \u2022 " + noOfMemories.toString(),
|
||||
style: textTheme.smallMuted,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.subTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
RecentSearches().add(searchResult.name());
|
||||
|
||||
if (onResultTap != null) {
|
||||
onResultTap!();
|
||||
} else {
|
||||
|
@ -132,6 +135,8 @@ class SearchResultWidget extends StatelessWidget {
|
|||
return "Description";
|
||||
case ResultType.magic:
|
||||
return "Magic";
|
||||
case ResultType.shared:
|
||||
return "Shared";
|
||||
default:
|
||||
return type.name.toUpperCase();
|
||||
}
|
||||
|
|
197
lib/ui/viewer/search/result/search_section_all_page.dart
Normal file
197
lib/ui/viewer/search/result/search_section_all_page.dart
Normal file
|
@ -0,0 +1,197 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_animate/flutter_animate.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/models/search/album_search_result.dart";
|
||||
import "package:photos/models/search/generic_search_result.dart";
|
||||
import "package:photos/models/search/recent_searches.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/ui/viewer/search/result/searchable_item.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class SearchSectionAllPage extends StatefulWidget {
|
||||
final SectionType sectionType;
|
||||
const SearchSectionAllPage({required this.sectionType, super.key});
|
||||
|
||||
@override
|
||||
State<SearchSectionAllPage> createState() => _SearchSectionAllPageState();
|
||||
}
|
||||
|
||||
class _SearchSectionAllPageState extends State<SearchSectionAllPage> {
|
||||
late Future<List<SearchResult>> sectionData;
|
||||
final streamSubscriptions = <StreamSubscription>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final streamsToListenTo = widget.sectionType.viewAllUpdateEvents();
|
||||
for (Stream<Event> stream in streamsToListenTo) {
|
||||
streamSubscriptions.add(
|
||||
stream.listen((event) async {
|
||||
setState(() {
|
||||
sectionData = widget.sectionType.getData();
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
sectionData = widget.sectionType.getData(limit: null, context: context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var subscriptions in streamSubscriptions) {
|
||||
subscriptions.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 48,
|
||||
leadingWidth: 48,
|
||||
leading: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.arrow_back_outlined,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TitleBarTitleWidget(
|
||||
title: widget.sectionType.sectionTitle(context),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: sectionData,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final sectionResults = snapshot.data!;
|
||||
return Text(sectionResults.length.toString())
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: FutureBuilder(
|
||||
future: sectionData,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final sectionResults = snapshot.data!;
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (sectionResults.length == index) {
|
||||
return SearchableItemPlaceholder(
|
||||
widget.sectionType,
|
||||
);
|
||||
}
|
||||
if (sectionResults[index] is AlbumSearchResult) {
|
||||
final albumSectionResult =
|
||||
sectionResults[index] as AlbumSearchResult;
|
||||
return SearchableItemWidget(
|
||||
albumSectionResult,
|
||||
resultCount:
|
||||
CollectionsService.instance.getFileCount(
|
||||
albumSectionResult
|
||||
.collectionWithThumbnail.collection,
|
||||
),
|
||||
onResultTap: () {
|
||||
RecentSearches()
|
||||
.add(sectionResults[index].name());
|
||||
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
albumSectionResult.collectionWithThumbnail,
|
||||
tagPrefix: "searchable_item" +
|
||||
albumSectionResult.heroTag(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (sectionResults[index]
|
||||
is GenericSearchResult) {
|
||||
final result =
|
||||
sectionResults[index] as GenericSearchResult;
|
||||
return SearchableItemWidget(
|
||||
sectionResults[index],
|
||||
onResultTap: result.onResultTap != null
|
||||
? () => result.onResultTap!(context)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
return SearchableItemWidget(
|
||||
sectionResults[index],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 10);
|
||||
},
|
||||
itemCount: sectionResults.length +
|
||||
(widget.sectionType.isCTAVisible ? 1 : 0),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
//This cache extend is needed for creating a new album
|
||||
//using SearchSectionCTATile to work. This is so that
|
||||
//SearchSectionCTATile doesn't get disposed when keyboard
|
||||
//is open and the widget is out of view.
|
||||
cacheExtent:
|
||||
widget.sectionType == SectionType.album ? 400 : null,
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 225),
|
||||
curve: Curves.easeIn,
|
||||
)
|
||||
.slide(
|
||||
begin: const Offset(0, -0.01),
|
||||
curve: Curves.easeIn,
|
||||
duration: const Duration(
|
||||
milliseconds: 225,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,15 +18,17 @@ class SearchThumbnailWidget extends StatelessWidget {
|
|||
return Hero(
|
||||
tag: tagPrefix + (file?.tag ?? ""),
|
||||
child: SizedBox(
|
||||
height: 58,
|
||||
width: 58,
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
borderRadius: const BorderRadius.horizontal(left: Radius.circular(4)),
|
||||
child: file != null
|
||||
? ThumbnailWidget(
|
||||
file!,
|
||||
)
|
||||
: const NoThumbnailWidget(),
|
||||
: const NoThumbnailWidget(
|
||||
addBorder: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
174
lib/ui/viewer/search/result/searchable_item.dart
Normal file
174
lib/ui/viewer/search/result/searchable_item.dart
Normal file
|
@ -0,0 +1,174 @@
|
|||
import "package:dotted_border/dotted_border.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/models/search/recent_searches.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/buttons/icon_button_widget.dart";
|
||||
import "package:photos/ui/viewer/search/result/search_result_page.dart";
|
||||
import "package:photos/ui/viewer/search/result/search_thumbnail_widget.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class SearchableItemWidget extends StatelessWidget {
|
||||
final SearchResult searchResult;
|
||||
final Future<int>? resultCount;
|
||||
final Function? onResultTap;
|
||||
const SearchableItemWidget(
|
||||
this.searchResult, {
|
||||
Key? key,
|
||||
this.resultCount,
|
||||
this.onResultTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//The "searchable_item" tag is to remove hero animation between section
|
||||
//examples and searchableItems in 'view all'. Animation should exist between
|
||||
//searchableItems and SearchResultPages, so passing the extra prefix to
|
||||
//SearchResultPage
|
||||
const additionalPrefix = "searchable_item";
|
||||
final heroTagPrefix = additionalPrefix + searchResult.heroTag();
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
RecentSearches().add(searchResult.name());
|
||||
if (onResultTap != null) {
|
||||
onResultTap!();
|
||||
} else {
|
||||
routeToPage(
|
||||
context,
|
||||
SearchResultPage(
|
||||
searchResult,
|
||||
tagPrefix: additionalPrefix,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: colorScheme.strokeFainter),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 6,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: SearchThumbnailWidget(
|
||||
searchResult.previewThumbnail(),
|
||||
heroTagPrefix,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
searchResult.name(),
|
||||
style: textTheme.body,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
future: resultCount ??
|
||||
Future.value(searchResult.resultFiles().length),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data! > 0) {
|
||||
final noOfMemories = snapshot.data;
|
||||
final String suffix =
|
||||
noOfMemories! > 1 ? " memories" : " memory";
|
||||
|
||||
return Text(
|
||||
noOfMemories.toString() + suffix,
|
||||
style: textTheme.smallMuted,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
flex: 1,
|
||||
child: IconButtonWidget(
|
||||
icon: Icons.chevron_right_outlined,
|
||||
iconButtonType: IconButtonType.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchableItemPlaceholder extends StatelessWidget {
|
||||
final SectionType sectionType;
|
||||
const SearchableItemPlaceholder(this.sectionType, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (sectionType.isCTAVisible == false) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 1),
|
||||
child: GestureDetector(
|
||||
onTap: sectionType.ctaOnTap(context),
|
||||
child: DottedBorder(
|
||||
strokeWidth: 2,
|
||||
borderType: BorderType.RRect,
|
||||
radius: const Radius.circular(4),
|
||||
padding: EdgeInsets.zero,
|
||||
dashPattern: const [4, 4],
|
||||
color: colorScheme.strokeFainter,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.horizontal(left: Radius.circular(4)),
|
||||
color: colorScheme.fillFaint,
|
||||
),
|
||||
child: Icon(
|
||||
sectionType.getCTAIcon(),
|
||||
color: colorScheme.strokeMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
sectionType.getCTAText(context),
|
||||
style: textTheme.body,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
266
lib/ui/viewer/search/search_section.dart
Normal file
266
lib/ui/viewer/search/search_section.dart
Normal file
|
@ -0,0 +1,266 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/models/search/album_search_result.dart";
|
||||
import "package:photos/models/search/generic_search_result.dart";
|
||||
import "package:photos/models/search/recent_searches.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
|
||||
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/ui/viewer/search/result/go_to_map_widget.dart";
|
||||
import "package:photos/ui/viewer/search/result/search_result_page.dart";
|
||||
import 'package:photos/ui/viewer/search/result/search_section_all_page.dart';
|
||||
import "package:photos/ui/viewer/search/search_section_cta.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class SearchSection extends StatefulWidget {
|
||||
final SectionType sectionType;
|
||||
final List<SearchResult> examples;
|
||||
final int limit;
|
||||
|
||||
const SearchSection({
|
||||
Key? key,
|
||||
required this.sectionType,
|
||||
required this.examples,
|
||||
required this.limit,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SearchSection> createState() => _SearchSectionState();
|
||||
}
|
||||
|
||||
class _SearchSectionState extends State<SearchSection> {
|
||||
late List<SearchResult> _examples;
|
||||
final streamSubscriptions = <StreamSubscription>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_examples = widget.examples;
|
||||
|
||||
final streamsToListenTo = widget.sectionType.sectionUpdateEvents();
|
||||
for (Stream<Event> stream in streamsToListenTo) {
|
||||
streamSubscriptions.add(
|
||||
stream.listen((event) async {
|
||||
_examples =
|
||||
await widget.sectionType.getData(limit: searchSectionLimit);
|
||||
setState(() {});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var subscriptions in streamSubscriptions) {
|
||||
subscriptions.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SearchSection oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_examples = widget.examples;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("Building section for ${widget.sectionType.name}");
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: widget.examples.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
widget.sectionType.sectionTitle(context),
|
||||
style: textTheme.largeBold,
|
||||
),
|
||||
),
|
||||
_examples.length < (widget.limit - 1)
|
||||
? const SizedBox.shrink()
|
||||
: GestureDetector(
|
||||
onTap: () {
|
||||
routeToPage(
|
||||
context,
|
||||
SearchSectionAllPage(
|
||||
sectionType: widget.sectionType,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
SearchExampleRow(_examples, widget.sectionType),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.sectionType.sectionTitle(context),
|
||||
style: textTheme.largeBold,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
widget.sectionType.getEmptyStateText(context),
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SearchSectionEmptyCTAIcon(widget.sectionType),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchExampleRow extends StatelessWidget {
|
||||
final SectionType sectionType;
|
||||
final List<SearchResult> examples;
|
||||
|
||||
const SearchExampleRow(this.examples, this.sectionType, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//Cannot use listView.builder here
|
||||
final scrollableExamples = <Widget>[];
|
||||
if (sectionType == SectionType.location) {
|
||||
scrollableExamples.add(const GoToMapWidget());
|
||||
}
|
||||
examples.forEachIndexed((index, element) {
|
||||
scrollableExamples.add(
|
||||
SearchExample(
|
||||
searchResult: examples.elementAt(index),
|
||||
),
|
||||
);
|
||||
});
|
||||
scrollableExamples.add(SearchSectionCTAIcon(sectionType));
|
||||
return SizedBox(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: scrollableExamples,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchExample extends StatelessWidget {
|
||||
final SearchResult searchResult;
|
||||
const SearchExample({required this.searchResult, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||
late final double width;
|
||||
if (textScaleFactor <= 1.0) {
|
||||
width = 85.0;
|
||||
} else {
|
||||
width = 85.0 + ((textScaleFactor - 1.0) * 64);
|
||||
}
|
||||
final heroTag =
|
||||
searchResult.heroTag() + (searchResult.previewThumbnail()?.tag ?? "");
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
RecentSearches().add(searchResult.name());
|
||||
|
||||
if (searchResult is GenericSearchResult) {
|
||||
final genericSearchResult = searchResult as GenericSearchResult;
|
||||
if (genericSearchResult.onResultTap != null) {
|
||||
genericSearchResult.onResultTap!(context);
|
||||
} else {
|
||||
routeToPage(
|
||||
context,
|
||||
SearchResultPage(searchResult),
|
||||
);
|
||||
}
|
||||
} else if (searchResult is AlbumSearchResult) {
|
||||
final albumSearchResult = searchResult as AlbumSearchResult;
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
albumSearchResult.collectionWithThumbnail,
|
||||
tagPrefix: albumSearchResult.heroTag(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: searchResult.previewThumbnail() != null
|
||||
? Hero(
|
||||
tag: heroTag,
|
||||
child: ClipOval(
|
||||
child: ThumbnailWidget(
|
||||
searchResult.previewThumbnail()!,
|
||||
shouldShowSyncStatus: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const ClipOval(
|
||||
child: NoThumbnailWidget(
|
||||
addBorder: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
searchResult.name(),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context).mini,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
109
lib/ui/viewer/search/search_section_cta.dart
Normal file
109
lib/ui/viewer/search/search_section_cta.dart
Normal file
|
@ -0,0 +1,109 @@
|
|||
import "package:dotted_border/dotted_border.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
||||
class SearchSectionCTAIcon extends StatelessWidget {
|
||||
final SectionType sectionType;
|
||||
|
||||
const SearchSectionCTAIcon(this.sectionType, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (sectionType.isCTAVisible == false) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return GestureDetector(
|
||||
onTap: sectionType.ctaOnTap(context),
|
||||
child: SizedBox(
|
||||
width: 84,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DottedBorder(
|
||||
color: colorScheme.strokeFaint,
|
||||
dashPattern: const [3.875, 3.875],
|
||||
borderType: BorderType.Circle,
|
||||
strokeWidth: 1.5,
|
||||
radius: const Radius.circular(33.25),
|
||||
child: SizedBox(
|
||||
width: 62.5,
|
||||
height: 62.5,
|
||||
child: Icon(
|
||||
sectionType.getCTAIcon() ?? Icons.add,
|
||||
color: colorScheme.strokeFaint,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.5,
|
||||
),
|
||||
Text(
|
||||
sectionType.getCTAText(context),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.miniFaint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSectionEmptyCTAIcon extends StatelessWidget {
|
||||
final SectionType sectionType;
|
||||
const SearchSectionEmptyCTAIcon(this.sectionType, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (sectionType.isCTAVisible == false) {
|
||||
return const SizedBox(height: 115);
|
||||
}
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return GestureDetector(
|
||||
onTap: sectionType.ctaOnTap(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 24, 8, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
DottedBorder(
|
||||
color: colorScheme.strokeFaint,
|
||||
dashPattern: const [3.875, 3.875],
|
||||
borderType: BorderType.Circle,
|
||||
strokeWidth: 1.5,
|
||||
radius: const Radius.circular(33.25),
|
||||
child: SizedBox(
|
||||
width: 62.5,
|
||||
height: 62.5,
|
||||
child: Icon(
|
||||
sectionType.getCTAIcon() ?? Icons.add,
|
||||
color: colorScheme.strokeFaint,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
sectionType.getCTAText(context),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.miniFaint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/viewer/search/search_widget.dart';
|
||||
|
||||
class SearchSuffixIcon extends StatefulWidget {
|
||||
final bool shouldShowSpinner;
|
||||
|
@ -13,6 +14,7 @@ class _SearchSuffixIconState extends State<SearchSuffixIcon>
|
|||
with TickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 175),
|
||||
child: widget.shouldShowSpinner
|
||||
|
@ -24,22 +26,23 @@ class _SearchSuffixIconState extends State<SearchSuffixIcon>
|
|||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.iconColor
|
||||
.withOpacity(0.5),
|
||||
color: colorScheme.strokeMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
splashRadius: 1,
|
||||
visualDensity: const VisualDensity(horizontal: -1, vertical: -1),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
final searchWidgetState =
|
||||
context.findAncestorStateOfType<SearchWidgetState>()!;
|
||||
searchWidgetState.textController.clear();
|
||||
searchWidgetState.focusNode.unfocus();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.iconColor.withOpacity(0.5),
|
||||
color: colorScheme.strokeMuted,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/models/search/album_search_result.dart';
|
||||
import 'package:photos/models/search/file_search_result.dart';
|
||||
import 'package:photos/models/search/generic_search_result.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/viewer/gallery/collection_page.dart';
|
||||
import 'package:photos/ui/viewer/search/result/file_result_widget.dart';
|
||||
import 'package:photos/ui/viewer/search/result/search_result_widget.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
|
||||
|
@ -21,40 +19,29 @@ class SearchSuggestionsWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.searchResultsColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
spreadRadius: -3,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 324,
|
||||
),
|
||||
child: Scrollbar(
|
||||
child: ListView.builder(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: results.length + 1,
|
||||
late final String title;
|
||||
final resultsCount = results.length;
|
||||
//todo: extract string
|
||||
if (resultsCount == 1) {
|
||||
title = "$resultsCount result found";
|
||||
} else {
|
||||
title = "$resultsCount results found";
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 32, 12, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: getEnteTextTheme(context).largeBold,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (results.length == index) {
|
||||
return Container(
|
||||
height: 6,
|
||||
color: Theme.of(context).colorScheme.searchResultsColor,
|
||||
);
|
||||
}
|
||||
final result = results[index];
|
||||
if (result is AlbumSearchResult) {
|
||||
final AlbumSearchResult albumSearchResult = result;
|
||||
|
@ -71,8 +58,6 @@ class SearchSuggestionsWidget extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
);
|
||||
} else if (result is FileSearchResult) {
|
||||
return FileSearchResultWidget(result);
|
||||
} else if (result is GenericSearchResult) {
|
||||
return SearchResultWidget(
|
||||
result,
|
||||
|
@ -86,10 +71,18 @@ class SearchSuggestionsWidget extends StatelessWidget {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
padding: EdgeInsets.only(
|
||||
bottom: (MediaQuery.sizeOf(context).height / 2) + 50,
|
||||
),
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 12);
|
||||
},
|
||||
itemCount: results.length,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,179 +1,194 @@
|
|||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/scheduler.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/tab_changed_event.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
import "package:photos/states/search_results_state.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import "package:photos/ui/map/enable_map.dart";
|
||||
import "package:photos/ui/map/map_screen.dart";
|
||||
import 'package:photos/ui/viewer/search/result/no_result_widget.dart';
|
||||
import 'package:photos/ui/viewer/search/search_suffix_icon_widget.dart';
|
||||
import 'package:photos/ui/viewer/search/search_suggestions.dart';
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import 'package:photos/utils/debouncer.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import "package:photos/ui/viewer/search/search_suffix_icon_widget.dart";
|
||||
import "package:photos/utils/date_time_util.dart";
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
|
||||
class SearchIconWidget extends StatefulWidget {
|
||||
const SearchIconWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SearchIconWidget> createState() => _SearchIconWidgetState();
|
||||
}
|
||||
|
||||
class _SearchIconWidgetState extends State<SearchIconWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Hero(
|
||||
tag: "search_icon",
|
||||
child: IconButtonWidget(
|
||||
iconButtonType: IconButtonType.primary,
|
||||
icon: Icons.search,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
TransparentRoute(
|
||||
builder: (BuildContext context) => const SearchWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
bool isSearchQueryEmpty = true;
|
||||
|
||||
class SearchWidget extends StatefulWidget {
|
||||
const SearchWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SearchWidget> createState() => _SearchWidgetState();
|
||||
State<SearchWidget> createState() => SearchWidgetState();
|
||||
}
|
||||
|
||||
class _SearchWidgetState extends State<SearchWidget> {
|
||||
String _query = "";
|
||||
final List<SearchResult> _results = [];
|
||||
class SearchWidgetState extends State<SearchWidget> {
|
||||
static String query = "";
|
||||
final _searchService = SearchService.instance;
|
||||
final _debouncer = Debouncer(const Duration(milliseconds: 200));
|
||||
final Logger _logger = Logger((_SearchWidgetState).toString());
|
||||
final Logger _logger = Logger((SearchWidgetState).toString());
|
||||
late FocusNode focusNode;
|
||||
StreamSubscription<TabDoubleTapEvent>? _tabDoubleTapEvent;
|
||||
double _bottomPadding = 0.0;
|
||||
double _distanceOfWidgetFromBottom = 0;
|
||||
GlobalKey widgetKey = GlobalKey();
|
||||
TextEditingController textController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.searchResultsBackgroundColor,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
height: 44,
|
||||
color: Theme.of(context).colorScheme.defaultBackgroundColor,
|
||||
child: TextFormField(
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
// Below parameters are to disable auto-suggestion
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
// Above parameters are to disable auto-suggestion
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).searchHintText,
|
||||
filled: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
),
|
||||
border: const UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
),
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
),
|
||||
prefixIcon: Hero(
|
||||
tag: "search_icon",
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.iconColor
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
/*Using valueListenableBuilder inside a stateful widget because this widget is only rebuild when
|
||||
setState is called when deboucncing is over and the spinner needs to be shown while debouncing */
|
||||
suffixIcon: ValueListenableBuilder(
|
||||
valueListenable: _debouncer.debounceActiveNotifier,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
bool isDebouncing,
|
||||
Widget? child,
|
||||
) {
|
||||
return SearchSuffixIcon(
|
||||
isDebouncing,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (value) async {
|
||||
_query = value;
|
||||
final List<SearchResult> allResults =
|
||||
await getSearchResultsForQuery(context, value);
|
||||
/*checking if _query == value to make sure that the results are from the current query
|
||||
and not from the previous query (race condition).*/
|
||||
if (mounted && _query == value) {
|
||||
setState(() {
|
||||
_results.clear();
|
||||
_results.addAll(allResults);
|
||||
});
|
||||
}
|
||||
},
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
_results.isNotEmpty
|
||||
? SearchSuggestionsWidget(_results)
|
||||
: _query.isNotEmpty
|
||||
? const NoResultWidget()
|
||||
: const NavigateToMap(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode = FocusNode();
|
||||
_tabDoubleTapEvent =
|
||||
Bus.instance.on<TabDoubleTapEvent>().listen((event) async {
|
||||
debugPrint("Firing now ${event.selectedIndex}");
|
||||
if (mounted && event.selectedIndex == 3) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
//This buffer is for doing this operation only after SearchWidget's
|
||||
//animation is complete.
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
final RenderBox box =
|
||||
widgetKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final heightOfWidget = box.size.height;
|
||||
final offsetPosition = box.localToGlobal(Offset.zero);
|
||||
final y = offsetPosition.dy;
|
||||
final heightOfScreen = MediaQuery.sizeOf(context).height;
|
||||
_distanceOfWidgetFromBottom = heightOfScreen - (y + heightOfWidget);
|
||||
});
|
||||
|
||||
textController.addListener(textControllerListener);
|
||||
});
|
||||
textController.text = query;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_bottomPadding =
|
||||
(MediaQuery.viewInsetsOf(context).bottom - _distanceOfWidgetFromBottom);
|
||||
if (_bottomPadding < 0) {
|
||||
_bottomPadding = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncer.cancelDebounce();
|
||||
focusNode.dispose();
|
||||
_tabDoubleTapEvent?.cancel();
|
||||
textController.removeListener(textControllerListener);
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> textControllerListener() async {
|
||||
//query in local varialbe
|
||||
final value = textController.text;
|
||||
isSearchQueryEmpty = value.isEmpty;
|
||||
//latest query in global variable
|
||||
query = textController.text;
|
||||
|
||||
final List<SearchResult> allResults =
|
||||
await getSearchResultsForQuery(context, value);
|
||||
/*checking if query == value to make sure that the results are from the current query
|
||||
and not from the previous query (race condition).*/
|
||||
//checking if query == value to make sure that the latest query's result
|
||||
//(allResults) is passed to updateResult. Due to race condition, the previous
|
||||
//query's allResults could be passed to updateResult after the lastest query's
|
||||
//allResults is passed.
|
||||
|
||||
if (mounted && query == value) {
|
||||
final inheritedSearchResults = InheritedSearchResults.of(context);
|
||||
inheritedSearchResults.updateResults(allResults);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return RepaintBoundary(
|
||||
key: widgetKey,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: _bottomPadding),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: colorScheme.backgroundBase,
|
||||
child: Container(
|
||||
height: 44,
|
||||
color: colorScheme.fillFaint,
|
||||
child: TextFormField(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
// Below parameters are to disable auto-suggestion
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
// Above parameters are to disable auto-suggestion
|
||||
decoration: InputDecoration(
|
||||
// hintText: S.of(context).searchHintText,
|
||||
hintText: "Search",
|
||||
filled: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
),
|
||||
border: const UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
),
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
),
|
||||
prefixIcon: Hero(
|
||||
tag: "search_icon",
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
color: colorScheme.strokeFaint,
|
||||
),
|
||||
),
|
||||
/*Using valueListenableBuilder inside a stateful widget because this widget is only rebuild when
|
||||
setState is called when deboucncing is over and the spinner needs to be shown while debouncing */
|
||||
suffixIcon: ValueListenableBuilder(
|
||||
valueListenable: _debouncer.debounceActiveNotifier,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
bool isDebouncing,
|
||||
Widget? child,
|
||||
) {
|
||||
return SearchSuffixIcon(
|
||||
isDebouncing,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<SearchResult>> getSearchResultsForQuery(
|
||||
BuildContext context,
|
||||
String query,
|
||||
|
@ -239,6 +254,9 @@ class _SearchWidgetState extends State<SearchWidget> {
|
|||
final magicResults =
|
||||
await _searchService.getMagicSearchResults(context, query);
|
||||
allResults.addAll(magicResults);
|
||||
|
||||
final peopleResults = await _searchService.getPeopleSearchResults(query);
|
||||
allResults.addAll(peopleResults);
|
||||
} catch (e, s) {
|
||||
_logger.severe("error during search", e, s);
|
||||
}
|
||||
|
@ -250,34 +268,3 @@ class _SearchWidgetState extends State<SearchWidget> {
|
|||
return yearAsInt != null && yearAsInt <= currentYear;
|
||||
}
|
||||
}
|
||||
|
||||
class NavigateToMap extends StatelessWidget {
|
||||
const NavigateToMap({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: IconButtonWidget(
|
||||
icon: Icons.map_sharp,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
defaultColor: colorScheme.backgroundElevated,
|
||||
pressedColor: colorScheme.backgroundElevated2,
|
||||
size: 28,
|
||||
onTap: () async {
|
||||
final bool result = await requestForMapEnable(context);
|
||||
if (result) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapScreen(
|
||||
filesFutureFn: SearchService.instance.getAllFiles,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
50
lib/ui/viewer/search/tab_empty_state.dart
Normal file
50
lib/ui/viewer/search/tab_empty_state.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/empty_state_item_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/settings/backup/backup_folder_selection_page.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class SearchTabEmptyState extends StatelessWidget {
|
||||
const SearchTabEmptyState({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = getEnteTextTheme(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Fast, on-device search", style: textStyle.h3Bold),
|
||||
const SizedBox(height: 24),
|
||||
const EmptyStateItemWidget("Photo dates, descriptions"),
|
||||
const SizedBox(height: 12),
|
||||
const EmptyStateItemWidget("Albums, file names, and types"),
|
||||
const SizedBox(height: 12),
|
||||
const EmptyStateItemWidget("Location"),
|
||||
const SizedBox(height: 12),
|
||||
const EmptyStateItemWidget("Coming soon: Photo contents, faces"),
|
||||
const SizedBox(height: 32),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.trailingIconPrimary,
|
||||
labelText: "Add your photos now",
|
||||
icon: Icons.arrow_forward_outlined,
|
||||
onTap: () async {
|
||||
routeToPage(
|
||||
context,
|
||||
const BackupFolderSelectionPage(
|
||||
buttonText: "Backup",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import "package:photos/models/typedefs.dart";
|
|||
|
||||
class Debouncer {
|
||||
final Duration _duration;
|
||||
|
||||
///in milliseconds
|
||||
final ValueNotifier<bool> _debounceActiveNotifier = ValueNotifier(false);
|
||||
|
||||
/// If executionInterval is not null, then the debouncer will execute the
|
||||
|
|
|
@ -2,7 +2,7 @@ import "package:dio/dio.dart";
|
|||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/services.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/search/button_result.dart";
|
||||
import 'package:photos/models/button_result.dart';
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
|
@ -233,7 +233,7 @@ ProgressDialog createProgressDialog(
|
|||
return dialog;
|
||||
}
|
||||
|
||||
//Can return ButtonResult? from ButtonWidget or Exception? from TextInputDialog
|
||||
///Can return ButtonResult? from ButtonWidget or Exception? from TextInputDialog
|
||||
Future<dynamic> showTextInputDialog(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
|
|
|
@ -14,6 +14,8 @@ class LocalSettings {
|
|||
static const kCollectionSortPref = "collection_sort_pref";
|
||||
static const kPhotoGridSize = "photo_grid_size";
|
||||
static const kEnableMagicSearch = "enable_magic_search";
|
||||
static const kRateUsShownCount = "rate_us_shown_count";
|
||||
static const kRateUsPromptThreshold = 2;
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
|
@ -51,4 +53,20 @@ class LocalSettings {
|
|||
Future<void> setShouldEnableMagicSearch(bool value) async {
|
||||
await _prefs.setBool(kEnableMagicSearch, value);
|
||||
}
|
||||
|
||||
int getRateUsShownCount() {
|
||||
if (_prefs.containsKey(kRateUsShownCount)) {
|
||||
return _prefs.getInt(kRateUsShownCount)!;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setRateUsShownCount(int value) async {
|
||||
await _prefs.setInt(kRateUsShownCount, value);
|
||||
}
|
||||
|
||||
bool shouldPromptToRateUs() {
|
||||
return getRateUsShownCount() < kRateUsPromptThreshold;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -475,6 +475,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.0"
|
||||
fade_indexed_stack:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fade_indexed_stack
|
||||
sha256: "0d625709d0bf6d0fa275cfa4eba84695fdea93d672c47413cdb49bcbe758a9f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -55,6 +55,7 @@ dependencies:
|
|||
expandable: ^5.0.1
|
||||
expansion_tile_card: ^3.0.0
|
||||
extended_image: ^8.1.1
|
||||
fade_indexed_stack: ^0.2.2
|
||||
fast_base58: ^0.2.1
|
||||
# https://github.com/incrediblezayed/file_saver/issues/86
|
||||
file_saver: 0.2.8
|
||||
|
@ -151,7 +152,7 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/ente-io/packages.git
|
||||
ref: android_video_roation_fix
|
||||
path: packages/video_player/video_player/p
|
||||
path: packages/video_player/video_player/
|
||||
video_thumbnail: ^0.5.3
|
||||
visibility_detector: ^0.3.3
|
||||
wakelock_plus: ^1.1.1
|
||||
|
|
Loading…
Reference in a new issue