mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
287 lines
11 KiB
Dart
287 lines
11 KiB
Dart
import 'package:device_apps/device_apps.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:localsend_app/gen/strings.g.dart';
|
|
import 'package:localsend_app/provider/apk_provider.dart';
|
|
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
|
|
import 'package:localsend_app/util/native/cross_file_converters.dart';
|
|
import 'package:localsend_app/util/ui/nav_bar_padding.dart';
|
|
import 'package:localsend_app/widget/file_thumbnail.dart';
|
|
import 'package:localsend_app/widget/responsive_list_view.dart';
|
|
import 'package:localsend_app/widget/sliver/sliver_pinned_header.dart';
|
|
import 'package:localsend_isolates/model/file_type.dart';
|
|
import 'package:localsend_isolates/util/file_size_helper.dart';
|
|
import 'package:refena_flutter/refena_flutter.dart';
|
|
import 'package:routerino/routerino.dart';
|
|
|
|
class ApkPickerPage extends StatefulWidget {
|
|
const ApkPickerPage({super.key});
|
|
|
|
@override
|
|
State<ApkPickerPage> createState() => _ApkPickerPageState();
|
|
}
|
|
|
|
class _ApkPickerPageState extends State<ApkPickerPage> with Refena {
|
|
final _textController = TextEditingController();
|
|
final List<Application> _selectedApps = [];
|
|
|
|
Future<void> _pickApp(Application app) async {
|
|
await ref
|
|
.redux(selectedSendingFilesProvider)
|
|
.dispatchAsync(
|
|
AddFilesAction(
|
|
files: [app],
|
|
converter: CrossFileConverters.convertApplication,
|
|
),
|
|
);
|
|
|
|
if (mounted) {
|
|
context.pop();
|
|
}
|
|
}
|
|
|
|
Future<void> _pickApps(List<Application> apps) async {
|
|
// ignore: discarded_futures
|
|
|
|
for (Application app in apps) {
|
|
await ref
|
|
.redux(selectedSendingFilesProvider)
|
|
.dispatchAsync(
|
|
AddFilesAction(
|
|
files: [app],
|
|
converter: CrossFileConverters.convertApplication,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (mounted) {
|
|
context.pop();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_textController.dispose();
|
|
ref.dispose(apkSearchParamProvider);
|
|
super.dispose();
|
|
}
|
|
|
|
void _appSelection(Application app) {
|
|
setState(() {
|
|
if (_selectedApps.contains(app)) {
|
|
_selectedApps.remove(app);
|
|
} else {
|
|
_selectedApps.add(app);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final apkParams = ref.watch(apkSearchParamProvider);
|
|
final apkAsync = ref.watch(apkProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(t.apkPickerPage.title),
|
|
actions: [
|
|
PopupMenuButton(
|
|
itemBuilder: (context) {
|
|
return [
|
|
CheckedPopupMenuItem<int>(
|
|
value: 0,
|
|
checked: !apkParams.includeSystemApps,
|
|
child: Text(t.apkPickerPage.excludeSystemApps),
|
|
),
|
|
CheckedPopupMenuItem<int>(
|
|
value: 1,
|
|
checked: apkParams.onlyAppsWithLaunchIntent,
|
|
child: Text(t.apkPickerPage.excludeAppsWithoutLaunchIntent),
|
|
),
|
|
];
|
|
},
|
|
onSelected: (value) {
|
|
switch (value) {
|
|
case 0:
|
|
ref.notifier(apkSearchParamProvider).setState((old) => old.copyWith(includeSystemApps: !old.includeSystemApps));
|
|
break;
|
|
case 1:
|
|
ref.notifier(apkSearchParamProvider).setState((old) => old.copyWith(onlyAppsWithLaunchIntent: !old.onlyAppsWithLaunchIntent));
|
|
break;
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: (_selectedApps.isEmpty)
|
|
? Container()
|
|
: FloatingActionButton.extended(
|
|
onPressed: () async => await _pickApps(_selectedApps),
|
|
label: Row(
|
|
children: [
|
|
const Icon(Icons.add),
|
|
const SizedBox(width: 5),
|
|
Text('Add ${_selectedApps.length} ${(_selectedApps.length == 1) ? "App" : "Apps"}'),
|
|
],
|
|
),
|
|
),
|
|
body: ResponsiveListView.single(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
tabletPadding: const EdgeInsets.symmetric(horizontal: 15),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
const SliverToBoxAdapter(
|
|
child: SizedBox(height: 10),
|
|
),
|
|
SliverPinnedHeader(
|
|
height: 80,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 10),
|
|
child: TextFormField(
|
|
controller: _textController,
|
|
autofocus: true,
|
|
onChanged: (s) {
|
|
ref.notifier(apkSearchParamProvider).setState((old) => old.copyWith(query: s));
|
|
setState(() {});
|
|
},
|
|
decoration: InputDecoration(
|
|
fillColor: ElevationOverlay.applySurfaceTint(
|
|
Theme.of(context).inputDecorationTheme.fillColor!,
|
|
Theme.of(context).colorScheme.surfaceTint,
|
|
3,
|
|
),
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: apkParams.query.isNotEmpty
|
|
? IconButton(
|
|
onPressed: () {
|
|
ref.notifier(apkSearchParamProvider).setState((old) => old.copyWith(query: ''));
|
|
_textController.clear();
|
|
},
|
|
icon: const Icon(Icons.clear),
|
|
)
|
|
: Text(apkParams.query),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: Row(
|
|
children: [
|
|
Text(t.apkPickerPage.apps(n: apkAsync.data?.length ?? 0)),
|
|
const Spacer(),
|
|
Row(
|
|
children: [
|
|
const Text('Select Multiple Apps'),
|
|
const SizedBox(width: 5),
|
|
Switch(
|
|
value: apkParams.selectMultipleApps,
|
|
onChanged: (bool newValue) {
|
|
setState(() {
|
|
apkParams.selectMultipleApps = !apkParams.selectMultipleApps;
|
|
});
|
|
},
|
|
activeTrackColor: Theme.of(context).colorScheme.primary,
|
|
activeThumbColor: Theme.of(context).colorScheme.onPrimary,
|
|
inactiveThumbColor: Theme.of(context).colorScheme.outline,
|
|
inactiveTrackColor: Theme.of(context).colorScheme.surface,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SliverToBoxAdapter(
|
|
child: SizedBox(height: 10),
|
|
),
|
|
apkAsync.when(
|
|
data: (appList) {
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
childCount: appList.length,
|
|
(context, index) {
|
|
final app = appList[index];
|
|
final thumbnail = (app as ApplicationWithIcon).icon;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: InkWell(
|
|
onTap: () async => (apkParams.selectMultipleApps) ? _appSelection(app) : _pickApp(app),
|
|
customBorder: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
MemoryThumbnail(
|
|
bytes: thumbnail,
|
|
size: 60,
|
|
fileType: FileType.apk,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
app.appName,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.fade,
|
|
softWrap: false,
|
|
),
|
|
Consumer(
|
|
builder: (context, ref) {
|
|
final appSize = ref.watch(apkSizeProvider(app.apkFilePath));
|
|
final appSizeString = appSize.maybeWhen(
|
|
data: (size) => '${size.asReadableFileSize} • ',
|
|
orElse: () => '',
|
|
);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'$appSizeString${app.versionName != null ? 'v${app.versionName}' : ''}',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
Text(
|
|
app.packageName,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (apkParams.selectMultipleApps)
|
|
Icon(
|
|
_selectedApps.contains(app) ? Icons.check_circle : Icons.radio_button_unchecked,
|
|
color: _selectedApps.contains(app) ? Theme.of(context).iconTheme.color : Colors.grey,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
error: (e, st) {
|
|
return SliverToBoxAdapter(child: Text('Error: $e\n$st'));
|
|
},
|
|
loading: () {
|
|
return const SliverToBoxAdapter(
|
|
child: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(height: getNavBarPadding(context) + 50),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|