第一次提交

This commit is contained in:
2025-06-27 20:20:51 +08:00
commit 33f86752de
153 changed files with 6743 additions and 0 deletions

13
lib/animations/slide.dart Normal file
View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import '../config.dart';
Widget slideTransition(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
final tween = Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero);
final curvedAnimation = CurvedAnimation(parent: animation, curve: AppAnimationConfig.slideCurve);
return SlideTransition(
position: tween.animate(curvedAnimation),
child: child,
);
}

39
lib/api/capatcha.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
import '../providers/api.dart';
class ApiImage extends HookConsumerWidget {
final String path;
const ApiImage(this.path, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final future = useMemoized(() async {
final client = ref.read(apiClientProvider);
final res = await client.get(path); // GET /avatar 或 /captcha
return res.bodyBytes;
}, [path]);
final snapshot = useFuture(future);
if (snapshot.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return const Icon(Icons.error);
}
return Image.memory(
snapshot.data!,
gaplessPlayback: true, // 避免闪烁
);
}
}
ApiImage captchaImage() => ApiImage('misc.php?mod=seccode');

122
lib/backup.dart Normal file
View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

34
lib/config.dart Normal file
View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import './models/enum_theme.dart';
class AppConfig {
static const seedColor = const Color(0xFFD1BCF1); // 跟随网站
static const themeMode = ThemeModeType.system;
static const useDynamicColor = false;
static const wideWidth = 600;
// static const apiBaseUrl = 'http://10.0.0.2:8000/api';
static const apiBaseUrl = 'https://www.shitangsweet.com/';
static ColorScheme defaultLightColorScheme() =>
ColorScheme.fromSeed(seedColor: seedColor);
static ColorScheme defaultDarkColorScheme() =>
ColorScheme.fromSeed(seedColor: seedColor, brightness: Brightness.dark);
}
class AppAnimationConfig {
static const duration = const Duration(milliseconds: 300);
static const pageViewCurve = Curves.ease;
static const slideCurve = Curves.easeInOut;
}
class AppSharedPreferencesKey {
static const themeMode = 'theme_mode';
static const useDynamicColor = 'use_dynamic_color';
static const cookieKey = 'cookie';
}

134
lib/demo.dart Normal file
View File

@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:styled_widget/styled_widget.dart';
class TestPage extends StatelessWidget {
const TestPage({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: Text('Test'), backgroundColor: cs.inversePrimary),
body: Center(child: Text('Hello', style: TextStyle(color: cs.primary))),
bottomNavigationBar: NavigationBar(
selectedIndex: 0,
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: '首页'),
NavigationDestination(icon: Icon(Icons.settings), label: '设置'),
],
),
);
}
}
class AllWidgetsDemo extends HookWidget {
const AllWidgetsDemo({super.key});
@override
Widget build(BuildContext context) {
final switchValue = useState(true);
final checkboxValue = useState(false);
final radioValue = useState(1);
final dropdownValue = useState('A');
return Scaffold(
appBar: AppBar(title: const Text('Flutter Hooks 组件 Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('✅ 按钮类').padding(bottom: 8),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
FilledButton(onPressed: () {}, child: const Text('Filled')),
ElevatedButton(onPressed: () {}, child: const Text('Elevated')),
OutlinedButton(onPressed: () {}, child: const Text('Outlined')),
TextButton(onPressed: () {}, child: const Text('Text')),
IconButton(onPressed: () {}, icon: const Icon(Icons.thumb_up)),
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
],
),
const Divider(height: 32),
const Text('✅ 表单 & 状态类'),
const SizedBox(height: 12),
TextField(
decoration: const InputDecoration(labelText: '文本框'),
),
const SizedBox(height: 8),
Switch(
value: switchValue.value,
onChanged: (v) => switchValue.value = v,
),
Checkbox(
value: checkboxValue.value,
onChanged: (v) => checkboxValue.value = v ?? false,
),
Radio(
value: 1,
groupValue: radioValue.value,
onChanged: (v) => radioValue.value = v ?? 1,
),
Radio(
value: 2,
groupValue: radioValue.value,
onChanged: (v) => radioValue.value = v ?? 1,
),
DropdownMenu<String>(
initialSelection: dropdownValue.value,
onSelected: (v) => dropdownValue.value = v!,
dropdownMenuEntries: const [
DropdownMenuEntry(value: 'A', label: '选项 A'),
DropdownMenuEntry(value: 'B', label: '选项 B'),
],
),
const Divider(height: 32),
const Text('✅ 展示类组件'),
const Chip(label: Text('示例 Chip')),
const Tooltip(message: '我是提示', child: Icon(Icons.info)),
const Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Text('这是一个卡片'),
),
),
const ListTile(
leading: Icon(Icons.person),
title: Text('列表项'),
subtitle: Text('副标题'),
trailing: Icon(Icons.arrow_forward_ios),
),
const Divider(height: 32),
const Text('✅ 进度组件'),
const SizedBox(height: 8),
const Center(
child: CircularProgressIndicator(),
),
const SizedBox(height: 8),
const LinearProgressIndicator(value: 0.6),
const Divider(height: 32),
const Text('✅ 导航与交互'),
Builder(
builder: (context) => ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('被点击了')),
);
},
child: const Text('弹出 SnackBar'),
),
),
],
),
);
}
}

