Compare commits
9
Commits
2ddf9ee1c6
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c03ee245 | ||
|
|
a3ac007759 | ||
|
|
8ec6df2be5 | ||
|
|
5ec4db151a | ||
|
|
525f58c150 | ||
|
|
a47af8c3ad | ||
|
|
0a02620564 | ||
|
|
78a33cd4f1 | ||
|
|
e8610b7aed |
No files matched your search
@@ -386,15 +386,29 @@ final class PipShadowCoordinator: NSObject, AVPictureInPictureControllerDelegate
|
|||||||
guard call.method == "shareText",
|
guard call.method == "shareText",
|
||||||
let arguments = call.arguments as? [String: Any],
|
let arguments = call.arguments as? [String: Any],
|
||||||
let text = arguments["text"] as? String,
|
let text = arguments["text"] as? String,
|
||||||
!text.isEmpty,
|
!text.isEmpty else {
|
||||||
let controller = self?.window?.rootViewController else {
|
|
||||||
result(FlutterError(code: "share_unavailable", message: "Share sheet is unavailable", details: nil))
|
result(FlutterError(code: "share_unavailable", message: "Share sheet is unavailable", details: nil))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil)
|
DispatchQueue.main.async {
|
||||||
activity.popoverPresentationController?.sourceView = controller.view
|
guard let controller = self?.sharePresenter() else {
|
||||||
activity.completionWithItemsHandler = { _, _, _, _ in result(nil) }
|
result(FlutterError(code: "share_unavailable", message: "Share sheet is unavailable", details: nil))
|
||||||
controller.present(activity, animated: true)
|
return
|
||||||
|
}
|
||||||
|
let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil)
|
||||||
|
if let popover = activity.popoverPresentationController {
|
||||||
|
popover.sourceView = controller.view
|
||||||
|
popover.sourceRect = CGRect(
|
||||||
|
x: controller.view.bounds.midX,
|
||||||
|
y: controller.view.bounds.midY,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
)
|
||||||
|
popover.permittedArrowDirections = []
|
||||||
|
}
|
||||||
|
activity.completionWithItemsHandler = { _, _, _, _ in result(nil) }
|
||||||
|
controller.present(activity, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let thumbnailChannel = FlutterMethodChannel(
|
let thumbnailChannel = FlutterMethodChannel(
|
||||||
@@ -459,6 +473,34 @@ final class PipShadowCoordinator: NSObject, AVPictureInPictureControllerDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当前前台 Scene 的根控制器之上,最适合展示系统分享面板的控制器。
|
||||||
|
private func sharePresenter() -> UIViewController? {
|
||||||
|
let keyWindow = UIApplication.shared.connectedScenes
|
||||||
|
.compactMap { $0 as? UIWindowScene }
|
||||||
|
.first { $0.activationState == .foregroundActive }?
|
||||||
|
.windows.first { $0.isKeyWindow }
|
||||||
|
guard let rootController = keyWindow?.rootViewController else { return nil }
|
||||||
|
let presenter = topPresentedController(from: rootController)
|
||||||
|
guard presenter.viewIfLoaded?.window != nil else { return nil }
|
||||||
|
return presenter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析已展示页面、导航栈和标签页中的当前可见控制器。
|
||||||
|
private func topPresentedController(from controller: UIViewController) -> UIViewController {
|
||||||
|
if let presented = controller.presentedViewController {
|
||||||
|
return topPresentedController(from: presented)
|
||||||
|
}
|
||||||
|
if let navigation = controller as? UINavigationController,
|
||||||
|
let visible = navigation.visibleViewController {
|
||||||
|
return topPresentedController(from: visible)
|
||||||
|
}
|
||||||
|
if let tabs = controller as? UITabBarController,
|
||||||
|
let selected = tabs.selectedViewController {
|
||||||
|
return topPresentedController(from: selected)
|
||||||
|
}
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
/// iOS 画中画:影子播放器通道,与 Android 共用同一 channel 名。
|
/// iOS 画中画:影子播放器通道,与 Android 共用同一 channel 名。
|
||||||
private func registerPipChannel(_ engineBridge: FlutterImplicitEngineBridge) {
|
private func registerPipChannel(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
let channel = FlutterMethodChannel(
|
let channel = FlutterMethodChannel(
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ abstract final class AppColors {
|
|||||||
static const overlaySecondary = Color(0xFFC7C7CC);
|
static const overlaySecondary = Color(0xFFC7C7CC);
|
||||||
static const overlayLabel = Color(0xFFE6E6EB);
|
static const overlayLabel = Color(0xFFE6E6EB);
|
||||||
|
|
||||||
|
/// 首页及剧集播放页视频信息文案:参考设计图的 65% 半透明白。
|
||||||
|
static const homeFeedVideoCopy = Color(0xA6FFFFFF);
|
||||||
|
|
||||||
/// 主操作、选中态、播放进度。旧品牌橙已全量映射为品牌红(2026-10 改版)。
|
/// 主操作、选中态、播放进度。旧品牌橙已全量映射为品牌红(2026-10 改版)。
|
||||||
static const blue = Color(0xFFF04E4E);
|
static const blue = Color(0xFFF04E4E);
|
||||||
static const primary = blue;
|
static const primary = blue;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const double _synopsisToggleWidthFraction = 0.55;
|
|||||||
const double _synopsisToggleGap = 6;
|
const double _synopsisToggleGap = 6;
|
||||||
const _synopsisMotion = Duration(milliseconds: 240);
|
const _synopsisMotion = Duration(milliseconds: 240);
|
||||||
const _synopsisStyle = TextStyle(
|
const _synopsisStyle = TextStyle(
|
||||||
color: AppColors.overlaySecondary,
|
|
||||||
fontSize: _synopsisFont,
|
fontSize: _synopsisFont,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
height: _synopsisLineHeight,
|
height: _synopsisLineHeight,
|
||||||
@@ -32,6 +31,7 @@ class ExpandableSynopsis extends StatefulWidget {
|
|||||||
required this.synopsis,
|
required this.synopsis,
|
||||||
this.keyPrefix = 'home-feed',
|
this.keyPrefix = 'home-feed',
|
||||||
this.expandedFooter,
|
this.expandedFooter,
|
||||||
|
this.textColor = AppColors.homeFeedVideoCopy,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 按当前语言获取的简介正文。
|
/// 按当前语言获取的简介正文。
|
||||||
@@ -43,6 +43,9 @@ class ExpandableSynopsis extends StatefulWidget {
|
|||||||
/// 展开后附加的声明等内容。
|
/// 展开后附加的声明等内容。
|
||||||
final Widget? expandedFooter;
|
final Widget? expandedFooter;
|
||||||
|
|
||||||
|
/// 简介正文颜色;默认使用视频信息区统一的半透明浅灰白。
|
||||||
|
final Color textColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ExpandableSynopsis> createState() => _ExpandableSynopsisState();
|
State<ExpandableSynopsis> createState() => _ExpandableSynopsisState();
|
||||||
}
|
}
|
||||||
@@ -88,7 +91,7 @@ class _ExpandableSynopsisState extends State<ExpandableSynopsis> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _needsToggle(double maxWidth) {
|
bool _needsToggle(double maxWidth) {
|
||||||
final painter = _painter(widget.synopsis, _synopsisStyle);
|
final painter = _painter(widget.synopsis, _bodyStyle);
|
||||||
painter.layout(maxWidth: maxWidth);
|
painter.layout(maxWidth: maxWidth);
|
||||||
final overflows = painter.didExceedMaxLines;
|
final overflows = painter.didExceedMaxLines;
|
||||||
painter.dispose();
|
painter.dispose();
|
||||||
@@ -120,11 +123,14 @@ class _ExpandableSynopsisState extends State<ExpandableSynopsis> {
|
|||||||
|
|
||||||
Key _key(String suffix) => Key('${widget.keyPrefix}-synopsis-$suffix');
|
Key _key(String suffix) => Key('${widget.keyPrefix}-synopsis-$suffix');
|
||||||
|
|
||||||
|
TextStyle get _bodyStyle => _synopsisStyle.copyWith(color: widget.textColor);
|
||||||
|
|
||||||
Widget _body() {
|
Widget _body() {
|
||||||
|
final style = _bodyStyle;
|
||||||
final text = Text.rich(
|
final text = Text.rich(
|
||||||
TextSpan(text: widget.synopsis, style: _synopsisStyle),
|
TextSpan(text: widget.synopsis, style: style),
|
||||||
key: _expanded ? _key('expanded') : Key('${widget.keyPrefix}-synopsis'),
|
key: _expanded ? _key('expanded') : Key('${widget.keyPrefix}-synopsis'),
|
||||||
style: _synopsisStyle,
|
style: style,
|
||||||
maxLines: _expanded ? null : _collapsedSynopsisLines,
|
maxLines: _expanded ? null : _collapsedSynopsisLines,
|
||||||
overflow: _expanded ? TextOverflow.clip : TextOverflow.ellipsis,
|
overflow: _expanded ? TextOverflow.clip : TextOverflow.ellipsis,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class _TagChip extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
strutStyle: _metadataStrutStyle,
|
strutStyle: _metadataStrutStyle,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.overlaySecondary,
|
color: AppColors.homeFeedVideoCopy,
|
||||||
fontSize: _tagFont,
|
fontSize: _tagFont,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
height: _metadataLineHeight,
|
height: _metadataLineHeight,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const double _progressHeight = 2;
|
|||||||
const double _progressHitHeight = 16;
|
const double _progressHitHeight = 16;
|
||||||
const double _pageHorizontal = 20;
|
const double _pageHorizontal = 20;
|
||||||
const double _railRight = 14;
|
const double _railRight = 14;
|
||||||
|
const double _railShiftRight = 8;
|
||||||
const double _railGap = 12;
|
const double _railGap = 12;
|
||||||
const double _railWidth = 56;
|
const double _railWidth = 56;
|
||||||
const double _infoWatchAllGap = 2;
|
const double _infoWatchAllGap = 2;
|
||||||
@@ -234,7 +235,10 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
|||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: _railBottomOffset,
|
bottom: _railBottomOffset,
|
||||||
),
|
),
|
||||||
child: _rail(),
|
child: Transform.translate(
|
||||||
|
offset: const Offset(_railShiftRight, 0),
|
||||||
|
child: _rail(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
|||||||
ExpandableSynopsis(
|
ExpandableSynopsis(
|
||||||
key: ValueKey(widget.drama.id),
|
key: ValueKey(widget.drama.id),
|
||||||
synopsis: synopsis,
|
synopsis: synopsis,
|
||||||
|
textColor: AppColors.homeFeedVideoCopy,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -136,7 +136,7 @@ void _expectExpandedSynopsis(WidgetTester tester) {
|
|||||||
final expanded = find.byKey(const Key('home-feed-synopsis-expanded'));
|
final expanded = find.byKey(const Key('home-feed-synopsis-expanded'));
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Text>(expanded).textSpan?.style?.color,
|
tester.widget<Text>(expanded).textSpan?.style?.color,
|
||||||
AppColors.overlaySecondary,
|
AppColors.homeFeedVideoCopy,
|
||||||
);
|
);
|
||||||
_expectNoShadows(tester);
|
_expectNoShadows(tester);
|
||||||
_expectWatchGap(tester);
|
_expectWatchGap(tester);
|
||||||
@@ -187,7 +187,7 @@ void main() {
|
|||||||
matching: find.byType(Text),
|
matching: find.byType(Text),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(tag.style?.color, AppColors.overlaySecondary);
|
expect(tag.style?.color, AppColors.homeFeedVideoCopy);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_skip_motion_when_system_disables_animations', (
|
testWidgets('should_skip_motion_when_system_disables_animations', (
|
||||||
@@ -343,7 +343,7 @@ void main() {
|
|||||||
final synopsis = tester.widget<Text>(
|
final synopsis = tester.widget<Text>(
|
||||||
find.byKey(const Key('home-feed-synopsis')),
|
find.byKey(const Key('home-feed-synopsis')),
|
||||||
);
|
);
|
||||||
expect(synopsis.style?.color, AppColors.overlaySecondary);
|
expect(synopsis.style?.color, AppColors.homeFeedVideoCopy);
|
||||||
expect(synopsis.style?.fontSize, 15);
|
expect(synopsis.style?.fontSize, 15);
|
||||||
expect(synopsis.maxLines, 2);
|
expect(synopsis.maxLines, 2);
|
||||||
final episode = tester.widget<Text>(
|
final episode = tester.widget<Text>(
|
||||||
@@ -353,7 +353,7 @@ void main() {
|
|||||||
expect(episode.style?.fontSize, 14);
|
expect(episode.style?.fontSize, 14);
|
||||||
expect(episode.data, 'Episode 2 / 24');
|
expect(episode.data, 'Episode 2 / 24');
|
||||||
final tag = tester.widget<Text>(find.text('Romance'));
|
final tag = tester.widget<Text>(find.text('Romance'));
|
||||||
expect(tag.style?.color, AppColors.overlaySecondary);
|
expect(tag.style?.color, AppColors.homeFeedVideoCopy);
|
||||||
expect(tag.style?.color, synopsis.style?.color);
|
expect(tag.style?.color, synopsis.style?.color);
|
||||||
expect(tag.style?.fontSize, 13);
|
expect(tag.style?.fontSize, 13);
|
||||||
final hotSearch = tester.widget<Text>(
|
final hotSearch = tester.widget<Text>(
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
tester.getRect(find.byKey(const Key('home-feed-action-rail'))).right,
|
tester.getRect(find.byKey(const Key('home-feed-action-rail'))).right,
|
||||||
closeTo(
|
closeTo(
|
||||||
tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 14,
|
tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 6,
|
||||||
0.1,
|
0.1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 职责:验证共用简介的短文案、展开声明、内容更新与减少动画行为。
|
// 职责:验证共用简介的短文案、展开声明、内容更新与减少动画行为。
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/expandable_synopsis.dart';
|
import 'package:fruitshorts/pages/player/widgets/expandable_synopsis.dart';
|
||||||
@@ -54,6 +55,14 @@ Future<void> _pump(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
testWidgets('should_use_video_copy_color_when_player_does_not_override', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _pump(tester, synopsis: _shortCopy);
|
||||||
|
final synopsis = tester.widget<Text>(find.byKey(_collapsed));
|
||||||
|
expect(synopsis.style?.color, AppColors.homeFeedVideoCopy);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_hide_toggle_when_synopsis_fits_two_lines', (
|
testWidgets('should_hide_toggle_when_synopsis_fits_two_lines', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
@@ -1517,7 +1517,7 @@ void main() {
|
|||||||
final style = line.style ?? const TextStyle();
|
final style = line.style ?? const TextStyle();
|
||||||
expect(style.fontSize, 15);
|
expect(style.fontSize, 15);
|
||||||
expect(style.fontWeight, FontWeight.w400);
|
expect(style.fontWeight, FontWeight.w400);
|
||||||
expect(style.color, AppColors.overlaySecondary);
|
expect(style.color, AppColors.homeFeedVideoCopy);
|
||||||
expect(style.shadows ?? <Shadow>[], isEmpty);
|
expect(style.shadows ?? <Shadow>[], isEmpty);
|
||||||
expect(line.textSpan!.toPlainText(), isNot(startsWith('Episode ')));
|
expect(line.textSpan!.toPlainText(), isNot(startsWith('Episode ')));
|
||||||
expect(line.textSpan!.toPlainText(), isNot(contains(' | ')));
|
expect(line.textSpan!.toPlainText(), isNot(contains(' | ')));
|
||||||
|
|||||||
Reference in new issue
Block a user