Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-30 12:49:30 +08:00
parent 133f45945c
commit e892411092
27 changed files with 617 additions and 780 deletions

View File

@@ -74,65 +74,61 @@ class FloatingNavigationBar extends StatelessWidget {
final padding = MediaQuery.viewPaddingOf(context);
return UnconstrainedBox(
child: Padding(
padding: .fromLTRB(
padding.left,
0,
padding.right,
bottomPadding + padding.bottom,
),
child: SizedBox(
height: _kNavigationHeight,
width: destinations.length * _kIndicatorWidth,
child: DecoratedBox(
decoration: ShapeDecoration(
color: ElevationOverlay.applySurfaceTint(
backgroundColor ??
navigationBarTheme.backgroundColor ??
defaults.backgroundColor!,
surfaceTintColor ??
navigationBarTheme.surfaceTintColor ??
defaults.surfaceTintColor,
elevation ??
navigationBarTheme.elevation ??
defaults.elevation!,
),
shape: RoundedSuperellipseBorder(
side: defaults.borderSide,
borderRadius: _kBorderRadius,
),
return Padding(
padding: .fromLTRB(
padding.left,
0,
padding.right,
bottomPadding + padding.bottom,
),
child: SizedBox(
height: _kNavigationHeight,
width: destinations.length * _kIndicatorWidth,
child: DecoratedBox(
decoration: ShapeDecoration(
color: ElevationOverlay.applySurfaceTint(
backgroundColor ??
navigationBarTheme.backgroundColor ??
defaults.backgroundColor!,
surfaceTintColor ??
navigationBarTheme.surfaceTintColor ??
defaults.surfaceTintColor,
elevation ?? navigationBarTheme.elevation ?? defaults.elevation!,
),
child: Padding(
padding: _kIndicatorPadding,
child: Row(
crossAxisAlignment: .stretch,
children: <Widget>[
for (int i = 0; i < destinations.length; i++)
Expanded(
child: _SelectableAnimatedBuilder(
duration: animationDuration,
isSelected: i == selectedIndex,
builder: (context, animation) {
return _NavigationDestinationInfo(
index: i,
selectedIndex: selectedIndex,
totalNumberOfDestinations: destinations.length,
selectedAnimation: animation,
labelBehavior: effectiveLabelBehavior,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
overlayColor: overlayColor,
onTap: _handleTap(i),
labelTextStyle: labelTextStyle,
labelPadding: labelPadding,
child: destinations[i],
);
},
),
shape: RoundedSuperellipseBorder(
side: defaults.borderSide,
borderRadius: _kBorderRadius,
),
),
child: Padding(
padding: _kIndicatorPadding,
child: Row(
crossAxisAlignment: .stretch,
children: <Widget>[
for (int i = 0; i < destinations.length; i++)
Expanded(
child: _SelectableAnimatedBuilder(
duration: animationDuration,
isSelected: i == selectedIndex,
builder: (context, animation) {
return _NavigationDestinationInfo(
index: i,
selectedIndex: selectedIndex,
totalNumberOfDestinations: destinations.length,
selectedAnimation: animation,
labelBehavior: effectiveLabelBehavior,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
overlayColor: overlayColor,
onTap: _handleTap(i),
labelTextStyle: labelTextStyle,
labelPadding: labelPadding,
child: destinations[i],
);
},
),
],
),
),
],
),
),
),

View File

@@ -1,370 +0,0 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsRole;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart' hide TabBarView;
/// A page view that displays the widget which corresponds to the currently
/// selected tab.
///
/// This widget is typically used in conjunction with a [TabBar].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
///
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
///
/// The tab controller's [TabController.length] must equal the length of the
/// [children] list and the length of the [TabBar.tabs] list.
///
/// To see a sample implementation, visit the [TabController] documentation.
class CustomTabBarView extends StatefulWidget {
/// Creates a page view with one child per tab.
///
/// The length of [children] must be the same as the [controller]'s length.
const CustomTabBarView({
super.key,
required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge,
this.scrollDirection = Axis.horizontal,
});
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController? controller;
/// One widget per tab.
///
/// Its length must match the length of the [TabBar.tabs]
/// list, as well as the [controller]'s [TabController.length].
final List<Widget> children;
/// How the page view should respond to user input.
///
/// For example, determines how the page view continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// Defaults to matching platform conventions.
final ScrollPhysics? physics;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.widgets.pageview.viewportFraction}
final double viewportFraction;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
final Axis scrollDirection;
@override
State<CustomTabBarView> createState() => _CustomTabBarViewState();
}
class _CustomTabBarViewState extends State<CustomTabBarView> {
TabController? _controller;
PageController? _pageController;
late List<Widget> _childrenWithKey;
int? _currentIndex;
int _warpUnderwayCount = 0;
int _scrollUnderwayCount = 0;
bool _debugHasScheduledValidChildrenCountCheck = false;
// If the TabBarView is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
// null and should not be accessed.
bool get _controllerIsValid => _controller?.animation != null;
void _updateTabController() {
final TabController? newController =
widget.controller ?? DefaultTabController.maybeOf(context);
assert(() {
if (newController == null) {
throw FlutterError(
'No TabController for ${widget.runtimeType}.\n'
'When creating a ${widget.runtimeType}, you must either provide an explicit '
'TabController using the "controller" property, or you must ensure that there '
'is a DefaultTabController above the ${widget.runtimeType}.\n'
'In this case, there was neither an explicit controller nor a default controller.',
);
}
return true;
}());
if (newController == _controller) {
return;
}
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = newController;
if (_controller != null) {
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
}
}
void _jumpToPage(int page) {
_warpUnderwayCount += 1;
_pageController!.jumpToPage(page);
_warpUnderwayCount -= 1;
}
Future<void> _animateToPage(
int page, {
required Duration duration,
required Curve curve,
}) async {
_warpUnderwayCount += 1;
await _pageController!.animateToPage(
page,
duration: duration,
curve: curve,
);
_warpUnderwayCount -= 1;
}
@override
void initState() {
super.initState();
_updateChildren();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateTabController();
_currentIndex = _controller!.index;
if (_pageController == null) {
_pageController = PageController(
initialPage: _currentIndex!,
viewportFraction: widget.viewportFraction,
);
} else {
_pageController!.jumpToPage(_currentIndex!);
}
}
@override
void didUpdateWidget(CustomTabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_currentIndex = _controller!.index;
_jumpToPage(_currentIndex!);
}
if (widget.viewportFraction != oldWidget.viewportFraction) {
_pageController?.dispose();
_pageController = PageController(
initialPage: _currentIndex!,
viewportFraction: widget.viewportFraction,
);
}
// While a warp is under way, we stop updating the tab page contents.
// This is tracked in https://github.com/flutter/flutter/issues/31269.
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
_updateChildren();
}
}
@override
void dispose() {
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = null;
_pageController?.dispose();
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
void _updateChildren() {
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
widget.children.map<Widget>((Widget child) {
return Semantics(role: SemanticsRole.tabPanel, child: child);
}).toList(),
);
}
void _handleTabControllerAnimationTick() {
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
return;
} // This widget is driving the controller's animation.
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
_warpToCurrentIndex();
}
}
void _warpToCurrentIndex() {
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
return;
}
final bool adjacentDestination =
(_currentIndex! - _controller!.previousIndex).abs() == 1;
if (adjacentDestination) {
_warpToAdjacentTab(_controller!.animationDuration);
} else {
_warpToNonAdjacentTab(_controller!.animationDuration);
}
}
Future<void> _warpToAdjacentTab(Duration duration) async {
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(_updateChildren);
}
return Future<void>.value();
}
Future<void> _warpToNonAdjacentTab(Duration duration) async {
final int previousIndex = _controller!.previousIndex;
assert((_currentIndex! - previousIndex).abs() > 1);
// initialPage defines which page is shown when starting the animation.
// This page is adjacent to the destination page.
final int initialPage = _currentIndex! > previousIndex
? _currentIndex! - 1
: _currentIndex! + 1;
setState(() {
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
final Widget temp = _childrenWithKey[initialPage];
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
_childrenWithKey[previousIndex] = temp;
});
// Make a first jump to the adjacent page.
_jumpToPage(initialPage);
// Jump or animate to the destination page.
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(_updateChildren);
}
}
void _syncControllerOffset() {
_controller!.offset = clampDouble(
_pageController!.page! - _controller!.index,
-1.0,
1.0,
);
}
// Called when the PageView scrolls
bool _handleScrollNotification(ScrollNotification notification) {
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
return false;
}
if (notification.depth != 0) {
return false;
}
if (!_controllerIsValid) {
return false;
}
_scrollUnderwayCount += 1;
final double page = _pageController!.page!;
if (notification is ScrollUpdateNotification &&
!_controller!.indexIsChanging) {
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
if (pageChanged) {
_controller!.index = page.round();
_currentIndex = _controller!.index;
}
_syncControllerOffset();
} else if (notification is ScrollEndNotification) {
_controller!.index = page.round();
_currentIndex = _controller!.index;
if (!_controller!.indexIsChanging) {
_syncControllerOffset();
}
}
_scrollUnderwayCount -= 1;
return false;
}
bool _debugScheduleCheckHasValidChildrenCount() {
if (_debugHasScheduledValidChildrenCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidChildrenCountCheck = false;
if (!mounted) {
return;
}
assert(() {
if (_controller!.length != widget.children.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of children (${widget.children.length}) present in TabBarView's children property.",
);
}
return true;
}());
}, debugLabel: 'TabBarView.validChildrenCountCheck');
_debugHasScheduledValidChildrenCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(_debugScheduleCheckHasValidChildrenCount());
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView(
scrollDirection: widget.scrollDirection,
dragStartBehavior: widget.dragStartBehavior,
clipBehavior: widget.clipBehavior,
controller: _pageController,
physics: widget.physics == null
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
: const PageScrollPhysics().applyTo(widget.physics),
children: _childrenWithKey,
),
);
}
}

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
show BoxParentData, BoxHitTestResult, ChildLayoutHelper;
enum MainType { sideBar, body, bottomNav }
class MainLayout
extends SlottedMultiChildRenderObjectWidget<MainType, RenderBox> {
const MainLayout({
super.key,
required this.sideBar,
required this.bottomNav,
required this.body,
});
final Widget? sideBar;
final Widget? bottomNav;
final Widget body;
@override
Iterable<MainType> get slots => MainType.values;
@override
Widget? childForSlot(slot) => switch (slot) {
.sideBar => sideBar,
.bottomNav => bottomNav,
.body => body,
};
@override
SlottedContainerRenderObjectMixin<MainType, RenderBox> createRenderObject(
BuildContext context,
) {
return _RenderMainLayout();
}
}
class _RenderMainLayout extends RenderBox
with SlottedContainerRenderObjectMixin<MainType, RenderBox> {
Offset _getOffset(RenderBox child) {
return (child.parentData as BoxParentData).offset;
}
void _setOffset(RenderBox child, Offset offset) {
(child.parentData as BoxParentData).offset = offset;
}
@override
void performLayout() {
final constraints = this.constraints;
size = constraints.biggest;
final Offset bodyOffset;
final BoxConstraints bodyConstraints;
final sideBar = childForSlot(.sideBar);
if (sideBar != null) {
final sideBarWidth = ChildLayoutHelper.layoutChild(
sideBar,
BoxConstraints.tightFor(height: constraints.maxHeight),
).width;
_setOffset(sideBar, .zero);
bodyOffset = Offset(sideBarWidth, 0);
bodyConstraints = BoxConstraints.tightFor(
width: constraints.maxWidth - sideBarWidth,
height: constraints.maxHeight,
);
} else {
final bottomNav = childForSlot(.bottomNav);
if (bottomNav != null) {
final bottomNavSize = ChildLayoutHelper.layoutChild(
bottomNav,
constraints.loosen(),
);
_setOffset(
bottomNav,
Offset(
(constraints.maxWidth - bottomNavSize.width) / 2,
constraints.maxHeight - bottomNavSize.height,
),
);
}
bodyOffset = .zero;
bodyConstraints = BoxConstraints.tightFor(
width: constraints.maxWidth,
height: constraints.maxHeight,
);
}
final body = childForSlot(.body)!..layout(bodyConstraints);
_setOffset(body, bodyOffset);
}
@override
void paint(PaintingContext context, Offset offset) {
for (final child in children) {
context.paintChild(child, _getOffset(child) + offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
for (final type in MainType.values.reversed) {
final child = childForSlot(type);
if (child == null) continue;
final bool isHit = result.addWithPaintOffset(
offset: _getOffset(child),
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
return child.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
class SimpleAppBar extends StatelessWidget {
const SimpleAppBar({
super.key,
required this.height,
required this.brightness,
Brightness? statusBarIconBrightness,
this.backgroundColor = Colors.black,
}) : statusBarIconBrightness = statusBarIconBrightness ?? .light;
final double height;
final Brightness brightness;
final Brightness statusBarIconBrightness;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarBrightness: .light,
statusBarColor: Colors.transparent,
statusBarIconBrightness: statusBarIconBrightness,
systemStatusBarContrastEnforced: false,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: brightness.reverse,
),
child: ColoredBox(
color: backgroundColor,
child: SizedBox(height: height, width: .infinity),
),
);
}
}

View File

@@ -90,11 +90,11 @@ class VideoCardV extends StatelessWidget {
clipBehavior: Clip.none,
children: [
Card(
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: onPushDetail,
onLongPress: onLongPress,
onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress,
borderRadius: const .all(.circular(12)),
child: Column(
crossAxisAlignment: .start,
children: [
@@ -111,7 +111,7 @@ class VideoCardV extends StatelessWidget {
src: videoItem.cover,
width: maxWidth,
height: maxHeight,
type: .emote,
borderRadius: const .vertical(top: .circular(12)),
),
if (videoItem.duration > 0)
PBadge(