feat: add two-tab timetable UI and glass dock
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../data/json_schedule_import.dart';
|
||||
import 'settings_page.dart';
|
||||
import 'import_preview.dart';
|
||||
|
||||
class ProfilePage extends StatelessWidget {
|
||||
const ProfilePage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onLogin,
|
||||
this.authStatus = '未登录',
|
||||
this.onLogout,
|
||||
});
|
||||
final AppController controller;
|
||||
final VoidCallback onLogin;
|
||||
final VoidCallback? onLogout;
|
||||
final String authStatus;
|
||||
Future<void> _import(BuildContext context) async {
|
||||
try {
|
||||
final file = await FilePicker.pickFile(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
if (file == null) return;
|
||||
final bytes = BytesBuilder(copy: false);
|
||||
var count = 0;
|
||||
await for (final chunk in file.readAsByteStream()) {
|
||||
count += chunk.length;
|
||||
if (count > 2 * 1024 * 1024) {
|
||||
throw const FormatException('课表文件不能超过 2 MB');
|
||||
}
|
||||
bytes.add(chunk);
|
||||
}
|
||||
final draft = JsonScheduleImport.parse(utf8.decode(bytes.takeBytes()));
|
||||
if (!context.mounted) return;
|
||||
await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: canvas,
|
||||
builder: (_) => ImportPreview(draft: draft, controller: controller),
|
||||
);
|
||||
} catch (error) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('导入未完成:$error')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imported = controller.snapshot.importMetadata;
|
||||
return ListView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
22,
|
||||
26,
|
||||
22,
|
||||
145 + MediaQuery.paddingOf(context).bottom,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'YOUR LITTLE RIVERSIDE',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
letterSpacing: 1.8,
|
||||
color: river.withValues(alpha: 0.75),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'我的河畔',
|
||||
style: TextStyle(fontSize: 29, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
Text(
|
||||
'把时间,留给真正重要的事。',
|
||||
style: TextStyle(fontSize: 13, color: ink.withValues(alpha: 0.5)),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 55,
|
||||
height: 55,
|
||||
decoration: BoxDecoration(
|
||||
color: river.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(19),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person_outline_rounded,
|
||||
size: 30,
|
||||
color: river,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'连接学校课表',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
Text(
|
||||
'电子科技大学 · 研究生\n$authStatus',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.8,
|
||||
color: ink.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: onLogin,
|
||||
child: Text(onLogout == null ? '登录学校账户' : '打开学校课表'),
|
||||
),
|
||||
),
|
||||
if (onLogout != null)
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: onLogout,
|
||||
child: const Text('退出学校登录'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
SurfaceCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${controller.snapshot.courses.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: river,
|
||||
),
|
||||
),
|
||||
const Text('已导入的课程安排', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
imported == null ? '尚未导入' : '已在本机保存',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
Text(
|
||||
imported == null
|
||||
? '离线查看,更安心'
|
||||
: '${imported.importedAt.month} 月 ${imported.importedAt.day} 日导入',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: ink.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
const Text(
|
||||
'安排你的每一天',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SurfaceCard(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
child: Column(
|
||||
children: [
|
||||
_tile(
|
||||
Icons.tune_rounded,
|
||||
'课表设置',
|
||||
'颜色、提醒、日历和学期',
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => SettingsPage(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: ink.withValues(alpha: 0.06)),
|
||||
_tile(
|
||||
Icons.file_download_outlined,
|
||||
'从文件导入',
|
||||
'先预览,再替换课表',
|
||||
() => _import(context),
|
||||
),
|
||||
Divider(height: 1, color: ink.withValues(alpha: 0.06)),
|
||||
_tile(
|
||||
Icons.auto_awesome_outlined,
|
||||
'体验示例课表',
|
||||
'无需登录,看看河畔的样子',
|
||||
controller.showDemo,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'河畔Everyday',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'今天也有值得期待的小事。',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: ink.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tile(
|
||||
IconData icon,
|
||||
String title,
|
||||
String subtitle,
|
||||
VoidCallback action,
|
||||
) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(icon, color: river, size: 23),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.6,
|
||||
color: ink.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: ink.withValues(alpha: 0.3),
|
||||
),
|
||||
onTap: action,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user