machine_learning_settings_page.dart 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import "dart:async";
  2. import "package:flutter/material.dart";
  3. import "package:intl/intl.dart";
  4. import "package:photos/core/event_bus.dart";
  5. import 'package:photos/events/embedding_updated_event.dart';
  6. import "package:photos/generated/l10n.dart";
  7. import "package:photos/services/feature_flag_service.dart";
  8. import "package:photos/services/semantic_search/semantic_search_service.dart";
  9. import "package:photos/theme/ente_theme.dart";
  10. import "package:photos/ui/common/loading_widget.dart";
  11. import "package:photos/ui/components/buttons/icon_button_widget.dart";
  12. import "package:photos/ui/components/captioned_text_widget.dart";
  13. import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
  14. import "package:photos/ui/components/menu_section_description_widget.dart";
  15. import "package:photos/ui/components/menu_section_title.dart";
  16. import "package:photos/ui/components/title_bar_title_widget.dart";
  17. import "package:photos/ui/components/title_bar_widget.dart";
  18. import "package:photos/ui/components/toggle_switch_widget.dart";
  19. import "package:photos/utils/local_settings.dart";
  20. class MachineLearningSettingsPage extends StatefulWidget {
  21. const MachineLearningSettingsPage({super.key});
  22. @override
  23. State<MachineLearningSettingsPage> createState() =>
  24. _MachineLearningSettingsPageState();
  25. }
  26. class _MachineLearningSettingsPageState
  27. extends State<MachineLearningSettingsPage> {
  28. @override
  29. Widget build(BuildContext context) {
  30. return Scaffold(
  31. body: CustomScrollView(
  32. primary: false,
  33. slivers: <Widget>[
  34. TitleBarWidget(
  35. flexibleSpaceTitle: TitleBarTitleWidget(
  36. title: S.of(context).machineLearning,
  37. ),
  38. actionIcons: [
  39. IconButtonWidget(
  40. icon: Icons.close_outlined,
  41. iconButtonType: IconButtonType.secondary,
  42. onTap: () {
  43. Navigator.pop(context);
  44. Navigator.pop(context);
  45. Navigator.pop(context);
  46. },
  47. ),
  48. ],
  49. ),
  50. SliverList(
  51. delegate: SliverChildBuilderDelegate(
  52. (delegateBuildContext, index) {
  53. return Padding(
  54. padding: const EdgeInsets.symmetric(horizontal: 16),
  55. child: Padding(
  56. padding: const EdgeInsets.symmetric(vertical: 20),
  57. child: Column(
  58. mainAxisSize: MainAxisSize.min,
  59. children: [
  60. _getMagicSearchSettings(context),
  61. ],
  62. ),
  63. ),
  64. );
  65. },
  66. childCount: 1,
  67. ),
  68. ),
  69. ],
  70. ),
  71. );
  72. }
  73. Widget _getMagicSearchSettings(BuildContext context) {
  74. final colorScheme = getEnteColorScheme(context);
  75. final hasEnabled = LocalSettings.instance.hasEnabledMagicSearch();
  76. return Column(
  77. children: [
  78. MenuItemWidget(
  79. captionedTextWidget: CaptionedTextWidget(
  80. title: S.of(context).magicSearch,
  81. ),
  82. menuItemColor: colorScheme.fillFaint,
  83. trailingWidget: ToggleSwitchWidget(
  84. value: () => LocalSettings.instance.hasEnabledMagicSearch(),
  85. onChanged: () async {
  86. await LocalSettings.instance.setShouldEnableMagicSearch(
  87. !LocalSettings.instance.hasEnabledMagicSearch(),
  88. );
  89. if (LocalSettings.instance.hasEnabledMagicSearch()) {
  90. unawaited(
  91. SemanticSearchService.instance
  92. .init(shouldSyncImmediately: true),
  93. );
  94. } else {
  95. await SemanticSearchService.instance.clearQueue();
  96. }
  97. setState(() {});
  98. },
  99. ),
  100. singleBorderRadius: 8,
  101. alignCaptionedTextToLeft: true,
  102. isGestureDetectorDisabled: true,
  103. ),
  104. const SizedBox(
  105. height: 4,
  106. ),
  107. MenuSectionDescriptionWidget(
  108. content: S.of(context).magicSearchDescription,
  109. ),
  110. const SizedBox(
  111. height: 12,
  112. ),
  113. hasEnabled
  114. ? Column(
  115. children: [
  116. FutureBuilder(
  117. future: SemanticSearchService.instance
  118. .getFrameworkInitializationStatus(),
  119. builder: (BuildContext context, AsyncSnapshot snapshot) {
  120. if (snapshot.hasData) {
  121. return const MagicSearchIndexStatsWidget();
  122. } else {
  123. return const ModelLoadingState();
  124. }
  125. },
  126. ),
  127. const SizedBox(
  128. height: 12,
  129. ),
  130. FeatureFlagService.instance.isInternalUserOrDebugBuild()
  131. ? MenuItemWidget(
  132. leadingIcon: Icons.delete_sweep_outlined,
  133. captionedTextWidget: CaptionedTextWidget(
  134. title: S.of(context).clearIndexes,
  135. ),
  136. menuItemColor: getEnteColorScheme(context).fillFaint,
  137. singleBorderRadius: 8,
  138. alwaysShowSuccessState: true,
  139. onTap: () async {
  140. await SemanticSearchService.instance.clearIndexes();
  141. if (mounted) {
  142. setState(() => {});
  143. }
  144. },
  145. )
  146. : const SizedBox.shrink(),
  147. ],
  148. )
  149. : const SizedBox.shrink(),
  150. ],
  151. );
  152. }
  153. }
  154. class ModelLoadingState extends StatelessWidget {
  155. const ModelLoadingState({super.key});
  156. @override
  157. Widget build(BuildContext context) {
  158. return Column(
  159. children: [
  160. MenuSectionTitle(title: S.of(context).status),
  161. MenuItemWidget(
  162. captionedTextWidget: CaptionedTextWidget(
  163. title: S.of(context).loadingModel,
  164. ),
  165. trailingWidget: EnteLoadingWidget(
  166. size: 12,
  167. color: getEnteColorScheme(context).fillMuted,
  168. ),
  169. singleBorderRadius: 8,
  170. alignCaptionedTextToLeft: true,
  171. isGestureDetectorDisabled: true,
  172. ),
  173. ],
  174. );
  175. }
  176. }
  177. class MagicSearchIndexStatsWidget extends StatefulWidget {
  178. const MagicSearchIndexStatsWidget({
  179. super.key,
  180. });
  181. @override
  182. State<MagicSearchIndexStatsWidget> createState() =>
  183. _MagicSearchIndexStatsWidgetState();
  184. }
  185. class _MagicSearchIndexStatsWidgetState
  186. extends State<MagicSearchIndexStatsWidget> {
  187. IndexStatus? _status;
  188. late StreamSubscription<EmbeddingUpdatedEvent> _eventSubscription;
  189. @override
  190. void initState() {
  191. super.initState();
  192. _eventSubscription =
  193. Bus.instance.on<EmbeddingUpdatedEvent>().listen((event) {
  194. _fetchIndexStatus();
  195. });
  196. _fetchIndexStatus();
  197. }
  198. void _fetchIndexStatus() {
  199. SemanticSearchService.instance.getIndexStatus().then((status) {
  200. _status = status;
  201. setState(() {});
  202. });
  203. }
  204. @override
  205. void dispose() {
  206. super.dispose();
  207. _eventSubscription.cancel();
  208. }
  209. @override
  210. Widget build(BuildContext context) {
  211. if (_status == null) {
  212. return const EnteLoadingWidget();
  213. }
  214. return Column(
  215. children: [
  216. Row(
  217. children: [
  218. MenuSectionTitle(title: S.of(context).status),
  219. Expanded(child: Container()),
  220. _status!.pendingItems > 0
  221. ? EnteLoadingWidget(
  222. color: getEnteColorScheme(context).fillMuted,
  223. )
  224. : const SizedBox.shrink(),
  225. ],
  226. ),
  227. MenuItemWidget(
  228. captionedTextWidget: CaptionedTextWidget(
  229. title: S.of(context).indexedItems,
  230. ),
  231. trailingWidget: Text(
  232. NumberFormat().format(_status!.indexedItems),
  233. style: Theme.of(context).textTheme.bodySmall,
  234. ),
  235. singleBorderRadius: 8,
  236. alignCaptionedTextToLeft: true,
  237. isGestureDetectorDisabled: true,
  238. // Setting a key here to ensure trailingWidget is refreshed
  239. key: ValueKey("indexed_items_" + _status!.indexedItems.toString()),
  240. ),
  241. MenuItemWidget(
  242. captionedTextWidget: CaptionedTextWidget(
  243. title: S.of(context).pendingItems,
  244. ),
  245. trailingWidget: Text(
  246. NumberFormat().format(_status!.pendingItems),
  247. style: Theme.of(context).textTheme.bodySmall,
  248. ),
  249. singleBorderRadius: 8,
  250. alignCaptionedTextToLeft: true,
  251. isGestureDetectorDisabled: true,
  252. // Setting a key here to ensure trailingWidget is refreshed
  253. key: ValueKey("pending_items_" + _status!.pendingItems.toString()),
  254. ),
  255. ],
  256. );
  257. }
  258. }