mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
feat(mobile): adjust padding between buttons in send tab to indicate that it's scrollable (#1660)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
## 1.15.4 (unreleased)
|
||||
|
||||
- feat: show tooltip on the "Scan" button (@Tienisto)
|
||||
- feat(mobile): adjust padding between buttons in send tab to indicate that it's scrollable (@Tienisto)
|
||||
- feat(windows): title bar color should match the system theme (@FutoTan)
|
||||
- fix(desktop): multiple monitor setup may cause the window to be invisible at app start (@Tienisto)
|
||||
- i18n: distinguish between "Exit" and "Quit" depending on the platform (@sergd88)
|
||||
|
||||
@@ -24,9 +24,11 @@ import 'package:localsend_app/widget/custom_icon_button.dart';
|
||||
import 'package:localsend_app/widget/dialogs/add_file_dialog.dart';
|
||||
import 'package:localsend_app/widget/dialogs/send_mode_help_dialog.dart';
|
||||
import 'package:localsend_app/widget/file_thumbnail.dart';
|
||||
import 'package:localsend_app/widget/horizontal_clip_list_view.dart';
|
||||
import 'package:localsend_app/widget/list_tile/device_list_tile.dart';
|
||||
import 'package:localsend_app/widget/list_tile/device_placeholder_list_tile.dart';
|
||||
import 'package:localsend_app/widget/opacity_slideshow.dart';
|
||||
import 'package:localsend_app/widget/responsive_builder.dart';
|
||||
import 'package:localsend_app/widget/responsive_list_view.dart';
|
||||
import 'package:localsend_app/widget/rotating_widget.dart';
|
||||
import 'package:refena_flutter/refena_flutter.dart';
|
||||
@@ -44,6 +46,8 @@ class SendTab extends StatelessWidget {
|
||||
provider: sendTabVmProvider,
|
||||
init: (context) => context.global.dispatchAsync(SendTabInitAction(context)), // ignore: discarded_futures
|
||||
builder: (context, vm) {
|
||||
final sizingInformation = SizingInformation(MediaQuery.sizeOf(context).width);
|
||||
final buttonWidth = sizingInformation.isDesktop ? BigButton.desktopWidth : BigButton.mobileWidth;
|
||||
final ref = context.ref;
|
||||
return ResponsiveListView(
|
||||
padding: EdgeInsets.zero,
|
||||
@@ -57,28 +61,22 @@ class SendTab extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
..._options.map((option) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 5),
|
||||
child: BigButton(
|
||||
icon: option.icon,
|
||||
label: option.label,
|
||||
filled: false,
|
||||
onTap: () async => ref.global.dispatchAsync(PickFileAction(
|
||||
option: option,
|
||||
context: context,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
HorizontalClipListView(
|
||||
outerHorizontalPadding: 15,
|
||||
outerVerticalPadding: 10,
|
||||
minPadding: 10,
|
||||
childWidth: buttonWidth,
|
||||
children: _options.map((option) {
|
||||
return BigButton(
|
||||
icon: option.icon,
|
||||
label: option.label,
|
||||
filled: false,
|
||||
onTap: () async => ref.global.dispatchAsync(PickFileAction(
|
||||
option: option,
|
||||
context: context,
|
||||
)),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
] else ...[
|
||||
Card(
|
||||
|
||||
@@ -3,6 +3,9 @@ import 'package:localsend_app/config/theme.dart';
|
||||
import 'package:localsend_app/widget/responsive_builder.dart';
|
||||
|
||||
class BigButton extends StatelessWidget {
|
||||
static const double desktopWidth = 100.0;
|
||||
static const double mobileWidth = 90.0;
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool filled;
|
||||
@@ -19,15 +22,10 @@ class BigButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final sizingInformation = SizingInformation(MediaQuery.sizeOf(context).width);
|
||||
final buttonWidth = sizingInformation.isDesktop ? 100.0 : 90.0;
|
||||
const buttonHeight = 65.0;
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: buttonWidth,
|
||||
minWidth: buttonWidth,
|
||||
minHeight: buttonHeight,
|
||||
maxHeight: buttonHeight,
|
||||
),
|
||||
final buttonWidth = sizingInformation.isDesktop ? desktopWidth : mobileWidth;
|
||||
return SizedBox(
|
||||
width: buttonWidth,
|
||||
height: 65.0,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: filled ? colorScheme.primary : colorScheme.secondaryContainerIfDark,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A horizontal list that adjusts the padding if the screen is too small.
|
||||
/// In this case, the padding increases until half of the next button is visible.
|
||||
/// This is useful to communicate to the user that there are more buttons to the right.
|
||||
class HorizontalClipListView extends StatelessWidget {
|
||||
final double outerHorizontalPadding;
|
||||
final double outerVerticalPadding;
|
||||
final double minPadding;
|
||||
final double childWidth;
|
||||
final List<Widget> children;
|
||||
|
||||
const HorizontalClipListView({
|
||||
super.key,
|
||||
required this.outerHorizontalPadding,
|
||||
required this.outerVerticalPadding,
|
||||
required this.minPadding,
|
||||
required this.childWidth,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth - outerHorizontalPadding;
|
||||
final requiredWidth = childWidth * (children.length - 1) + childWidth * 0.2 + minPadding * (children.length - 1);
|
||||
final padding = switch (requiredWidth <= availableWidth) {
|
||||
true => minPadding,
|
||||
false => _calcPadding(
|
||||
availableWidth: availableWidth,
|
||||
childWidth: childWidth,
|
||||
childrenCount: children.length,
|
||||
minPadding: minPadding,
|
||||
),
|
||||
};
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
key: ValueKey(padding),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: outerHorizontalPadding,
|
||||
vertical: outerVerticalPadding,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (int i = 0; i < children.length; i++)
|
||||
i == children.length - 1
|
||||
? children[i]
|
||||
: Padding(
|
||||
key: ValueKey(i),
|
||||
padding: EdgeInsets.only(right: padding),
|
||||
child: children[i],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double _calcPadding({
|
||||
required double availableWidth,
|
||||
required double childWidth,
|
||||
required int childrenCount,
|
||||
required double minPadding,
|
||||
}) {
|
||||
final possiblePaddings = const [0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].map((percentage) => _calcPaddingFormula(
|
||||
availableWidth: availableWidth,
|
||||
childWidth: childWidth,
|
||||
childrenCount: childrenCount,
|
||||
minPadding: minPadding,
|
||||
clipPercentage: percentage,
|
||||
));
|
||||
|
||||
return max(minPadding, possiblePaddings.reduce(min));
|
||||
}
|
||||
|
||||
double _calcPaddingFormula({
|
||||
required double availableWidth,
|
||||
required double childWidth,
|
||||
required int childrenCount,
|
||||
required double minPadding,
|
||||
required double clipPercentage,
|
||||
}) {
|
||||
int visibleChildren = 0;
|
||||
for (int i = 1; i <= childrenCount; i++) {
|
||||
if (childWidth * i + minPadding * (i - 1) <= availableWidth + childWidth * clipPercentage) {
|
||||
visibleChildren++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final padding = (availableWidth + (childWidth * clipPercentage) - childWidth * visibleChildren) / (visibleChildren - 1);
|
||||
|
||||
return padding;
|
||||
}
|
||||
Reference in New Issue
Block a user