Browse Source
## 等待态
原来是 assets/lottie/meeting_wait.json —— 一张「JOIN」对话气泡插画,和本页在做的事
(把音频变成文稿)没有任何关系,配色也与全站的 222° 冷蓝灰不搭,深色主题下更是一块
突兀的白。改成自绘:上面一排声波表示音频正在被读,下面几条带流光的骨架行预告即将
生成的文稿形状,两者共用同一个 AnimationController。
自绘而不是再换一个 Lottie:① 不用为一个等待态再背一份 json 资源;② 颜色全部走
AppPalette,明暗主题自动跟随,插画做不到;③ 四个 tab(概览/发言/全文/思维导图)
共用同一个等待态,自绘好调。meeting_wait.json 一并删除(全仓零引用;assets/lottie/
在 pubspec 里是按目录声明的,不用改配置)。
## 重进后等待态消失 —— tasktype 在两张表里各存一份
真机上那条记录的实际状态是 meeting.tasktype=1 / meetingdetails.tasktype=0:
列表页读前者,显示「转写中」;详情页读后者,拿到 0,于是什么等待态都没有。
两张表分头写,而且中间态漏了:
- submitTask / refreshTask / readTask 只调 editMeetingTitle,**只写 meeting**
- _queryTask 里写 meetingdetails 的两次都藏在里程碑分支里(>=3、==5/6),
1→2(等待转写→转写中)这一跳、以及失败态 10002,meetingdetails 一次都不写
首次进入之所以有动画,靠的是详情控制器里那句内存赋值 meetingDetails.value.tasktype=1;
控制器一销毁,重进读库就是 0 —— 表现为「退出重进特效就不见了」。
改法:
1. SqfliteApi.setMeetingTaskType(id, type):一个事务同时写两张表,成为写 tasktype 的
**唯一入口**。⚠️ 别再直接 editMeetingTitle({'tasktype': x}),那只写一张。
2. 轮询里的状态推进从两个里程碑分支里提到分支之后无条件执行 —— 数据先落、状态最后推,
详情页的 ever(tasktype) 监听读到新状态时数据已经就位。tasktype.value 同样不再只在
分支里更新,1→2 这一跳现在也会通知 UI。
3. 详情页以 meeting 表那份为准(它是历史上唯一每次状态变化都会写的),给**存量**错位
数据兜底:不用等下一次状态跳变就能立刻正常。
真机验证(华为 ALN-AL80):用那条 details=0/meeting=1 的错位记录进详情,等待态正常
(改前是空白页);退出重进,等待态仍在;退出后查库两张表都是 1;轮询把状态推到失败态后
两张表都是 10002(改前 details 恒为 0);终态下等待态正确消失、显示「分析失败」。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main
6 changed files with 217 additions and 56 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,175 @@ |
|||
import 'dart:math' as math; |
|||
|
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
|
|||
import '../../../../core/theme/app_palette.dart'; |
|||
|
|||
/// 转写 / 分析进行中的等待态。 |
|||
/// |
|||
/// 取代原来的 `assets/lottie/meeting_wait.json`(一张「JOIN」对话气泡插画)—— |
|||
/// 那张图和本页在做的事(把音频变成文稿)没有任何关系,风格也与全站的 |
|||
/// 222° 冷蓝灰不搭。现在改成**自绘**:上面一排声波表示音频正在被读, |
|||
/// 下面几条带流光的骨架行预告即将生成的文稿形状,两者共用同一个 |
|||
/// [AnimationController]。 |
|||
/// |
|||
/// 自绘而不是再换一个 Lottie,是因为:① 不用为一个等待态再背一份 json 资源; |
|||
/// ② 颜色全部走 [AppPalette],明暗主题自动跟随,插画做不到; |
|||
/// ③ 四个 tab(概览/发言/全文/思维导图)共用同一个等待态,自绘好调。 |
|||
class TranscribingPlaceholder extends StatefulWidget { |
|||
const TranscribingPlaceholder(this.title, {super.key}); |
|||
|
|||
/// 状态文案,已含省略号(`transcribing` / `analyzing`),这里不要再补点。 |
|||
final String title; |
|||
|
|||
@override |
|||
State<TranscribingPlaceholder> createState() => |
|||
_TranscribingPlaceholderState(); |
|||
} |
|||
|
|||
class _TranscribingPlaceholderState extends State<TranscribingPlaceholder> |
|||
with SingleTickerProviderStateMixin { |
|||
late final AnimationController _c; |
|||
|
|||
/// 骨架行的宽度占比,模拟一段自然段落的长短参差。 |
|||
static const List<double> _lineWidths = <double>[1.0, 0.94, 0.82, 0.62]; |
|||
|
|||
/// 声波柱数。奇数,让中间那根成为视觉锚点。 |
|||
static const int _barCount = 7; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_c = AnimationController( |
|||
vsync: this, |
|||
duration: const Duration(milliseconds: 1600), |
|||
)..repeat(); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_c.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final bool isDark = Theme.of(context).brightness == Brightness.dark; |
|||
return Center( |
|||
child: Padding( |
|||
// 往上抬一点:等待态居中偏上比死居中更像「内容马上要从这里长出来」 |
|||
padding: EdgeInsets.only(bottom: 64.h, left: 24.w, right: 24.w), |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
_waveform(isDark), |
|||
SizedBox(height: 36.h), |
|||
_skeleton(isDark), |
|||
SizedBox(height: 32.h), |
|||
Text( |
|||
widget.title, |
|||
style: TextStyle( |
|||
fontSize: 15.sp, |
|||
fontWeight: FontWeight.w500, |
|||
color: AppPalette.textSecondary(isDark), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 声波:每根柱子给一个固定相位差,整排看起来像一道行进的波而不是随机跳动。 |
|||
Widget _waveform(bool isDark) { |
|||
return SizedBox( |
|||
height: 40.h, |
|||
child: AnimatedBuilder( |
|||
animation: _c, |
|||
builder: (_, __) { |
|||
return Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
crossAxisAlignment: CrossAxisAlignment.center, |
|||
children: List<Widget>.generate(_barCount, (int i) { |
|||
final double phase = i / _barCount; |
|||
// 0..1 的正弦,取绝对值让波谷也留一点高度,柱子不会缩到看不见 |
|||
final double v = |
|||
(math.sin((_c.value + phase) * 2 * math.pi) + 1) / 2; |
|||
return Padding( |
|||
padding: EdgeInsets.symmetric(horizontal: 3.w), |
|||
child: Container( |
|||
width: 4.w, |
|||
height: 10.h + 26.h * v, |
|||
decoration: BoxDecoration( |
|||
color: AppPalette.accent.withValues(alpha: 0.35 + 0.5 * v), |
|||
borderRadius: BorderRadius.circular(2.r), |
|||
), |
|||
), |
|||
); |
|||
}), |
|||
); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 骨架行 + 流光。流光用 [ShaderMask] 叠在实心色块上, |
|||
/// `srcATop` 保证亮带只出现在色块内部、不会溢到行与行之间的空隙。 |
|||
Widget _skeleton(bool isDark) { |
|||
final Color base = AppPalette.border(isDark); |
|||
final Color highlight = isDark |
|||
? AppPalette.brand500.withValues(alpha: 0.85) |
|||
: Colors.white.withValues(alpha: 0.9); |
|||
|
|||
return AnimatedBuilder( |
|||
animation: _c, |
|||
builder: (_, __) { |
|||
return ShaderMask( |
|||
blendMode: BlendMode.srcATop, |
|||
shaderCallback: (Rect rect) => LinearGradient( |
|||
begin: Alignment.centerLeft, |
|||
end: Alignment.centerRight, |
|||
colors: <Color>[ |
|||
highlight.withValues(alpha: 0), |
|||
highlight, |
|||
highlight.withValues(alpha: 0), |
|||
], |
|||
stops: const <double>[0.30, 0.50, 0.70], |
|||
transform: _SlideGradient(_c.value * 2 - 1), |
|||
).createShader(rect), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: <Widget>[ |
|||
for (int i = 0; i < _lineWidths.length; i++) ...<Widget>[ |
|||
if (i > 0) SizedBox(height: 14.h), |
|||
FractionallySizedBox( |
|||
widthFactor: _lineWidths[i], |
|||
child: Container( |
|||
height: 12.h, |
|||
decoration: BoxDecoration( |
|||
color: base, |
|||
borderRadius: BorderRadius.circular(6.r), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
|
|||
/// 把渐变横向平移 [dx](以自身宽度为单位)。 |
|||
/// |
|||
/// Flutter 只内置了 [GradientRotation],要让亮带扫过去只能自己写一个。 |
|||
class _SlideGradient extends GradientTransform { |
|||
const _SlideGradient(this.dx); |
|||
|
|||
final double dx; |
|||
|
|||
@override |
|||
Matrix4 transform(Rect bounds, {TextDirection? textDirection}) => |
|||
Matrix4.translationValues(bounds.width * dx, 0, 0); |
|||
} |
|||
Loading…
Reference in new issue