1+ import 'dart:async' ;
2+
3+ import 'package:collapsible/collapsible.dart' ;
4+ import 'package:flutter/material.dart' ;
5+ import 'package:go_router/go_router.dart' ;
6+ import 'package:logging/logging.dart' ;
7+ import 'package:saber/components/home/export_note_button.dart' ;
8+ import 'package:saber/components/home/masonry_files.dart' ;
9+ import 'package:saber/components/home/move_note_button.dart' ;
10+ import 'package:saber/components/home/new_note_button.dart' ;
11+ import 'package:saber/components/home/rename_note_button.dart' ;
12+ import 'package:saber/data/file_manager/file_manager.dart' ;
13+ import 'package:saber/data/prefs.dart' ;
14+ import 'package:saber/data/routes.dart' ;
15+ import 'package:saber/i18n/strings.g.dart' ;
16+ import 'package:saber/pages/editor/editor.dart' ;
17+
18+ class SearchPage extends StatefulWidget {
19+ const SearchPage ({super .key});
20+
21+ @override
22+ State <SearchPage > createState () => _SearchPageState ();
23+ }
24+
25+ class _SearchPageState extends State <SearchPage > {
26+ final List <String > filePaths = [];
27+ List <String > filteredFiles = [];
28+ bool failed = false ;
29+
30+ ValueNotifier <List <String >> selectedFiles = ValueNotifier ([]);
31+ final log = Logger ('SearchPage' );
32+
33+ /// Mitigates a bug where files got imported starting with `null/` instead of `/` .
34+ ///
35+ /// This caused them to be written to `Documents/Sabernull/...` instead of `Documents/Saber/...` .
36+ ///
37+ /// See https://github.com/saber-notes/saber/issues/996
38+ /// and https://github.com/saber-notes/saber/pull/977.
39+ void moveIncorrectlyImportedFiles () async {
40+ for (final filePath in Prefs .recentFiles.value) {
41+ if (filePath.startsWith ('/' )) continue ;
42+
43+ final String newFilePath;
44+ if (filePath.startsWith ('null/' )) {
45+ newFilePath = await FileManager .suffixFilePathToMakeItUnique (
46+ filePath.substring ('null' .length));
47+ } else {
48+ newFilePath =
49+ await FileManager .suffixFilePathToMakeItUnique ('/$filePath ' );
50+ }
51+
52+ log.warning (
53+ 'Found incorrectly imported file at `$filePath `; moving to `$newFilePath `' );
54+ await FileManager .moveFile (filePath, newFilePath);
55+ }
56+ }
57+
58+ @override
59+ void initState () {
60+ findAllNotes ();
61+ fileWriteSubscription =
62+ FileManager .fileWriteStream.stream.listen (fileWriteListener);
63+ selectedFiles.addListener (_setState);
64+
65+ super .initState ();
66+ moveIncorrectlyImportedFiles ();
67+ }
68+
69+ @override
70+ void dispose () {
71+ selectedFiles.removeListener (_setState);
72+ fileWriteSubscription? .cancel ();
73+ super .dispose ();
74+ }
75+
76+ void filterFiles (String search) {
77+ if (search== '' ) {
78+ filteredFiles = filePaths;
79+ }
80+ search = search.toLowerCase ();
81+ filteredFiles = filePaths.where ((file) => file.toLowerCase ().contains (search)).toList ();
82+ }
83+
84+ StreamSubscription ? fileWriteSubscription;
85+ void fileWriteListener (FileOperation event) {
86+ findAllNotes (fromFileListener: true );
87+ }
88+
89+ void _setState () => setState (() {});
90+
91+ Future findAllNotes ({bool fromFileListener = false }) async {
92+ if (! mounted) return ;
93+
94+ if (fromFileListener) {
95+ // don't refresh if we're not on the home page
96+ final location = GoRouterState .of (context).uri.toString ();
97+ if (! location.startsWith (RoutePaths .prefixOfHome)) return ;
98+ }
99+
100+ final children = await FileManager .getAllFiles ();
101+ filePaths.clear ();
102+ if (children.isEmpty) {
103+ failed = true ;
104+ } else {
105+ failed = false ;
106+ filePaths.addAll (children);
107+ filteredFiles = filePaths;
108+ }
109+
110+ if (mounted) setState (() {});
111+ }
112+
113+ @override
114+ Widget build (BuildContext context) {
115+ final platform = Theme .of (context).platform;
116+ final cupertino =
117+ platform == TargetPlatform .iOS || platform == TargetPlatform .macOS;
118+ final crossAxisCount = MediaQuery .sizeOf (context).width ~ / 300 + 1 ;
119+ return Scaffold (
120+ body: RefreshIndicator (
121+ onRefresh: () => Future .wait (
122+ [Future .delayed (const Duration (milliseconds: 500 ))]),
123+ child: CustomScrollView (
124+ slivers: [
125+ SliverPadding (
126+ padding: const EdgeInsets .only (
127+ bottom: 8 ,
128+ ),
129+ sliver: SliverAppBar (
130+ toolbarHeight: 70 ,
131+ title: Center (
132+ child: SearchBar (
133+ keyboardType: TextInputType .text,
134+ hintText: t.home.titles.search,
135+ onChanged: (value) {
136+ setState (() {
137+ filterFiles (value);
138+ });
139+ },
140+ ),
141+ )
142+ )
143+ ),
144+ SliverSafeArea (
145+ minimum: const EdgeInsets .only (
146+ bottom: 70
147+ ),
148+ sliver: MasonryFiles (
149+ crossAxisCount: crossAxisCount,
150+ files: [
151+ for (String filePath in filteredFiles) filePath,
152+ ],
153+ selectedFiles: selectedFiles,
154+ ),
155+ )
156+ ]
157+ )
158+ ),floatingActionButton: NewNoteButton (
159+ cupertino: cupertino,
160+ ),
161+ persistentFooterButtons: selectedFiles.value.isEmpty
162+ ? null
163+ : [
164+ Collapsible (
165+ axis: CollapsibleAxis .vertical,
166+ collapsed: selectedFiles.value.length != 1 ,
167+ child: RenameNoteButton (
168+ existingPath: selectedFiles.value.isEmpty
169+ ? ''
170+ : selectedFiles.value.first,
171+ unselectNotes: () => selectedFiles.value = [],
172+ ),
173+ ),
174+ MoveNoteButton (
175+ filesToMove: selectedFiles.value,
176+ unselectNotes: () => selectedFiles.value = [],
177+ ),
178+ IconButton (
179+ padding: EdgeInsets .zero,
180+ tooltip: t.home.deleteNote,
181+ onPressed: () async {
182+ await Future .wait ([
183+ for (String filePath in selectedFiles.value)
184+ () async {
185+ bool oldExtension = FileManager .doesFileExist (filePath + Editor .extensionOldJson);
186+ await FileManager .deleteFile (filePath + (oldExtension
187+ ? Editor .extensionOldJson
188+ : Editor .extension ));
189+ }(),
190+ ]);
191+ selectedFiles.value = [];
192+ },
193+ icon: const Icon (Icons .delete_forever),
194+ ),
195+ ExportNoteButton (
196+ selectedFiles: selectedFiles.value,
197+ ),
198+ ],
199+ );
200+ }
201+ }
0 commit comments