Skip to content

Commit 210b473

Browse files
committed
files: Add drive detection support with udisks
Very barebones but at least support is there hooray!
1 parent 4166d0a commit 210b473

File tree

7 files changed

+287
-30
lines changed

7 files changed

+287
-30
lines changed

lib/backend/drive_provider.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:udisks/udisks.dart';
5+
6+
class DriveProvider with ChangeNotifier {
7+
final UDisksClient _client = UDisksClient();
8+
late final StreamSubscription _blockDeviceAddSub;
9+
late final StreamSubscription _blockDeviceRemoveSub;
10+
late final StreamSubscription _driveAddSub;
11+
late final StreamSubscription _driveRemoveSub;
12+
13+
final List<UDisksBlockDevice> _blockDevices = [];
14+
final List<UDisksDrive> _drives = [];
15+
16+
List<UDisksBlockDevice> get blockDevices => List.of(_blockDevices);
17+
List<UDisksDrive> get drives => List.of(_drives);
18+
19+
Future<void> init() async {
20+
await _client.connect();
21+
22+
_blockDevices.addAll(_client.blockDevices);
23+
_drives.addAll(_client.drives);
24+
25+
_blockDeviceAddSub = _client.blockDeviceAdded.listen(_onBlockDeviceAdded);
26+
_blockDeviceRemoveSub =
27+
_client.blockDeviceRemoved.listen(_onBlockDeviceRemoved);
28+
29+
_driveAddSub = _client.driveAdded.listen(_onDriveAdded);
30+
_driveRemoveSub = _client.driveRemoved.listen(_onDriveRemoved);
31+
}
32+
33+
@override
34+
Future<void> dispose() async {
35+
await _blockDeviceAddSub.cancel();
36+
await _blockDeviceRemoveSub.cancel();
37+
await _driveAddSub.cancel();
38+
await _driveRemoveSub.cancel();
39+
40+
await _client.close();
41+
42+
super.dispose();
43+
}
44+
45+
void _onBlockDeviceAdded(UDisksBlockDevice event) {
46+
_blockDevices.add(event);
47+
notifyListeners();
48+
}
49+
50+
void _onBlockDeviceRemoved(UDisksBlockDevice event) {
51+
_blockDevices.removeWhere((e) => event.id == e.id);
52+
notifyListeners();
53+
}
54+
55+
void _onDriveAdded(UDisksDrive event) {
56+
_drives.add(event);
57+
notifyListeners();
58+
}
59+
60+
void _onDriveRemoved(UDisksDrive event) {
61+
_drives.removeWhere((e) => event.id == e.id);
62+
notifyListeners();
63+
}
64+
}

lib/backend/providers.dart

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

33
import 'package:files/backend/database/helper.dart';
44
import 'package:files/backend/database/model.dart';
5+
import 'package:files/backend/drive_provider.dart';
56
import 'package:files/backend/folder_provider.dart';
67
import 'package:files/backend/stat_cache_proxy.dart';
78
import 'package:path/path.dart' as p;
@@ -27,12 +28,18 @@ Future<void> initProviders() async {
2728
registerServiceInstance<FolderProvider>(folderProvider);
2829
registerService<EntityStatCacheHelper>(EntityStatCacheHelper.new);
2930
registerService<StatCacheProxy>(StatCacheProxy.new);
31+
registerService<DriveProvider>(
32+
DriveProvider.new,
33+
dispose: (s) => s.dispose(),
34+
);
3035
}
3136

3237
Isar get isar => getService<Isar>();
3338

3439
FolderProvider get folderProvider => getService<FolderProvider>();
3540

41+
DriveProvider get driveProvider => getService<DriveProvider>();
42+
3643
EntityStatCacheHelper get helper => getService<EntityStatCacheHelper>();
3744

3845
StatCacheProxy get cacheProxy => getService<StatCacheProxy>();

lib/main.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'package:flutter/material.dart';
2424
Future<void> main() async {
2525
WidgetsFlutterBinding.ensureInitialized();
2626
await initProviders();
27+
await driveProvider.init();
2728

2829
runApp(const Files());
2930
}