55
lib/main.dart Normal file
View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dynamic_color/dynamic_color.dart';
import './config.dart';
import './router.dart';
import './providers/theme.dart';
void main() {
runApp(ProviderScope(
child: const MyApp()
));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final useDynamicColor = ref.watch(useDynamicColorProvider);
return DynamicColorBuilder(
builder: (light, dark) {
final defaultLight = AppConfig.defaultLightColorScheme();
final lightColorScheme = useDynamicColor ? (light ?? defaultLight) : defaultLight;
final defaultDark = AppConfig.defaultDarkColorScheme();
final darkColorScheme = useDynamicColor ? (dark ?? defaultDark) : defaultDark;
final appBarTheme = AppBarTheme(
backgroundColor: lightColorScheme.inversePrimary,
foregroundColor: lightColorScheme.onInverseSurface,
elevation: 0,
);
return MaterialApp.router(
theme: ThemeData(
useMaterial3: true,
colorScheme: lightColorScheme,
appBarTheme: AppBarTheme(
backgroundColor: lightColorScheme.inversePrimary,
// foregroundColor: lightColorScheme.onInverseSurface,
)
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: darkColorScheme,
appBarTheme: AppBarTheme(
backgroundColor: darkColorScheme.inversePrimary,
)
),
themeMode: ref.watch(themeModeProvider).toThemeMode(),
routerConfig: router,
);
},
);
}
}

View File

