mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
369 lines
17 KiB
Dart
369 lines
17 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:localsend_app/config/theme.dart';
|
|
import 'package:localsend_app/gen/strings.g.dart';
|
|
import 'package:localsend_app/model/persistence/color_mode.dart';
|
|
import 'package:localsend_app/pages/receive_options_page.dart';
|
|
import 'package:localsend_app/pages/verify_page.dart';
|
|
import 'package:localsend_app/pages/web_share_page.dart';
|
|
import 'package:localsend_app/provider/favorites_provider.dart';
|
|
import 'package:localsend_app/provider/selection/selected_receiving_files_provider.dart';
|
|
import 'package:localsend_app/provider/settings_provider.dart';
|
|
import 'package:localsend_app/util/device_type_ext.dart';
|
|
import 'package:localsend_app/util/favorites.dart';
|
|
import 'package:localsend_app/util/native/platform_check.dart';
|
|
import 'package:localsend_app/util/native/taskbar_helper.dart';
|
|
import 'package:localsend_app/util/ui/snackbar.dart';
|
|
import 'package:localsend_app/widget/device_bage.dart';
|
|
import 'package:localsend_app/widget/responsive_list_view.dart';
|
|
import 'package:localsend_isolates/model/device.dart';
|
|
import 'package:localsend_isolates/model/dto/file_dto.dart';
|
|
import 'package:localsend_isolates/model/session_status.dart';
|
|
import 'package:refena_flutter/addons.dart';
|
|
import 'package:refena_flutter/refena_flutter.dart';
|
|
import 'package:routerino/routerino.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class ReceivePageVm {
|
|
final SessionStatus? status;
|
|
final Device sender;
|
|
|
|
/// Show verify button and device model.
|
|
final bool showSenderInfo;
|
|
final List<FileDto> files;
|
|
final String? message;
|
|
final bool isLink;
|
|
final void Function() onAccept;
|
|
final void Function() onDecline;
|
|
final void Function() onClose;
|
|
|
|
ReceivePageVm({
|
|
required this.status,
|
|
required this.sender,
|
|
required this.showSenderInfo,
|
|
required this.files,
|
|
required this.message,
|
|
required this.onAccept,
|
|
required this.onDecline,
|
|
required this.onClose,
|
|
}) : isLink = message != null && !message.trim().contains(RegExp(r'\s')) && (Uri.tryParse(message.trim())?.isAbsolute ?? false);
|
|
}
|
|
|
|
class ReceivePage extends StatefulWidget {
|
|
final ViewProvider<ReceivePageVm> vm;
|
|
|
|
const ReceivePage(this.vm);
|
|
|
|
@override
|
|
State<ReceivePage> createState() => _ReceivePageState();
|
|
}
|
|
|
|
class _ReceivePageState extends State<ReceivePage> with Refena {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final vm = context.watch(
|
|
widget.vm,
|
|
listener: (prev, next) {
|
|
if (prev.status != next.status) {
|
|
// ignore: discarded_futures
|
|
TaskbarHelper.visualizeStatus(next.status);
|
|
}
|
|
},
|
|
);
|
|
|
|
if (vm.status == null && vm.message == null) {
|
|
return const Scaffold(
|
|
body: SizedBox(),
|
|
);
|
|
}
|
|
|
|
final senderFavoriteEntry = ref.watch(favoritesProvider.select((state) => state.findDevice(vm.sender)));
|
|
|
|
return ViewModelBuilder(
|
|
provider: (ref) => widget.vm,
|
|
dispose: (ref) {
|
|
ref.dispose(widget.vm);
|
|
unawaited(TaskbarHelper.clearProgressBar());
|
|
},
|
|
builder: (context, vm) {
|
|
return PopScope(
|
|
onPopInvokedWithResult: (didPop, result) {
|
|
if (didPop) {
|
|
vm.onDecline();
|
|
}
|
|
},
|
|
canPop: true,
|
|
child: Scaffold(
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: ResponsiveListView.defaultMaxWidth),
|
|
child: Builder(
|
|
builder: (context) {
|
|
final height = MediaQuery.of(context).size.height;
|
|
final smallUi = vm.message != null && height < 600;
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: smallUi ? 20 : 30),
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Column(
|
|
children: [
|
|
if (vm.showSenderInfo && !smallUi)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Icon(vm.sender.deviceType.icon, size: 64),
|
|
),
|
|
Builder(
|
|
builder: (context) {
|
|
final alias = senderFavoriteEntry?.alias ?? vm.sender.alias;
|
|
if (alias.isEmpty) {
|
|
return Text('', style: TextStyle(fontSize: smallUi ? 32 : 48));
|
|
}
|
|
return FittedBox(
|
|
child: Text(
|
|
alias,
|
|
style: TextStyle(fontSize: smallUi ? 32 : 48),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (vm.showSenderInfo && vm.sender.deviceModel != null) ...[
|
|
const SizedBox(height: 10),
|
|
Center(
|
|
child: DeviceBadge(
|
|
backgroundColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
|
foregroundColor: Theme.of(context).colorScheme.onInverseSurface,
|
|
label: vm.sender.deviceModel!,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
|
|
Column(
|
|
children: [
|
|
Text(
|
|
vm.message != null
|
|
? (vm.isLink ? t.receivePage.subTitleLink : t.receivePage.subTitleMessage)
|
|
: t.receivePage.subTitle(n: vm.files.length),
|
|
style: smallUi ? null : Theme.of(context).textTheme.titleLarge,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (vm.showSenderInfo && vm.message == null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
TextButton.icon(
|
|
onPressed: !vm.sender.https ? null : () async => await context.push(
|
|
() => VerifyPage(
|
|
fingerprint: CombinedFingerprint.load(context, vm.sender.fingerprint),
|
|
),
|
|
),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
icon: Icon(Icons.verified_user),
|
|
label: Text(t.verifyPage.title),
|
|
),
|
|
TextButton.icon(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
onPressed: () async {
|
|
await context.push(() => ReceiveOptionsPage(vm));
|
|
},
|
|
icon: const Icon(Icons.settings),
|
|
label: Text(t.receiveOptionsPage.title),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (vm.message != null)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 20),
|
|
child: SizedBox(
|
|
height: 100,
|
|
child: Card(
|
|
child: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: SelectableText(
|
|
vm.message!,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (vm.showSenderInfo)
|
|
Padding(
|
|
padding: const EdgeInsetsDirectional.only(end: 20),
|
|
child: ElevatedButton.icon(
|
|
onPressed: () async => await context.push(
|
|
() => VerifyPage(
|
|
fingerprint: CombinedFingerprint.load(context, vm.sender.fingerprint),
|
|
),
|
|
),
|
|
icon: Icon(Icons.verified_user),
|
|
label: Text(t.verifyPage.title),
|
|
),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
unawaited(
|
|
Clipboard.setData(ClipboardData(text: vm.message!)),
|
|
);
|
|
if (checkPlatformIsDesktop()) {
|
|
context.showSnackBar(t.general.copiedToClipboard);
|
|
}
|
|
vm.onAccept();
|
|
context.global.dispatch(NavigateAction.popUntil<WebSharePage>());
|
|
},
|
|
icon: Icon(Icons.copy),
|
|
label: Text(t.general.copy),
|
|
),
|
|
if (vm.isLink)
|
|
Padding(
|
|
padding: const EdgeInsetsDirectional.only(start: 20),
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
),
|
|
onPressed: () {
|
|
// ignore: discarded_futures
|
|
launchUrl(Uri.parse(vm.message!), mode: LaunchMode.externalApplication);
|
|
vm.onAccept();
|
|
context.global.dispatch(NavigateAction.popUntil<WebSharePage>());
|
|
},
|
|
icon: Icon(Icons.open_in_new),
|
|
label: Text(t.general.open),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_Actions(vm),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Actions extends StatelessWidget {
|
|
final ReceivePageVm vm;
|
|
|
|
const _Actions(this.vm);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final selectedFiles = context.watch(selectedReceivingFilesProvider);
|
|
final colorMode = context.watch(settingsProvider.select((state) => state.colorMode));
|
|
|
|
if (vm.message != null) {
|
|
return Center(
|
|
child: TextButton.icon(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
onPressed: () {
|
|
vm.onAccept();
|
|
context.global.dispatch(NavigateAction.popUntil<WebSharePage>());
|
|
},
|
|
icon: const Icon(Icons.close),
|
|
label: Text(t.general.close),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (vm.status == SessionStatus.canceledBySender) {
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: Text(
|
|
t.receivePage.canceled,
|
|
style: TextStyle(color: Theme.of(context).colorScheme.warning),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
Center(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
vm.onClose();
|
|
context.global.dispatch(NavigateAction.popUntil<WebSharePage>());
|
|
},
|
|
icon: const Icon(Icons.check_circle),
|
|
label: Text(t.general.close),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
elevation: colorMode == ColorMode.yaru ? 0 : null,
|
|
backgroundColor: colorMode == ColorMode.yaru ? Theme.of(context).colorScheme.surface : Theme.of(context).colorScheme.error,
|
|
foregroundColor: colorMode == ColorMode.yaru ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.onError,
|
|
),
|
|
onPressed: () {
|
|
vm.onDecline();
|
|
context.global.dispatch(NavigateAction.popUntil<WebSharePage>());
|
|
},
|
|
icon: const Icon(Icons.close),
|
|
label: Text(t.general.decline),
|
|
),
|
|
const SizedBox(width: 20),
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
),
|
|
onPressed: selectedFiles.isEmpty ? null : () => vm.onAccept(),
|
|
icon: const Icon(Icons.check_circle),
|
|
label: Text(t.general.accept),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|