lib/widgets/drive_list.dart

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
4+
import 'package:files/backend/providers.dart';
5+
import 'package:filesize/filesize.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:udisks/udisks.dart';
8+
9+
class DriveList extends StatelessWidget {
10+
final ValueChanged<String>? onDriveTap;
11+
12+
const DriveList({this.onDriveTap, super.key});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return AnimatedBuilder(
17+
animation: driveProvider,
18+
builder: (context, _) {
19+
return Column(
20+
children: driveProvider.blockDevices
21+
.where(
22+
(e) =>
23+
!e.userspaceMountOptions.contains("x-gdu.hide") &&
24+
!e.userspaceMountOptions.contains("x-gvfs-hide"),
25+
)
26+
.where((e) => !e.hintIgnore && e.filesystem != null)
27+
.map(
28+
(e) => _DriveTile(
29+
blockDevice: e,
30+
onTap: onDriveTap,
31+
),
32+
)
33+
.toList(),
34+
);
35+
},
36+
);
37+
}
38+
}
39+
40+
class _DriveTile extends StatefulWidget {
41+
final UDisksBlockDevice blockDevice;
42+
final ValueChanged<String>? onTap;
43+
44+
const _DriveTile({
45+
required this.blockDevice,
46+
this.onTap,
47+
});
48+
49+
@override
50+
State<_DriveTile> createState() => _DriveTileState();
51+
}
52+
53+
class _DriveTileState extends State<_DriveTile> {
54+
late Timer _pollingTimer;
55+
56+
late String? mountPoint;
57+
58+
@override
59+
void initState() {
60+
super.initState();
61+
mountPoint = getMountPoint();
62+
_pollingTimer = Timer.periodic(const Duration(milliseconds: 100), _onPoll);
63+
}
64+
65+
@override
66+
void dispose() {
67+
_pollingTimer.cancel();
68+
super.dispose();
69+
}
70+
71+
void _onPoll(Timer ref) {
72+
final String? currentMountPoint = getMountPoint();
73+
74+
if (mountPoint != currentMountPoint) {
75+
mountPoint = currentMountPoint;
76+
setState(() {});
77+
}
78+
}
79+
80+
String? getMountPoint() {
81+
return widget.blockDevice.filesystem!.mountPoints.isNotEmpty
82+
? widget.blockDevice.filesystem!.mountPoints.first.decode()
83+
: null;
84+
}
85+
86+
@override
87+
Widget build(BuildContext context) {
88+
String? mountPoint = widget.blockDevice.filesystem!.mountPoints.isNotEmpty
89+
? widget.blockDevice.filesystem!.mountPoints.first.decode()
90+
: null;
91+
92+
final String? idLabel = widget.blockDevice.idLabel.isNotEmpty
93+
? widget.blockDevice.idLabel
94+
: null;
95+
final String? hintName = widget.blockDevice.hintName.isNotEmpty
96+
? widget.blockDevice.hintName
97+
: null;
98+
99+
return ListTile(
100+
dense: true,
101+
leading: Icon(
102+
widget.blockDevice.drive?.ejectable == true ? Icons.usb : Icons.storage,
103+
size: 20,
104+
),
105+
title: Text(
106+
idLabel ?? hintName ?? "${filesize(widget.blockDevice.size, 1)} drive",
107+
),
108+
subtitle: mountPoint != null ? Text(mountPoint) : null,
109+
trailing: mountPoint != null
110+
? IconButton(
111+
onPressed: () async {
112+
await widget.blockDevice.filesystem!.unmount();
113+
setState(() {});
114+
},
115+
icon: const Icon(Icons.eject),
116+
iconSize: 16,
117+
splashRadius: 16,
118+
)
119+
: null,
120+
onTap: () async {
121+
if (mountPoint == null) {
122+
mountPoint = await widget.blockDevice.filesystem!.mount();
123+
setState(() {});
124+
}
125+
126+
widget.onTap?.call(mountPoint!);
127+
},
128+
);
129+
}
130+
}
131+
132+
extension on List<int> {
133+
String decode() {
134+
return utf8.decode(sublist(0, length - 1));
135+
}
136+
}

lib/widgets/side_pane.dart

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:files/backend/folder_provider.dart';
22
import 'package:files/backend/workspace.dart';
33
import 'package:files/widgets/context_menu.dart';
4+
import 'package:files/widgets/drive_list.dart';
45
import 'package:flutter/material.dart';
56

