Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b9a090e0 | ||
|
|
c4adbd0665 | ||
|
|
66c03ee245 | ||
|
|
a3ac007759 | ||
|
|
525f58c150 | ||
|
|
0a02620564 | ||
|
|
78a33cd4f1 | ||
|
|
e8610b7aed |
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'dev_config.dart';
|
import 'dev_config.dart';
|
||||||
import 'prod_config.dart';
|
import 'prod_config.dart';
|
||||||
|
|
||||||
@@ -9,12 +11,10 @@ import 'prod_config.dart';
|
|||||||
/// - `dev_config.dart`:[DevConfig](本机联调)
|
/// - `dev_config.dart`:[DevConfig](本机联调)
|
||||||
/// - `prod_config.dart`:[ProdConfig](生产)
|
/// - `prod_config.dart`:[ProdConfig](生产)
|
||||||
///
|
///
|
||||||
/// 切换环境只改下面这一个布尔值,不用传任何构建参数、也不做网络探测。
|
/// Debug 使用开发环境,Release 自动使用生产环境,不做网络探测。
|
||||||
abstract final class AppConfig {
|
abstract final class AppConfig {
|
||||||
/// **环境开关**:true 用开发环境,false 用生产环境。
|
/// **环境开关**:Debug 使用开发环境,Release 使用生产环境。
|
||||||
///
|
static const bool useDevConfig = !kReleaseMode;
|
||||||
/// 打生产包前必须改成 false;release 构建若仍为 true 会在启动时直接报错。
|
|
||||||
static const bool useDevConfig = true;
|
|
||||||
|
|
||||||
/// 当前选中的环境地址。
|
/// 当前选中的环境地址。
|
||||||
static String get baseUrl =>
|
static String get baseUrl =>
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ abstract final class AppColors {
|
|||||||
/// 首页及剧集播放页视频信息文案:参考设计图的 65% 半透明白。
|
/// 首页及剧集播放页视频信息文案:参考设计图的 65% 半透明白。
|
||||||
static const homeFeedVideoCopy = Color(0xA6FFFFFF);
|
static const homeFeedVideoCopy = Color(0xA6FFFFFF);
|
||||||
|
|
||||||
/// 首页观看全集按钮:与浮层玻璃语言一致的半透明白表面。
|
|
||||||
static const watchAllSurface = Color(0x26FFFFFF);
|
|
||||||
|
|
||||||
/// 观看全集按钮的极淡白色轮廓。
|
|
||||||
static const watchAllBorder = Color(0x0FFFFFFF);
|
|
||||||
|
|
||||||
/// 主操作、选中态、播放进度。旧品牌橙已全量映射为品牌红(2026-10 改版)。
|
/// 主操作、选中态、播放进度。旧品牌橙已全量映射为品牌红(2026-10 改版)。
|
||||||
static const blue = Color(0xFFF04E4E);
|
static const blue = Color(0xFFF04E4E);
|
||||||
static const primary = blue;
|
static const primary = blue;
|
||||||
|
|||||||
@@ -35,8 +35,13 @@ const _metadataStrutStyle = StrutStyle(
|
|||||||
const double _watchAllInset = 12;
|
const double _watchAllInset = 12;
|
||||||
const double _watchAllRadius = 6;
|
const double _watchAllRadius = 6;
|
||||||
const double _watchAllFont = 16;
|
const double _watchAllFont = 16;
|
||||||
|
const _watchAllGradient = LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: [AppColors.mineStatShade, AppColors.brandRed, AppColors.vipButtonEnd],
|
||||||
|
);
|
||||||
|
|
||||||
/// 「观看完整剧集」按钮高度,overlay 用它安排底部避让。
|
/// 「观看完整剧集」渐变按钮高度,overlay 用它安排底部避让。
|
||||||
const double homeFeedWatchAllHeight = _watchAllHeight;
|
const double homeFeedWatchAllHeight = _watchAllHeight;
|
||||||
|
|
||||||
/// 播放量紧凑展示(跟随语言):zh 20万 / en 200K / id 200 rb。
|
/// 播放量紧凑展示(跟随语言):zh 20万 / en 200K / id 200 rb。
|
||||||
@@ -207,13 +212,8 @@ class HomeFeedWatchAllBar extends StatelessWidget {
|
|||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(_watchAllRadius),
|
borderRadius: BorderRadius.circular(_watchAllRadius),
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
key: const Key('home-feed-full-series-surface'),
|
key: const Key('home-feed-full-series-gradient'),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(gradient: _watchAllGradient),
|
||||||
color: AppColors.watchAllSurface,
|
|
||||||
border: Border.fromBorderSide(
|
|
||||||
BorderSide(color: AppColors.watchAllBorder),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Material(
|
child: Material(
|
||||||
key: const Key('home-feed-full-series-bg'),
|
key: const Key('home-feed-full-series-bg'),
|
||||||
color: AppColors.transparent,
|
color: AppColors.transparent,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ const double _defaultIconSize = 32;
|
|||||||
const double _defaultCountFont = 12;
|
const double _defaultCountFont = 12;
|
||||||
const double _defaultIconCountGap = 2;
|
const double _defaultIconCountGap = 2;
|
||||||
const double _defaultItemGap = 24;
|
const double _defaultItemGap = 24;
|
||||||
const _tapDuration = Duration(milliseconds: 220);
|
const _tapDuration = Duration(milliseconds: 360);
|
||||||
const double _popScale = 1.14;
|
const double _popScale = 1.34;
|
||||||
const double _cancelScale = 0.86;
|
const double _cancelScale = 0.68;
|
||||||
const double _popWeight = 40;
|
const double _popWeight = 40;
|
||||||
const double _settleWeight = 60;
|
const double _settleWeight = 60;
|
||||||
final _growTween = _tapTween(_popScale);
|
final _growTween = _tapTween(_popScale);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class PlayerTapGestureRegion extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> {
|
class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> {
|
||||||
static const Duration _singleTapDelay = Duration(milliseconds: 240);
|
static const Duration _singleTapDelay = Duration(milliseconds: 320);
|
||||||
|
|
||||||
Timer? _singleTapTimer;
|
Timer? _singleTapTimer;
|
||||||
DateTime? _lastDownAt;
|
DateTime? _lastDownAt;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
// 应用环境选择测试。
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:fruitshorts/core/config/app_config.dart';
|
||||||
|
import 'package:fruitshorts/core/config/dev_config.dart';
|
||||||
|
import 'package:fruitshorts/core/config/prod_config.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('should_use_environment_matching_build_mode', () {
|
||||||
|
expect(AppConfig.useDevConfig, !kReleaseMode);
|
||||||
|
expect(
|
||||||
|
AppConfig.baseUrl,
|
||||||
|
kReleaseMode ? ProdConfig.baseUrl : DevConfig.baseUrl,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -260,16 +260,23 @@ void main() {
|
|||||||
_expectNoShadows(tester);
|
_expectNoShadows(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_use_reference_surface_when_watching_all', (tester) async {
|
testWidgets('should_use_small_radius_and_brand_gradient_when_watching_all', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
await _pump(tester);
|
await _pump(tester);
|
||||||
final background = find.byKey(const Key('home-feed-full-series-bg'));
|
final background = find.byKey(const Key('home-feed-full-series-bg'));
|
||||||
final decorated = tester.widget<DecoratedBox>(
|
final decorated = tester.widget<DecoratedBox>(
|
||||||
find.byKey(const Key('home-feed-full-series-surface')),
|
find.byKey(const Key('home-feed-full-series-gradient')),
|
||||||
);
|
);
|
||||||
final decoration = decorated.decoration as BoxDecoration;
|
final decoration = decorated.decoration as BoxDecoration;
|
||||||
expect(decoration.color, AppColors.watchAllSurface);
|
expect((decoration.gradient! as LinearGradient).colors, [
|
||||||
expect(decoration.border?.top.color, AppColors.watchAllBorder);
|
AppColors.mineStatShade,
|
||||||
expect(decoration.gradient, isNull);
|
AppColors.brandRed,
|
||||||
|
AppColors.vipButtonEnd,
|
||||||
|
]);
|
||||||
|
final gradient = decoration.gradient! as LinearGradient;
|
||||||
|
expect(gradient.begin, Alignment.centerLeft);
|
||||||
|
expect(gradient.end, Alignment.centerRight);
|
||||||
expect(tester.widget<Text>(find.text('Watch all')).style?.fontSize, 16);
|
expect(tester.widget<Text>(find.text('Watch all')).style?.fontSize, 16);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Text>(find.text('Watch all')).style?.fontWeight,
|
tester.widget<Text>(find.text('Watch all')).style?.fontWeight,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
// 职责:检查观看全集按钮的玻璃表面确实被绘制出来。
|
// 职责:检查观看全集按钮实际绘制像素,防止渐变只配置而不可见。
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
@@ -7,19 +7,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
|
||||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/home_feed_recommend_panel.dart';
|
import 'package:fruitshorts/pages/player/widgets/home_feed_recommend_panel.dart';
|
||||||
|
|
||||||
const _boundaryKey = Key('watch-surface-boundary');
|
const _boundaryKey = Key('watch-gradient-boundary');
|
||||||
const _width = 280.0;
|
const _width = 280.0;
|
||||||
const _sampleInset = 16;
|
const _sampleInset = 16;
|
||||||
const _sampleY = 4;
|
const _sampleY = 4;
|
||||||
const _rgbaChannels = 4;
|
const _rgbaChannels = 4;
|
||||||
const _surfaceTolerance = 0.01;
|
const _minimumRedDelta = 50;
|
||||||
const _drama = Drama(
|
const _drama = Drama(
|
||||||
id: 'surface',
|
id: 'gradient',
|
||||||
title: 'Surface',
|
title: 'Gradient',
|
||||||
synopsis: '',
|
synopsis: '',
|
||||||
posterUrl: '',
|
posterUrl: '',
|
||||||
);
|
);
|
||||||
@@ -29,16 +28,12 @@ Future<void> _pumpButton(WidgetTester tester) => tester.pumpWidget(
|
|||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
backgroundColor: Colors.black,
|
|
||||||
body: Center(
|
body: Center(
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
key: _boundaryKey,
|
key: _boundaryKey,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: _width,
|
width: _width,
|
||||||
child: ColoredBox(
|
child: HomeFeedWatchAllBar(drama: _drama, onTap: () {}),
|
||||||
color: Colors.black,
|
|
||||||
child: HomeFeedWatchAllBar(drama: _drama, onTap: () {}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -57,7 +52,7 @@ Color _sample(ByteData bytes, int width, int x) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('should_paint_reference_dark_surface_when_watching_all', (
|
testWidgets('should_paint_visible_red_gradient_instead_of_flat_fill', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await _pumpButton(tester);
|
await _pumpButton(tester);
|
||||||
@@ -73,21 +68,11 @@ void main() {
|
|||||||
final middle = _sample(bytes, image.width, image.width ~/ 2);
|
final middle = _sample(bytes, image.width, image.width ~/ 2);
|
||||||
final right = _sample(bytes, image.width, image.width - _sampleInset);
|
final right = _sample(bytes, image.width, image.width - _sampleInset);
|
||||||
image.dispose();
|
image.dispose();
|
||||||
for (final sample in [left, middle, right]) {
|
expect(left.a, 1);
|
||||||
expect(sample.a, 1);
|
expect(right.a, 1);
|
||||||
expect(
|
expect((right.r - left.r) * 255, greaterThan(_minimumRedDelta));
|
||||||
sample.r,
|
expect(left.g, lessThan(middle.g));
|
||||||
closeTo(AppColors.watchAllSurface.a, _surfaceTolerance),
|
expect(middle.g, lessThan(right.g));
|
||||||
);
|
|
||||||
expect(
|
|
||||||
sample.g,
|
|
||||||
closeTo(AppColors.watchAllSurface.a, _surfaceTolerance),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
sample.b,
|
|
||||||
closeTo(AppColors.watchAllSurface.a, _surfaceTolerance),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -16,10 +16,8 @@ import '../support/fake_haptics.dart';
|
|||||||
import '../support/fake_interaction_repositories.dart';
|
import '../support/fake_interaction_repositories.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('should_like_without_pausing_when_double_tap_within_240ms', (
|
testWidgets('double tap within 320ms never pauses', (tester) async {
|
||||||
tester,
|
for (final delay in [120, 250, 300]) {
|
||||||
) async {
|
|
||||||
for (final delay in [80, 160, 220]) {
|
|
||||||
final repository = FakeInteractionRepository();
|
final repository = FakeInteractionRepository();
|
||||||
final haptics = FakeHaptics();
|
final haptics = FakeHaptics();
|
||||||
final key = GlobalKey<_HarnessState>();
|
final key = GlobalKey<_HarnessState>();
|
||||||
@@ -55,7 +53,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_toggle_twice_when_taps_are_260ms_apart', (tester) async {
|
testWidgets('taps 500ms apart toggle playback twice', (tester) async {
|
||||||
final key = GlobalKey<_HarnessState>();
|
final key = GlobalKey<_HarnessState>();
|
||||||
final clock = _TestClock();
|
final clock = _TestClock();
|
||||||
await _pumpHarness(
|
await _pumpHarness(
|
||||||
@@ -69,15 +67,15 @@ void main() {
|
|||||||
await tester.tapAt(
|
await tester.tapAt(
|
||||||
tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
|
tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
|
||||||
);
|
);
|
||||||
clock.advance(const Duration(milliseconds: 260));
|
clock.advance(const Duration(milliseconds: 500));
|
||||||
await tester.pump(const Duration(milliseconds: 260));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
expect(key.currentState!.pauseToggles, 1);
|
expect(key.currentState!.pauseToggles, 1);
|
||||||
|
|
||||||
await tester.tapAt(
|
await tester.tapAt(
|
||||||
tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
|
tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
|
||||||
);
|
);
|
||||||
clock.advance(const Duration(milliseconds: 260));
|
clock.advance(const Duration(milliseconds: 500));
|
||||||
await tester.pump(const Duration(milliseconds: 260));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
expect(key.currentState!.pauseToggles, 2);
|
expect(key.currentState!.pauseToggles, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ const _popFrame = Duration(milliseconds: 100);
|
|||||||
const _buttonSize = 48.0;
|
const _buttonSize = 48.0;
|
||||||
const _compactGap = 6.0;
|
const _compactGap = 6.0;
|
||||||
const _epsilon = 0.01;
|
const _epsilon = 0.01;
|
||||||
const _cancelledMinimum = 0.8;
|
const _cancelledMaximum = 0.8;
|
||||||
const _popMinimum = 1.03;
|
const _popMinimum = 1.22;
|
||||||
const _popMaximum = 1.18;
|
|
||||||
const _burstKey = Key('rail-tap-burst');
|
const _burstKey = Key('rail-tap-burst');
|
||||||
const _directionFrame = Duration(milliseconds: 60);
|
const _directionFrame = Duration(milliseconds: 60);
|
||||||
const _directionFrames = 2;
|
const _directionFrames = 4;
|
||||||
const _motionCompletion = Duration(milliseconds: 230);
|
|
||||||
|
|
||||||
Future<void> _pumpRail(
|
Future<void> _pumpRail(
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
@@ -117,9 +115,7 @@ void main() {
|
|||||||
await tester.pump(_popFrame);
|
await tester.pump(_popFrame);
|
||||||
expect(
|
expect(
|
||||||
_scale(tester, key),
|
_scale(tester, key),
|
||||||
selected
|
selected ? lessThan(_cancelledMaximum) : greaterThan(_popMinimum),
|
||||||
? allOf(greaterThan(_cancelledMinimum), lessThan(1))
|
|
||||||
: allOf(greaterThan(_popMinimum), lessThan(_popMaximum)),
|
|
||||||
);
|
);
|
||||||
_expectBurst(tester, key, selected);
|
_expectBurst(tester, key, selected);
|
||||||
expect(tester.getRect(_button(key)), rect);
|
expect(tester.getRect(_button(key)), rect);
|
||||||
@@ -159,15 +155,6 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('should_finish_feedback_within_230ms', (tester) async {
|
|
||||||
await _pumpRail(tester, onTap: () {});
|
|
||||||
await tester.tap(_button(_likeKey));
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(_motionCompletion);
|
|
||||||
expect(find.byKey(_burstKey), findsNothing);
|
|
||||||
expect(_scale(tester, _likeKey), closeTo(1, _epsilon));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('should_keep_callbacks_without_motion_when_reduced', (
|
testWidgets('should_keep_callbacks_without_motion_when_reduced', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
Reference in new issue
Block a user