Skip to content

Commit fc91b40

Browse files
committed
files: Cleanup folder provider and breadcrumbs bar
Also cool new feature the breadcrumbs bar will show a customized chip if the user is navigating inside a builtin folder (much like how windows and ubuntu already do it)
1 parent e41fca1 commit fc91b40

File tree

7 files changed

+202
-67
lines changed

7 files changed

+202
-67
lines changed

lib/backend/folder_provider.dart

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,29 @@ limitations under the License.
1818

1919
import 'dart:io';
2020

21+
import 'package:collection/collection.dart';
2122
import 'package:flutter/material.dart';
2223
import 'package:windows_path_provider/windows_path_provider.dart';
2324
import 'package:xdg_directories/xdg_directories.dart';
2425

2526
class FolderProvider {
26-
final Map<String, IconData> directories;
27+
final List<BuiltinFolder> _folders;
28+
29+
List<BuiltinFolder> get folders => List.from(_folders);
2730

2831
static Future<FolderProvider> init() async {
29-
final Map<String, IconData> directories = {};
32+
final List<BuiltinFolder> folders = [];
3033

3134
if (Platform.isWindows) {
3235
for (final WindowsFolder folder in WindowsFolder.values) {
3336
final String? path = await WindowsPathProvider.getPath(folder);
3437

35-
if (path != null) {
36-
directories[path] = icons[windowsFolderToString(folder)]!;
37-
}
38+
if (path == null) continue;
39+
40+
final FolderType? type = folder.toFolderType();
41+
if (type == null) continue;
42+
43+
folders.add(BuiltinFolder(type, Directory(path)));
3844
}
3945
} else if (Platform.isLinux) {
4046
final Set<String> dirNames = getUserDirectoryNames();
@@ -43,31 +49,77 @@ class FolderProvider {
4349
.path
4450
.split(Platform.pathSeparator)
4551
..removeLast();
46-
directories[backDir.join(Platform.pathSeparator)] = icons["HOME"]!;
52+
folders.add(
53+
BuiltinFolder(
54+
FolderType.home,
55+
Directory(backDir.join(Platform.pathSeparator)),
56+
),
57+
);
4758

4859
for (final String element in dirNames) {
49-
directories[getUserDirectory(element)!.path] = icons[element]!;
60+
final FolderType? type = FolderType.fromString(element);
61+
if (type == null) continue;
62+
63+
folders.add(
64+
BuiltinFolder(
65+
type,
66+
Directory(getUserDirectory(element)!.path),
67+
),
68+
);
5069
}
5170
} else {
5271
throw Exception("Platform not supported");
5372
}
5473

55-
return FolderProvider(directories);
74+
return FolderProvider._(folders);
5675
}
5776

58-
const FolderProvider(this.directories);
77+
const FolderProvider._(this._folders);
78+
79+
IconData getIconForType(FolderType type) {
80+
return _icons[type]!;
81+
}
82+
83+
BuiltinFolder? isBuiltinFolder(String path) {
84+
return _folders.firstWhereOrNull((v) => v.directory.path == path);
85+
}
5986
}
6087

61-
const Map<String, IconData> icons = {
62-
"HOME": Icons.home_filled,
63-
"DESKTOP": Icons.desktop_windows,
64-
"DOCUMENTS": Icons.note_outlined,
65-
"PICTURES": Icons.photo_library_outlined,
66-
"DOWNLOAD": Icons.file_download,
67-
"VIDEOS": Icons.videocam_outlined,
68-
"MUSIC": Icons.music_note_outlined,
69-
"PUBLICSHARE": Icons.public_outlined,
70-
"TEMPLATES": Icons.file_copy_outlined,
88+
enum FolderType {
89+
home,
90+
desktop,
91+
documents,
92+
pictures,
93+
download,
94+
videos,
95+
music,
96+
publicShare,
97+
templates;
98+
99+
static FolderType? fromString(String value) {
100+
return FolderType.values
101+
.asNameMap()
102+
.map((k, v) => MapEntry(k.toUpperCase(), v))[value.toUpperCase()];
103+
}
104+
}
105+
106+
class BuiltinFolder {
107+
final FolderType type;
108+
final Directory directory;
109+
110+
const BuiltinFolder(this.type, this.directory);
111+
}
112+
113+
const Map<FolderType, IconData> _icons = {
114+
FolderType.home: Icons.home_filled,
115+
FolderType.desktop: Icons.desktop_windows,
116+
FolderType.documents: Icons.note_outlined,
117+
FolderType.pictures: Icons.photo_library_outlined,
118+
FolderType.download: Icons.file_download,
119+
FolderType.videos: Icons.videocam_outlined,
120+
FolderType.music: Icons.music_note_outlined,
121+
FolderType.publicShare: Icons.public_outlined,
122+
FolderType.templates: Icons.file_copy_outlined,
71123
};
72124

73125
String windowsFolderToString(WindowsFolder folder) {
@@ -92,3 +144,16 @@ String windowsFolderToString(WindowsFolder folder) {
92144
return "TEMPLATES";
93145
}
94146
}
147+
148+
extension on WindowsFolder {
149+
FolderType? toFolderType() {
150+
switch (this) {
151+
case WindowsFolder.profile:
152+
return FolderType.home;
153+
case WindowsFolder.public:
154+
return FolderType.publicShare;
155+
default:
156+
return FolderType.fromString(name);
157+
}
158+
}
159+
}

lib/backend/path_parts.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ class PathParts {
4040
return "$root$path";
4141
}
4242

43+
PathParts trim(int numOfParts) {
44+
assert(numOfParts >= 0 && numOfParts <= parts.length);
45+
final List<String> transformedPathParts = parts.sublist(0, numOfParts);
46+
47+
return PathParts(root, transformedPathParts, separator);
48+
}
49+
50+
List<String> get integralParts => [root, ...parts];
51+
4352
@override
4453
String toString() {
4554
return {

lib/backend/providers.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Future<void> initProviders() async {
1919
final isar = await Isar.open(
2020
[EntityStatSchema],
2121
directory: _isarPath(dir),
22+
inspector: false,
2223
);
2324
final folderProvider = await FolderProvider.init();
2425

lib/backend/utils.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:io';
22

3+
import 'package:files/backend/folder_provider.dart';
34
import 'package:files/backend/path_parts.dart';
45
import 'package:files/backend/providers.dart';
56
import 'package:flutter/material.dart';
@@ -58,14 +59,17 @@ class Utils {
5859
}
5960

6061
static IconData iconForFolder(String path) {
61-
final IconData? builtinFolderIcon = folderProvider.directories[path];
62+
final BuiltinFolder? builtinFolder = folderProvider.isBuiltinFolder(path);
63+
final IconData? builtinFolderIcon = builtinFolder != null
64+
? folderProvider.getIconForType(builtinFolder.type)
65+
: null;
6266

6367
return builtinFolderIcon ?? Icons.folder;
6468
}
6569

6670
static String getEntityName(String path) {
6771
final PathParts pathParts = PathParts.parse(path);
68-
return pathParts.parts.isNotEmpty ? pathParts.parts.last : pathParts.root;
72+
return pathParts.integralParts.last;
6973
}
7074

7175
static void moveFileToDest(FileSystemEntity origin, String dest) {

lib/main.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import 'package:animations/animations.dart';
18+
import 'package:files/backend/folder_provider.dart';
1819
import 'package:files/backend/providers.dart';
1920
import 'package:files/backend/utils.dart';
2021
import 'package:files/widgets/context_menu/context_menu_theme.dart';
@@ -90,13 +91,12 @@ class _FilesHomeState extends State<FilesHome> {
9091
@override
9192
void initState() {
9293
super.initState();
93-
for (final MapEntry<String, IconData> element
94-
in folderProvider.directories.entries) {
94+
for (final BuiltinFolder element in folderProvider.folders) {
9595
sideDestinations.add(
9696
SideDestination(
97-
element.value,
98-
Utils.getEntityName(element.key),
99-
element.key,
97+
folderProvider.getIconForType(element.type),
98+
Utils.getEntityName(element.directory.path),
99+
element.directory.path,
100100
),
101101
);
102102
}

lib/widgets/breadcrumbs_bar.dart

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import 'dart:io';
22

3+
import 'package:collection/collection.dart';
4+
import 'package:files/backend/folder_provider.dart';
35
import 'package:files/backend/path_parts.dart';
6+
import 'package:files/backend/providers.dart';
47
import 'package:files/backend/utils.dart';
58
import 'package:flutter/material.dart';
69

710
class BreadcrumbsBar extends StatefulWidget {
811
final PathParts path;
9-
final ValueChanged<int>? onBreadcrumbPress;
12+
final ValueChanged<String>? onBreadcrumbPress;
1013
final ValueChanged<String>? onPathSubmitted;
1114
final List<Widget>? leading;
1215
final List<Widget>? actions;
@@ -86,38 +89,7 @@ class _BreadcrumbsBarState extends State<BreadcrumbsBar> {
8689
child: Container(
8790
height: double.infinity,
8891
alignment: AlignmentDirectional.centerStart,
89-
child: focusNode.hasFocus
90-
? TextField(
91-
decoration: const InputDecoration(
92-
border: InputBorder.none,
93-
contentPadding:
94-
EdgeInsets.symmetric(horizontal: 8),
95-
isCollapsed: true,
96-
),
97-
focusNode: focusNode,
98-
controller: controller,
99-
style: const TextStyle(fontSize: 14),
100-
onSubmitted: widget.onPathSubmitted,
101-
)
102-
: ListView.separated(
103-
scrollDirection: Axis.horizontal,
104-
itemBuilder: (context, index) {
105-
if (index == 0) {
106-
return _buildItemChip(
107-
index,
108-
widget.path.root,
109-
);
110-
} else {
111-
return _buildItemChip(
112-
index,
113-
widget.path.parts[index - 1],
114-
);
115-
}
116-
},
117-
itemCount: widget.path.parts.length + 1,
118-
separatorBuilder: (context, index) =>
119-
const VerticalDivider(width: 2),
120-
),
92+
child: _guts,
12193
),
12294
),
12395
),
@@ -133,25 +105,111 @@ class _BreadcrumbsBarState extends State<BreadcrumbsBar> {
133105
);
134106
}
135107

136-
Widget _buildItemChip(int index, String part) {
108+
Widget get _guts {
109+
if (focusNode.hasFocus) {
110+
return TextField(
111+
decoration: const InputDecoration(
112+
border: InputBorder.none,
113+
contentPadding: EdgeInsets.symmetric(horizontal: 8),
114+
isCollapsed: true,
115+
),
116+
focusNode: focusNode,
117+
controller: controller,
118+
style: const TextStyle(fontSize: 14),
119+
onSubmitted: widget.onPathSubmitted,
120+
);
121+
} else {
122+
final List<PathParts> actualParts;
123+
124+
// We need home folder on last position here to emulate a low priority entry
125+
final List<BuiltinFolder> sortedFolders = folderProvider.folders;
126+
final int homeIndex =
127+
sortedFolders.indexWhere((e) => e.type == FolderType.home);
128+
sortedFolders.add(sortedFolders.removeAt(homeIndex));
129+
130+
final BuiltinFolder? builtinFolder = sortedFolders.firstWhereOrNull(
131+
(e) => widget.path.toPath().startsWith(e.directory.path),
132+
);
133+
134+
if (builtinFolder != null) {
135+
final PathParts builtinParts =
136+
PathParts.parse(builtinFolder.directory.path);
137+
actualParts = [
138+
builtinParts,
139+
...List.generate(
140+
widget.path.integralParts.length -
141+
builtinParts.integralParts.length,
142+
(index) =>
143+
widget.path.trim(index + builtinParts.integralParts.length),
144+
),
145+
];
146+
} else {
147+
actualParts = List.generate(
148+
widget.path.integralParts.length,
149+
(index) => widget.path.trim(index),
150+
);
151+
}
152+
153+
return ListView.separated(
154+
scrollDirection: Axis.horizontal,
155+
itemBuilder: (context, index) {
156+
final bool isInsideBuiltin = builtinFolder != null &&
157+
actualParts[index].toPath() == builtinFolder.directory.path;
158+
159+
return _BreadcrumbChip(
160+
path: actualParts[index],
161+
onTap: widget.onBreadcrumbPress,
162+
childOverride: isInsideBuiltin
163+
? Row(
164+
children: [
165+
Icon(
166+
folderProvider.getIconForType(builtinFolder.type),
167+
size: 16,
168+
),
169+
const SizedBox(width: 8),
170+
Text(Utils.getEntityName(builtinFolder.directory.path)),
171+
],
172+
)
173+
: null,
174+
);
175+
},
176+
itemCount: actualParts.length,
177+
separatorBuilder: (context, index) => const VerticalDivider(width: 2),
178+
);
179+
}
180+
}
181+
}
182+
183+
class _BreadcrumbChip extends StatelessWidget {
184+
const _BreadcrumbChip({
185+
required this.path,
186+
this.onTap,
187+
this.childOverride,
188+
});
189+
190+
final PathParts path;
191+
final ValueChanged<String>? onTap;
192+
final Widget? childOverride;
193+
194+
@override
195+
Widget build(BuildContext context) {
137196
return SizedBox(
138197
height: double.infinity,
139198
child: DragTarget<FileSystemEntity>(
140-
onAccept: (data) =>
141-
Utils.moveFileToDest(data, widget.path.toPath(index)),
199+
onAccept: (data) => Utils.moveFileToDest(data, path.toPath()),
142200
builder: (context, candidateData, rejectedData) {
143201
return InkWell(
144202
child: Row(
145203
children: [
146204
Container(
147205
alignment: Alignment.center,
148206
padding: const EdgeInsetsDirectional.only(start: 12, end: 4),
149-
child: Text(part),
207+
child: childOverride ?? Text(path.integralParts.last),
150208
),
151209
const Icon(Icons.chevron_right, size: 16),
152210
],
153211
),
154-
onTap: () => widget.onBreadcrumbPress?.call(index),
212+
onTap: () => onTap?.call(path.toPath()),
155213
);
156214
},
157215
),

0 commit comments

Comments
 (0)