67
typedef NewTabCallback = void Function(String);
@@ -53,40 +54,53 @@ class _SidePaneState extends State<SidePane> {
5354
width: 304,
5455
child: Material(
5556
color: Theme.of(context).colorScheme.surface,
56-
child: ListView.builder(
57+
child: ListView.separated(
5758
padding: const EdgeInsets.only(top: 56),
58-
itemCount: widget.destinations.length,
59-
itemBuilder: (context, index) => ContextMenu(
60-
entries: [
61-
ContextMenuItem(
62-
child: const Text("Open"),
59+
itemCount: widget.destinations.length + 1,
60+
separatorBuilder: (context, index) {
61+
if (index == widget.destinations.length - 1) {
62+
return const Divider();
63+
}
64+
65+
return const SizedBox();
66+
},
67+
itemBuilder: (context, index) {
68+
if (index == widget.destinations.length) {
69+
return DriveList(onDriveTap: widget.workspace.changeCurrentDir);
70+
}
71+
72+
return ContextMenu(
73+
entries: [
74+
ContextMenuItem(
75+
child: const Text("Open"),
76+
onTap: () => widget.workspace
77+
.changeCurrentDir(widget.destinations[index].path),
78+
),
79+
ContextMenuItem(
80+
child: const Text("Open in new tab"),
81+
onTap: () => widget.onNewTab(widget.destinations[index].path),
82+
),
83+
ContextMenuItem(
84+
child: const Text("Open in new window"),
85+
onTap: () {},
86+
enabled: false,
87+
),
88+
],
89+
child: ListTile(
90+
dense: true,
91+
leading: Icon(widget.destinations[index].icon),
92+
selected: widget.workspace.currentDir ==
93+
widget.destinations[index].path,
94+
selectedTileColor:
95+
Theme.of(context).colorScheme.primary.withOpacity(0.1),
96+
title: Text(
97+
widget.destinations[index].label,
98+
),
6399
onTap: () => widget.workspace
64100
.changeCurrentDir(widget.destinations[index].path),
65101
),
66-
ContextMenuItem(
67-
child: const Text("Open in new tab"),
68-
onTap: () => widget.onNewTab(widget.destinations[index].path),
69-
),
70-
ContextMenuItem(
71-
child: const Text("Open in new window"),
72-
onTap: () {},
73-
enabled: false,
74-
),
75-
],
76-
child: ListTile(
77-
dense: true,
78-
leading: Icon(widget.destinations[index].icon),
79-
selected: widget.workspace.currentDir ==
80-
widget.destinations[index].path,
81-
selectedTileColor:
82-
Theme.of(context).colorScheme.primary.withOpacity(0.1),
83-
title: Text(
84-
widget.destinations[index].label,
85-
),
86-
onTap: () => widget.workspace
87-
.changeCurrentDir(widget.destinations[index].path),
88-
),
89-
),
102+
);
103+
},
90104
),
91105
),
92106
);

pubspec.lock

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ packages:
169169
url: "https://pub.dev"
170170
source: hosted
171171
version: "1.1.0"
172+
dbus:
173+
dependency: transitive
174+
description:
175+
name: dbus
176+
sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263"
177+
url: "https://pub.dev"
178+
source: hosted
179+
version: "0.7.8"
172180
ffi:
173181
dependency: "direct main"
174182
description:
@@ -443,6 +451,14 @@ packages:
443451
url: "https://pub.dev"
444452
source: hosted
445453
version: "2.1.4"
454+
petitparser:
455+
dependency: transitive
456+
description:
457+
name: petitparser
458+
sha256: a9346a3fbba7546a28374bdbcd7f54ea48bb47772bf3a7ab4bfaadc40bc8b8c6
459+
url: "https://pub.dev"
460+
source: hosted
461+
version: "5.3.0"
446462
platform:
447463
dependency: transitive
448464
description:
@@ -616,6 +632,15 @@ packages:
616632
url: "https://pub.dev"
617633
source: hosted
618634
version: "0.2.0"
635+
udisks:
636+
dependency: "direct main"
637+
description:
638+
path: "."
639+
ref: HEAD
640+
resolved-ref: "300a7827d60b2f2069c36913b5b39d886a3a2dbe"
641+
url: "https://github.com/HrX03/udisks.dart"
642+
source: git
643+
version: "0.4.0"
619644
url_launcher:
620645
dependency: "direct main"
621646
description:
@@ -729,6 +754,14 @@ packages:
729754
url: "https://pub.dev"
730755
source: hosted
731756
version: "1.0.0"
757+
xml:
758+
dependency: transitive
759+
description:
760+
name: xml
761+
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
762+
url: "https://pub.dev"
763+
source: hosted
764+
version: "6.2.2"
732765
xxh3:
733766
dependency: transitive
734767
description:

pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ dependencies:
2828

2929
recase: ^4.1.0
3030
ubuntu_service: ^0.2.0
31+
udisks:
32+
git: https://github.com/HrX03/udisks.dart
3133
url_launcher: any
3234
windows_path_provider:
3335
git:

0 commit comments

Comments
 (0)