mirror of
https://github.com/localsend/localsend.git
synced 2026-04-29 03:00:23 -04:00
feat: implement multi send prototype
This commit is contained in:
@@ -9,6 +9,8 @@ part 'send_session_state.freezed.dart';
|
||||
@freezed
|
||||
class SendSessionState with _$SendSessionState {
|
||||
const factory SendSessionState({
|
||||
required String sessionId,
|
||||
required bool background,
|
||||
required SessionStatus status,
|
||||
required Device target,
|
||||
required Map<String, SendingFile> files, // file id as key
|
||||
|
||||
@@ -23,9 +23,15 @@ import 'package:routerino/routerino.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
class ProgressPage extends ConsumerStatefulWidget {
|
||||
final bool showAppBar;
|
||||
final bool closeSessionOnClose;
|
||||
final String sessionId;
|
||||
|
||||
const ProgressPage({required this.sessionId});
|
||||
const ProgressPage({
|
||||
required this.showAppBar,
|
||||
required this.closeSessionOnClose,
|
||||
required this.sessionId,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgressPage> createState() => _ProgressPageState();
|
||||
@@ -74,6 +80,20 @@ class _ProgressPageState extends ConsumerState<ProgressPage> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
final receiveSession = ref.watch(serverProvider.select((s) => s?.session));
|
||||
final sendSession = ref.watch(sendProvider)[widget.sessionId];
|
||||
final SessionStatus? status = receiveSession?.status ?? sendSession?.status;
|
||||
if (status == null) {
|
||||
return true;
|
||||
}
|
||||
if (!widget.closeSessionOnClose && status == SessionStatus.sending) {
|
||||
// keep session except [closeSessionOnClose] is true and the session is active
|
||||
return true;
|
||||
}
|
||||
return _askCancelConfirmation(status);
|
||||
}
|
||||
|
||||
Future<bool> _askCancelConfirmation(SessionStatus status) async {
|
||||
final bool result = status == SessionStatus.sending ? await context.pushBottomSheet(() => const CancelSessionDialog()) : true;
|
||||
if (result) {
|
||||
@@ -91,7 +111,7 @@ class _ProgressPageState extends ConsumerState<ProgressPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ProgressNotifier progressNotifier = ref.watch(progressProvider);
|
||||
final progressNotifier = ref.watch(progressProvider);
|
||||
final currBytes = _files.fold<int>(0, (prev, curr) => prev + ((progressNotifier.getProgress(sessionId: widget.sessionId, fileId: curr.id) * curr.size).round()));
|
||||
|
||||
final receiveSession = ref.watch(serverProvider.select((s) => s?.session));
|
||||
@@ -104,6 +124,7 @@ class _ProgressPageState extends ConsumerState<ProgressPage> {
|
||||
);
|
||||
}
|
||||
|
||||
final title = receiveSession != null ? t.progressPage.titleReceiving : t.progressPage.titleSending;
|
||||
final startTime = receiveSession?.startTime ?? sendSession?.startTime;
|
||||
final endTime = receiveSession?.endTime ?? sendSession?.endTime;
|
||||
final int? speedInBytes;
|
||||
@@ -120,8 +141,11 @@ class _ProgressPageState extends ConsumerState<ProgressPage> {
|
||||
}
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () => _askCancelConfirmation(status),
|
||||
onWillPop: _onWillPop,
|
||||
child: Scaffold(
|
||||
appBar: widget.showAppBar ? AppBar(
|
||||
title: Text(title),
|
||||
) : null,
|
||||
body: Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
@@ -140,10 +164,7 @@ class _ProgressPageState extends ConsumerState<ProgressPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
receiveSession != null ? t.progressPage.titleReceiving : t.progressPage.titleSending,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
if (checkPlatformWithFileSystem() && receiveSession != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
|
||||
@@ -265,7 +265,11 @@ class _ReceivePageState extends ConsumerState<ReceivePage> {
|
||||
_accept(ref, receiveSession);
|
||||
context.pushAndRemoveUntilImmediately(
|
||||
removeUntil: ReceivePage,
|
||||
builder: () => ProgressPage(sessionId: sessionId),
|
||||
builder: () => ProgressPage(
|
||||
showAppBar: false,
|
||||
closeSessionOnClose: true,
|
||||
sessionId: sessionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.check_circle),
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:localsend_app/model/session_status.dart';
|
||||
import 'package:localsend_app/pages/progress_page.dart';
|
||||
import 'package:localsend_app/pages/selected_files_page.dart';
|
||||
import 'package:localsend_app/pages/send_page.dart';
|
||||
import 'package:localsend_app/pages/troubleshoot_page.dart';
|
||||
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
|
||||
import 'package:localsend_app/provider/network/scan_provider.dart';
|
||||
import 'package:localsend_app/provider/network/send_provider.dart';
|
||||
import 'package:localsend_app/provider/network_info_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/file_picker.dart';
|
||||
@@ -55,6 +60,7 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sendMode = ref.watch(settingsProvider.select((s) => s.sendMode));
|
||||
final selectedFiles = ref.watch(selectedSendingFilesProvider);
|
||||
final networkInfo = ref.watch(networkStateProvider);
|
||||
final nearbyDevicesState = ref.watch(nearbyDevicesProvider);
|
||||
@@ -200,6 +206,7 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
ref.read(sendProvider.notifier).startSession(
|
||||
target: device,
|
||||
files: files,
|
||||
background: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -224,21 +231,24 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
padding: const EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
|
||||
child: Hero(
|
||||
tag: 'device-${device.ip}',
|
||||
child: DeviceListTile(
|
||||
device: device,
|
||||
onTap: () {
|
||||
final files = ref.read(selectedSendingFilesProvider);
|
||||
if (files.isEmpty) {
|
||||
context.pushBottomSheet(() => const NoFilesDialog());
|
||||
return;
|
||||
}
|
||||
child: sendMode == SendMode.multiple
|
||||
? _MultiSendDeviceListTile(device: device)
|
||||
: DeviceListTile(
|
||||
device: device,
|
||||
onTap: () {
|
||||
final files = ref.read(selectedSendingFilesProvider);
|
||||
if (files.isEmpty) {
|
||||
context.pushBottomSheet(() => const NoFilesDialog());
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(sendProvider.notifier).startSession(
|
||||
target: device,
|
||||
files: files,
|
||||
);
|
||||
},
|
||||
),
|
||||
ref.read(sendProvider.notifier).startSession(
|
||||
target: device,
|
||||
files: files,
|
||||
background: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -478,3 +488,88 @@ class _SendModeButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An advanced list tile which shows the progress of the file transfer.
|
||||
class _MultiSendDeviceListTile extends ConsumerWidget {
|
||||
final Device device;
|
||||
|
||||
const _MultiSendDeviceListTile({
|
||||
required this.device,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final session = ref.watch(sendProvider).values.firstWhereOrNull((s) => s.target.ip == device.ip);
|
||||
final double? progress;
|
||||
if (session != null) {
|
||||
final files = session.files.values.where((f) => f.token != null);
|
||||
final progressNotifier = ref.watch(progressProvider);
|
||||
final currBytes = files.fold<int>(0, (prev, curr) => prev + ((progressNotifier.getProgress(sessionId: session.sessionId, fileId: curr.file.id) * curr.file.size).round()));
|
||||
final totalBytes = files.fold<int>(0, (prev, curr) => prev + curr.file.size);
|
||||
progress = totalBytes == 0 ? 0 : currBytes / totalBytes;
|
||||
} else {
|
||||
progress = null;
|
||||
}
|
||||
return DeviceListTile(
|
||||
device: device,
|
||||
info: session?.status.humanString,
|
||||
progress: progress,
|
||||
onTap: () async {
|
||||
if (session != null) {
|
||||
if (session.status == SessionStatus.waiting) {
|
||||
ref.read(sendProvider.notifier).setBackground(session.sessionId, false);
|
||||
await context.push(() => SendPage(sessionId: session.sessionId), transition: RouterinoTransition.fade);
|
||||
ref.read(sendProvider.notifier).setBackground(session.sessionId, true);
|
||||
return;
|
||||
} else if (session.status == SessionStatus.sending) {
|
||||
ref.read(sendProvider.notifier).setBackground(session.sessionId, false);
|
||||
await context.push(() => ProgressPage(showAppBar: true, closeSessionOnClose: false, sessionId: session.sessionId));
|
||||
ref.read(sendProvider.notifier).setBackground(session.sessionId, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final files = ref.read(selectedSendingFilesProvider);
|
||||
if (files.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.pushBottomSheet(() => const NoFilesDialog());
|
||||
return;
|
||||
}
|
||||
|
||||
if (session != null) {
|
||||
// close old session
|
||||
ref.read(sendProvider.notifier).cancelSession(session.sessionId);
|
||||
}
|
||||
|
||||
ref.read(sendProvider.notifier).startSession(
|
||||
target: device,
|
||||
files: files,
|
||||
background: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on SessionStatus {
|
||||
String? get humanString {
|
||||
switch (this) {
|
||||
case SessionStatus.waiting:
|
||||
return t.sendPage.waiting;
|
||||
case SessionStatus.recipientBusy:
|
||||
return t.sendPage.busy;
|
||||
case SessionStatus.declined:
|
||||
return t.sendPage.rejected;
|
||||
case SessionStatus.sending:
|
||||
return null;
|
||||
case SessionStatus.finished:
|
||||
return t.general.finished;
|
||||
case SessionStatus.finishedWithErrors:
|
||||
return t.progressPage.total.title.finishedError;
|
||||
case SessionStatus.canceledBySender:
|
||||
return t.progressPage.total.title.canceledSender;
|
||||
case SessionStatus.canceledByReceiver:
|
||||
return t.progressPage.total.title.canceledReceiver;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:localsend_app/model/dto/info_dto.dart';
|
||||
import 'package:localsend_app/model/dto/send_request_dto.dart';
|
||||
import 'package:localsend_app/model/file_status.dart';
|
||||
import 'package:localsend_app/model/file_type.dart';
|
||||
import 'package:localsend_app/model/send_mode.dart';
|
||||
import 'package:localsend_app/model/state/send/send_session_state.dart';
|
||||
import 'package:localsend_app/model/state/send/sending_file.dart';
|
||||
import 'package:localsend_app/model/session_status.dart';
|
||||
@@ -19,7 +20,10 @@ import 'package:localsend_app/pages/send_page.dart';
|
||||
import 'package:localsend_app/provider/device_info_provider.dart';
|
||||
import 'package:localsend_app/provider/dio_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/api_route_builder.dart';
|
||||
import 'package:localsend_app/util/cache_helper.dart';
|
||||
import 'package:routerino/routerino.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -36,15 +40,22 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
|
||||
SendNotifier(this._ref) : super({});
|
||||
|
||||
/// Starts a session.
|
||||
/// If [background] is true, then the session closes itself on success and no pages will be open
|
||||
/// If [background] is false, then this method will open pages by itself and waits for user input to close the session.
|
||||
Future<void> startSession({
|
||||
required Device target,
|
||||
required List<CrossFile> files,
|
||||
required bool background,
|
||||
}) async {
|
||||
final requestDio = _ref.read(dioProvider(DioType.longLiving));
|
||||
final uploadDio = _ref.read(dioProvider(DioType.longLiving));
|
||||
final cancelToken = CancelToken();
|
||||
final sessionId = _uuid.v4();
|
||||
|
||||
final requestState = SendSessionState(
|
||||
sessionId: sessionId,
|
||||
background: background,
|
||||
status: SessionStatus.waiting,
|
||||
target: target,
|
||||
files: Map.fromEntries(await Future.wait(files.map((file) async {
|
||||
@@ -77,7 +88,6 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
);
|
||||
|
||||
final originDevice = _ref.read(deviceInfoProvider);
|
||||
final sessionId = _uuid.v4();
|
||||
final requestDto = SendRequestDto(
|
||||
info: InfoDto(
|
||||
alias: originDevice.alias,
|
||||
@@ -94,8 +104,10 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
state: (_) => requestState,
|
||||
);
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.push(() => SendPage(sessionId: sessionId), transition: RouterinoTransition.fade);
|
||||
if (!background) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.push(() => SendPage(sessionId: sessionId), transition: RouterinoTransition.fade);
|
||||
}
|
||||
|
||||
final Response response;
|
||||
try {
|
||||
@@ -135,8 +147,11 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
if (responseMap.isEmpty) {
|
||||
// receiver has nothing selected
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.pushRootImmediately(() => const HomePage(appStart: false));
|
||||
if (state[sessionId]?.background == false) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.pushRootImmediately(() => const HomePage(appStart: false));
|
||||
}
|
||||
|
||||
state = state.removeSession(_ref, sessionId);
|
||||
return;
|
||||
}
|
||||
@@ -147,11 +162,19 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
responseMap.containsKey(file.file.id) ? file.copyWith(token: responseMap[file.file.id]) : file.copyWith(status: FileStatus.skipped),
|
||||
};
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.pushAndRemoveUntilImmediately(
|
||||
removeUntil: SendPage,
|
||||
builder: () => ProgressPage(sessionId: sessionId),
|
||||
);
|
||||
if (state[sessionId]?.background == false) {
|
||||
final background = _ref.read(settingsProvider.select((s) => s.sendMode == SendMode.multiple));
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.pushAndRemoveUntilImmediately(
|
||||
removeUntil: SendPage,
|
||||
builder: () => ProgressPage(
|
||||
showAppBar: background,
|
||||
closeSessionOnClose: !background,
|
||||
sessionId: sessionId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
@@ -165,11 +188,6 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
}
|
||||
|
||||
Future<void> _send(String sessionId, Dio dio, Device target, Map<String, SendingFile> files) async {
|
||||
final sessionState = state[sessionId];
|
||||
if (sessionState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasError = false;
|
||||
|
||||
state = state.updateSession(
|
||||
@@ -209,10 +227,10 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
data: file.path != null ? File(file.path!).openRead() : Stream.fromIterable([file.bytes!]),
|
||||
onSendProgress: (curr, total) {
|
||||
_ref.read(progressProvider.notifier).setProgress(
|
||||
sessionId: sessionId,
|
||||
fileId: file.file.id,
|
||||
progress: curr / total,
|
||||
);
|
||||
sessionId: sessionId,
|
||||
fileId: file.file.id,
|
||||
progress: curr / total,
|
||||
);
|
||||
},
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
@@ -229,18 +247,23 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
);
|
||||
}
|
||||
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.copyWith(
|
||||
status: hasError ? SessionStatus.finishedWithErrors : SessionStatus.finished,
|
||||
endTime: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
if (state[sessionId]?.background == true) {
|
||||
state = state.removeSession(_ref, sessionId);
|
||||
} else {
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.copyWith(
|
||||
status: hasError ? SessionStatus.finishedWithErrors : SessionStatus.finished,
|
||||
endTime: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
print('Files sent successfully.');
|
||||
}
|
||||
|
||||
/// Closes the send-session and sends a cancel event to the receiver.
|
||||
Future<void> cancelSession(String sessionId) async {
|
||||
void cancelSession(String sessionId) {
|
||||
final sessionState = state[sessionId];
|
||||
if (sessionState == null) {
|
||||
return;
|
||||
@@ -248,9 +271,20 @@ class SendNotifier extends StateNotifier<Map<String, SendSessionState>> {
|
||||
final target = sessionState.target;
|
||||
sessionState.cancelToken?.cancel(); // cancel current request
|
||||
state = state.removeSession(_ref, sessionId);
|
||||
try {
|
||||
await _ref.read(dioProvider(DioType.discovery)).post(ApiRoute.cancel.target(target));
|
||||
} catch (_) {}
|
||||
if (_ref.read(settingsProvider.select((s) => s.sendMode == SendMode.single))) {
|
||||
// clear selected files
|
||||
_ref.read(selectedSendingFilesProvider.notifier).reset();
|
||||
clearCache();
|
||||
}
|
||||
|
||||
// notify the receiver
|
||||
_ref.read(dioProvider(DioType.discovery)).post(ApiRoute.cancel.target(target)).then((_) {}).catchError((e) {
|
||||
print(e);
|
||||
});
|
||||
}
|
||||
|
||||
void setBackground(String sessionId, bool background) {
|
||||
state = state.updateSession(sessionId: sessionId, state: (s) => s?.copyWith(background: background));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,10 +313,13 @@ extension on Map<String, SendSessionState> {
|
||||
extension on SendSessionState {
|
||||
SendSessionState withFileStatus(String fileId, FileStatus status, String? errorMessage) {
|
||||
return copyWith(
|
||||
files: {...files}..update(fileId, (file) => file.copyWith(
|
||||
status: status,
|
||||
errorMessage: errorMessage,
|
||||
)),
|
||||
files: {...files}..update(
|
||||
fileId,
|
||||
(file) => file.copyWith(
|
||||
status: status,
|
||||
errorMessage: errorMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,11 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
|
||||
if (quickSave) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.pushImmediately(() => ProgressPage(sessionId: sessionId));
|
||||
Routerino.context.pushImmediately(() => ProgressPage(
|
||||
showAppBar: false,
|
||||
closeSessionOnClose: true,
|
||||
sessionId: sessionId,
|
||||
));
|
||||
}
|
||||
|
||||
return _response(200, body: {
|
||||
@@ -315,10 +319,10 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
onProgress: (savedBytes) {
|
||||
if (receivingFile.file.size != 0) {
|
||||
_ref.read(progressProvider.notifier).setProgress(
|
||||
sessionId: receiveState.sessionId,
|
||||
fileId: fileId,
|
||||
progress: savedBytes / receivingFile.file.size,
|
||||
);
|
||||
sessionId: receiveState.sessionId,
|
||||
fileId: fileId,
|
||||
progress: savedBytes / receivingFile.file.size,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -363,10 +367,10 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
}
|
||||
|
||||
_ref.read(progressProvider.notifier).setProgress(
|
||||
sessionId: receiveState.sessionId,
|
||||
fileId: fileId,
|
||||
progress: 1,
|
||||
);
|
||||
sessionId: receiveState.sessionId,
|
||||
fileId: fileId,
|
||||
progress: 1,
|
||||
);
|
||||
|
||||
if (state!.session!.files.values
|
||||
.every((f) => f.status == FileStatus.finished || f.status == FileStatus.skipped || f.status == FileStatus.failed)) {
|
||||
@@ -388,9 +392,7 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
print('Received all files.');
|
||||
}
|
||||
|
||||
return state?.session?.files[fileId]?.status == FileStatus.finished
|
||||
? _response(200)
|
||||
: _response(500, message: 'Could not save file');
|
||||
return state?.session?.files[fileId]?.status == FileStatus.finished ? _response(200) : _response(500, message: 'Could not save file');
|
||||
});
|
||||
|
||||
router.post(ApiRoute.cancel.path, (Request request) {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:localsend_app/util/ip_helper.dart';
|
||||
import 'package:localsend_app/widget/custom_progress_bar.dart';
|
||||
import 'package:localsend_app/widget/device_bage.dart';
|
||||
import 'package:localsend_app/widget/list_tile/custom_list_tile.dart';
|
||||
|
||||
class DeviceListTile extends StatelessWidget {
|
||||
final Device device;
|
||||
final String? info;
|
||||
final double? progress;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const DeviceListTile({required this.device, this.onTap});
|
||||
const DeviceListTile({required this.device, this.info, this.progress, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -19,15 +22,28 @@ class DeviceListTile extends StatelessWidget {
|
||||
runSpacing: 10,
|
||||
spacing: 10,
|
||||
children: [
|
||||
DeviceBadge(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
label: '#${device.ip.visualId}',
|
||||
),
|
||||
if (device.deviceModel != null)
|
||||
DeviceBadge(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
label: device.deviceModel!,
|
||||
),
|
||||
if (info != null)
|
||||
Text(info!, style: const TextStyle(color: Colors.grey))
|
||||
else if (progress != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: CustomProgressBar(
|
||||
progress: progress!,
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
),
|
||||
)
|
||||
else
|
||||
...[
|
||||
DeviceBadge(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
label: '#${device.ip.visualId}',
|
||||
),
|
||||
if (device.deviceModel != null)
|
||||
DeviceBadge(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
label: device.deviceModel!,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
onTap: onTap,
|
||||
|
||||
Reference in New Issue
Block a user