Compare commits

...

2 Commits

Author SHA1 Message Date
ivan
b05d3c0b0e 优化:启动优化 2026-03-30 01:40:08 +08:00
ivan
f06c49f22a 优化:启动优化 2026-03-30 01:39:47 +08:00
26 changed files with 107 additions and 30 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 69 B

View File

@ -3,4 +3,7 @@
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="fill|clip_vertical" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 69 B

View File

@ -3,4 +3,7 @@
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="fill|clip_vertical" android:src="@drawable/splash"/>
</item>
</layer-list>

View File

@ -4,11 +4,12 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#0a0a12</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#0a0a12</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@ -3,8 +3,8 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">

View File

@ -4,11 +4,12 @@
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#0a0a12</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#0a0a12</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@ -3,8 +3,8 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 346 KiB

View File

@ -16,14 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleAspectFill" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="0.04" green="0.04" blue="0.07" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="0.039215686274509803" green="0.039215686274509803" blue="0.07058823529411765" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
</viewController>
@ -33,6 +38,7 @@
</scene>
</scenes>
<resources>
<image name="LaunchBackground" width="375" height="812"/>
<image name="LaunchImage" width="512" height="512"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>

View File

@ -51,6 +51,8 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@ -197,8 +197,8 @@ class AuthService {
const retryDelay = Duration(seconds: 2);
try {
// 访 App
await Future<void>.delayed(const Duration(seconds: 2));
//
await Future<void>.delayed(const Duration(milliseconds: 400));
final deviceId = await _getDeviceId();
_logMsg('init: deviceId=$deviceId');

View File

@ -18,7 +18,8 @@ class ReferrerService {
static final Completer<AdjustAttribution?> _attributionCallbackCompleter =
Completer<AdjustAttribution?>();
static const int _adjustTimeoutMs = 15000;
/// [AuthService.init] getReferrer Adjust Play Install Referrer fallback
static const int _adjustTimeoutMs = 4500;
/// Adjust attributionCallback getAttributionWithTimeout
static void receiveAttributionFromCallback(AdjustAttribution attribution) {

View File

@ -1,4 +1,7 @@
import 'package:flutter/foundation.dart';
/// Navigator pop [VisibilityDetector] [_MainScaffoldState.didPopNext]
/// [VisibilityDetector] [VideoPlayer]
///
/// pop Tab
/// / ext `HomeScreen._requestPlaybackPipeline`
final ValueNotifier<int> homePlaybackResumeNonce = ValueNotifier<int>(0);

View File

@ -27,7 +27,7 @@ class HomeScreen extends StatefulWidget {
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
List<CategoryItem> _categories = [];
CategoryItem? _selectedCategory;
List<TaskItem> _tasks = [];
@ -44,8 +44,13 @@ class _HomeScreenState extends State<HomeScreen> {
/// vs
bool _visibilityHadMeaningfulReport = false;
/// IndexedStack [homePlaybackResumeNonce] [VisibilityDetector]
/// [notifyNow] Modal / pop My Gallery +
/// nonce [VideoCard] + [_onHomePlaybackResumeSignal]
void _requestPlaybackPipeline() {
if (!mounted || !widget.isActive) return;
homePlaybackResumeNonce.value++;
}
/// [homePlaybackResumeNonce] + [notifyNow]
void _scheduleVisibilityRefresh() {
if (!mounted || !widget.isActive) return;
void notify() {
@ -80,6 +85,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
UserState.needShowVideoMenu.addListener(_onExtConfigChanged);
UserState.extConfigItems.addListener(_onExtConfigChanged);
AuthService.isLoginComplete.addListener(_onExtConfigChanged);
@ -106,6 +112,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
homePlaybackResumeNonce.removeListener(_onHomePlaybackResumeSignal);
UserState.needShowVideoMenu.removeListener(_onExtConfigChanged);
UserState.extConfigItems.removeListener(_onExtConfigChanged);
@ -120,11 +127,19 @@ class _HomeScreenState extends State<HomeScreen> {
_scheduleVisibilityRefresh();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed && mounted && widget.isActive) {
_requestPlaybackPipeline();
}
}
void _onExtConfigChanged() {
if (mounted) setState(() {});
// Timer listener cancel notify
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();
if (mounted && widget.isActive) _requestPlaybackPipeline();
});
}
@ -139,7 +154,7 @@ class _HomeScreenState extends State<HomeScreen> {
static const int _maxConcurrentHomeVideos = 16;
void _onGridCardVisibilityChanged(int index, VisibilityInfo info) {
if (!mounted) return;
if (!mounted || !widget.isActive) return;
if (info.visibleFraction >= _videoVisibilityThreshold) {
_cardVisibleFraction[index] = info.visibleFraction;
_visibilityHadMeaningfulReport = true;
@ -155,6 +170,7 @@ class _HomeScreenState extends State<HomeScreen> {
}
void _reconcileVisibleVideoIndicesFromDetector() {
if (!mounted || !widget.isActive) return;
final tasks = _displayTasks;
final scored = <MapEntry<int, double>>[];
for (final e in _cardVisibleFraction.entries) {
@ -200,7 +216,7 @@ class _HomeScreenState extends State<HomeScreen> {
super.didUpdateWidget(oldWidget);
if (widget.isActive && !oldWidget.isActive) {
refreshAccount();
_scheduleVisibilityRefresh();
_requestPlaybackPipeline();
}
}
@ -300,7 +316,7 @@ class _HomeScreenState extends State<HomeScreen> {
setState(() => _categoriesLoading = false);
// pets [_loadTasks]
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();
if (mounted && widget.isActive) _requestPlaybackPipeline();
});
}
}
@ -332,7 +348,7 @@ class _HomeScreenState extends State<HomeScreen> {
setState(() => _tasksLoading = false);
//
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();
if (mounted && widget.isActive) _requestPlaybackPipeline();
});
}
}
@ -352,7 +368,7 @@ class _HomeScreenState extends State<HomeScreen> {
_visibilityHadMeaningfulReport = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();
if (mounted && widget.isActive) _requestPlaybackPipeline();
});
} else {
_loadTasks(c.id);
@ -447,6 +463,8 @@ class _HomeScreenState extends State<HomeScreen> {
_placeholderImage,
videoUrl: task.previewVideoUrl,
credits: credits,
playbackResumeListenable:
homePlaybackResumeNonce,
isActive: widget.isActive &&
_visibleVideoIndices.contains(index) &&
!_userPausedVideoIndices

View File

@ -1,6 +1,7 @@
import 'dart:async' show unawaited;
import 'dart:io' show File;
import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'
show DefaultCacheManager, DownloadProgress, FileInfo;
@ -22,6 +23,8 @@ class VideoCard extends StatefulWidget {
this.showCreditsBadge = true,
this.showBottomGenerateButton = true,
required this.isActive,
/// nonce [isActive] /
this.playbackResumeListenable,
required this.onPlayRequested,
required this.onStopRequested,
this.onGenerateSimilar,
@ -35,6 +38,7 @@ class VideoCard extends StatefulWidget {
final bool showCreditsBadge;
final bool showBottomGenerateButton;
final bool isActive;
final ValueListenable<int>? playbackResumeListenable;
final VoidCallback onPlayRequested;
final VoidCallback onStopRequested;
final VoidCallback? onGenerateSimilar;
@ -57,6 +61,7 @@ class _VideoCardState extends State<VideoCard> {
@override
void initState() {
super.initState();
widget.playbackResumeListenable?.addListener(_onPlaybackResumeSignal);
if (widget.isActive) {
WidgetsBinding.instance.addPostFrameCallback((_) => _loadAndPlay());
}
@ -64,13 +69,43 @@ class _VideoCardState extends State<VideoCard> {
@override
void dispose() {
widget.playbackResumeListenable?.removeListener(_onPlaybackResumeSignal);
_disposeController();
super.dispose();
}
void _onPlaybackResumeSignal() {
if (!mounted || !widget.isActive) return;
// [homePlaybackResumeNonce] dispose
// [_kickPlaybackAfterGlobalResume] setState
Future.microtask(() {
if (!mounted || !widget.isActive) return;
_kickPlaybackAfterGlobalResume();
});
}
/// Visibility pause [play] [didUpdateWidget]
void _kickPlaybackAfterGlobalResume() {
if (widget.videoUrl == null || widget.videoUrl!.isEmpty) return;
final c = _controller;
if (c != null && c.value.isInitialized) {
c.setVolume(0);
c.setLooping(true);
if (!c.value.isPlaying) {
unawaited(c.play());
}
return;
}
_loadAndPlay();
}
@override
void didUpdateWidget(covariant VideoCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.playbackResumeListenable != widget.playbackResumeListenable) {
oldWidget.playbackResumeListenable?.removeListener(_onPlaybackResumeSignal);
widget.playbackResumeListenable?.addListener(_onPlaybackResumeSignal);
}
if (oldWidget.isActive && !widget.isActive) {
_releaseVideoDecoder();
} else if (!oldWidget.isActive && widget.isActive) {

View File

@ -16,9 +16,7 @@ import 'features/recharge/google_play_purchase_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
_initAdjust();
// Facebook/ App activateApp app.dart AutoLog
// Adjust ReferrerService Adjust.getAttributionWithTimeout
await ReferrerService.init();
// await ReferrerService.init()Adjust [AuthService.init] await getReferrer()
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: AppColors.surface,

View File

@ -53,14 +53,20 @@ flutter_launcher_icons:
image_path: "assets/images/logo.png"
remove_alpha_ios: true
# 启动屏:与 Android 12+ 统一深色底,避免系统用默认 launcher 图标导致「中间图标过小」;
# android_12 必须单独配 image否则 12+ 只用 ic_launcher圆内显得很小且易有白边感。
flutter_native_splash:
background_image: "assets/images/splash.png"
color: "#0a0a12"
image: assets/images/logo.png
android: true
ios: true
# Android 12+ 使用不同 API不支持全屏背景图需单独配置
android_gravity: fill|clip_vertical
ios_content_mode: scaleAspectFill
fullscreen: true
android_12:
color: "#0a0a12"
image: "assets/images/logo.png"
image: assets/images/logo.png
icon_background_color: "#0a0a12"
flutter:
uses-material-design: true