feat: норм кружки и гс

Голосовые: индекс opus/ogg со срезами страниц, общий аудиоконтроллер
воспроизведения, переписанный ogg-энкодер. Кружки: предзагрузка видео,
геометрия кольца прогресса.

third_party/kolibri в .gitignore — локальный dev-клон, зависимость
берётся с pub.dev.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VMWkmNx3Ns9SEuKeGEiaR4
This commit is contained in:
Jganenokk
2026-08-07 00:23:52 +07:00
co-authored by Claude Opus 5
parent 79c743e86a
commit b9d69ea37b
14 changed files with 2274 additions and 393 deletions
+141
View File
@@ -0,0 +1,141 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/frontend/widgets/attachment/bubbles/video_note_bubble.dart';
const _extent = 210.0;
const _knob = 7.0;
const _geometry = NoteRingGeometry(extent: _extent, knobRadius: _knob);
void main() {
test('ручка идёт по ободу от 12 часов по часовой', () {
final center = _geometry.center;
final r = _geometry.radius;
expect(_geometry.knobCenter(0).dx, closeTo(center.dx, 0.001));
expect(_geometry.knobCenter(0).dy, closeTo(center.dy - r, 0.001));
expect(_geometry.knobCenter(0.25).dx, closeTo(center.dx + r, 0.001));
expect(_geometry.knobCenter(0.25).dy, closeTo(center.dy, 0.001));
expect(_geometry.knobCenter(0.5).dy, closeTo(center.dy + r, 0.001));
expect(_geometry.knobCenter(0.75).dx, closeTo(center.dx - r, 0.001));
expect(_geometry.knobCenter(1).dx, closeTo(center.dx, 0.001));
expect(_geometry.knobCenter(1).dy, closeTo(center.dy - r, 0.001));
});
test('ручка целиком помещается в бокс и не обрезается', () {
for (var i = 0; i <= 100; i++) {
final knob = _geometry.knobCenter(i / 100);
expect(knob.dx - _knob, greaterThanOrEqualTo(0));
expect(knob.dy - _knob, greaterThanOrEqualTo(0));
expect(knob.dx + _knob, lessThanOrEqualTo(_extent));
expect(knob.dy + _knob, lessThanOrEqualTo(_extent));
}
});
test('центр кружка остаётся под тап, обод и ручка ловят драг', () {
expect(
_geometry.grabs(_geometry.center, 0),
isFalse,
reason: 'центр должен переключать воспроизведение, а не мотать',
);
expect(_geometry.grabs(_geometry.knobCenter(0.4), 0.4), isTrue);
expect(
_geometry.grabs(_geometry.knobCenter(0.4) + const Offset(0, 18), 0.4),
isTrue,
reason: 'промах мимо ручки в пределах допуска всё ещё считается',
);
final onBand = _geometry.center + Offset(_geometry.radius, 0);
expect(_geometry.grabs(onBand, 0), isTrue);
final wellInside = _geometry.center + Offset(_geometry.radius / 2, 0);
expect(_geometry.grabs(wellInside, 0), isFalse);
});
test('тап по ободу попадает ровно в свою долю', () {
for (final progress in [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 0.999]) {
expect(
_geometry.progressAt(_geometry.knobCenter(progress)),
closeTo(progress, 0.0001),
reason: 'тап по точке $progress',
);
}
});
test('тап работает на любом удалении от обода вдоль того же луча', () {
final center = _geometry.center;
for (final radius in [_geometry.radius - 10, _geometry.radius + 5]) {
final point = center + Offset(radius, 0);
expect(_geometry.progressAt(point), closeTo(0.25, 0.0001));
}
});
test('захват абсолютный: драг ведёт ручку ровно под пальцем', () {
for (final route in [
[0.1, 0.3],
[0.4, 0.2],
[0.7, 0.95],
[0.0, 0.15],
]) {
final from = _geometry.knobCenter(route[0]);
final to = _geometry.knobCenter(route[1]);
final grabbed = _geometry.progressAt(from);
expect(
grabbed,
closeTo(route[0], 1e-4),
reason: 'захват должен встать в точку пальца, а не в текущую позицию',
);
final delta = NoteRingGeometry.angleDelta(
_geometry.angleAt(from),
_geometry.angleAt(to),
);
expect(
_geometry.advance(grabbed, delta),
closeTo(route[1], 1e-4),
reason: 'ручка отстала от пальца на маршруте $route',
);
}
});
test('прокрутка мимо конца упирается, а не заворачивается в начало', () {
final from = _geometry.knobCenter(0.9);
final to = _geometry.knobCenter(0.1);
final delta = NoteRingGeometry.angleDelta(
_geometry.angleAt(from),
_geometry.angleAt(to),
);
expect(delta, greaterThan(0));
expect(_geometry.advance(_geometry.progressAt(from), delta), 1.0);
});
test('переход через 12 часов не перебрасывает позицию', () {
final before = _geometry.angleAt(
_geometry.center + const Offset(-4, -90),
);
final after = _geometry.angleAt(_geometry.center + const Offset(4, -90));
final delta = NoteRingGeometry.angleDelta(before, after);
expect(delta.abs(), lessThan(0.3), reason: 'скачок вместо плавного шага');
expect(delta, greaterThan(0));
expect(_geometry.advance(0.99, delta), 1.0);
expect(_geometry.advance(0.01, -delta), greaterThanOrEqualTo(0.0));
});
test('прогресс зажат в границах при бесконечной прокрутке', () {
expect(_geometry.advance(0.5, 100 * math.pi), 1.0);
expect(_geometry.advance(0.5, -100 * math.pi), 0.0);
});
test('увеличенный кружок сохраняет пропорции обода', () {
const big = NoteRingGeometry(extent: 357, knobRadius: 9);
expect(big.radius, closeTo(357 / 2 - 10, 0.001));
expect(big.knobCenter(0).dy, closeTo(big.center.dy - big.radius, 0.001));
expect(big.knobCenter(0.5).dy + 9, lessThanOrEqualTo(357));
});
}