improve auto scroll behaviour
This commit is contained in:
+223
-261
@@ -42,7 +42,6 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/ta
|
|||||||
import 'package:weblibre/presentation/widgets/share_tile.dart';
|
import 'package:weblibre/presentation/widgets/share_tile.dart';
|
||||||
import 'package:weblibre/presentation/widgets/website_feed_tile.dart';
|
import 'package:weblibre/presentation/widgets/website_feed_tile.dart';
|
||||||
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
|
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
|
||||||
import 'package:weblibre/utils/debouncer.dart';
|
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
|
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
|
||||||
@@ -82,54 +81,35 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
).select((value) => value.valueOrNull ?? const []),
|
).select((value) => value.valueOrNull ?? const []),
|
||||||
);
|
);
|
||||||
|
|
||||||
final initialScrollDebouncer = useMemoized(
|
final scrolledTo = useRef(0.0);
|
||||||
() => Debouncer(const Duration(milliseconds: 150)),
|
|
||||||
);
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (!initialScrollDebouncer.hasRan) {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
initialScrollDebouncer.eventOccured(() {
|
final header = headerKey.currentContext?.findRenderObject();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
final text = textFieldKey.currentContext?.findRenderObject();
|
||||||
final header = headerKey.currentContext?.findRenderObject();
|
|
||||||
final text = textFieldKey.currentContext?.findRenderObject();
|
|
||||||
|
|
||||||
if (header case final RenderBox headerBox) {
|
if (header case final RenderBox headerBox) {
|
||||||
if (text case final RenderBox textBox) {
|
if (text case final RenderBox textBox) {
|
||||||
final totalHeight =
|
final totalHeight =
|
||||||
headerBox.size.height +
|
headerBox.size.height + textBox.size.height + kToolbarHeight;
|
||||||
textBox.size.height +
|
|
||||||
kToolbarHeight;
|
|
||||||
|
|
||||||
final relative =
|
final relative = totalHeight / MediaQuery.of(context).size.height;
|
||||||
totalHeight / MediaQuery.of(context).size.height;
|
|
||||||
|
|
||||||
draggableScrollableController.jumpTo(relative);
|
if (draggableScrollableController.size < relative &&
|
||||||
}
|
relative > scrolledTo.value) {
|
||||||
|
await draggableScrollableController.animateTo(
|
||||||
|
relative,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
curve: Curves.bounceIn,
|
||||||
|
);
|
||||||
|
scrolledTo.value = relative;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// final changes = useRef(0.0);
|
|
||||||
// useEffect(() {
|
|
||||||
// final emitter = KeyboardHeightEmitter();
|
|
||||||
|
|
||||||
// emitter.onKeyboardHeightChanged((height) {
|
|
||||||
// final diff =
|
|
||||||
// height / MediaQuery.of(context).size.height - changes.value;
|
|
||||||
|
|
||||||
// draggableScrollableController.jumpTo(
|
|
||||||
// draggableScrollableController.size + diff,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// changes.value += diff;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return () => emitter.dispose();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
final changes = useRef(0.0);
|
final changes = useRef(0.0);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -148,231 +128,213 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [MediaQuery.of(context).viewInsets.bottom]);
|
}, [MediaQuery.of(context).viewInsets.bottom]);
|
||||||
|
|
||||||
return Column(
|
return NestedScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
children: [
|
SliverToBoxAdapter(
|
||||||
DraggableScrollableHeader(
|
child: DraggableScrollableHeader(
|
||||||
key: headerKey,
|
key: headerKey,
|
||||||
controller: draggableScrollableController,
|
controller: draggableScrollableController,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
draggableScrollableController.jumpTo(1.0);
|
draggableScrollableController.jumpTo(1.0);
|
||||||
},
|
},
|
||||||
child: WebsiteTitleTile(initialTabState),
|
child: WebsiteTitleTile(initialTabState),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
CertificateTile(),
|
||||||
CertificateTile(),
|
const Divider(),
|
||||||
const Divider(),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: FadingScroll(
|
|
||||||
fadingSize: 25,
|
|
||||||
controller: sheetScrollController,
|
|
||||||
builder: (context, controller) {
|
|
||||||
return ListView(
|
|
||||||
controller: controller,
|
|
||||||
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
child: SiteSearch(
|
|
||||||
key: textFieldKey,
|
|
||||||
domain: initialTabState.url.host,
|
|
||||||
availableBangs: availableBangs,
|
|
||||||
initialText: initialTabState.url.toString(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MdiIcons.contentCopy),
|
|
||||||
title: const Text('Copy address'),
|
|
||||||
onTap: () async {
|
|
||||||
await Clipboard.setData(
|
|
||||||
ClipboardData(text: initialTabState.url.toString()),
|
|
||||||
);
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
onTap: () async {
|
|
||||||
await ui_helper.launchUrlFeedback(
|
|
||||||
context,
|
|
||||||
initialTabState.url,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
leading: const Icon(Icons.open_in_browser),
|
|
||||||
title: const Text('Launch External'),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MdiIcons.tabPlus),
|
|
||||||
title: const Text('Clone tab'),
|
|
||||||
onTap: () async {
|
|
||||||
final tabId = await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.addTab(url: initialTabState.url);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
//save reference before pop `ref` gets disposed
|
|
||||||
final repo = ref.read(tabRepositoryProvider.notifier);
|
|
||||||
|
|
||||||
ui_helper.showTabSwitchMessage(
|
|
||||||
context,
|
|
||||||
onSwitch: () async {
|
|
||||||
await repo.selectTab(tabId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MdiIcons.tabUnselected),
|
|
||||||
title: const Text('Clone as private tab'),
|
|
||||||
onTap: () async {
|
|
||||||
final tabId = await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.addTab(url: initialTabState.url, private: true);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
//save reference before pop `ref` gets disposed
|
|
||||||
final repo = ref.read(tabRepositoryProvider.notifier);
|
|
||||||
|
|
||||||
ui_helper.showTabSwitchMessage(
|
|
||||||
context,
|
|
||||||
onSwitch: () async {
|
|
||||||
await repo.selectTab(tabId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MdiIcons.folderArrowUpDownOutline),
|
|
||||||
title: const Text('Assign container'),
|
|
||||||
onTap: () async {
|
|
||||||
final targetContainerId = await ContainerSelectionRoute()
|
|
||||||
.push<String?>(context);
|
|
||||||
|
|
||||||
if (targetContainerId != null) {
|
|
||||||
final containerData = await ref
|
|
||||||
.read(containerRepositoryProvider.notifier)
|
|
||||||
.getContainerData(targetContainerId);
|
|
||||||
|
|
||||||
if (containerData != null) {
|
|
||||||
await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.assignContainer(
|
|
||||||
initialTabState.id,
|
|
||||||
containerData,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Consumer(
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(MdiIcons.folderCancelOutline),
|
|
||||||
title: const Text('Unassign container'),
|
|
||||||
onTap: () async {
|
|
||||||
await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.unassignContainer(initialTabState.id);
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final containerId = ref.watch(
|
|
||||||
watchContainerTabIdProvider(
|
|
||||||
initialTabState.id,
|
|
||||||
).select((value) => value.valueOrNull),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Visibility(
|
|
||||||
visible: containerId != null,
|
|
||||||
child: child!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ShareTile(
|
|
||||||
onTap: () async {
|
|
||||||
await SharePlus.instance.share(
|
|
||||||
ShareParams(uri: initialTabState.url),
|
|
||||||
);
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
},
|
|
||||||
onTapQr: () async {
|
|
||||||
await showQrCode(context, initialTabState.url.toString());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.mobile_screen_share),
|
|
||||||
title: const Text('Share screenshot'),
|
|
||||||
onTap: () async {
|
|
||||||
final screenshot = await ref
|
|
||||||
.read(selectedTabSessionNotifierProvider)
|
|
||||||
.requestScreenshot();
|
|
||||||
|
|
||||||
if (screenshot != null) {
|
|
||||||
ui.decodeImageFromList(screenshot, (result) async {
|
|
||||||
final png = await result.toByteData(
|
|
||||||
format: ui.ImageByteFormat.png,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (png != null) {
|
|
||||||
final file = XFile.fromData(
|
|
||||||
png.buffer.asUint8List(),
|
|
||||||
mimeType: 'image/png',
|
|
||||||
);
|
|
||||||
|
|
||||||
await SharePlus.instance.share(
|
|
||||||
ShareParams(
|
|
||||||
files: [file],
|
|
||||||
subject: initialTabState.title,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
WebsiteFeedTile(initialTabState),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
body: FadingScroll(
|
||||||
|
fadingSize: 15,
|
||||||
|
controller: sheetScrollController,
|
||||||
|
builder: (context, controller) {
|
||||||
|
return ListView(
|
||||||
|
controller: controller,
|
||||||
|
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: SiteSearch(
|
||||||
|
key: textFieldKey,
|
||||||
|
domain: initialTabState.url.host,
|
||||||
|
availableBangs: availableBangs,
|
||||||
|
initialText: initialTabState.url.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.contentCopy),
|
||||||
|
title: const Text('Copy address'),
|
||||||
|
onTap: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(text: initialTabState.url.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
onTap: () async {
|
||||||
|
await ui_helper.launchUrlFeedback(
|
||||||
|
context,
|
||||||
|
initialTabState.url,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: const Icon(Icons.open_in_browser),
|
||||||
|
title: const Text('Launch External'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.tabPlus),
|
||||||
|
title: const Text('Clone tab'),
|
||||||
|
onTap: () async {
|
||||||
|
final tabId = await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(url: initialTabState.url);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
//save reference before pop `ref` gets disposed
|
||||||
|
final repo = ref.read(tabRepositoryProvider.notifier);
|
||||||
|
|
||||||
|
ui_helper.showTabSwitchMessage(
|
||||||
|
context,
|
||||||
|
onSwitch: () async {
|
||||||
|
await repo.selectTab(tabId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.tabUnselected),
|
||||||
|
title: const Text('Clone as private tab'),
|
||||||
|
onTap: () async {
|
||||||
|
final tabId = await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(url: initialTabState.url, private: true);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
//save reference before pop `ref` gets disposed
|
||||||
|
final repo = ref.read(tabRepositoryProvider.notifier);
|
||||||
|
|
||||||
|
ui_helper.showTabSwitchMessage(
|
||||||
|
context,
|
||||||
|
onSwitch: () async {
|
||||||
|
await repo.selectTab(tabId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.folderArrowUpDownOutline),
|
||||||
|
title: const Text('Assign container'),
|
||||||
|
onTap: () async {
|
||||||
|
final targetContainerId = await ContainerSelectionRoute()
|
||||||
|
.push<String?>(context);
|
||||||
|
|
||||||
|
if (targetContainerId != null) {
|
||||||
|
final containerData = await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.getContainerData(targetContainerId);
|
||||||
|
|
||||||
|
if (containerData != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.assignContainer(initialTabState.id, containerData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Consumer(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(MdiIcons.folderCancelOutline),
|
||||||
|
title: const Text('Unassign container'),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.unassignContainer(initialTabState.id);
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final containerId = ref.watch(
|
||||||
|
watchContainerTabIdProvider(
|
||||||
|
initialTabState.id,
|
||||||
|
).select((value) => value.valueOrNull),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Visibility(
|
||||||
|
visible: containerId != null,
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ShareTile(
|
||||||
|
onTap: () async {
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(uri: initialTabState.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
},
|
||||||
|
onTapQr: () async {
|
||||||
|
await showQrCode(context, initialTabState.url.toString());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.mobile_screen_share),
|
||||||
|
title: const Text('Share screenshot'),
|
||||||
|
onTap: () async {
|
||||||
|
final screenshot = await ref
|
||||||
|
.read(selectedTabSessionNotifierProvider)
|
||||||
|
.requestScreenshot();
|
||||||
|
|
||||||
|
if (screenshot != null) {
|
||||||
|
ui.decodeImageFromList(screenshot, (result) async {
|
||||||
|
final png = await result.toByteData(
|
||||||
|
format: ui.ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (png != null) {
|
||||||
|
final file = XFile.fromData(
|
||||||
|
png.buffer.asUint8List(),
|
||||||
|
mimeType: 'image/png',
|
||||||
|
);
|
||||||
|
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
files: [file],
|
||||||
|
subject: initialTabState.title,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).dismiss();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
WebsiteFeedTile(initialTabState),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user