初始化
This commit is contained in:
381
lib/main.dart
Normal file
381
lib/main.dart
Normal file
@@ -0,0 +1,381 @@
|
||||
// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables
|
||||
|
||||
import 'package:english_words/english_words.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
// 创建并提供应用状态实例
|
||||
create: (context) => MyAppState(),
|
||||
child: MaterialApp(
|
||||
title: 'HeurAMS - Flutter Shell',
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color.fromARGB(255, 22, 96, 165)),
|
||||
),
|
||||
home: MyHomePage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyAppState extends ChangeNotifier {
|
||||
// 当前显示的单词对
|
||||
var current = WordPair.random();
|
||||
// 历史记录列表
|
||||
var history = <WordPair>[];
|
||||
|
||||
// 用于控制历史列表动画的全局键
|
||||
GlobalKey? historyListKey;
|
||||
|
||||
// 获取下一个单词对
|
||||
void getNext() {
|
||||
// 将当前单词添加到历史记录开头
|
||||
history.insert(0, current);
|
||||
// 获取动画列表状态并插入新项(触发动画)
|
||||
var animatedList = historyListKey?.currentState as AnimatedListState?;
|
||||
animatedList?.insertItem(0);
|
||||
// 生成新的随机单词对
|
||||
current = WordPair.random();
|
||||
// 通知所有监听者状态已改变
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 收藏夹列表
|
||||
var favorites = <WordPair>[];
|
||||
|
||||
// 切换收藏状态
|
||||
void toggleFavorite([WordPair? pair]) {
|
||||
pair = pair ?? current;
|
||||
if (favorites.contains(pair)) {
|
||||
// 如果已收藏,则移除
|
||||
favorites.remove(pair);
|
||||
} else {
|
||||
// 如果未收藏,则添加
|
||||
favorites.add(pair);
|
||||
}
|
||||
// 通知所有监听者状态已改变
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 移除收藏
|
||||
void removeFavorite(WordPair pair) {
|
||||
favorites.remove(pair);
|
||||
// 通知所有监听者状态已改变
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
// 当前选中的页面索引(0:生成器,1:收藏夹)
|
||||
var selectedIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
Widget page;
|
||||
// 根据选中的索引决定显示哪个页面
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
page = GeneratorPage();
|
||||
break;
|
||||
case 1:
|
||||
page = FavoritesPage();
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError('没有为 $selectedIndex 实现的组件');
|
||||
}
|
||||
|
||||
// 当前页面的容器,带有背景色和 subtle 切换动画
|
||||
var mainArea = ColoredBox(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 200),
|
||||
child: page,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 窄屏设备(如手机)使用底部导航栏
|
||||
if (constraints.maxWidth < 450) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: mainArea),
|
||||
SafeArea(
|
||||
child: BottomNavigationBar(
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: '主页',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.favorite),
|
||||
label: '收藏夹',
|
||||
),
|
||||
],
|
||||
currentIndex: selectedIndex,
|
||||
onTap: (value) {
|
||||
setState(() {
|
||||
selectedIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// 宽屏设备(如平板、桌面)使用左侧导航栏
|
||||
return Row(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: NavigationRail(
|
||||
extended: constraints.maxWidth >= 600,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.home),
|
||||
label: Text('主页'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.favorite),
|
||||
label: Text('收藏夹'),
|
||||
),
|
||||
],
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: (value) {
|
||||
setState(() {
|
||||
selectedIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(child: mainArea),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratorPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 监听应用状态变化
|
||||
var appState = context.watch<MyAppState>();
|
||||
var pair = appState.current;
|
||||
|
||||
IconData icon;
|
||||
// 根据当前单词是否被收藏决定显示哪个图标
|
||||
if (appState.favorites.contains(pair)) {
|
||||
icon = Icons.favorite;
|
||||
} else {
|
||||
icon = Icons.favorite_border;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: HistoryListView(),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
BigCard(pair: pair),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
appState.toggleFavorite();
|
||||
},
|
||||
icon: Icon(icon),
|
||||
label: Text('收藏'),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
appState.getNext();
|
||||
},
|
||||
child: Text('下一个'),
|
||||
),
|
||||
],
|
||||
),
|
||||
Spacer(flex: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BigCard extends StatelessWidget {
|
||||
const BigCard({
|
||||
Key? key,
|
||||
required this.pair,
|
||||
}) : super(key: key);
|
||||
|
||||
final WordPair pair;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var style = theme.textTheme.displayMedium!.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
);
|
||||
|
||||
return Card(
|
||||
color: theme.colorScheme.primary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: AnimatedSize(
|
||||
duration: Duration(milliseconds: 200),
|
||||
// 确保复合单词在窗口过窄时正确换行
|
||||
child: MergeSemantics(
|
||||
child: Wrap(
|
||||
children: [
|
||||
Text(
|
||||
pair.first,
|
||||
style: style.copyWith(fontWeight: FontWeight.w200),
|
||||
),
|
||||
Text(
|
||||
pair.second,
|
||||
style: style.copyWith(fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FavoritesPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var appState = context.watch<MyAppState>();
|
||||
|
||||
// 如果收藏夹为空,显示提示信息
|
||||
if (appState.favorites.isEmpty) {
|
||||
return Center(
|
||||
child: Text('无收藏'),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Text('你有 '
|
||||
'${appState.favorites.length} 个收藏:'),
|
||||
),
|
||||
Expanded(
|
||||
// 在宽窗口中使用网格布局以更好地利用空间
|
||||
child: GridView(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 400,
|
||||
childAspectRatio: 400 / 80,
|
||||
),
|
||||
children: [
|
||||
for (var pair in appState.favorites)
|
||||
ListTile(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.delete_outline, semanticLabel: '删除'),
|
||||
color: theme.colorScheme.primary,
|
||||
onPressed: () {
|
||||
appState.removeFavorite(pair);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
pair.asLowerCase,
|
||||
semanticsLabel: pair.asPascalCase,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryListView extends StatefulWidget {
|
||||
const HistoryListView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<HistoryListView> createState() => _HistoryListViewState();
|
||||
}
|
||||
|
||||
class _HistoryListViewState extends State<HistoryListView> {
|
||||
/// 需要此键以便 [MyAppState] 可以告诉下面的 [AnimatedList] 对新增项进行动画处理
|
||||
final _key = GlobalKey();
|
||||
|
||||
/// 用于在顶部“淡化”历史记录项,暗示列表的延续性
|
||||
static const Gradient _maskingGradient = LinearGradient(
|
||||
// 此渐变从完全透明到完全不透明的黑色...
|
||||
colors: [Colors.transparent, Colors.black],
|
||||
// ...从顶部(透明)到底部中途(0.5)
|
||||
stops: [0.0, 0.5],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<MyAppState>();
|
||||
appState.historyListKey = _key;
|
||||
|
||||
return ShaderMask(
|
||||
shaderCallback: (bounds) => _maskingGradient.createShader(bounds),
|
||||
// 此混合模式获取着色器的不透明度(即我们的渐变)并将其应用于目标(即我们的动画列表)
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: AnimatedList(
|
||||
key: _key,
|
||||
reverse: true,
|
||||
padding: EdgeInsets.only(top: 100),
|
||||
initialItemCount: appState.history.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
final pair = appState.history[index];
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
appState.toggleFavorite(pair);
|
||||
},
|
||||
icon: appState.favorites.contains(pair)
|
||||
? Icon(Icons.favorite, size: 12)
|
||||
: SizedBox(),
|
||||
label: Text(
|
||||
pair.asLowerCase,
|
||||
semanticsLabel: pair.asPascalCase,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user