@@ -0,0 +1,68 @@
import 'dart:io';
class CookieEntry {
final String value;
final DateTime? expires;
final String? path;
final String? domain;
CookieEntry({
required this.value,
this.expires,
this.path,
this.domain,
});
factory CookieEntry.fromSetCookie(String raw) {
final parts = raw.split(';');
final kv = parts[0].split('=');
final name = kv[0].trim();
final value = kv.length > 1 ? kv.sublist(1).join('=').trim() : '';
String? path;
String? domain;
DateTime? expires;
for (final part in parts.skip(1)) {
final kv = part.trim().split('=');
final key = kv[0].toLowerCase();
final val = kv.length > 1 ? kv.sublist(1).join('=').trim() : '';
switch (key) {
case 'expires':
try {
expires = HttpDate.parse(val);
} catch (_) {}
break;
case 'path':
path = val;
break;
case 'domain':
domain = val;
break;
}
}
return CookieEntry(
value: value,
expires: expires,
path: path,
domain: domain,
);
}
Map<String, dynamic> toJson() => {
'value': value,
'expires': expires?.toIso8601String(),
'path': path,
'domain': domain,
};
static CookieEntry fromJson(Map<String, dynamic> json) => CookieEntry(
value: json['value'],
expires: json['expires'] != null ? DateTime.parse(json['expires']) : null,
path: json['path'],
domain: json['domain'],
);
String toCookieString(String name) => '$name=$value';
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class DestinationItem {
final String label;
final IconData icon;
final Widget child;
final String? title; // 可选标题
const DestinationItem({
required this.label,
required this.icon,
required this.child,
this.title,
});
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum ThemeModeType {
system,
light,
dark;
ThemeMode toThemeMode() {
switch (this) {
case ThemeModeType.system:
return ThemeMode.system;
case ThemeModeType.light:
return ThemeMode.light;
case ThemeModeType.dark:
return ThemeMode.dark;
}
}
static ThemeModeType fromIndex(int index) => ThemeModeType.values[index];
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../config.dart';
import '../animations/slide.dart';
GoRoute animationRoute({
required String path,
required Widget child,
}) {
return GoRoute(
path: path,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: child,
transitionDuration: AppAnimationConfig.duration,
transitionsBuilder: slideTransition,
),
);
}

146
lib/providers/api.dart Normal file
View File

@@ -0,0 +1,146 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import '../config.dart';
import '../models/class_cookie_entry.dart';
const _cookieKey = AppSharedPreferencesKey.cookieKey;
final cookieProvider = StateNotifierProvider<CookieNotifier, Map<String, CookieEntry>>((ref) {
return CookieNotifier();
});
final cookieHeaderProvider = Provider<String>((ref) {
final cookies = ref.watch(cookieProvider);
final now = DateTime.now();
return cookies.entries
.where((e) => e.value.expires == null || e.value.expires!.isAfter(now))
.map((e) => e.value.toCookieString(e.key))
.join('; ');
});
final apiClientProvider = Provider<ApiClient>((ref) {
final cookieHeader = ref.watch(cookieHeaderProvider);
final client = ApiClient(ref: ref);
ref.onDispose(client.dispose);
return client;
});
class ApiClient {
final Ref ref;
final http.Client _client;
static const String baseUrl = AppConfig.apiBaseUrl;
ApiClient({required this.ref, http.Client? client})
: _client = client ?? http.Client(); // 复用或新建
Map<String, String> _buildHeaders({Map<String, String>? extra}) {
final cookieHeader = ref.read(cookieHeaderProvider);
return {
'Content-Type': 'application/json',
if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,
...?extra,
};
}
Future<http.Response> _send(Future<http.Response> Function(http.Client client) request) async {
final response = await request(_client);
final setCookie = response.headers['set-cookie'];
if (setCookie != null) {
await ref.read(cookieProvider.notifier).updateFromSetCookie(setCookie.split(','));
}
return response;
}
Future<http.Response> get(String path, {Map<String, String>? headers}) {
return _send((client) => client.get(
Uri.parse('$baseUrl/$path'),
headers: _buildHeaders(extra: headers),
));
}
Future<http.Response> post(String path, {Map<String, String>? headers, String? body}) {
return _send((client) => client.post(
Uri.parse('$baseUrl/$path'),
headers: _buildHeaders(extra: headers),
body: body,
));
}
void _checkResponse(http.Response res) {
if (res.statusCode >= 300) {
throw Exception('HTTP ${res.statusCode}: ${res.body}');
}
}
void dispose() {
_client.close(); // 记得释放资源
}
}
class CookieNotifier extends StateNotifier<Map<String, CookieEntry>> {
CookieNotifier() : super({}) {
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(_cookieKey) ?? [];
final map = <String, CookieEntry>{};
for (final item in list) {
final split = item.split('|');
if (split.length == 2) {
final key = split[0];
final valueMap = Map<String, dynamic>.from(jsonDecode(split[1]));
map[key] = CookieEntry.fromJson(valueMap);
}
}
state = map;
}
Future<void> _persist() async {
final prefs = await SharedPreferences.getInstance();
final list = state.entries.map((e) => '${e.key}|${jsonEncode(e.value.toJson())}').toList();
await prefs.setStringList(_cookieKey, list);
}
Future<void> setAll(Map<String, CookieEntry> map) async {
state = Map<String, CookieEntry>.from(map);
await _persist();
}
Future<void> clear() async {
state = {};
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_cookieKey);
}
Future<void> updateFromSetCookie(List<String> setCookies) async {
final updated = Map<String, CookieEntry>.from(state);
for (final raw in setCookies) {
final parts = raw.split(';');
final kv = parts[0].split('=');
if (kv.length < 2) continue;
final name = kv[0].trim();
final entry = CookieEntry.fromSetCookie(raw);
updated[name] = entry;
}
state = updated;
await _persist();
}
bool get isLoggedIn =>
state.containsKey('auth') || state.containsKey('sessionid');
}

55
lib/providers/theme.dart Normal file
View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/enum_theme.dart';
import '../config.dart';
const _keyThemeMode = AppSharedPreferencesKey.themeMode;
const _keyUseDynamic = AppSharedPreferencesKey.useDynamicColor;
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeModeType>(
(ref) => ThemeModeNotifier(),
);
final useDynamicColorProvider = StateNotifierProvider<UseDynamicColorNotifier, bool>(
(ref) => UseDynamicColorNotifier(),
);
class UseDynamicColorNotifier extends StateNotifier<bool> {
UseDynamicColorNotifier() : super(true) {
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
state = prefs.getBool(_keyUseDynamic) ?? AppConfig.useDynamicColor;
}
Future<void> set(bool value) async {
state = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyUseDynamic, value);
}
}
class ThemeModeNotifier extends StateNotifier<ThemeModeType> {
ThemeModeNotifier() : super(ThemeModeType.system) {
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final index = prefs.getInt(_keyThemeMode);
if (index != null) {
state = ThemeModeType.fromIndex(index);
} else state = AppConfig.themeMode;
}
Future<void> set(ThemeModeType mode) async {
state = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_keyThemeMode, mode.index);
}
}

