feat: Integrate shared preferences for storing recently searched profiles in ExplorePage

+7
ios/Podfile.lock
··· 13 13 - FlutterMacOS 14 14 - share_plus (0.0.1): 15 15 - Flutter 16 + - shared_preferences_foundation (0.0.1): 17 + - Flutter 18 + - FlutterMacOS 16 19 - sqflite_darwin (0.0.4): 17 20 - Flutter 18 21 - FlutterMacOS ··· 27 30 - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 28 31 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 29 32 - share_plus (from `.symlinks/plugins/share_plus/ios`) 33 + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 30 34 - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 31 35 - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 32 36 ··· 45 49 :path: ".symlinks/plugins/path_provider_foundation/darwin" 46 50 share_plus: 47 51 :path: ".symlinks/plugins/share_plus/ios" 52 + shared_preferences_foundation: 53 + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 48 54 sqflite_darwin: 49 55 :path: ".symlinks/plugins/sqflite_darwin/darwin" 50 56 url_launcher_ios: ··· 58 64 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 59 65 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 60 66 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f 67 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 61 68 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d 62 69 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 63 70
+166 -3
lib/screens/explore_page.dart
··· 1 1 import 'dart:async'; 2 2 3 3 import 'package:flutter/material.dart'; 4 + import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 5 import 'package:grain/api.dart'; 5 6 import 'package:grain/app_icons.dart'; 6 7 import 'package:grain/models/profile.dart'; 8 + import 'package:grain/providers/profile_provider.dart'; 7 9 import 'package:grain/widgets/app_image.dart'; 8 10 import 'package:grain/widgets/plain_text_field.dart'; 11 + import 'package:shared_preferences/shared_preferences.dart'; 9 12 10 13 import 'profile_page.dart'; 11 14 12 - class ExplorePage extends StatefulWidget { 15 + class ExplorePage extends ConsumerStatefulWidget { 13 16 const ExplorePage({super.key}); 14 17 15 18 @override 16 - State<ExplorePage> createState() => _ExplorePageState(); 19 + ConsumerState<ExplorePage> createState() => _ExplorePageState(); 17 20 } 18 21 19 - class _ExplorePageState extends State<ExplorePage> { 22 + class _ExplorePageState extends ConsumerState<ExplorePage> { 20 23 final TextEditingController _controller = TextEditingController(); 21 24 List<Profile> _results = []; 25 + List<Profile> _recentlySearched = []; 26 + static const String _recentlySearchedKey = 'recently_searched_dids'; 22 27 bool _loading = false; 23 28 bool _searched = false; 24 29 Timer? _debounce; ··· 26 31 @override 27 32 void initState() { 28 33 super.initState(); 34 + _loadRecentlySearched(); 35 + } 36 + 37 + Future<void> _loadRecentlySearched() async { 38 + final prefs = await SharedPreferences.getInstance(); 39 + final dids = prefs.getStringList(_recentlySearchedKey) ?? []; 40 + if (dids.isNotEmpty) { 41 + final profiles = <Profile>[]; 42 + for (final did in dids) { 43 + try { 44 + final asyncProfile = ref.watch(profileNotifierProvider(did)); 45 + if (asyncProfile.hasValue && asyncProfile.value != null) { 46 + profiles.add(asyncProfile.value!.profile); 47 + } else { 48 + final profileWithGalleries = await ref.refresh(profileNotifierProvider(did).future); 49 + if (profileWithGalleries != null) { 50 + profiles.add(profileWithGalleries.profile); 51 + } 52 + } 53 + } catch (_) {} 54 + } 55 + if (mounted) { 56 + setState(() { 57 + _recentlySearched = profiles; 58 + }); 59 + } 60 + } 61 + } 62 + 63 + Future<void> _saveRecentlySearched() async { 64 + final prefs = await SharedPreferences.getInstance(); 65 + final dids = _recentlySearched.map((p) => p.did).toList(); 66 + await prefs.setStringList(_recentlySearchedKey, dids); 29 67 } 30 68 31 69 void _onSearchChanged(String value) { ··· 94 132 : null, 95 133 ), 96 134 ), 135 + if (_controller.text.isEmpty && _recentlySearched.isNotEmpty) 136 + Padding( 137 + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), 138 + child: Column( 139 + crossAxisAlignment: CrossAxisAlignment.start, 140 + children: [ 141 + Padding( 142 + padding: const EdgeInsets.only(left: 4.0, bottom: 4.0), 143 + child: Text( 144 + 'Recent Searches', 145 + style: theme.textTheme.titleSmall?.copyWith( 146 + fontWeight: FontWeight.bold, 147 + color: theme.textTheme.bodyMedium?.color, 148 + ), 149 + ), 150 + ), 151 + SizedBox( 152 + height: 96, 153 + child: ListView.separated( 154 + scrollDirection: Axis.horizontal, 155 + itemCount: _recentlySearched.length, 156 + separatorBuilder: (context, index) => SizedBox(width: 16), 157 + itemBuilder: (context, index) { 158 + final profile = _recentlySearched[index]; 159 + return SizedBox( 160 + width: 80, 161 + child: Stack( 162 + children: [ 163 + SizedBox( 164 + width: 80, 165 + height: 96, 166 + child: Column( 167 + mainAxisAlignment: MainAxisAlignment.center, 168 + children: [ 169 + Expanded( 170 + child: GestureDetector( 171 + onTap: () async { 172 + FocusScope.of(context).unfocus(); 173 + _debounce?.cancel(); 174 + setState(() { 175 + _searched = false; 176 + _loading = false; 177 + }); 178 + if (context.mounted) { 179 + Navigator.of(context).push( 180 + MaterialPageRoute( 181 + builder: (context) => 182 + ProfilePage(did: profile.did, showAppBar: true), 183 + ), 184 + ); 185 + } 186 + }, 187 + child: CircleAvatar( 188 + radius: 32, 189 + backgroundColor: theme.colorScheme.surfaceContainerHighest, 190 + backgroundImage: profile.avatar?.isNotEmpty == true 191 + ? NetworkImage(profile.avatar!) 192 + : null, 193 + child: profile.avatar?.isNotEmpty == true 194 + ? null 195 + : Icon( 196 + AppIcons.accountCircle, 197 + color: theme.iconTheme.color, 198 + size: 40, 199 + ), 200 + ), 201 + ), 202 + ), 203 + SizedBox(height: 4), 204 + SizedBox( 205 + width: 80, 206 + child: Text( 207 + profile.displayName?.isNotEmpty == true 208 + ? profile.displayName! 209 + : '@${profile.handle}', 210 + style: theme.textTheme.bodyMedium, 211 + maxLines: 1, 212 + overflow: TextOverflow.ellipsis, 213 + textAlign: TextAlign.center, 214 + ), 215 + ), 216 + ], 217 + ), 218 + ), 219 + Positioned( 220 + top: 4, 221 + right: 4, 222 + child: GestureDetector( 223 + onTap: () async { 224 + setState(() { 225 + _recentlySearched.removeAt(index); 226 + }); 227 + await _saveRecentlySearched(); 228 + }, 229 + child: Container( 230 + padding: const EdgeInsets.all(2), 231 + decoration: BoxDecoration( 232 + color: theme.colorScheme.surface, 233 + shape: BoxShape.circle, 234 + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2)], 235 + ), 236 + child: Icon(Icons.close, size: 18, color: theme.iconTheme.color), 237 + ), 238 + ), 239 + ), 240 + ], 241 + ), 242 + ); 243 + }, 244 + ), 245 + ), 246 + ], 247 + ), 248 + ), 97 249 if (_controller.text.isNotEmpty) 98 250 Padding( 99 251 padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4), ··· 159 311 setState(() { 160 312 _searched = false; 161 313 _loading = false; 314 + // Move to front if already present, else add to front 315 + final existingIndex = _recentlySearched.indexWhere((p) => p.did == profile.did); 316 + if (existingIndex != -1) { 317 + _recentlySearched.removeAt(existingIndex); 318 + } 319 + _recentlySearched.insert(0, profile); 320 + // Limit to 10 recent users 321 + if (_recentlySearched.length > 10) { 322 + _recentlySearched.removeLast(); 323 + } 162 324 }); 325 + await _saveRecentlySearched(); 163 326 if (context.mounted) { 164 327 Navigator.of(context).push( 165 328 MaterialPageRoute(
+2
macos/Flutter/GeneratedPluginRegistrant.swift
··· 12 12 import package_info_plus 13 13 import path_provider_foundation 14 14 import share_plus 15 + import shared_preferences_foundation 15 16 import sqflite_darwin 16 17 import url_launcher_macos 17 18 import window_to_front ··· 24 25 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 25 26 PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 26 27 SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 28 + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 27 29 SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 28 30 UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 29 31 WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
+56
pubspec.lock
··· 1032 1032 url: "https://pub.dev" 1033 1033 source: hosted 1034 1034 version: "6.0.0" 1035 + shared_preferences: 1036 + dependency: "direct main" 1037 + description: 1038 + name: shared_preferences 1039 + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" 1040 + url: "https://pub.dev" 1041 + source: hosted 1042 + version: "2.5.3" 1043 + shared_preferences_android: 1044 + dependency: transitive 1045 + description: 1046 + name: shared_preferences_android 1047 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" 1048 + url: "https://pub.dev" 1049 + source: hosted 1050 + version: "2.4.10" 1051 + shared_preferences_foundation: 1052 + dependency: transitive 1053 + description: 1054 + name: shared_preferences_foundation 1055 + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" 1056 + url: "https://pub.dev" 1057 + source: hosted 1058 + version: "2.5.4" 1059 + shared_preferences_linux: 1060 + dependency: transitive 1061 + description: 1062 + name: shared_preferences_linux 1063 + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 1064 + url: "https://pub.dev" 1065 + source: hosted 1066 + version: "2.4.1" 1067 + shared_preferences_platform_interface: 1068 + dependency: transitive 1069 + description: 1070 + name: shared_preferences_platform_interface 1071 + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 1072 + url: "https://pub.dev" 1073 + source: hosted 1074 + version: "2.4.1" 1075 + shared_preferences_web: 1076 + dependency: transitive 1077 + description: 1078 + name: shared_preferences_web 1079 + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 1080 + url: "https://pub.dev" 1081 + source: hosted 1082 + version: "2.4.3" 1083 + shared_preferences_windows: 1084 + dependency: transitive 1085 + description: 1086 + name: shared_preferences_windows 1087 + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 1088 + url: "https://pub.dev" 1089 + source: hosted 1090 + version: "2.4.1" 1035 1091 shelf: 1036 1092 dependency: transitive 1037 1093 description:
+1
pubspec.yaml
··· 60 60 url_launcher: ^6.3.1 61 61 reorderables: ^0.6.0 62 62 exif: ^3.3.0 63 + shared_preferences: ^2.5.3 63 64 64 65 dependency_overrides: 65 66 analyzer: 7.3.0