新增毛玻璃主题

This commit is contained in:
DelLevin-Home
2026-08-12 22:28:33 +08:00
parent be3299a0b3
commit ad6ab51afd
78 changed files with 672 additions and 237 deletions

View File

@@ -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(

View File

@@ -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(),
);

View 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,
);
},
);
}

View File

@@ -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,

View File

@@ -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 {
),
);
}
}
}

View File

@@ -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,

View File

@@ -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))),

View File

@@ -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,

View 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%
],
);
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;