34
lib/router.dart Normal file
View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import './models/func_animation_route.dart';
import './routes/root.dart';
import './routes/settings.dart';
import './routes/settings/theme.dart';
import './routes/about.dart';
final router = GoRouter(
initialLocation: '/',
routes: [
ShellRoute(
builder: (context, state, child) {
return child;
},
routes: [
GoRoute(path: '/', builder: (context, state) => const RootRoute()),
animationRoute(
path: '/settings',
child: const SettingsPage(),
),
animationRoute(
path: '/settings/theme',
child: const SettingsThemePage(),
),
animationRoute(
path: '/about',
child: const AboutPage(),
),
],
),
],
);

37
lib/routes/about.dart Normal file
View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:gap/gap.dart';
import '../../src/version.dart';
import '../../config.dart';
class AboutPage extends HookWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('关于')),
body: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('images/favicon.png', width: 120, height: 120).clipOval(),
const SizedBox(height: 20),
Text(
'拾糖论坛',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 24),
),
],
).constrained(height: 200, width: double.infinity),
const Divider(height: 16),
ListTile(
title: const Text('App 版本'),
subtitle: Text('v${packageVersion}'),
),
],
)
);
}
}

120
lib/routes/root.dart Normal file
View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
import '../config.dart';
import '../demo.dart';
import './root_pages/home.dart';
import './root_pages/me.dart';
import '../models/class_destination_item.dart';
const destinations = [
DestinationItem(
label: '首页',
icon: Icons.home,
child: HomePage(),
title: '拾糖',
),
// DestinationItem(
// label: '展示',
// icon: Icons.message,
// child: AllWidgetsDemo(),
// title: '组件演示',
// ),
DestinationItem(
label: '我的',
icon: Icons.person,
child: MePage(),
),
];
class RootRoute extends HookConsumerWidget {
const RootRoute({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final index = useState(0);
final controller = usePageController();
final jumping = useState(false);
useEffect(() {
if (!jumping.value) return null;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (controller.hasClients) {
await controller.animateToPage(
index.value,
duration: AppAnimationConfig.duration,
curve: AppAnimationConfig.pageViewCurve,
);
jumping.value = false;
}
});
return null;
}, [index.value, jumping.value]);
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= AppConfig.wideWidth;
return Scaffold(
appBar: AppBar(
title: Text(destinations[index.value].title ?? destinations[index.value].label),
),
body: Row(
children: [
if (isWide)
NavigationRail(
backgroundColor: Theme.of(context).colorScheme.surface,
selectedIndex: index.value,
onDestinationSelected: (i) {
if (i != index.value) {
index.value = i;
jumping.value = true;
}
},
labelType: NavigationRailLabelType.all,
destinations: destinations.map((d) {
return NavigationRailDestination(
icon: Icon(d.icon),
label: Text(d.label),
);
}).toList(),
),
// 内容区域
Expanded(
child: PageView(
controller: controller,
onPageChanged: (i) {
if (!jumping.value) {
index.value = i;
}
},
children: <Widget>[
for (final d in destinations) d.child,
],
),
),
],
),
bottomNavigationBar: isWide
? null
: NavigationBar(
selectedIndex: index.value,
onDestinationSelected: (i) {
if (i != index.value) {
index.value = i;
jumping.value = true;
}
},
destinations: destinations.map((d) {
return NavigationDestination(
icon: Icon(d.icon),
label: d.label,
);
}).toList(),
),
);
},
);
}
}

