Author SHA1 Message Date
Zhou Weidong 45e867561d chore(ios): 补充 CocoaPods 配置(Podfile 与 Pods xcconfig 引用) 2026-10-19 13:23:03 +08:00
Zhou Weidong 64fa3b1efe build: release 开启 R8 混淆与资源压缩(反编译保护) 2026-10-17 03:03:52 +08:00
Zhou Weidong faaf2f7283 feat: C3 客户端埋点 SDK(EventTracker 批量上报 + 核心事件) 2026-10-17 02:55:47 +08:00
Zhou Weidong 070ec78668 feat: A3 数据层 AppCatalogController(feed/详情/分类加载,供页面切换) 2026-10-17 02:43:41 +08:00
Zhou Weidong 88a401512d feat: A3-A6 数据层(内容/播放/记录收藏/互动的 HTTP 对接)
- AppCatalog 模型(feed/section/card/detail/episode/banner/category)
- HttpCatalogRepository 对接首页/发现/分类/详情
- PlaybackRepository 播放票据、WatchRepository 记录收藏
- InteractionRepository 评论点赞举报、WalletRepository 钱包解锁
2026-10-17 02:31:47 +08:00
Zhou Weidong a7ac3b7fc7 feat: A1 网络层与鉴权 + A2 账号登录对接
- 引入 dio,封装统一 ApiClient(BaseUrl/Token 注入/401 自动刷新/{code,message,data} 解包)
- TokenStorage 令牌持久化,AuthController 登录状态管理
- AuthRepository 封装 otp/login/logout 接口
- 登录页对接手机号/邮箱验证码登录(Google 登录待 OAuth client 配置)
2026-10-17 02:30:16 +08:00
Zhou Weidong 015677d1a8 feat: 新增登录页 + 主题改橙红对齐设计稿
- 新增登录页(Google / 手机号 / 邮箱 + 游客浏览 + 首登送金币)
- seedColor 改橙红 #FF5B3D,themeMode 改 dark,背景对齐设计稿 #0B0C10
- app 启动入口改为 LoginPage
2026-10-17 02:24:30 +08:00
21 changed files with 1490 additions and 82 deletions

No files matched your search

