diff --git a/lib/main.dart b/lib/main.dart index f271e50..aa75ec5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ -import 'package:desktop_adb_file_browser/pages/browser.dart'; -import 'package:desktop_adb_file_browser/pages/devices.dart'; -import 'package:desktop_adb_file_browser/pages/logger.dart'; +import 'package:desktop_adb_file_browser/pages/main/browser.dart'; +import 'package:desktop_adb_file_browser/pages/main/devices.dart'; +import 'package:desktop_adb_file_browser/pages/main/logger.dart'; +import 'package:desktop_adb_file_browser/pages/main_page.dart'; import 'package:desktop_adb_file_browser/src/pigeon_impl.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/foundation.dart'; @@ -13,10 +14,16 @@ import 'package:trace/trace.dart'; import 'package:path/path.dart' as path; final routes = RouteMap(routes: { - '/': (_) => const Redirect('/devices'), - - '/devices': (_) => - const MaterialPage(key: ValueKey("devices"), child: DevicesPage()), + '/': (_) => const MaterialPage( + key: ValueKey("main"), + child: MainPage(), + ), + + '/devices': (_) => const MaterialPage( + key: ValueKey("devices"), + child: DevicesPage( + canNavigate: true, + )), '/browser/:device/:path': (info) => MaterialPage( key: const ValueKey("browser"), @@ -45,6 +52,8 @@ void main() async { yield LicenseEntryWithLineBreaks(['google_fonts'], license); }); + WidgetsFlutterBinding.ensureInitialized(); + final ConsoleLogger logger = ConsoleLogger( filter: DefaultLogFilter( LogLevel.verbose, @@ -68,7 +77,6 @@ void main() async { } catch (e) { Trace.error("Suffered error while setting up file logger: $e"); } - WidgetsFlutterBinding.ensureInitialized(); final token = ServicesBinding.rootIsolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(token!); diff --git a/lib/pages/browser.dart b/lib/pages/main/browser.dart similarity index 95% rename from lib/pages/browser.dart rename to lib/pages/main/browser.dart index 0cd58ff..271920e 100644 --- a/lib/pages/browser.dart +++ b/lib/pages/main/browser.dart @@ -8,7 +8,7 @@ import 'package:desktop_adb_file_browser/utils/storage.dart'; import 'package:desktop_adb_file_browser/widgets/browser/file_data.dart'; import 'package:desktop_adb_file_browser/widgets/browser/file_table.dart'; import 'package:desktop_adb_file_browser/widgets/browser/file_widget.dart'; -import 'package:desktop_adb_file_browser/widgets/browser/upload_file.dart'; +import 'package:desktop_adb_file_browser/widgets/progress_snackbar.dart'; import 'package:desktop_adb_file_browser/widgets/shortcuts.dart'; import 'package:desktop_adb_file_browser/widgets/watchers.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -111,18 +111,21 @@ class _DeviceBrowserPageState extends State { ), ); + var conditionalExitButton = + Routemaster.of(context).history.canGoBack ? exitButton : null; + return Focus( autofocus: true, canRequestFocus: false, descendantsAreFocusable: true, skipTraversal: true, - onKey: _onKeyHandler, + onKeyEvent: _onKeyHandler, child: DefaultTabController( initialIndex: 0, length: 2, child: Scaffold( appBar: AppBar( - elevation: 0.8, + elevation: 2.8, // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: _AppBarActions( @@ -131,10 +134,11 @@ class _DeviceBrowserPageState extends State { serial: widget.serial, onUpload: _uploadFiles, ), - leading: exitButton, + leading: conditionalExitButton, + automaticallyImplyLeading: true, actions: [listViewButton], ), - body: _buildBody(context), + body: _buildBody(), bottomNavigationBar: SizedBox( height: Theme.of(context).buttonTheme.height, child: _PathBreadCumbs( @@ -147,7 +151,7 @@ class _DeviceBrowserPageState extends State { ); } - MultiSplitViewTheme _buildBody(BuildContext context) { + MultiSplitViewTheme _buildBody() { return MultiSplitViewTheme( data: MultiSplitViewThemeData(dividerThickness: 5.5), child: MultiSplitView( @@ -158,7 +162,7 @@ class _DeviceBrowserPageState extends State { serial: widget.serial, onWatchAdd: onWatchAdd, ), - Center(child: _fileListContainer(context)) + Center(child: _fileListContainer()) ], dividerBuilder: (axis, index, resizable, dragging, highlighted, themeData) => @@ -185,7 +189,7 @@ class _DeviceBrowserPageState extends State { return KeyEventResult.ignored; } - DropTarget _fileListContainer(BuildContext context) { + DropTarget _fileListContainer() { return DropTarget( onDragDone: (detail) => _uploadFiles(detail.files.map((e) => e.path)), onDragEntered: (detail) { @@ -226,8 +230,9 @@ class _DeviceBrowserPageState extends State { files: list, key: ValueKey(list), filterController: _filterController, - builder: (context, filteredFiles) => - _viewAsListMode ? _viewAsList(filteredFiles) : _viewAsGrid(filteredFiles), + builder: (context, filteredFiles) => _viewAsListMode + ? _viewAsList(filteredFiles) + : _viewAsGrid(filteredFiles), ); } @@ -300,22 +305,16 @@ class _DeviceBrowserPageState extends State { return Adb.uploadFile(widget.serial, path, dest); }); - // this is so scuffed - // I do this to automatically update the snack bar progress - var tasksDone = 0; - var notifier = ValueNotifier(0); - - Future.forEach(tasks, (e) async { - tasksDone++; - notifier.value = tasksDone / tasks.length; - }); - // Snack bar var snackBar = ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: UploadingFilesWidget( - progressIndications: notifier, - taskAmount: tasks.length, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: ProgressSnackbar( + futures: tasks, + stringBuilder: (futureCount, futureRemaining) => + "Uploading files! ${(futureCount - futureRemaining) / futureCount * 100}%", + ), ), duration: const Duration(days: 365), // year old snackbar width: 680.0, // Width of the SnackBar. diff --git a/lib/pages/devices.dart b/lib/pages/main/devices.dart similarity index 89% rename from lib/pages/devices.dart rename to lib/pages/main/devices.dart index 948f134..5f3de6b 100644 --- a/lib/pages/devices.dart +++ b/lib/pages/main/devices.dart @@ -1,4 +1,5 @@ import 'package:desktop_adb_file_browser/pages/adb_check.dart'; +import 'package:desktop_adb_file_browser/routes.dart'; import 'package:desktop_adb_file_browser/utils/adb.dart'; import 'package:desktop_adb_file_browser/widgets/device_card.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; @@ -6,9 +7,11 @@ import 'package:flutter/material.dart'; import 'package:routemaster/routemaster.dart'; class DevicesPage extends StatefulWidget { - const DevicesPage({super.key}); + const DevicesPage( + {super.key, this.serialSelector, required this.canNavigate}); - static const String title = "Devices"; + final ValueNotifier? serialSelector; + final bool canNavigate; @override State createState() => _DevicesPageState(); @@ -43,7 +46,7 @@ class _DevicesPageState extends State { appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. - title: const Text(DevicesPage.title), + title: const Text("Devices"), automaticallyImplyLeading: Routemaster.of(context).history.canGoBack, actions: [ IconButton( @@ -53,9 +56,10 @@ class _DevicesPageState extends State { ], ), body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: _loadDevices()), + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: _loadDevices(), + ), floatingActionButton: FloatingActionButton( onPressed: () { _refreshDevices(); @@ -110,8 +114,22 @@ class _DevicesPageState extends State { return ListView( padding: const EdgeInsets.all(4.0), - children: - devices.map((e) => DeviceCard(device: e)).toList(growable: false)); + children: devices + .map((e) => DeviceCard( + device: e, + onTap: _onDeviceSelect, + showLogButton: widget.canNavigate, + selected: widget.serialSelector?.value == e.serialName, + )) + .toList(growable: false)); + } + + void _onDeviceSelect(Device device) { + widget.serialSelector?.value = device.serialName; + + if (widget.canNavigate) { + Routes.browse(context, device.serialName); + } } Future _connectDialog() async { diff --git a/lib/pages/logger.dart b/lib/pages/main/logger.dart similarity index 62% rename from lib/pages/logger.dart rename to lib/pages/main/logger.dart index a45f06c..8067615 100644 --- a/lib/pages/logger.dart +++ b/lib/pages/main/logger.dart @@ -9,10 +9,10 @@ import 'package:flutter/material.dart'; import 'package:routemaster/routemaster.dart'; import 'package:trace/trace.dart'; -import '../utils/scroll.dart'; +import '../../utils/scroll.dart'; class LogPage extends StatefulWidget { - LogPage({super.key, required this.serial}); + const LogPage({super.key, required this.serial}); final String serial; @@ -39,45 +39,47 @@ class _LogPageState extends State { @override void initState() { super.initState(); - _logFuture.then((values) { - final (process, stream) = values; - try { - _streamSubscription = stream.listen((event) { - setState(() { - var newLogs = event - .split(PlatformUtils.platformFileEnding) - .where((element) => element.trim().isNotEmpty); - _logs.addAll(newLogs); - - if (waitForSave) { - var timeSinceSend = DateTime.now().difference(lastStreamSend); - if (timeSinceSend.inMilliseconds > 30) { - _saveLog(); - waitForSave = false; - } - } - - lastStreamSend = DateTime.now(); - }); - }); - _streamSubscription?.onError((e) { - Trace.verbose(e); - _showError(e); - }); - _streamSubscription?.onDone(() { - Trace.verbose("Done"); - }); - } catch (e) { - Trace.verbose(e.toString()); - _showError(e.toString()); - } - }).onError((error, stackTrace) { + _logFuture.then(_handleLogFuture).onError((error, stackTrace) { Trace.verbose("Error $error"); Trace.verbose(stackTrace.toString()); _showError(error.toString()); }); } + FutureOr _handleLogFuture((Process, Stream) values) { + final (process, stream) = values; + try { + _streamSubscription = stream.listen((event) { + setState(() { + var newLogs = event + .split(PlatformUtils.platformFileEnding) + .where((element) => element.trim().isNotEmpty); + _logs.addAll(newLogs); + + if (waitForSave) { + var timeSinceSend = DateTime.now().difference(lastStreamSend); + if (timeSinceSend.inMilliseconds > 30) { + _saveLog(); + waitForSave = false; + } + } + + lastStreamSend = DateTime.now(); + }); + }); + _streamSubscription?.onError((e) { + Trace.verbose(e); + _showError(e); + }); + _streamSubscription?.onDone(() { + Trace.verbose("Done"); + }); + } catch (e) { + Trace.verbose(e.toString()); + _showError(e.toString()); + } + } + @override void dispose() { super.dispose(); @@ -90,36 +92,53 @@ class _LogPageState extends State { @override Widget build(BuildContext context) { + var exitButton = IconButton( + icon: const Icon( + FluentIcons.arrow_left_24_filled, + size: 24, + ), + onPressed: () { + Routemaster.of(context).history.back(); + }, + ); + + var conditionalExitButton = + Routemaster.of(context).history.canGoBack ? exitButton : null; + + var visibilityAction = Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + alignment: WrapAlignment.spaceAround, + children: [ + const Text( + "Show logs", + style: TextStyle(fontSize: 25), + ), + const SizedBox( + width: 10, + ), + Switch( + value: _showLogs, + onChanged: (v) => setState(() { + _showLogs = v; + })), + ], + )); + var saveAction = Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: _queueSave, + icon: const Icon( + FluentIcons.save_28_regular, + size: 28, + )), + ); return Scaffold( appBar: AppBar( title: const Text("Logcat"), - leading: IconButton( - icon: const Icon( - FluentIcons.arrow_left_24_filled, - size: 24, - ), - onPressed: () { - Routemaster.of(context).history.back(); - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: IconButton( - onPressed: _queueSave, - icon: const Icon( - FluentIcons.save_28_regular, - size: 28, - )), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Switch( - value: _showLogs, - onChanged: (v) => setState(() { - _showLogs = v; - }))) - ], + leading: conditionalExitButton, + automaticallyImplyLeading: true, + actions: [saveAction, visibilityAction], ), body: Padding( padding: const EdgeInsets.all(8.0), @@ -177,7 +196,7 @@ class _LogPageState extends State { return ListView.builder( key: _listKey, shrinkWrap: true, - controller: _scrollController, + // controller: _scrollController, itemBuilder: ((context, index) => SelectableText( _logs[index], key: ValueKey(index), diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart new file mode 100644 index 0000000..63e39ac --- /dev/null +++ b/lib/pages/main_page.dart @@ -0,0 +1,105 @@ +import 'package:desktop_adb_file_browser/pages/main/browser.dart'; +import 'package:desktop_adb_file_browser/pages/main/devices.dart'; +import 'package:desktop_adb_file_browser/pages/main/logger.dart'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; + +enum _Page { + devices("Devices", FluentIcons.phone_48_regular), + browser("Browser", FluentIcons.folder_48_regular), + logger("Logger", FluentIcons.code_block_48_regular); + + const _Page(this.name, this.icon); + + final String name; + final IconData icon; +} + +_Page _pageForIndex(int v) => + _Page.values.firstWhere((element) => element.index == v); + +class MainPage extends StatefulWidget { + const MainPage({super.key}); + + @override + State createState() => _MainPageState(); +} + +class _MainPageState extends State { + _Page _currentPage = _Page.devices; + final ValueNotifier _selectedDevice = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _selectedDevice.addListener(_onDeviceSelect); + } + + @override + void dispose() { + super.dispose(); + _selectedDevice.removeListener(_onDeviceSelect); + _selectedDevice.dispose(); + } + + void _onDeviceSelect() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + var dests = [ + NavigationRailDestination( + icon: Icon(_Page.devices.icon), + label: Text(_Page.devices.name), + ), + NavigationRailDestination( + icon: Icon(_Page.browser.icon), + label: Text(_Page.browser.name), + disabled: _selectedDevice.value == null, + ), + NavigationRailDestination( + icon: Icon(_Page.logger.icon), + label: Text(_Page.logger.name), + disabled: _selectedDevice.value == null, + ), + ]; + + return Scaffold( + body: Row(children: [ + NavigationRail( + backgroundColor: Theme.of(context).colorScheme.surface.darken(2), + indicatorShape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + labelType: NavigationRailLabelType.selected, + selectedIndex: _currentPage.index, + destinations: dests, + onDestinationSelected: (v) => setState( + () => _currentPage = _pageForIndex(v), + ), + ), + Expanded( + child: _buildCurrentPage(_currentPage), + ) + ]), + ); + } + + Widget _buildCurrentPage(_Page p) => switch (p) { + _Page.devices => DevicesPage( + key: const ValueKey("devices"), + serialSelector: _selectedDevice, + canNavigate: false, + ), + _Page.browser => DeviceBrowserPage( + key: const ValueKey("browser"), + initialAddress: "/sdcard/", + serial: _selectedDevice.value!, + ), + _Page.logger => LogPage( + key: const ValueKey("logger"), + serial: _selectedDevice.value!, + ), + }; +} diff --git a/lib/widgets/browser/upload_file.dart b/lib/widgets/browser/upload_file.dart deleted file mode 100644 index f837da5..0000000 --- a/lib/widgets/browser/upload_file.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class UploadingFilesWidget extends StatefulWidget { - final int taskAmount; - - final ValueListenable progressIndications; - const UploadingFilesWidget( - {super.key, required this.taskAmount, required this.progressIndications}); - - @override - State createState() => _UploadingFilesWidgetState(); -} - -class _UploadingFilesWidgetState extends State { - @override - Widget build(BuildContext context) { - var progressIndications = widget.progressIndications; - var taskAmount = widget.taskAmount; - - return ValueListenableBuilder( - valueListenable: progressIndications, - builder: (BuildContext context, double progress, _) { - var theme = Theme.of(context); - - return SizedBox( - height: 50, - child: Column( - children: [ - // Reverse calculation because less data needed to be passed! - Text( - "Uploading ${(progress * taskAmount).round()}/$taskAmount (${(progress * 100).round()}%)", - style: theme.textTheme.labelLarge?.copyWith( - color: theme.snackBarTheme.contentTextStyle?.color), - ), - const SizedBox(height: 20), - LinearProgressIndicator( - value: progress, - color: Theme.of(context).colorScheme.secondary, - ) - ], - ), - ); - }); - } -} diff --git a/lib/widgets/device_card.dart b/lib/widgets/device_card.dart index 67f6344..ca9799f 100644 --- a/lib/widgets/device_card.dart +++ b/lib/widgets/device_card.dart @@ -8,17 +8,51 @@ class DeviceCard extends StatelessWidget { const DeviceCard({ super.key, required this.device, + required this.onTap, + this.selected, + this.showLogButton = true, }); final Device device; + final void Function(Device device) onTap; + final bool showLogButton; + final bool? selected; @override Widget build(BuildContext context) { + var logButton = Visibility( + visible: showLogButton, + child: IconButton( + onPressed: () => Routes.log(context, device.serialName), + icon: const Icon( + FluentIcons.notepad_24_filled, + size: 24, + )), + ); + + var wirelessButton = IconButton( + onPressed: () => _enableWireless(context), + icon: const Icon( + FluentIcons.wifi_1_24_filled, + size: 24, + )); + + const deviceIcon = Icon( + FluentIcons.phone_24_regular, + size: 24 * 1.6, + ); + + const checkMark = Icon(FluentIcons.checkmark_24_regular, size: 24 * 1.6); + + final leading = selected ?? false + ? const Wrap(children: [checkMark, deviceIcon]) + : deviceIcon; + return ListTile( - leading: const Icon( - FluentIcons.phone_24_regular, - size: 24 * 1.6, - ), + selected: selected ?? false, + leading: leading, + isThreeLine: true, + onTap: () => onTap(device), title: Text(device.modelName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -31,24 +65,9 @@ class DeviceCard extends StatelessWidget { ), ], ), - onTap: () => Routes.browse(context, device.serialName), - isThreeLine: true, trailing: Wrap( crossAxisAlignment: WrapCrossAlignment.end, - children: [ - IconButton( - onPressed: () => Routes.log(context, device.serialName), - icon: const Icon( - FluentIcons.notepad_24_filled, - size: 24, - )), - IconButton( - onPressed: () => _enableWireless(context), - icon: const Icon( - FluentIcons.wifi_1_24_filled, - size: 24, - )) - ], + children: [logButton, wirelessButton], ), ); } diff --git a/lib/widgets/progress_snackbar.dart b/lib/widgets/progress_snackbar.dart new file mode 100644 index 0000000..f700507 --- /dev/null +++ b/lib/widgets/progress_snackbar.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +typedef StringBuilderFn = String Function( + int totalFutures, int remainingFutures); + +class ProgressSnackbar extends StatefulWidget { + const ProgressSnackbar( + {super.key, required this.futures, required this.stringBuilder}); + + final Iterable futures; + final StringBuilderFn stringBuilder; + + @override + State createState() => _ProgressSnackbarState(); +} + +class _ProgressSnackbarState extends State { + late final futuresList = widget.futures.toList(); + + int futureCount = 0; + + @override + void initState() { + super.initState(); + updateFutureCount(); + } + + void updateFutureCount() async { + for (int i = 0; i < futuresList.length; i++) { + await futuresList[futureCount]; + // update state + setState(() { + futureCount = i; + }); + } + } + + @override + Widget build(BuildContext context) { + final String content = + widget.stringBuilder(futuresList.length, futureCount); + + return Column( + children: [ + Text(content), + const SizedBox(height: 20), + LinearProgressIndicator( + value: futureCount / futuresList.length, + ) + ], + ); + ; + } +}