View File

@@ -0,0 +1,40 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import '../../providers/api.dart';
import '../../api/capatcha.dart';
class HomePage extends HookConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cookies = ref.watch(cookieHeaderProvider);
final future = useMemoized(() async {
final client = ref.read(apiClientProvider);
final res = await client.get('');
return res.bodyBytes;
}, []);
final snapshot = useFuture(future);
return ListView(
padding: EdgeInsets.all(16),
children:[
// if (snapshot.hasData) Text('${utf8.decode(snapshot.data!)}'),
const Text('HTTP API Client 状态:'),
const Gap(16),
Text('Cookies: ${cookies}'),
if (snapshot.hasData) ...[
const Gap(16),
Text('${utf8.decode(snapshot.data!)}')
]
]
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
class MePage extends HookConsumerWidget {
const MePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('images/test.png', width: 120, height: 120).clipOval(),
const SizedBox(height: 20),
Text(
'UnknownMp',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 24),
),
],
).constrained(height: 320, width: double.infinity),
const Divider(height: 16),
Column(
children: [
InkWell(
onTap: () => context.push('/settings'),
child: ListTile(
leading: const Icon(Icons.settings),
title: const Text('设置'),
trailing: const Icon(Icons.arrow_forward),
),
),
]
),
],
);
}
}

40
lib/routes/settings.dart Normal file
View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import '../config.dart';
class SettingsPage extends HookConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
const Gap(8),
InkWell(
onTap: () => context.push('/settings/theme'),
child: ListTile(
leading: const Icon(Icons.palette),
title: const Text('外观'),
// subtitle: const Text('亮 / 暗模式, 动态取色?'),
),
),
const Divider(height: 16),
InkWell(
onTap: () => context.push('/about'),
child: ListTile(
leading: const Icon(Icons.info),
title: const Text('关于'),
subtitle: Text('拾糖论坛'),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:gap/gap.dart';
import '../../config.dart';
import '../../providers/theme.dart';
import '../../models/enum_theme.dart';
class SettingsThemePage extends HookConsumerWidget {
const SettingsThemePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('外观')),
body: ListView(
children: [
const Gap(16),
ListTile(
title: const Text('配色方案'),
trailing: DropdownMenu<ThemeModeType>(
initialSelection: ref.watch(themeModeProvider),
onSelected: (value) { if (value != null) ref.read(themeModeProvider.notifier).set(value); },
dropdownMenuEntries: const [
DropdownMenuEntry(value: ThemeModeType.system, label: '跟随系统'),
DropdownMenuEntry(value: ThemeModeType.light, label: '亮色'),
DropdownMenuEntry(value: ThemeModeType.dark, label: '暗色'),
],
),
),
const Gap(16),
SwitchListTile(
title: Text('动态配色'),
value: ref.watch(useDynamicColorProvider),
onChanged: (value) { ref.read(useDynamicColorProvider.notifier).set(value); },
),
//const Divider(height: 16),
],
),
);
}
}