From c4fd49bd7e1abe2e7ddc0844a00273d46a9e6b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Fri, 31 Jul 2026 12:50:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E7=A9=BF=E6=90=AD=E7=94=9F?= =?UTF-8?q?=E6=88=90=20+=20=E6=96=B9=E6=A1=88=E6=B5=81=20+=20=E6=95=88?= =?UTF-8?q?=E6=9E=9C=E5=9B=BE=20+=203D=20=E5=8C=96=E8=BA=AB=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - outfit_provider:方案列表/生成任务轮询(pending→planning→scoring→rendering→done)/详情/选定主方案/收藏 - outfit_page:日期范围 + 地点生成表单,生成中显示任务进度,方案列表卡片 - plan_viewer_page:PageView 手指滑动切换方案,穿衣清单(衣橱/新品标记)、发型发色、设为主方案/收藏 - plan_effect_page:效果图 3 视角(正面/侧面/背面)查看 - avatar_viewer:three_dart + flutter_gl 3D 渲染实现(自动旋转),GLB 资产未接入时占位展示,kEnable3D 开关 - avatar_viewer_page:化身查看页 + 重新构建 - 新增 /plan-viewer /plan-effect /avatar-viewer 路由 --- lib/features/outfit/outfit_page.dart | 251 +++++++++++++++- lib/features/outfit/outfit_provider.dart | 245 ++++++++++++++++ lib/features/outfit/plan_effect_page.dart | 120 ++++++++ lib/features/outfit/plan_viewer_page.dart | 287 +++++++++++++++++++ lib/features/profile/avatar_viewer_page.dart | 73 +++++ lib/features/profile/profile_page.dart | 16 +- lib/main.dart | 17 ++ lib/shared/widgets/avatar_viewer.dart | 242 ++++++++++++++++ pubspec.lock | 254 ++++++++-------- pubspec.yaml | 1 + 10 files changed, 1374 insertions(+), 132 deletions(-) create mode 100644 lib/features/outfit/outfit_provider.dart create mode 100644 lib/features/outfit/plan_effect_page.dart create mode 100644 lib/features/outfit/plan_viewer_page.dart create mode 100644 lib/features/profile/avatar_viewer_page.dart create mode 100644 lib/shared/widgets/avatar_viewer.dart diff --git a/lib/features/outfit/outfit_page.dart b/lib/features/outfit/outfit_page.dart index a9c9dbc..c9de778 100644 --- a/lib/features/outfit/outfit_page.dart +++ b/lib/features/outfit/outfit_page.dart @@ -1,10 +1,257 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:go_router/go_router.dart'; -class OutfitPage extends StatelessWidget { +import '../../shared/widgets/empty_view.dart'; +import '../../shared/widgets/error_view.dart'; +import '../../shared/widgets/loading_view.dart'; +import 'outfit_provider.dart'; + +/// 穿搭:生成表单 + 方案列表 +class OutfitPage extends ConsumerStatefulWidget { const OutfitPage({super.key}); + @override + ConsumerState createState() => _OutfitPageState(); +} + +class _OutfitPageState extends ConsumerState { + DateTimeRange? _dateRange; + final _locationCtrl = TextEditingController(); + bool _submitting = false; + + @override + void dispose() { + _locationCtrl.dispose(); + super.dispose(); + } + + Future _pickDateRange() async { + final now = DateTime.now(); + final range = await showDateRangePicker( + context: context, + firstDate: now, + lastDate: now.add(const Duration(days: 30)), + initialDateRange: _dateRange ?? + DateTimeRange(start: now, end: now.add(const Duration(days: 2))), + ); + if (range != null) setState(() => _dateRange = range); + } + + String _fmt(DateTime d) => + '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + + Future _generate() async { + final range = _dateRange; + final location = _locationCtrl.text.trim(); + if (range == null) { + Fluttertoast.showToast(msg: '请选择日期范围'); + return; + } + if (location.isEmpty) { + Fluttertoast.showToast(msg: '请输入地点'); + return; + } + if (range.duration.inDays < 1) { + Fluttertoast.showToast(msg: '日期范围至少 1 天'); + return; + } + setState(() => _submitting = true); + final ok = await ref.read(generateProvider.notifier).generate( + startDate: _fmt(range.start), + endDate: _fmt(range.end), + location: location, + ); + if (!mounted) return; + setState(() => _submitting = false); + if (ok) { + Fluttertoast.showToast(msg: '生成完成'); + ref.read(generateProvider.notifier).reset(); + context.go('/plan-viewer'); + } + } + @override Widget build(BuildContext context) { - return const Center(child: Text('穿搭方案(开发中)')); + final plans = ref.watch(outfitPlanProvider); + final gen = ref.watch(generateProvider); + final scheme = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar(title: const Text('穿搭方案')), + body: RefreshIndicator( + onRefresh: () => ref.read(outfitPlanProvider.notifier).refresh(), + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('生成穿搭方案', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: scheme.primary)), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: _submitting ? null : _pickDateRange, + icon: const Icon(Icons.date_range_outlined), + label: Text(_dateRange == null + ? '选择日期范围' + : '${_fmt(_dateRange!.start)} ~ ${_fmt(_dateRange!.end)}'), + ), + const SizedBox(height: 12), + TextField( + controller: _locationCtrl, + enabled: !_submitting, + decoration: const InputDecoration( + labelText: '地点(如:上海·外滩)', + hintText: '影响天气与穿搭建议', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + if (gen.phase == GeneratePhase.running) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2)), + const SizedBox(width: 8), + Expanded( + child: Text(gen.statusText, + style: const TextStyle(fontSize: 13)), + ), + ], + ), + ), + FilledButton( + onPressed: _submitting ? null : _generate, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14)), + child: const Text('生成'), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Text('已有方案', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + plans.when( + loading: () => const Padding( + padding: EdgeInsets.only(top: 48), + child: LoadingView(text: '加载方案...')), + error: (e, _) => Padding( + padding: const EdgeInsets.only(top: 48), + child: ErrorView( + message: e.toString().replaceFirst('Exception: ', ''), + onRetry: () => ref.read(outfitPlanProvider.notifier).refresh(), + ), + ), + data: (list) { + if (list.isEmpty) { + return const Padding( + padding: EdgeInsets.only(top: 48), + child: EmptyView( + message: '还没有穿搭方案,先设置日期与地点生成吧'), + ); + } + return Column( + children: [ + for (final p in list) ...[ + _PlanCard(plan: p), + const SizedBox(height: 8), + ], + ], + ); + }, + ), + ], + ), + ), + ); + } +} + +class _PlanCard extends ConsumerWidget { + final OutfitPlanInfo plan; + + const _PlanCard({required this.plan}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final scheme = Theme.of(context).colorScheme; + return Card( + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => context.go('/plan-viewer'), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: scheme.primaryContainer, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.checkroom, color: scheme.primary), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text(plan.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 15)), + ), + if (plan.isMain) ...[ + const SizedBox(width: 6), + const Chip( + label: Text('主方案'), + labelStyle: + TextStyle(color: Colors.white, fontSize: 11), + backgroundColor: Colors.indigo, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ), + ], + ], + ), + const SizedBox(height: 4), + Text( + '${plan.dateRange} · ${plan.location} · 评分 ${plan.score}' + '${plan.fromAi ? ' · AI 生成' : ' · 规则生成'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: Colors.grey), + ], + ), + ), + ), + ); } } diff --git a/lib/features/outfit/outfit_provider.dart b/lib/features/outfit/outfit_provider.dart new file mode 100644 index 0000000..ce51ee1 --- /dev/null +++ b/lib/features/outfit/outfit_provider.dart @@ -0,0 +1,245 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../core/auth/auth_provider.dart'; + +class OutfitPlanInfo { + final int id; + final String title; + final String source; // ai / rule + final int score; + final int mainFlag; + final String dateRange; + final String location; + final int hairstyleId; + final String hairColor; + + const OutfitPlanInfo({ + required this.id, + required this.title, + required this.source, + required this.score, + required this.mainFlag, + required this.dateRange, + required this.location, + required this.hairstyleId, + required this.hairColor, + }); + + bool get isMain => mainFlag == 1; + bool get fromAi => source == 'ai'; +} + +class PlanItemInfo { + final int id; + final String slot; // 上衣/下装/鞋/配饰 + final String source; // wardrobe / new + final int wardrobeItemId; + final String name; + final String desc; + + const PlanItemInfo({ + required this.id, + required this.slot, + required this.source, + required this.wardrobeItemId, + required this.name, + required this.desc, + }); + + bool get fromWardrobe => source == 'wardrobe'; +} + +class PlanEffectImageInfo { + final int id; + final String angle; // front / side / back + final String url; + final String status; // pending / rendering / done / failed + + const PlanEffectImageInfo({ + required this.id, + required this.angle, + required this.url, + required this.status, + }); + + bool get done => status == 'done' && url.isNotEmpty; +} + +class PlanDetail { + final OutfitPlanInfo plan; + final List items; + final List images; + final String hairstyleName; + + const PlanDetail({ + required this.plan, + required this.items, + required this.images, + required this.hairstyleName, + }); +} + +/// 方案列表 +class OutfitPlanNotifier extends AsyncNotifier> { + @override + Future> build() async { + final api = ref.read(apiClientProvider); + final list = await api.get>('/outfit/plan/list'); + return list + .map((e) => OutfitPlanInfo( + id: (e['id'] as num).toInt(), + title: e['title'] as String? ?? '', + source: e['source'] as String? ?? 'rule', + score: (e['score'] as num?)?.toInt() ?? 0, + mainFlag: (e['main_flag'] as num?)?.toInt() ?? 0, + dateRange: e['date_range'] as String? ?? '', + location: e['location'] as String? ?? '', + hairstyleId: (e['hairstyle_id'] as num?)?.toInt() ?? 0, + hairColor: e['hair_color'] as String? ?? '', + )) + .toList(); + } + + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard(build); + } + + Future selectMain(int planId) async { + final api = ref.read(apiClientProvider); + await api.post('/outfit/plan/select-main', {'plan_id': planId}); + await refresh(); + } + + Future review(int planId, String action) async { + final api = ref.read(apiClientProvider); + await api.post('/outfit/plan/review', {'plan_id': planId, 'action': action}); + } +} + +final outfitPlanProvider = + AsyncNotifierProvider>( + OutfitPlanNotifier.new); + +/// 生成任务状态(轮询后端任务状态机) +enum GeneratePhase { idle, running, done, failed } + +class GenerateState { + final GeneratePhase phase; + final String statusText; + final String error; + + const GenerateState({ + this.phase = GeneratePhase.idle, + this.statusText = '', + this.error = '', + }); +} + +class GenerateNotifier extends Notifier { + @override + GenerateState build() => const GenerateState(); + + Future generate({ + required String startDate, + required String endDate, + required String location, + }) async { + state = + const GenerateState(phase: GeneratePhase.running, statusText: '任务创建中...'); + final api = ref.read(apiClientProvider); + final data = await api.post>('/outfit/generate', { + 'start_date': startDate, + 'end_date': endDate, + 'location': location, + }); + final taskId = (data['task_id'] as num).toInt(); + + for (var i = 0; i < 90; i++) { + await Future.delayed(const Duration(seconds: 2)); + final st = await api.get>('/outfit/task/status', + query: {'task_id': taskId}); + final status = st['status'] as String? ?? ''; + final err = st['error'] as String? ?? ''; + state = GenerateState( + phase: GeneratePhase.running, statusText: _statusText(status)); + if (status == 'done') { + state = const GenerateState(phase: GeneratePhase.done, statusText: '完成'); + break; + } + if (status == 'failed') { + state = GenerateState( + phase: GeneratePhase.failed, + error: err.isEmpty ? '生成失败,请稍后重试' : err); + return false; + } + } + await ref.read(outfitPlanProvider.notifier).refresh(); + return state.phase == GeneratePhase.done; + } + + String _statusText(String status) { + switch (status) { + case 'pending': + return '排队中...'; + case 'planning': + return 'AI 规划穿搭中...'; + case 'scoring': + return '方案评分中...'; + case 'rendering': + return '生成效果图中...'; + case 'failed': + return '生成失败'; + default: + return '处理中...'; + } + } + + void reset() => state = const GenerateState(); +} + +final generateProvider = + NotifierProvider(GenerateNotifier.new); + +/// 方案详情 +final planDetailProvider = FutureProvider.family((ref, planId) async { + final api = ref.read(apiClientProvider); + final data = await api.get>('/outfit/plan/detail', + query: {'plan_id': planId}); + final planData = data['plan'] as Map? ?? {}; + final items = (data['items'] as List? ?? []) + .map((e) => PlanItemInfo( + id: (e['id'] as num).toInt(), + slot: e['slot'] as String? ?? '', + source: e['source'] as String? ?? 'new', + wardrobeItemId: (e['wardrobe_item_id'] as num?)?.toInt() ?? 0, + name: e['name'] as String? ?? '', + desc: e['desc'] as String? ?? '', + )) + .toList(); + final images = (data['images'] as List? ?? []) + .map((e) => PlanEffectImageInfo( + id: (e['id'] as num).toInt(), + angle: e['angle'] as String? ?? '', + url: e['url'] as String? ?? '', + status: e['status'] as String? ?? 'pending', + )) + .toList(); + final hairstyle = data['hairstyle'] as Map?; + return PlanDetail( + plan: OutfitPlanInfo( + id: (planData['id'] as num).toInt(), + title: planData['title'] as String? ?? '', + source: planData['source'] as String? ?? 'rule', + score: (planData['score'] as num?)?.toInt() ?? 0, + mainFlag: (planData['main_flag'] as num?)?.toInt() ?? 0, + dateRange: planData['date_range'] as String? ?? '', + location: planData['location'] as String? ?? '', + hairstyleId: (planData['hairstyle_id'] as num?)?.toInt() ?? 0, + hairColor: planData['hair_color'] as String? ?? '', + ), + items: items, + images: images, + hairstyleName: hairstyle?['name'] as String? ?? '', + ); +}); diff --git a/lib/features/outfit/plan_effect_page.dart b/lib/features/outfit/plan_effect_page.dart new file mode 100644 index 0000000..ac9db06 --- /dev/null +++ b/lib/features/outfit/plan_effect_page.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; + +import '../../core/config/app_config.dart'; +import '../../shared/widgets/empty_view.dart'; +import '../../shared/widgets/loading_view.dart'; +import 'outfit_provider.dart'; + +const _angleLabels = { + 'front': '正面', + 'side': '侧面', + 'back': '背面', +}; + +/// 效果图查看:3 视角(正面/侧面/背面)切换 +class PlanEffectPage extends StatefulWidget { + final List images; + + const PlanEffectPage({super.key, required this.images}); + + @override + State createState() => _PlanEffectPageState(); +} + +class _PlanEffectPageState extends State { + String? _selected; // 当前角度,null = 展示全部 + + @override + Widget build(BuildContext context) { + final done = widget.images.where((i) => i.done).toList(); + final pending = widget.images.where((i) => !i.done).toList(); + final angles = widget.images.map((i) => i.angle).toSet(); + + return Scaffold( + appBar: AppBar(title: const Text('效果图')), + body: Column( + children: [ + if (angles.length > 1) + SizedBox( + height: 48, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12), + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: const Text('全部'), + selected: _selected == null, + onSelected: (_) => setState(() => _selected = null), + ), + ), + for (final a in angles) + Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(_angleLabels[a] ?? a), + selected: _selected == a, + onSelected: (_) => setState(() => _selected = a), + ), + ), + ], + ), + ), + Expanded( + child: done.isEmpty + ? (pending.isEmpty + ? const EmptyView( + message: '暂无效果图,选定主方案后自动生成(每日限 3 次)') + : const LoadingView(text: '效果图生成中,请稍后刷新...')) + : GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 0.8, + ), + itemCount: _selected == null + ? done.length + : done.where((i) => i.angle == _selected).length, + itemBuilder: (ctx, idx) { + final shown = _selected == null + ? done + : done.where((i) => i.angle == _selected).toList(); + final img = shown[idx]; + return Card( + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Image.network( + AppConfig.resolveUrl(img.url), + fit: BoxFit.cover, + errorBuilder: (_, _, _) => const Icon( + Icons.broken_image_outlined, + size: 48, + color: Colors.grey), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Text(_angleLabels[img.angle] ?? img.angle, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold)), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/outfit/plan_viewer_page.dart b/lib/features/outfit/plan_viewer_page.dart new file mode 100644 index 0000000..bd30552 --- /dev/null +++ b/lib/features/outfit/plan_viewer_page.dart @@ -0,0 +1,287 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/config/app_config.dart'; +import '../../shared/widgets/loading_view.dart'; +import 'outfit_provider.dart'; + +/// 方案流:手指左右滑动切换方案,底部操作(效果图/选定/收藏) +class PlanViewerPage extends ConsumerWidget { + const PlanViewerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final plans = ref.watch(outfitPlanProvider); + return Scaffold( + appBar: AppBar(title: const Text('穿搭方案')), + body: plans.when( + loading: () => const LoadingView(text: '加载方案...'), + error: (e, _) => Center(child: Text('加载失败:$e')), + data: (list) { + if (list.isEmpty) { + return const Center(child: Text('暂无方案')); + } + return PageView.builder( + itemCount: list.length, + itemBuilder: (ctx, i) => + _PlanDetailView(planId: list[i].id, key: ValueKey(list[i].id)), + ); + }, + ), + ); + } +} + +class _PlanDetailView extends ConsumerStatefulWidget { + final int planId; + + const _PlanDetailView({required this.planId, super.key}); + + @override + ConsumerState<_PlanDetailView> createState() => _PlanDetailViewState(); +} + +class _PlanDetailViewState extends ConsumerState<_PlanDetailView> { + bool _selecting = false; + + Future _selectMain(int planId) async { + setState(() => _selecting = true); + try { + await ref.read(outfitPlanProvider.notifier).selectMain(planId); + if (!mounted) return; + Fluttertoast.showToast(msg: '已设为主方案,正在生成效果图'); + } catch (e) { + if (!mounted) return; + Fluttertoast.showToast( + msg: '操作失败:${e.toString().replaceFirst('Exception: ', '')}'); + } finally { + if (mounted) setState(() => _selecting = false); + } + } + + Future _review(int planId, String action) async { + try { + await ref.read(outfitPlanProvider.notifier).review(planId, action); + if (!mounted) return; + Fluttertoast.showToast( + msg: action == 'fav' ? '已收藏' : '已取消收藏'); + } catch (e) { + if (!mounted) return; + Fluttertoast.showToast( + msg: '操作失败:${e.toString().replaceFirst('Exception: ', '')}'); + } + } + + @override + Widget build(BuildContext context) { + final detail = ref.watch(planDetailProvider(widget.planId)); + final scheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.all(16), + child: detail.when( + loading: () => const LoadingView(text: '加载方案详情...'), + error: (e, _) => Center(child: Text('加载失败:$e')), + data: (d) { + final plan = d.plan; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(plan.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold)), + ), + if (plan.isMain) + const Chip( + label: Text('主方案'), + labelStyle: + TextStyle(color: Colors.white, fontSize: 11), + backgroundColor: Colors.indigo, + visualDensity: VisualDensity.compact, + ), + if (!plan.isMain) + Chip( + label: Text(plan.fromAi ? 'AI 生成' : '规则生成'), + labelStyle: TextStyle( + color: scheme.primary, fontSize: 11), + backgroundColor: scheme.primaryContainer, + visualDensity: VisualDensity.compact, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${plan.dateRange} · ${plan.location} · 评分 ${plan.score}', + style: const TextStyle(color: Colors.grey), + ), + const SizedBox(height: 12), + if (d.hairstyleName.isNotEmpty || plan.hairColor.isNotEmpty) + Card( + color: scheme.primaryContainer.withValues(alpha: 0.4), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + const Icon(Icons.content_cut, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + '发型:${d.hairstyleName.isNotEmpty ? d.hairstyleName : '默认'}' + '${plan.hairColor.isNotEmpty ? ' · 发色:${plan.hairColor}' : ''}', + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + Text('穿衣清单', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: scheme.primary)), + const SizedBox(height: 8), + if (d.items.isEmpty) + const Text('暂无穿搭单品', + style: TextStyle(color: Colors.grey)), + for (final item in d.items) + Card( + child: ListTile( + dense: true, + leading: Icon(_slotIcon(item.slot)), + title: Text(item.name), + subtitle: Text(item.desc), + trailing: item.fromWardrobe + ? const Chip( + label: Text('衣橱'), + labelStyle: TextStyle( + color: Colors.white, fontSize: 11), + backgroundColor: Colors.teal, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ) + : const Chip( + label: Text('新品'), + labelStyle: TextStyle( + color: Colors.white, fontSize: 11), + backgroundColor: Colors.orange, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ), + ), + ), + const SizedBox(height: 8), + Text('效果图', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: scheme.primary)), + const SizedBox(height: 8), + if (d.images.every((img) => !img.done)) + const Text('效果图生成中,选定主方案后生成(每日限 3 次)', + style: TextStyle(color: Colors.grey, fontSize: 13)) + else + SizedBox( + height: 96, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (final img in d.images.where((i) => i.done)) + Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => context.push('/plan-effect', + extra: d.images + .where((i) => i.done) + .toList()), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + AppConfig.resolveUrl(img.url), + width: 80, + height: 96, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container( + width: 80, + height: 96, + color: Colors.grey.shade200, + child: const Icon(Icons.image, + color: Colors.grey), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: plan.isMain + ? null + : _selecting + ? null + : () => _selectMain(plan.id), + icon: const Icon(Icons.star_outline), + label: Text(plan.isMain ? '主方案' : '设为主方案'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: () => context.push('/plan-effect', + extra: d.images), + icon: const Icon(Icons.image_outlined), + label: const Text('效果图'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: () => _review(plan.id, 'fav'), + icon: const Icon(Icons.favorite_outline), + label: const Text('收藏'), + ), + ), + ], + ), + ], + ); + }, + ), + ); + } + + IconData _slotIcon(String slot) { + switch (slot) { + case '上衣': + return Icons.checkroom; + case '下装': + return Icons.airline_seat_legroom_normal; + case '鞋': + return Icons.directions_walk; + case '配饰': + return Icons.watch_outlined; + default: + return Icons.style_outlined; + } + } +} diff --git a/lib/features/profile/avatar_viewer_page.dart b/lib/features/profile/avatar_viewer_page.dart new file mode 100644 index 0000000..4c709d7 --- /dev/null +++ b/lib/features/profile/avatar_viewer_page.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +import '../../shared/widgets/avatar_viewer.dart'; +import '../../shared/widgets/loading_view.dart'; +import 'avatar_provider.dart'; + +/// 3D 化身查看页 +class AvatarViewerPage extends ConsumerWidget { + const AvatarViewerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final avatar = ref.watch(avatarProvider); + return Scaffold( + appBar: AppBar(title: const Text('我的 3D 化身')), + body: avatar.when( + loading: () => const LoadingView(text: '加载化身...'), + error: (e, _) => Center(child: Text('加载失败:$e')), + data: (a) => ListView( + padding: const EdgeInsets.all(16), + children: [ + AvatarViewer( + glbUrl: a.glbUrl, + title: '我的 3D 化身', + subtitle: a.built + ? '脸型模板 ${a.faceTemplateId} · 体型模板 ${a.bodyTemplateId} · 肤色 ${a.skinToneIndex}' + : '尚未构建化身', + ), + const SizedBox(height: 12), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('构建说明', + style: TextStyle( + fontSize: 15, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + const Text( + '化身由脸型(20 档)× 体型(6 档)× 肤色(5 档)模板组合而成,' + '依据大头照、全身照与身形参数自动匹配。' + '3D 渲染在预烘焙 GLB 资产接入后启用。', + style: TextStyle(fontSize: 13, color: Colors.grey), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () async { + await ref + .read(avatarProvider.notifier) + .buildAvatar(); + if (context.mounted) { + Fluttertoast.showToast(msg: '化身构建完成'); + } + }, + icon: const Icon(Icons.refresh), + label: const Text('重新构建化身'), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/profile/profile_page.dart b/lib/features/profile/profile_page.dart index 808166e..e97addb 100644 --- a/lib/features/profile/profile_page.dart +++ b/lib/features/profile/profile_page.dart @@ -67,13 +67,15 @@ class ProfilePage extends ConsumerWidget { text: a.built ? '已构建(模板 ${a.faceTemplateId}/${a.bodyTemplateId})' : '尚未构建化身,需要先完成照片与身形', - actionText: a.built ? '重新构建' : '立即构建', - onAction: () async { - await ref.read(avatarProvider.notifier).buildAvatar(); - if (context.mounted) { - Fluttertoast.showToast(msg: '化身构建完成'); - } - }, + actionText: a.built ? '查看' : '立即构建', + onAction: a.built + ? () => context.go('/avatar-viewer') + : () async { + await ref.read(avatarProvider.notifier).buildAvatar(); + if (context.mounted) { + Fluttertoast.showToast(msg: '化身构建完成'); + } + }, ), ), ); diff --git a/lib/main.dart b/lib/main.dart index ef439af..0cdebdc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,10 @@ import 'features/auth/login_page.dart'; import 'features/commercial/commercial_page.dart'; import 'features/home/home_page.dart'; import 'features/outfit/outfit_page.dart'; +import 'features/outfit/outfit_provider.dart'; +import 'features/outfit/plan_effect_page.dart'; +import 'features/outfit/plan_viewer_page.dart'; +import 'features/profile/avatar_viewer_page.dart'; import 'features/profile/body_tune_page.dart'; import 'features/profile/photo_guide_page.dart'; import 'features/profile/profile_page.dart'; @@ -75,6 +79,19 @@ final routerProvider = Provider((ref) { path: '/commercial', builder: (ctx, state) => const CommercialPage(), ), + GoRoute( + path: '/plan-viewer', + builder: (ctx, state) => const PlanViewerPage(), + ), + GoRoute( + path: '/plan-effect', + builder: (ctx, state) => + PlanEffectPage(images: state.extra as List), + ), + GoRoute( + path: '/avatar-viewer', + builder: (ctx, state) => const AvatarViewerPage(), + ), ], ); }); diff --git a/lib/shared/widgets/avatar_viewer.dart b/lib/shared/widgets/avatar_viewer.dart new file mode 100644 index 0000000..9dd33bb --- /dev/null +++ b/lib/shared/widgets/avatar_viewer.dart @@ -0,0 +1,242 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gl/flutter_gl.dart'; +import 'package:three_dart/three_dart.dart' as three; +import 'package:three_dart_jsm/three_dart_jsm.dart' as three_jsm; + +import '../../core/config/app_config.dart'; + +/// 是否启用 three_dart 3D 渲染。 +/// MVP 阶段服务端尚无预烘焙 GLB 资产(/workspace/templates/*.glb 为构建产物), +/// 默认关闭展示信息占位;接入真实 GLB 资源后置为 true 即可启用。 +const bool kEnable3D = false; + +/// 3D 化身查看组件:three_dart 渲染 GLB,加载失败/未启用时展示信息占位 +class AvatarViewer extends StatelessWidget { + final String? glbUrl; + final String title; + final String subtitle; + final double height; + + const AvatarViewer({ + super.key, + required this.glbUrl, + required this.title, + required this.subtitle, + this.height = 360, + }); + + @override + Widget build(BuildContext context) { + if (!kEnable3D || glbUrl == null || glbUrl!.isEmpty) { + return _AvatarPlaceholder( + title: title, subtitle: subtitle, height: height); + } + return _ThreeDAvatarViewer( + url: AppConfig.resolveUrl(glbUrl!), + title: title, + subtitle: subtitle, + height: height, + ); + } +} + +/// three_dart 实现:flutter_gl 初始化 + GLTFLoader 加载 + 自动旋转渲染 +class _ThreeDAvatarViewer extends StatefulWidget { + final String url; + final String title; + final String subtitle; + final double height; + + const _ThreeDAvatarViewer({ + required this.url, + required this.title, + required this.subtitle, + required this.height, + }); + + @override + State<_ThreeDAvatarViewer> createState() => _ThreeDAvatarViewerState(); +} + +class _ThreeDAvatarViewerState extends State<_ThreeDAvatarViewer> { + FlutterGlPlugin? _gl; + three.WebGLRenderer? _renderer; + three.Scene? _scene; + three.Camera? _camera; + three.Object3D? _model; + dynamic _sourceTexture; + double _width = 300; + double _dpr = 1; + bool _ready = false; + bool _failed = false; + bool _disposed = false; + Timer? _timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _init()); + } + + @override + void dispose() { + _disposed = true; + _timer?.cancel(); + super.dispose(); + } + + Future _init() async { + final mq = MediaQuery.of(context); + _width = mq.size.width; + _dpr = mq.devicePixelRatio; + try { + final gl = FlutterGlPlugin(); + await gl.initialize(options: { + 'antialias': true, + 'alpha': false, + 'width': _width.toInt(), + 'height': widget.height.toInt(), + 'dpr': _dpr, + }); + await gl.prepareContext(); + + final renderer = three.WebGLRenderer({ + 'width': _width, + 'height': widget.height, + 'gl': gl.gl, + 'antialias': true, + 'canvas': gl.element, + }); + renderer.setPixelRatio(_dpr); + renderer.setSize(_width, widget.height, false); + + final target = three.WebGLMultisampleRenderTarget( + (_width * _dpr).toInt(), (widget.height * _dpr).toInt(), + three.WebGLRenderTargetOptions({'format': three.RGBAFormat})); + target.samples = 4; + renderer.setRenderTarget(target); + final sourceTexture = renderer.getRenderTargetGLTexture(target); + + await _loadModel(gl, renderer); + + _gl = gl; + _renderer = renderer; + _sourceTexture = sourceTexture; + if (mounted) setState(() => _ready = true); + _timer = Timer.periodic( + const Duration(milliseconds: 33), (_) => _render()); + } catch (e) { + debugPrint('3D 渲染不可用,回退占位:$e'); + if (mounted) setState(() => _failed = true); + } + } + + Future _loadModel( + FlutterGlPlugin gl, three.WebGLRenderer renderer) async { + final scene = three.Scene(); + final camera = three.PerspectiveCamera(45, _width / widget.height, 0.1, 100); + camera.position.set(0, 1.4, 3.2); + scene.add(three.AmbientLight(0xffffff, 0.9)); + final keyLight = three.DirectionalLight(0xffffff, 0.9); + keyLight.position.set(2, 4, 3); + scene.add(keyLight); + scene.add(camera); + camera.lookAt(three.Vector3(0, 1, 0)); + + final loader = three_jsm.GLTFLoader(null); + final result = await loader.loadAsync(widget.url); + final model = result['scene'] as three.Object3D?; + if (model != null) { + model.rotation.y = 0.6; + scene.add(model); + } + + _scene = scene; + _camera = camera; + _model = model; + } + + void _render() { + if (_disposed || _renderer == null || _scene == null || _camera == null) { + return; + } + final model = _model; + if (model != null) { + model.rotation.y += 0.01; // 自动旋转 + } + _renderer!.render(_scene!, _camera!); + _gl!.gl.flush(); + if (!kIsWeb) { + _gl!.updateTexture(_sourceTexture); + } + } + + @override + Widget build(BuildContext context) { + if (_failed) { + return _AvatarPlaceholder( + title: widget.title, subtitle: widget.subtitle, height: widget.height); + } + return SizedBox( + height: widget.height, + width: double.infinity, + child: _ready + ? (kIsWeb + ? HtmlElementView(viewType: _gl!.textureId!.toString()) + : Texture(textureId: _gl!.textureId!)) + : const Center(child: CircularProgressIndicator()), + ); + } +} + +/// 占位:GLB 资产未接入或加载失败时展示化身信息 +class _AvatarPlaceholder extends StatelessWidget { + final String title; + final String subtitle; + final double height; + + const _AvatarPlaceholder({ + required this.title, + required this.subtitle, + required this.height, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Container( + height: height, + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [scheme.primaryContainer, scheme.primary.withValues(alpha: 0.3)], + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.accessibility_new, size: 72, color: scheme.primary), + const SizedBox(height: 12), + Text(title, + style: + const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + subtitle.isEmpty ? '3D 渲染接入中(v2)' : subtitle, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0ec744d..11df863 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _fe_analyzer_shared sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "99.0.0" analyzer: @@ -14,7 +14,7 @@ packages: description: name: analyzer sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "12.1.0" archive: @@ -22,7 +22,7 @@ packages: description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.6.1" args: @@ -30,7 +30,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" async: @@ -38,7 +38,7 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.13.1" boolean_selector: @@ -46,7 +46,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" characters: @@ -54,7 +54,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" charcode: @@ -62,7 +62,7 @@ packages: description: name: charcode sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" cli_config: @@ -70,7 +70,7 @@ packages: description: name: cli_config sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" clock: @@ -78,7 +78,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" collection: @@ -86,7 +86,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.1" convert: @@ -94,7 +94,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.2" coverage: @@ -102,7 +102,7 @@ packages: description: name: coverage sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.15.1" cross_file: @@ -110,7 +110,7 @@ packages: description: name: cross_file sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.5+4" crypto: @@ -118,7 +118,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" csslib: @@ -126,7 +126,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" cupertino_icons: @@ -134,31 +134,31 @@ packages: description: name: cupertino_icons sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.9" dio: dependency: "direct main" description: name: dio - sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 - url: "https://pub.flutter-io.cn" + sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c" + url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.11.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 - url: "https://pub.flutter-io.cn" + sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c" + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" fake_async: dependency: transitive description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.3" ffi: @@ -166,7 +166,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" file: @@ -174,7 +174,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" file_selector_linux: @@ -182,7 +182,7 @@ packages: description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.4" file_selector_macos: @@ -190,7 +190,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -198,7 +198,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" file_selector_windows: @@ -206,7 +206,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.3+5" fixnum: @@ -214,7 +214,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" flutter: @@ -223,11 +223,11 @@ packages: source: sdk version: "0.0.0" flutter_gl: - dependency: transitive + dependency: "direct main" description: name: flutter_gl sha256: de74c88f77228f47dd280e2092b50eb49fe1fc008de74047d195a27f2db0b491 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.21" flutter_gl_macos: @@ -235,7 +235,7 @@ packages: description: name: flutter_gl_macos sha256: "62aa244d4aa9127115df651baec070893718dd27571c8dbca5374dbcb9fd4849" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.5" flutter_gl_platform_interface: @@ -243,7 +243,7 @@ packages: description: name: flutter_gl_platform_interface sha256: "03062491fac26d0fda80703e4a01894ecc4fec4f7e5cec131ef021fbf46b2fda" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.4" flutter_gl_web: @@ -251,7 +251,7 @@ packages: description: name: flutter_gl_web sha256: "005dc72618ee14659dff7dfe72d6b1fce0efdda745cfebfe4618ce7ac2044af9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.5" flutter_gl_windows: @@ -259,7 +259,7 @@ packages: description: name: flutter_gl_windows sha256: cd9259fb8178863de9e667129f73447fa174447a3c4abd16defa07af648850e4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.4" flutter_lints: @@ -267,7 +267,7 @@ packages: description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_plugin_android_lifecycle: @@ -275,17 +275,17 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.35" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" - url: "https://pub.flutter-io.cn" + sha256: "56e81e662e6e54e59b231da57e90ac0d06ac67d8e3f2273c129a37ae6282b40c" + url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.2" flutter_test: dependency: "direct dev" description: flutter @@ -301,7 +301,7 @@ packages: description: name: fluttertoast sha256: "7903c9d5339173497bfecbc23bc4212f5a87e0edfac2e1693fb74465ea67da7e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.1.0" frontend_server_client: @@ -309,7 +309,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.0" glob: @@ -317,7 +317,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" go_router: @@ -325,7 +325,7 @@ packages: description: name: go_router sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "17.3.0" html: @@ -333,7 +333,7 @@ packages: description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.15.6" http: @@ -341,7 +341,7 @@ packages: description: name: http sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.13.6" http_multi_server: @@ -349,7 +349,7 @@ packages: description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.2" http_parser: @@ -357,7 +357,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.2" image: @@ -365,7 +365,7 @@ packages: description: name: image sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.3.0" image_picker: @@ -373,7 +373,7 @@ packages: description: name: image_picker sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.3" image_picker_android: @@ -381,7 +381,7 @@ packages: description: name: image_picker_android sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.13+19" image_picker_for_web: @@ -389,7 +389,7 @@ packages: description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.1" image_picker_ios: @@ -397,7 +397,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.13+6" image_picker_linux: @@ -405,7 +405,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" image_picker_macos: @@ -413,7 +413,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -421,7 +421,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.1" image_picker_windows: @@ -429,7 +429,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" io: @@ -437,7 +437,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" leak_tracker: @@ -445,7 +445,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -453,7 +453,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.10" leak_tracker_testing: @@ -461,7 +461,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" lints: @@ -469,15 +469,23 @@ packages: description: name: lints sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.0" + listen: + dependency: transitive + description: + name: listen + sha256: cb0ad74d7453d8f83a7c1fae401828681ef5df21d41a3a26a04af590c985d085 + url: "https://pub.dev" + source: hosted + version: "1.0.0-beta.4" logging: dependency: transitive description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" matcher: @@ -485,7 +493,7 @@ packages: description: name: matcher sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.19" material_color_utilities: @@ -493,7 +501,7 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.13.0" meta: @@ -501,7 +509,7 @@ packages: description: name: meta sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.18.0" mime: @@ -509,7 +517,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" node_preamble: @@ -517,7 +525,7 @@ packages: description: name: node_preamble sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" opentype_dart: @@ -525,7 +533,7 @@ packages: description: name: opentype_dart sha256: "4bd96aeed494289a87e92bde20afe60f59648dcef253c0a7159b65ffa23899dc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.1" package_config: @@ -533,7 +541,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" path: @@ -541,7 +549,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" path_provider_linux: @@ -549,7 +557,7 @@ packages: description: name: path_provider_linux sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.2" path_provider_platform_interface: @@ -557,7 +565,7 @@ packages: description: name: path_provider_platform_interface sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" path_provider_windows: @@ -565,7 +573,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" petitparser: @@ -573,7 +581,7 @@ packages: description: name: petitparser sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.2" platform: @@ -581,7 +589,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.6" plugin_platform_interface: @@ -589,7 +597,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.8" pool: @@ -597,7 +605,7 @@ packages: description: name: pool sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.2" pub_semver: @@ -605,23 +613,23 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" riverpod: dependency: transitive description: name: riverpod - sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" - url: "https://pub.flutter-io.cn" + sha256: "84351b3472a447f7b89f961f95c73287009d58c59d8cc1267425d995f78e50f1" + url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.2" shared_preferences: dependency: "direct main" description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.5" shared_preferences_android: @@ -629,7 +637,7 @@ packages: description: name: shared_preferences_android sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.27" shared_preferences_foundation: @@ -637,7 +645,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.6" shared_preferences_linux: @@ -645,7 +653,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -653,7 +661,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.2" shared_preferences_web: @@ -661,7 +669,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.3" shared_preferences_windows: @@ -669,7 +677,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" shelf: @@ -677,7 +685,7 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.2" shelf_packages_handler: @@ -685,7 +693,7 @@ packages: description: name: shelf_packages_handler sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" shelf_static: @@ -693,7 +701,7 @@ packages: description: name: shelf_static sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.3" shelf_web_socket: @@ -701,7 +709,7 @@ packages: description: name: shelf_web_socket sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" sky_engine: @@ -714,7 +722,7 @@ packages: description: name: source_map_stack_trace sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" source_maps: @@ -722,7 +730,7 @@ packages: description: name: source_maps sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.10.13" source_span: @@ -730,7 +738,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.10.2" stack_trace: @@ -738,7 +746,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.1" state_notifier: @@ -746,7 +754,7 @@ packages: description: name: state_notifier sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" stream_channel: @@ -754,7 +762,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" string_scanner: @@ -762,7 +770,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" term_glyph: @@ -770,7 +778,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" test: @@ -778,7 +786,7 @@ packages: description: name: test sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.31.0" test_api: @@ -786,7 +794,7 @@ packages: description: name: test_api sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.11" test_core: @@ -794,7 +802,7 @@ packages: description: name: test_core sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.17" three_dart: @@ -802,7 +810,7 @@ packages: description: name: three_dart sha256: "102ff2cc65c2dcb805166c7dc5fa00a9a3252b5aec34fb435cc558a7487dc40b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.16" three_dart_jsm: @@ -810,7 +818,7 @@ packages: description: name: three_dart_jsm sha256: "26a71aff4aa842ac8178d2ba8e64b997c434ce9f604fa4fa3ee49f343d2c84b4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.10" typed_data: @@ -818,7 +826,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" typr_dart: @@ -826,7 +834,7 @@ packages: description: name: typr_dart sha256: e8aa717c1445ceccd77bd6ba471683c8e17a9b14797ac09db3bd52d6fbd2fe7b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.2" universal_html: @@ -834,7 +842,7 @@ packages: description: name: universal_html sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" universal_io: @@ -842,7 +850,7 @@ packages: description: name: universal_io sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.1" uuid: @@ -850,7 +858,7 @@ packages: description: name: uuid sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.6.0" vector_math: @@ -858,7 +866,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" vm_service: @@ -866,7 +874,7 @@ packages: description: name: vm_service sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "15.2.0" watcher: @@ -874,7 +882,7 @@ packages: description: name: watcher sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" web: @@ -882,7 +890,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" web_socket: @@ -890,7 +898,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" web_socket_channel: @@ -898,7 +906,7 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" webkit_inspection_protocol: @@ -906,7 +914,7 @@ packages: description: name: webkit_inspection_protocol sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" xdg_directories: @@ -914,7 +922,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" xml: @@ -922,7 +930,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.6.1" yaml: @@ -930,7 +938,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.3" sdks: diff --git a/pubspec.yaml b/pubspec.yaml index af8f402..f8d429c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: fluttertoast: ^9.1.0 three_dart: ^0.0.16 three_dart_jsm: ^0.0.10 + flutter_gl: ^0.0.20 dev_dependencies: flutter_test: