Add system dark mode support
This commit is contained in:
@@ -4,7 +4,7 @@ Luban Imager 是一个基于 Flutter 的一键图片压缩 App,目标是把优
|
|||||||
|
|
||||||
## 截图
|
## 截图
|
||||||
|
|
||||||
<img src="docs/images/luban-imager-screenshot.jpg" alt="鲁班压图截图" width="720">
|
<img src="docs/images/luban-imager-screenshot.webp" alt="鲁班压图浅色与暗黑模式截图" width="960">
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ Luban Imager 是一个基于 Flutter 的一键图片压缩 App,目标是把优
|
|||||||
- 一键压缩:选择图片后自动执行 Luban 风格压缩,不需要手动填写质量、尺寸等参数。
|
- 一键压缩:选择图片后自动执行 Luban 风格压缩,不需要手动填写质量、尺寸等参数。
|
||||||
- 多入口导入:支持从 App 内选择图片,也支持从系统分享菜单接收图片。
|
- 多入口导入:支持从 App 内选择图片,也支持从系统分享菜单接收图片。
|
||||||
- 压缩结果预览:展示原图、压缩图、前后对比三种视图,并显示尺寸、文件大小和节省比例。
|
- 压缩结果预览:展示原图、压缩图、前后对比三种视图,并显示尺寸、文件大小和节省比例。
|
||||||
|
- 暗黑模式:支持跟随系统设置自动切换浅色/暗黑模式。
|
||||||
- 安全回退:如果压缩结果比原图更大,会自动使用原图,避免负优化。
|
- 安全回退:如果压缩结果比原图更大,会自动使用原图,避免负优化。
|
||||||
- 分享导出:压缩完成后可通过系统分享面板导出压缩图片。
|
- 分享导出:压缩完成后可通过系统分享面板导出压缩图片。
|
||||||
- 原位覆盖:对于系统授权可写入的图片来源,支持覆盖原图;覆盖前会进行两次确认。
|
- 原位覆盖:对于系统授权可写入的图片来源,支持覆盖原图;覆盖前会进行两次确认。
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 478 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 648 KiB |
+68
-23
@@ -49,25 +49,65 @@ class _LubanImagerAppState extends State<LubanImagerApp>
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: _appName,
|
title: _appName,
|
||||||
theme: ThemeData(
|
theme: AppTheme.light(),
|
||||||
useMaterial3: true,
|
darkTheme: AppTheme.dark(),
|
||||||
colorScheme: ColorScheme.fromSeed(
|
themeMode: ThemeMode.system,
|
||||||
seedColor: const Color(0xFF2F6F73),
|
|
||||||
brightness: Brightness.light,
|
|
||||||
),
|
|
||||||
scaffoldBackgroundColor: const Color(0xFFF7F8F6),
|
|
||||||
appBarTheme: const AppBarTheme(
|
|
||||||
centerTitle: false,
|
|
||||||
backgroundColor: Color(0xFFF7F8F6),
|
|
||||||
foregroundColor: Color(0xFF172122),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
home: ImagerHomePage(appName: _appName),
|
home: ImagerHomePage(appName: _appName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
static const _seedColor = Color(0xFF2F6F73);
|
||||||
|
static const _lightBackground = Color(0xFFF7F8F6);
|
||||||
|
static const _darkBackground = Color(0xFF101817);
|
||||||
|
|
||||||
|
static ThemeData light() {
|
||||||
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: _seedColor,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
);
|
||||||
|
return _build(
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
scaffoldBackgroundColor: _lightBackground,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData dark() {
|
||||||
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: _seedColor,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
return _build(
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
scaffoldBackgroundColor: _darkBackground,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData _build({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
|
required Color scaffoldBackgroundColor,
|
||||||
|
required SystemUiOverlayStyle systemOverlayStyle,
|
||||||
|
}) {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
scaffoldBackgroundColor: scaffoldBackgroundColor,
|
||||||
|
canvasColor: scaffoldBackgroundColor,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
centerTitle: false,
|
||||||
|
backgroundColor: scaffoldBackgroundColor,
|
||||||
|
foregroundColor: colorScheme.onSurface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
systemOverlayStyle: systemOverlayStyle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AppName {
|
class AppName {
|
||||||
static const english = 'Luban Imager';
|
static const english = 'Luban Imager';
|
||||||
static const chinese = '鲁班压图';
|
static const chinese = '鲁班压图';
|
||||||
@@ -529,16 +569,18 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
Widget _buildEmptyState() {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.image_search_outlined,
|
Icons.image_search_outlined,
|
||||||
size: 72,
|
size: 72,
|
||||||
color: Color(0xFF5D6F70),
|
color: colorScheme.outline,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
@@ -575,6 +617,7 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
|
|||||||
final compressed = job.compressed;
|
final compressed = job.compressed;
|
||||||
final canCompare = compressed != null;
|
final canCompare = compressed != null;
|
||||||
final mode = canCompare ? _previewMode : PreviewMode.original;
|
final mode = canCompare ? _previewMode : PreviewMode.original;
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@@ -691,12 +734,12 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFEDEFEA),
|
color: colorScheme.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
foregroundDecoration: BoxDecoration(
|
foregroundDecoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFDADFD8)),
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
child: _buildImagePreview(job, mode),
|
child: _buildImagePreview(job, mode),
|
||||||
),
|
),
|
||||||
@@ -964,12 +1007,14 @@ class _MetricBlock extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFDADFD8)),
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -978,9 +1023,9 @@ class _MetricBlock extends StatelessWidget {
|
|||||||
label,
|
label,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
context,
|
color: colorScheme.onSurfaceVariant,
|
||||||
).textTheme.labelMedium?.copyWith(color: const Color(0xFF5D6F70)),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 7),
|
const SizedBox(height: 7),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
Reference in New Issue
Block a user