+3
View File
@@ -32,6 +32,9 @@ android {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
// 反编译保护(法律红线配套):开启 R8 混淆 + 资源压缩,发布前替换正式签名。
isMinifyEnabled = true
isShrinkResources = true
}
}
}
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
+32 -7
View File
@@ -4,14 +4,21 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'core/config/locale_controller.dart';
import 'core/data/repositories/auth_repository.dart';
import 'core/data/repositories/catalog_repository.dart';
import 'core/data/repositories/engagement_repository.dart';
import 'core/data/repositories/http_catalog_repository.dart';
import 'core/data/services/api_client.dart';
import 'core/data/services/app_catalog_controller.dart';
import 'core/data/services/auth_controller.dart';
import 'core/data/services/catalog_controller.dart';
import 'core/data/services/engagement_controller.dart';
import 'core/data/services/event_tracker.dart';
import 'core/data/services/playback_session.dart';
import 'core/data/services/token_storage.dart';
import 'core/data/services/video_player_port.dart';
import 'l10n/app_localizations.dart';
import 'pages/home/home_view.dart';
import 'pages/login/login_view.dart';
class LumaDramaApp extends StatelessWidget {
const LumaDramaApp({super.key, this.playerFactory = VideoPlayerPort.new});
@@ -24,6 +31,24 @@ class LumaDramaApp extends StatelessWidget {
providers: [
Provider(create: (_) => PlaybackCoordinator()),
Provider<PlayerFactory>(create: (_) => playerFactory),
Provider(create: (_) => ApiClient()),
Provider(create: (ctx) => EventTracker(ctx.read<ApiClient>())),
Provider(create: (_) => TokenStorage()),
Provider(
create: (ctx) => AuthRepository(
ctx.read<ApiClient>(),
ctx.read<TokenStorage>(),
),
),
ChangeNotifierProvider(
create: (ctx) => AuthController(ctx.read<AuthRepository>()),
),
Provider(
create: (ctx) => HttpCatalogRepository(ctx.read<ApiClient>()),
),
ChangeNotifierProvider(
create: (ctx) => AppCatalogController(ctx.read<HttpCatalogRepository>()),
),
ChangeNotifierProvider(
create: (_) => CatalogController(DemoCatalogRepository())..load(),
),
@@ -49,23 +74,23 @@ class LumaDramaApp extends StatelessWidget {
return supported.first;
},
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system,
themeMode: ThemeMode.dark,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6D4AFF),
seedColor: const Color(0xFFFF5B3D),
),
scaffoldBackgroundColor: const Color(0xFFF8F7FC),
scaffoldBackgroundColor: const Color(0xFF0B0C10),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF9B86FF),
seedColor: const Color(0xFFFF5B3D),
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF101019),
scaffoldBackgroundColor: const Color(0xFF0B0C10),
),
home: const HomePage(),
home: const LoginPage(),
),
),
);
+15
View File
@@ -0,0 +1,15 @@
// Author: fruit.com.cn
/// 接口地址与超时配置。
class ApiConfig {
ApiConfig._();
/// 客户端 app-api 地址,可用 --dart-define=API_BASE_URL=... 覆盖。
static const String baseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://127.0.0.1:8081',
);
static const int connectTimeoutSeconds = 10;
static const int receiveTimeoutSeconds = 15;
}
+188
View File
@@ -0,0 +1,188 @@
// Author: fruit.com.cn
// 与后端 /app/** 内容接口对齐的数据模型。
class AppEpisode {
AppEpisode({
required this.id,
required this.number,
required this.title,
required this.durationSec,
required this.locked,
required this.unlockCoin,
this.playUrl,
});
final int id;
final int number;
final String title;
final int durationSec;
final bool locked;
final int unlockCoin;
final String? playUrl;
factory AppEpisode.fromJson(Map<String, dynamic> json) => AppEpisode(
id: json['id'] as int,
number: json['number'] as int,
title: json['title'] as String? ?? '',
durationSec: json['durationSec'] as int? ?? 0,
locked: json['locked'] as bool? ?? false,
unlockCoin: json['unlockCoin'] as int? ?? 0,
playUrl: json['playUrl'] as String?,
);
}
class AppDramaCard {
AppDramaCard({
required this.id,
required this.title,
required this.cover,
required this.synopsis,
required this.genre,
required this.category,
required this.totalEpisodes,
required this.isHot,
required this.viewCount,
this.preview,
});
final int id;
final String title;
final String cover;
final String synopsis;
final String genre;
final String category;
final int totalEpisodes;
final bool isHot;
final int viewCount;
final AppEpisode? preview;
factory AppDramaCard.fromJson(Map<String, dynamic> json) => AppDramaCard(
id: json['id'] as int,
title: json['title'] as String? ?? '',
cover: json['cover'] as String? ?? '',
synopsis: json['synopsis'] as String? ?? '',
genre: json['genre'] as String? ?? '',
category: json['category'] as String? ?? '',
totalEpisodes: json['totalEpisodes'] as int? ?? 0,
isHot: json['isHot'] as bool? ?? false,
viewCount: json['viewCount'] as int? ?? 0,
preview: json['preview'] == null
? null
: AppEpisode.fromJson(json['preview'] as Map<String, dynamic>),
);
}
class AppDramaDetail {
AppDramaDetail({
required this.id,
required this.title,
required this.synopsis,
required this.cover,
required this.genre,
required this.category,
required this.totalEpisodes,
required this.freeEpisodes,
required this.unlockCoin,
required this.isHot,
required this.viewCount,
required this.episodes,
});
final int id;
final String title;
final String synopsis;
final String cover;
final String genre;
final String category;
final int totalEpisodes;
final int freeEpisodes;
final int unlockCoin;
final bool isHot;
final int viewCount;
final List<AppEpisode> episodes;
factory AppDramaDetail.fromJson(Map<String, dynamic> json) => AppDramaDetail(
id: json['id'] as int,
title: json['title'] as String? ?? '',
synopsis: json['synopsis'] as String? ?? '',
cover: json['cover'] as String? ?? '',
genre: json['genre'] as String? ?? '',
category: json['category'] as String? ?? '',
totalEpisodes: json['totalEpisodes'] as int? ?? 0,
freeEpisodes: json['freeEpisodes'] as int? ?? 0,
unlockCoin: json['unlockCoin'] as int? ?? 0,
isHot: json['isHot'] as bool? ?? false,
viewCount: json['viewCount'] as int? ?? 0,
episodes: (json['episodes'] as List? ?? const [])
.map((e) => AppEpisode.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
class AppBanner {
AppBanner({
required this.id,
required this.imageUrl,
required this.linkType,
required this.linkValue,
});
final int id;
final String imageUrl;
final String linkType;
final String linkValue;
factory AppBanner.fromJson(Map<String, dynamic> json) => AppBanner(
id: json['id'] as int,
imageUrl: json['imageUrl'] as String? ?? '',
linkType: json['linkType'] as String? ?? '',
linkValue: json['linkValue'] as String? ?? '',
);
}
class AppCategory {
AppCategory({required this.id, required this.name});
final int id;
final String name;
factory AppCategory.fromJson(Map<String, dynamic> json) =>
AppCategory(id: json['id'] as int, name: json['name'] as String? ?? '');
}
class AppSection {
AppSection({
required this.code,
required this.title,
required this.dramas,
});
final String code;
final String title;
final List<AppDramaCard> dramas;
factory AppSection.fromJson(Map<String, dynamic> json) => AppSection(
code: json['code'] as String? ?? '',
title: json['title'] as String? ?? '',
dramas: (json['dramas'] as List? ?? const [])
.map((e) => AppDramaCard.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
class AppFeed {
AppFeed({required this.banners, required this.sections});
final List<AppBanner> banners;
final List<AppSection> sections;
factory AppFeed.fromJson(Map<String, dynamic> json) => AppFeed(
banners: (json['banners'] as List? ?? const [])
.map((e) => AppBanner.fromJson(e as Map<String, dynamic>))
.toList(),
sections: (json['sections'] as List? ?? const [])
.map((e) => AppSection.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@@ -0,0 +1,91 @@
// Author: fruit.com.cn
import 'api_client.dart';
import 'token_storage.dart';
/// 登录/刷新成功后返回的会话令牌。
class AuthSession {
AuthSession({
required this.accessToken,
required this.refreshToken,
required this.expiresIn,
required this.newUser,
});
final String accessToken;
final String refreshToken;
final int expiresIn;
final bool newUser;
factory AuthSession.fromJson(Map<String, dynamic> json) {
return AuthSession(
accessToken: json['accessToken'] as String,
refreshToken: json['refreshToken'] as String,
expiresIn: (json['expiresIn'] as num?)?.toInt() ?? 7200,
newUser: json['newUser'] as bool? ?? false,
);
}
}
/// 账号与鉴权接口封装。
class AuthRepository {
AuthRepository(this._client, this._tokenStorage);
final ApiClient _client;
final TokenStorage _tokenStorage;
/// 发送验证码。
Future<void> sendOtp({
required String scene,
required String targetType,
required String target,
}) async {
await _client.post('/app/auth/otp', data: {
'scene': scene,
'targetType': targetType,
'target': target,
});
}
/// 验证码登录。
Future<AuthSession> loginWithCode({
required String targetType,
required String target,
required String code,
}) async {
final data = await _client.post('/app/auth/login', data: {
'targetType': targetType,
'target': target,
'code': code,
});
return AuthSession.fromJson(data as Map<String, dynamic>);
}
/// 密码登录。
Future<AuthSession> loginWithPassword({
required String targetType,
required String target,
required String password,
}) async {
final data = await _client.post('/app/auth/login', data: {
'targetType': targetType,
'target': target,
'password': password,
});
return AuthSession.fromJson(data as Map<String, dynamic>);
}
/// 保存会话令牌。
Future<void> saveSession(AuthSession session) async {
await _tokenStorage.saveTokens(
access: session.accessToken,
refresh: session.refreshToken,
);
}
/// 登出并清理本地令牌。
Future<void> logout() async {
await _client.post('/app/auth/logout');
await _tokenStorage.clear();
}
}
@@ -0,0 +1,33 @@
// Author: fruit.com.cn
import '../models/app_catalog.dart';
import '../services/api_client.dart';
/// 对接后端 `/app/**` 内容接口(A3)。
class HttpCatalogRepository {
HttpCatalogRepository(this._client);
final ApiClient _client;
Future<AppFeed> fetchHomeFeed() async {
final data = await _client.get('/app/home/feed');
return AppFeed.fromJson(data as Map<String, dynamic>);
}
Future<AppFeed> fetchDiscoverFeed() async {
final data = await _client.get('/app/discover/feed');
return AppFeed.fromJson(data as Map<String, dynamic>);
}
Future<List<AppCategory>> fetchCategories() async {
final data = await _client.get('/app/categories');
return (data as List)
.map((e) => AppCategory.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<AppDramaDetail> fetchDramaDetail(int id) async {
final data = await _client.get('/app/dramas/$id');
return AppDramaDetail.fromJson(data as Map<String, dynamic>);
}
}
@@ -0,0 +1,61 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 评论、点赞、举报(A6)。
class InteractionRepository {
InteractionRepository(this._client);
final ApiClient _client;
Future<void> postComment({
required int dramaId,
required String content,
int? rootId,
}) async {
await _client.post('/app/comments', data: {
'dramaId': dramaId,
'content': content,
if (rootId != null) 'rootId': rootId,
});
}
Future<List<dynamic>> fetchComments({
required int dramaId,
int? cursor,
int size = 20,
}) async {
final data = await _client.get('/app/comments', query: {
'dramaId': dramaId,
if (cursor != null) 'cursor': cursor,
'size': size,
});
return data as List;
}
Future<void> deleteComment(int id) async {
await _client.delete('/app/comments/$id');
}
Future<void> toggleLike({
required String targetType,
required int targetId,
}) async {
await _client.post('/app/likes/toggle', data: {
'targetType': targetType,
'targetId': targetId,
});
}
Future<void> report({
required String targetType,
required int targetId,
required String reason,
}) async {
await _client.post('/app/reports', data: {
'targetType': targetType,
'targetId': targetId,
'reason': reason,
});
}
}
@@ -0,0 +1,16 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 播放票据(A4)。
class PlaybackRepository {
PlaybackRepository(this._client);
final ApiClient _client;
/// 签发播放票据与签名地址,客户端原样使用 playUrl。
Future<Map<String, dynamic>> fetchPlayTicket(int episodeId) async {
final data = await _client.get('/app/episodes/$episodeId/play');
return data as Map<String, dynamic>;
}
}
@@ -0,0 +1,25 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 钱包、金币解锁(09 收费支付)。
class WalletRepository {
WalletRepository(this._client);
final ApiClient _client;
Future<Map<String, dynamic>> fetchWallet() async {
final data = await _client.get('/app/wallet');
return data as Map<String, dynamic>;
}
Future<Map<String, dynamic>> unlockEpisode(int episodeId) async {
final data = await _client.post('/app/episodes/$episodeId/unlock');
return data as Map<String, dynamic>;
}
Future<List<dynamic>> fetchOrders() async {
final data = await _client.get('/app/orders');
return data as List;
}
}
@@ -0,0 +1,59 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 观看记录与收藏(A5)。
class WatchRepository {
WatchRepository(this._client);
final ApiClient _client;
Future<void> reportProgress({
required int episodeId,
required int positionSec,
required int durationSec,
}) async {
await _client.post('/app/watch/progress', data: {
'episodeId': episodeId,
'positionSec': positionSec,
'durationSec': durationSec,
});
}
Future<Map<String, dynamic>> fetchProgress(int dramaId) async {
final data = await _client.get('/app/watch/progress', query: {
'dramaId': dramaId,
});
return data as Map<String, dynamic>;
}
Future<List<dynamic>> fetchRecent({int limit = 20}) async {
final data = await _client.get('/app/watch/recent', query: {'limit': limit});
return data as List;
}
Future<void> deleteProgress(int dramaId) async {
await _client.delete('/app/watch/progress/$dramaId');
}
Future<void> clearProgress() async {
await _client.delete('/app/watch/progress');
}
Future<bool> toggleFavorite(int dramaId) async {
final data = await _client.post('/app/favorites/toggle', data: {
'dramaId': dramaId,
});
final map = data as Map<String, dynamic>;
return map['favorited'] as bool? ?? false;
}
Future<List<dynamic>> fetchFavorites() async {
final data = await _client.get('/app/favorites');
return data as List;
}
Future<void> deleteFavorite(int dramaId) async {
await _client.delete('/app/favorites/$dramaId');
}
}
+136
View File
@@ -0,0 +1,136 @@
// Author: fruit.com.cn
import 'package:dio/dio.dart';
import '../../config/api_config.dart';
import 'token_storage.dart';
/// 统一业务异常:code 为后端业务码,message 可直接展示。
class ApiException implements Exception {
ApiException(this.code, this.message);
final int code;
final String message;
@override
String toString() => 'ApiException($code): $message';
}
/// 统一 HTTP 客户端:BaseUrl、Token 注入、401 自动刷新、{code,message,data} 解包。
class ApiClient {
ApiClient({Dio? dio, TokenStorage? tokenStorage})
: _dio = dio ?? Dio(),
_tokenStorage = tokenStorage ?? TokenStorage() {
_dio.options
..baseUrl = ApiConfig.baseUrl
..connectTimeout = const Duration(
seconds: ApiConfig.connectTimeoutSeconds,
)
..receiveTimeout = const Duration(
seconds: ApiConfig.receiveTimeoutSeconds,
);
_dio.interceptors.add(
InterceptorsWrapper(onRequest: _onRequest, onError: _onError),
);
}
final Dio _dio;
final TokenStorage _tokenStorage;
bool _refreshing = false;
Future<void> _onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final access = await _tokenStorage.readAccessToken();
if (access != null && access.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $access';
}
// 设备标识:第一阶段固定占位,A2 接入真实 deviceId。
options.headers['X-Device-Id'] = 'device-0001';
handler.next(options);
}
Future<void> _onError(
DioException error,
ErrorInterceptorHandler handler,
) async {
final status = error.response?.statusCode;
final path = error.requestOptions.path;
final isAuthEndpoint =
path.endsWith('/auth/refresh') || path.endsWith('/auth/login');
if (status == 401 && !isAuthEndpoint && !_refreshing) {
final refreshed = await _refresh();
if (refreshed) {
handler.resolve(await _retry(error.requestOptions));
return;
}
}
handler.next(error);
}
Future<bool> _refresh() async {
_refreshing = true;
try {
final refresh = await _tokenStorage.readRefreshToken();
if (refresh == null || refresh.isEmpty) return false;
final response = await _dio.post<dynamic>(
'/app/auth/refresh',
data: {'refreshToken': refresh},
);
final body = response.data;
if (body is Map && body['code'] == 0) {
final data = body['data'] as Map;
await _tokenStorage.saveTokens(
access: data['accessToken'] as String,
refresh: data['refreshToken'] as String,
);
return true;
}
return false;
} catch (_) {
return false;
} finally {
_refreshing = false;
}
}
Future<Response<dynamic>> _retry(RequestOptions options) {
return _dio.fetch<dynamic>(options);
}
Future<dynamic> get(String path, {Map<String, dynamic>? query}) {
return _request(() => _dio.get<dynamic>(path, queryParameters: query));
}
Future<dynamic> post(String path, {Object? data}) {
return _request(() => _dio.post<dynamic>(path, data: data));
}
Future<dynamic> put(String path, {Object? data}) {
return _request(() => _dio.put<dynamic>(path, data: data));
}
Future<dynamic> delete(String path, {Object? data}) {
return _request(() => _dio.delete<dynamic>(path, data: data));
}
Future<dynamic> _request(Future<Response<dynamic>> Function() send) async {
try {
final response = await send();
final body = response.data;
if (body is Map && body.containsKey('code')) {
final code = body['code'];
if (code == 0) return body['data'];
throw ApiException(code as int, (body['message'] as String?) ?? '');
}
return body;
} on DioException catch (e) {
throw ApiException(
e.response?.statusCode ?? -1,
e.message ?? 'network error',
);
}
}
}
@@ -0,0 +1,39 @@
// Author: fruit.com.cn
import 'package:flutter/foundation.dart';
import '../models/app_catalog.dart';
import '../repositories/http_catalog_repository.dart';
/// 首页/发现页 feed 状态(A3 数据层)。
class AppCatalogController extends ChangeNotifier {
AppCatalogController(this._repository);
final HttpCatalogRepository _repository;
AppFeed? feed;
bool loading = false;
String? error;
Future<void> loadFeed() async {
loading = true;
error = null;
notifyListeners();
try {
feed = await _repository.fetchHomeFeed();
} catch (e) {
error = e.toString();
} finally {
loading = false;
notifyListeners();
}
}
Future<AppDramaDetail> loadDetail(int id) {
return _repository.fetchDramaDetail(id);
}
Future<List<AppCategory>> loadCategories() {
return _repository.fetchCategories();
}
}
@@ -0,0 +1,65 @@
// Author: fruit.com.cn
import 'package:flutter/foundation.dart';
import '../repositories/auth_repository.dart';
enum AuthStatus { unknown, authenticated, unauthenticated }
/// 登录状态与账号操作。
class AuthController extends ChangeNotifier {
AuthController(this._repository);
final AuthRepository _repository;
AuthStatus _status = AuthStatus.unknown;
AuthStatus get status => _status;
Future<void> sendOtp({
required String scene,
required String targetType,
required String target,
}) {
return _repository.sendOtp(
scene: scene,
targetType: targetType,
target: target,
);
}
Future<void> loginWithCode({
required String targetType,
required String target,
required String code,
}) async {
final session = await _repository.loginWithCode(
targetType: targetType,
target: target,
code: code,
);
await _repository.saveSession(session);
_status = AuthStatus.authenticated;
notifyListeners();
}
Future<void> loginWithPassword({
required String targetType,
required String target,
required String password,
}) async {
final session = await _repository.loginWithPassword(
targetType: targetType,
target: target,
password: password,
);
await _repository.saveSession(session);
_status = AuthStatus.authenticated;
notifyListeners();
}
Future<void> logout() async {
await _repository.logout();
_status = AuthStatus.unauthenticated;
notifyListeners();
}
}
+64
View File
@@ -0,0 +1,64 @@
// Author: fruit.com.cn
import 'dart:async';
import 'api_client.dart';
/// 核心埋点事件名(第一阶段口径,后续与运营对齐后可扩展)。
class AppEvents {
AppEvents._();
static const String playbackStart = 'playback_start';
static const String playbackEnd = 'playback_end';
static const String paySuccess = 'pay_success';
static const String register = 'register';
static const String click = 'click';
}
/// 埋点上报:本地缓冲、批量上报,失败静默丢弃不影响主流程。
class EventTracker {
EventTracker(this._api);
final ApiClient _api;
static const int _flushSize = 10;
static const Duration _flushInterval = Duration(seconds: 10);
final List<Map<String, Object?>> _buffer = [];
Timer? _timer;
void track(
String eventName, {
String? page,
Map<String, Object?>? properties,
}) {
_buffer.add({
'eventName': eventName,
'eventTime': DateTime.now().toIso8601String(),
'page': page,
'properties': properties,
});
if (_buffer.length >= _flushSize) {
flush();
} else {
_timer ??= Timer(_flushInterval, flush);
}
}
Future<void> flush() async {
if (_buffer.isEmpty) return;
final events = List<Map<String, Object?>>.from(_buffer);
_buffer.clear();
_timer?.cancel();
_timer = null;
try {
await _api.post('/app/events', data: {
'deviceId': 'device-0001',
'sessionId': 'session-0001',
'events': events,
});
} catch (_) {
// 埋点失败不影响主流程,静默丢弃。
}
}
}
+34
View File
@@ -0,0 +1,34 @@
// Author: fruit.com.cn
import 'package:shared_preferences/shared_preferences.dart';
/// 令牌本地存储。第一阶段用 shared_preferences,后续可换 secure storage。
class TokenStorage {
static const _accessKey = 'auth_access_token';
static const _refreshKey = 'auth_refresh_token';
Future<void> saveTokens({
required String access,
required String refresh,
}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_accessKey, access);
await prefs.setString(_refreshKey, refresh);
}
Future<String?> readAccessToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_accessKey);
}
Future<String?> readRefreshToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_refreshKey);
}
Future<void> clear() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_accessKey);
await prefs.remove(_refreshKey);
}
}
+468
View File
@@ -0,0 +1,468 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/data/services/auth_controller.dart';
import '../home/home_view.dart';
/// 登录页:Google / 手机号 / 邮箱 三种登录入口 + 游客浏览。
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
static const _orange = Color(0xFFFF5B3D);
static const _purple = Color(0xFF9278EA);
void _enterHome() {
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(builder: (_) => const HomePage()),
);
}
Future<void> _showLoginSheet(String targetType) async {
final auth = context.read<AuthController>();
final targetCtrl = TextEditingController();
final codeCtrl = TextEditingController();
final isPhone = targetType == 'PHONE';
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF161822),
builder: (sheetContext) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isPhone ? 'Phone Login' : 'Email Login',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
const SizedBox(height: 16),
TextField(
controller: targetCtrl,
keyboardType: isPhone
? TextInputType.phone
: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: isPhone ? 'Phone number' : 'Email',
hintStyle: const TextStyle(color: Colors.white38),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: codeCtrl,
keyboardType: TextInputType.number,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Code',
hintStyle: const TextStyle(color: Colors.white38),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
),
const SizedBox(width: 12),
TextButton(
onPressed: () async {
try {
await auth.sendOtp(
scene: 'LOGIN',
targetType: targetType,
target: targetCtrl.text.trim(),
);
if (sheetContext.mounted) {
ScaffoldMessenger.of(sheetContext).showSnackBar(
const SnackBar(content: Text('Code sent')),
);
}
} catch (e) {
if (sheetContext.mounted) {
ScaffoldMessenger.of(sheetContext).showSnackBar(
SnackBar(content: Text('$e')),
);
}
}
},
child: const Text('Send Code'),
),
],
),
const SizedBox(height: 20),
FilledButton(
onPressed: () async {
try {
await auth.loginWithCode(
targetType: targetType,
target: targetCtrl.text.trim(),
code: codeCtrl.text.trim(),
);
if (sheetContext.mounted) {
Navigator.of(sheetContext).pop();
}
_enterHome();
} catch (e) {
if (sheetContext.mounted) {
ScaffoldMessenger.of(sheetContext).showSnackBar(
SnackBar(content: Text('$e')),
);
}
}
},
style: FilledButton.styleFrom(
backgroundColor: _orange,
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text('Sign In'),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
// 顶部:关闭 + 游客浏览
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_IconButton(icon: Icons.close, onTap: () {}),
TextButton(
onPressed: _enterHome,
style: TextButton.styleFrom(
foregroundColor: Colors.white54,
backgroundColor: Colors.white.withValues(alpha: 0.05),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
shape: const StadiumBorder(),
),
child: const Text('Guest Explore'),
),
],
),
),
// 中央内容
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const _BrandLogo(),
const SizedBox(height: 20),
const Text(
'DramaPulse',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Text(
'Sign in to sync your progress and coins',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.white54,
),
),
const SizedBox(height: 36),
_GoogleButton(onTap: _enterHome),
const SizedBox(height: 14),
_DarkButton(
icon: Icons.phone_iphone,
iconColor: _orange,
label: 'Continue with Phone Number',
onTap: () => _showLoginSheet('PHONE'),
),
const SizedBox(height: 14),
_DarkButton(
icon: Icons.email_outlined,
iconColor: _purple,
label: 'Continue with Email',
onTap: () => _showLoginSheet('EMAIL'),
),
const SizedBox(height: 32),
const _BonusPill(),
],
),
),
),
),
// 底部协议
const Padding(
padding: EdgeInsets.only(bottom: 24),
child: Text(
'By continuing, you agree to DramaPulse\'s\n'
'Terms of Service · Privacy Policy · Content Policy',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 11, color: Colors.white38),
),
),
],
),
),
);
}
}
class _IconButton extends StatelessWidget {
const _IconButton({required this.icon, required this.onTap});
final IconData icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(24),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
border: Border.all(color: Colors.white12),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white54, size: 20),
),
);
}
}
class _BrandLogo extends StatelessWidget {
const _BrandLogo();
@override
Widget build(BuildContext context) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFF5B3D), Color(0xFFD8536C), Color(0xFF9278EA)],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white24),
),
child: const Icon(Icons.play_arrow_rounded, color: Colors.white, size: 40),
);
}
}
class _GoogleButton extends StatelessWidget {
const _GoogleButton({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return _AuthButton(
onTap: onTap,
background: Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const _GoogleG(),
const SizedBox(width: 12),
Text(
'Continue with Google',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade900,
),
),
],
),
);
}
}
/// 简化版 Google「G」logo。
class _GoogleG extends StatelessWidget {
const _GoogleG();
@override
Widget build(BuildContext context) {
return Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF4285F4),
),
child: const Center(
child: Text(
'G',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
),
);
}
}
class _DarkButton extends StatelessWidget {
const _DarkButton({
required this.icon,
required this.iconColor,
required this.label,
required this.onTap,
});
final IconData icon;
final Color iconColor;
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return _AuthButton(
onTap: onTap,
background: const Color(0xFF161822),
border: const Color(0xFF232634),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: iconColor, size: 20),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
),
);
}
}
class _AuthButton extends StatelessWidget {
const _AuthButton({
required this.onTap,
required this.background,
required this.child,
this.border,
});
final VoidCallback onTap;
final Color background;
final Color? border;
final Widget child;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 52,
child: Material(
color: background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: border == null ? BorderSide.none : BorderSide(color: border!),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: child,
),
),
);
}
}
class _BonusPill extends StatelessWidget {
const _BonusPill();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.04),
border: Border.all(color: Colors.white12),
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Color(0xFF34D399),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
const Text(
'Get ',
style: TextStyle(fontSize: 11, color: Colors.white54),
),
const Text(
'100 Free Bonus Coins',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const Text(
' upon first sign-in',
style: TextStyle(fontSize: 11, color: Colors.white54),
),
],
),
);
}
}
+115 -75
View File
@@ -6,7 +6,7 @@ packages:
description:
name: archive
sha256: "0d58d1fa1fddf6588251ccf2819f3fb3b6d0452c1b2813ac7413a05592bfbefe"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.5.2"
args:
@@ -14,7 +14,7 @@ packages:
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
@@ -22,7 +22,7 @@ packages:
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
@@ -30,7 +30,7 @@ packages:
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
@@ -38,7 +38,7 @@ packages:
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
@@ -46,7 +46,7 @@ packages:
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
@@ -54,23 +54,23 @@ packages:
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
url: "https://pub.flutter-io.cn"
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
csslib:
@@ -78,7 +78,7 @@ packages:
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
@@ -86,15 +86,31 @@ packages:
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dio:
dependency: "direct main"
description:
name: dio
sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1"
url: "https://pub.dev"
source: hosted
version: "5.11.1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "3a1b2cd7be71086f38504956e3ebcd2837288d231ff454bafa78021244102bfc"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
@@ -102,7 +118,7 @@ packages:
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
@@ -110,7 +126,7 @@ packages:
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
@@ -123,7 +139,7 @@ packages:
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
@@ -131,7 +147,7 @@ packages:
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
@@ -154,31 +170,39 @@ packages:
description:
name: html
sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.15.7"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
intl:
dependency: transitive
description:
name: intl
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.flutter-io.cn"
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.3"
version: "0.20.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
@@ -186,7 +210,7 @@ packages:
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
@@ -194,7 +218,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
@@ -202,7 +226,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
@@ -210,39 +234,47 @@ packages:
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.flutter-io.cn"
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.20"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.flutter-io.cn"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.18.0"
mime:
dependency: transitive
description:
name: mime
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
url: "https://pub.dev"
source: hosted
version: "2.1.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
@@ -250,7 +282,7 @@ packages:
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
@@ -258,7 +290,7 @@ packages:
description:
name: path_provider_linux
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_platform_interface:
@@ -266,7 +298,7 @@ packages:
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
@@ -274,7 +306,7 @@ packages:
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
@@ -282,7 +314,7 @@ packages:
description:
name: platform
sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
plugin_platform_interface:
@@ -290,7 +322,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
@@ -298,7 +330,7 @@ packages:
description:
name: posix
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.5.2"
provider:
@@ -306,7 +338,7 @@ packages:
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
shared_preferences:
@@ -314,7 +346,7 @@ packages:
description:
name: shared_preferences
sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_android:
@@ -322,7 +354,7 @@ packages:
description:
name: shared_preferences_android
sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.28"
shared_preferences_foundation:
@@ -330,7 +362,7 @@ packages:
description:
name: shared_preferences_foundation
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
shared_preferences_linux:
@@ -338,7 +370,7 @@ packages:
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
@@ -346,7 +378,7 @@ packages:
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
@@ -354,7 +386,7 @@ packages:
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
@@ -362,7 +394,7 @@ packages:
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
@@ -375,23 +407,23 @@ packages:
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490"
url: "https://pub.flutter-io.cn"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.2"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
@@ -399,7 +431,7 @@ packages:
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
@@ -407,31 +439,39 @@ packages:
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.flutter-io.cn"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.12"
version: "0.7.11"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d"
url: "https://pub.flutter-io.cn"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.4.3"
version: "2.2.0"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: "07ae1e03f2d4c8e2e9a75ee678f90acbc4506d4458a9c628512d119a1eee5754"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.14.1"
video_player_android:
@@ -439,7 +479,7 @@ packages:
description:
name: video_player_android
sha256: d27054dea34d748a44f06d433a57005d2aac69944485b0abbaed3303dbcfa3f1
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.12.2"
video_player_avfoundation:
@@ -447,7 +487,7 @@ packages:
description:
name: video_player_avfoundation
sha256: "4f8a845173377ab250a50f9b9dfe4a43500cc76a7c49dd8410d02d8fc29dfd8b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.12.1"
video_player_platform_interface:
@@ -455,7 +495,7 @@ packages:
description:
name: video_player_platform_interface
sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.9.0"
video_player_web:
@@ -463,23 +503,23 @@ packages:
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
url: "https://pub.flutter-io.cn"
sha256: "89b11d62e1040ee681af3b4f1c97a803ecbf010c6e8b9c5896ee5b08c3bf7b78"
url: "https://pub.dev"
source: hosted
version: "15.3.0"
version: "15.4.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
@@ -487,7 +527,7 @@ packages:
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
@@ -495,7 +535,7 @@ packages:
description:
name: yaml
sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
sdks:
+1
View File
@@ -39,6 +39,7 @@ dependencies:
provider: ^6.1.5+1
video_player: ^2.14.0
shared_preferences: ^2.5.5
dio: ^5.7.0
flutter_localizations:
sdk: flutter