generated from dellevin/template
新增毛玻璃主题
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import '../providers/app_provider.dart';
|
||||
import '../utils/user_prefs.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 新增记录弹窗 — 供底部导航栏和 NavigationRail 共用
|
||||
|
||||
@@ -98,7 +99,7 @@ void showAddSheet(BuildContext context, AppProvider provider) {
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
showDialog(
|
||||
appDialog(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final bc = Theme.of(ctx).colorScheme;
|
||||
@@ -121,7 +122,7 @@ void showAddSheet(BuildContext context, AppProvider provider) {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showModalBottomSheet(
|
||||
appModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/user_prefs.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 添加类型选择弹窗
|
||||
Future<void> showAddTypeDialog(BuildContext context) async {
|
||||
final result = await showDialog<int>(
|
||||
final result = await appDialog<int>(
|
||||
context: context,
|
||||
builder: (ctx) => const _AddTypeDialog(),
|
||||
);
|
||||
@@ -15,7 +16,7 @@ Future<void> showAddTypeDialog(BuildContext context) async {
|
||||
|
||||
/// 返回选中的类型索引 (0=影视, 1=阅读, 2=笔记, 3=游戏),取消返回 null
|
||||
Future<int?> showAddTypeSelector(BuildContext context) {
|
||||
return showDialog<int>(
|
||||
return appDialog<int>(
|
||||
context: context,
|
||||
builder: (ctx) => const _AddTypeDialog(),
|
||||
);
|
||||
|
||||
102
lib/widgets/app_overlay.dart
Normal file
102
lib/widgets/app_overlay.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
|
||||
/// 毛玻璃主题下的通用磨砂面板:真实背景模糊 + 半透明深色
|
||||
/// 仅在毛玻璃模式下生效,其余主题直接透传 child,不影响原样式
|
||||
class FrostedPanel extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final Color color;
|
||||
|
||||
const FrostedPanel({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
this.color = const Color(0xCC121418),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final frosted = context.watch<AppProvider>().frostedActive;
|
||||
if (!frosted) return child;
|
||||
return ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 22, sigmaY: 22),
|
||||
child: ColoredBox(color: color, child: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 毛玻璃版底部弹层:毛玻璃模式下自动加磨砂,其余主题与原 showModalBottomSheet 一致
|
||||
Future<T?> appModalBottomSheet<T>({
|
||||
required BuildContext context,
|
||||
Color? backgroundColor,
|
||||
Color? barrierColor,
|
||||
double? elevation,
|
||||
ShapeBorder? shape,
|
||||
bool isScrollControlled = false,
|
||||
bool isDismissible = true,
|
||||
bool enableDrag = true,
|
||||
bool useSafeArea = false,
|
||||
bool showDragHandle = false,
|
||||
BoxConstraints? constraints,
|
||||
bool useRootNavigator = false,
|
||||
required WidgetBuilder builder,
|
||||
}) {
|
||||
final frosted = context.read<AppProvider>().frostedActive;
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: frosted ? Colors.transparent : backgroundColor,
|
||||
barrierColor: barrierColor,
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
isScrollControlled: isScrollControlled,
|
||||
isDismissible: isDismissible,
|
||||
enableDrag: enableDrag,
|
||||
useSafeArea: useSafeArea,
|
||||
showDragHandle: showDragHandle,
|
||||
constraints: constraints,
|
||||
useRootNavigator: useRootNavigator,
|
||||
builder: (ctx) {
|
||||
final content = builder(ctx);
|
||||
if (!frosted) return content;
|
||||
return FrostedPanel(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 毛玻璃版对话框:毛玻璃模式下自动加磨砂
|
||||
Future<T?> appDialog<T>({
|
||||
required BuildContext context,
|
||||
bool barrierDismissible = true,
|
||||
Color? barrierColor,
|
||||
bool useSafeArea = true,
|
||||
RouteSettings? routeSettings,
|
||||
bool useRootNavigator = true,
|
||||
required WidgetBuilder builder,
|
||||
}) {
|
||||
final frosted = context.read<AppProvider>().frostedActive;
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierColor: barrierColor,
|
||||
useSafeArea: useSafeArea,
|
||||
routeSettings: routeSettings,
|
||||
useRootNavigator: useRootNavigator,
|
||||
builder: (ctx) {
|
||||
final content = builder(ctx);
|
||||
if (!frosted) return content;
|
||||
return FrostedPanel(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import '../providers/app_provider.dart';
|
||||
import '../widgets/fade_in_local_image.dart';
|
||||
import '../widgets/animated_star_rating.dart';
|
||||
import '../utils/toast_util.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 书籍列表项组件 - 网格布局设计
|
||||
class BookListItem extends StatelessWidget {
|
||||
@@ -73,7 +74,7 @@ class BookListItem extends StatelessWidget {
|
||||
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
appDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
@@ -10,9 +11,6 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Container(
|
||||
@@ -21,48 +19,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 56,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.15 : 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: -2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
colors: colors,
|
||||
icon: Icons.home_outlined,
|
||||
activeIcon: Icons.home,
|
||||
isActive: provider.bottomNavIndex == 0,
|
||||
onTap: () => provider.setBottomNavIndex(0),
|
||||
),
|
||||
_buildAddButton(context, provider),
|
||||
_buildNavItem(
|
||||
colors: colors,
|
||||
icon: Icons.person_outline,
|
||||
activeIcon: Icons.person,
|
||||
isActive: provider.bottomNavIndex == 2,
|
||||
onTap: () => provider.setBottomNavIndex(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildDock(context, provider: provider),
|
||||
SizedBox(height: bottomPadding + 8),
|
||||
],
|
||||
),
|
||||
@@ -71,6 +28,71 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Dock 悬浮导航条;毛玻璃模式下加真实模糊,保证可读
|
||||
Widget _buildDock(BuildContext context, {required AppProvider provider}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final isFrosted = provider.frostedActive;
|
||||
|
||||
final dock = Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.15 : 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
spreadRadius: -2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
colors: colors,
|
||||
icon: Icons.home_outlined,
|
||||
activeIcon: Icons.home,
|
||||
isActive: provider.bottomNavIndex == 0,
|
||||
onTap: () => provider.setBottomNavIndex(0),
|
||||
),
|
||||
_buildAddButton(context, provider),
|
||||
_buildNavItem(
|
||||
colors: colors,
|
||||
icon: Icons.person_outline,
|
||||
activeIcon: Icons.person,
|
||||
isActive: provider.bottomNavIndex == 2,
|
||||
onTap: () => provider.setBottomNavIndex(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 毛玻璃模式下:裁剪 + 模糊区域与胶囊同宽(外围留白),避免整行全宽模糊
|
||||
Widget result = dock;
|
||||
if (isFrosted) {
|
||||
result = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25),
|
||||
child: dock,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem({
|
||||
required ColorScheme colors,
|
||||
required IconData icon,
|
||||
@@ -116,4 +138,4 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../pages/character/character_form_page.dart';
|
||||
import 'fade_in_local_image.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 角色信息底部弹窗
|
||||
///
|
||||
@@ -26,7 +27,7 @@ class CharacterInfoSheet extends StatefulWidget {
|
||||
required String entityId,
|
||||
required dynamic character,
|
||||
}) {
|
||||
return showModalBottomSheet<bool>(
|
||||
return appModalBottomSheet<bool>(
|
||||
context: context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
isScrollControlled: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -27,6 +28,7 @@ import '../pages/profile/settings_page.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'fade_in_local_image.dart';
|
||||
import 'shimmer_skeleton.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 自定义侧边栏
|
||||
class CustomDrawer extends StatefulWidget {
|
||||
@@ -194,6 +196,18 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
);
|
||||
|
||||
if (widget.embedded) return content;
|
||||
final isFrosted = context.read<AppProvider>().frostedActive;
|
||||
if (isFrosted) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25),
|
||||
child: Container(color: colors.surfaceContainerHigh, child: content),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Drawer(
|
||||
backgroundColor: colors.surfaceContainerHigh,
|
||||
child: content,
|
||||
@@ -689,7 +703,7 @@ class _CustomDrawerState extends State<CustomDrawer> {
|
||||
final dayNotes = notes.where((n) => !n.isDeleted && DateTime(n.createdAt.year, n.createdAt.month, n.createdAt.day) == date).toList();
|
||||
final dayGames = games.where((g) => !g.isDeleted && DateTime(g.createdAt.year, g.createdAt.month, g.createdAt.day) == date).toList();
|
||||
|
||||
showModalBottomSheet(
|
||||
appModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 时长选择器(时:分两个滚轮的底部弹窗)
|
||||
/// 返回总分钟数,取消返回 null
|
||||
@@ -11,7 +12,7 @@ class DurationPicker {
|
||||
int initialMinutes = 0,
|
||||
String title = '影视总时长',
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
return appModalBottomSheet<int>(
|
||||
context: context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
isScrollControlled: true,
|
||||
|
||||
35
lib/widgets/frosted_background.dart
Normal file
35
lib/widgets/frosted_background.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 毛玻璃主题背景层:封面图 + 模糊 + 深色遮罩
|
||||
/// 用 ImageFiltered 模糊静态图片(开销低于 BackdropFilter 实时模糊)
|
||||
class FrostedBackground extends StatelessWidget {
|
||||
final String? coverPath;
|
||||
|
||||
const FrostedBackground({super.key, this.coverPath});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (coverPath == null || coverPath!.isEmpty) {
|
||||
// 无封面兜底:纯色玻璃底
|
||||
return const ColoredBox(color: Color(0xFF121418));
|
||||
}
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ImageFiltered(
|
||||
imageFilter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25),
|
||||
child: Image.file(
|
||||
File(coverPath!),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const ColoredBox(color: Color(0xFF121418)),
|
||||
),
|
||||
),
|
||||
// 深色遮罩,保证内容可读
|
||||
const ColoredBox(color: Color(0x73000000)), // 黑 ~45%
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import '../providers/app_provider.dart';
|
||||
import '../widgets/fade_in_local_image.dart';
|
||||
import '../widgets/animated_star_rating.dart';
|
||||
import '../utils/toast_util.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 游戏列表项组件 - 网格布局设计
|
||||
class GameListItem extends StatelessWidget {
|
||||
@@ -73,7 +74,7 @@ class GameListItem extends StatelessWidget {
|
||||
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
appDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 类型/标签选择右侧弹窗(影视类型、导演、编剧、主演、书籍类型通用)
|
||||
class GenreSelectorPage extends StatefulWidget {
|
||||
@@ -109,7 +110,7 @@ class _GenreSelectorPageState extends State<GenreSelectorPage> {
|
||||
|
||||
void _editItem(int index, String oldValue) {
|
||||
final editController = TextEditingController(text: oldValue);
|
||||
showDialog<String>(
|
||||
appDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../providers/app_provider.dart';
|
||||
import '../widgets/fade_in_local_image.dart';
|
||||
import '../widgets/animated_star_rating.dart';
|
||||
import '../utils/toast_util.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 观影列表项组件 - 网格布局设计
|
||||
class MovieListItem extends StatelessWidget {
|
||||
@@ -73,7 +74,7 @@ class MovieListItem extends StatelessWidget {
|
||||
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
appDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'fade_in_local_image.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 笔记列表项组件 - 卡片式设计,内容展示在卡片内
|
||||
class NoteListItem extends StatelessWidget {
|
||||
@@ -189,7 +190,7 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
|
||||
void _showActions(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
appModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
@@ -272,7 +273,7 @@ class _NoteListItemContent extends StatelessWidget {
|
||||
|
||||
void _showDeleteConfirm(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
appDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../pages/people/person_detail_page.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import 'fade_in_local_image.dart';
|
||||
import 'person_avatar.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 人物信息浮动面板(底部 ModalBottomSheet)
|
||||
/// 展示人物基本信息 + 关联作品,点击「查看全部」跳转到原详情页
|
||||
@@ -14,7 +15,7 @@ class PersonInfoSheet extends StatefulWidget {
|
||||
const PersonInfoSheet({super.key, required this.person});
|
||||
|
||||
static Future<void> show(BuildContext context, Person person) {
|
||||
return showModalBottomSheet(
|
||||
return appModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
isScrollControlled: true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:uuid/uuid.dart';
|
||||
import '../models/data_models.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import 'fade_in_local_image.dart';
|
||||
import '../widgets/app_overlay.dart';
|
||||
|
||||
/// 人物详情页使用的作品关联结果(按媒体类型分组)
|
||||
class WorkSelectionResult {
|
||||
@@ -382,7 +383,7 @@ class _WorkSelectorPageState extends State<WorkSelectorPage> {
|
||||
final title = isVoiceActor ? '配音角色' : '饰演角色';
|
||||
final hint = isVoiceActor ? '如:米老鼠' : '如:关羽';
|
||||
final ctrl = TextEditingController(text: current ?? '');
|
||||
showDialog(
|
||||
appDialog(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
|
||||
Reference in New Issue
Block a user