第一次提交
This commit is contained in:
13
lib/animations/slide.dart
Normal file
13
lib/animations/slide.dart
Normal 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
39
lib/api/capatcha.dart
Normal 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
122
lib/backup.dart
Normal 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
34
lib/config.dart
Normal 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
134
lib/demo.dart
Normal 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
55
lib/main.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
68
lib/models/class_cookie_entry.dart
Normal file
68
lib/models/class_cookie_entry.dart
Normal 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';
|
||||
}
|
15
lib/models/class_destination_item.dart
Normal file
15
lib/models/class_destination_item.dart
Normal 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,
|
||||
});
|
||||
}
|
22
lib/models/enum_theme.dart
Normal file
22
lib/models/enum_theme.dart
Normal 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];
|
||||
}
|
||||
|
20
lib/models/func_animation_route.dart
Normal file
20
lib/models/func_animation_route.dart
Normal 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
146
lib/providers/api.dart
Normal 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
55
lib/providers/theme.dart
Normal 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
34
lib/router.dart
Normal 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
37
lib/routes/about.dart
Normal 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
120
lib/routes/root.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
40
lib/routes/root_pages/home.dart
Normal file
40
lib/routes/root_pages/home.dart
Normal 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!)}')
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
41
lib/routes/root_pages/me.dart
Normal file
41
lib/routes/root_pages/me.dart
Normal 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
40
lib/routes/settings.dart
Normal 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('拾糖论坛'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
44
lib/routes/settings/theme.dart
Normal file
44
lib/routes/settings/theme.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user