196 lines
5.5 KiB
Dart
196 lines
5.5 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/app_colors.dart';
|
|
|
|
const double _height = 52;
|
|
const double _heightWithSubtitle = 64;
|
|
const double _horizontal = 14;
|
|
const double _backSize = 40;
|
|
const double _backGap = 12;
|
|
const double _titleFontSize = 18;
|
|
const double _subtitleFontSize = 12;
|
|
const double _actionGap = 8;
|
|
const double _iconSize = 22;
|
|
const double _iconWeight = 600;
|
|
const double _borderWidth = 1;
|
|
const double _shadowBlur = 8;
|
|
const Offset _lightShadowOffset = Offset(0, 2);
|
|
|
|
/// 二级页面统一圆形操作按钮,返回、分享和更多共用同一尺寸与表面样式。
|
|
class AppSubpageCircleButton extends StatelessWidget {
|
|
const AppSubpageCircleButton({
|
|
super.key,
|
|
required this.buttonKey,
|
|
required this.tooltip,
|
|
required this.icon,
|
|
required this.onPressed,
|
|
this.dark = false,
|
|
});
|
|
|
|
final Key buttonKey;
|
|
final String tooltip;
|
|
final IconData icon;
|
|
final VoidCallback onPressed;
|
|
final bool dark;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
key: buttonKey,
|
|
width: _backSize,
|
|
height: _backSize,
|
|
child: Tooltip(
|
|
message: tooltip,
|
|
child: DecoratedBox(
|
|
key: const Key('subpage-back-shadow'),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: dark ? AppColors.shadowSoft : AppColors.shadowSubtle,
|
|
blurRadius: _shadowBlur,
|
|
offset: dark ? Offset.zero : _lightShadowOffset,
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: dark ? AppColors.headerScrim : AppColors.card,
|
|
shape: CircleBorder(
|
|
side: dark
|
|
? BorderSide.none
|
|
: const BorderSide(
|
|
color: AppColors.border,
|
|
width: _borderWidth,
|
|
),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: InkWell(
|
|
customBorder: const CircleBorder(),
|
|
onTap: onPressed,
|
|
child: Center(
|
|
child: Icon(
|
|
icon,
|
|
color: AppColors.onSurface,
|
|
size: _iconSize,
|
|
weight: _iconWeight,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 二级页面统一返回按钮,浅色页使用白底描边,深色页使用半透明黑底。
|
|
class AppSubpageBackButton extends StatelessWidget {
|
|
const AppSubpageBackButton({
|
|
super.key,
|
|
required this.backKey,
|
|
this.dark = false,
|
|
this.onBack,
|
|
});
|
|
|
|
final Key backKey;
|
|
final bool dark;
|
|
final VoidCallback? onBack;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AppSubpageCircleButton(
|
|
buttonKey: backKey,
|
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
|
icon: Icons.chevron_left_rounded,
|
|
dark: dark,
|
|
onPressed: onBack ?? () => Navigator.of(context).maybePop(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 二级页面统一页头:返回按钮、左对齐标题、可选副标题与右侧操作。
|
|
class AppSubpageHeader extends StatelessWidget {
|
|
const AppSubpageHeader({
|
|
super.key,
|
|
required this.title,
|
|
required this.backKey,
|
|
this.subtitle,
|
|
this.actions,
|
|
this.onBack,
|
|
this.dark = false,
|
|
this.showBack = true,
|
|
});
|
|
|
|
final String title;
|
|
final Key backKey;
|
|
final String? subtitle;
|
|
final Widget? actions;
|
|
final VoidCallback? onBack;
|
|
final bool dark;
|
|
final bool showBack;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final foreground = AppColors.onSurface;
|
|
final secondary = AppColors.muted;
|
|
return SizedBox(
|
|
key: const Key('subpage-header'),
|
|
height: subtitle == null ? _height : _heightWithSubtitle,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: _horizontal),
|
|
child: Row(
|
|
children: [
|
|
if (showBack) ...[
|
|
AppSubpageBackButton(
|
|
backKey: backKey,
|
|
dark: dark,
|
|
onBack: onBack,
|
|
),
|
|
const SizedBox(width: _backGap),
|
|
],
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
key: const Key('subpage-title'),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: foreground,
|
|
fontSize: _titleFontSize,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
if (subtitle != null) ...[
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle!,
|
|
key: const Key('subpage-subtitle'),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: secondary,
|
|
fontSize: _subtitleFontSize,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
if (actions != null) ...[
|
|
const SizedBox(width: _actionGap),
|
|
actions!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|