235 Commits
Author SHA1 Message Date
Zhou Weidong 0cb57d084d merge: 将 develop 合入 release/v1.0.0
# Conflicts:
#	lib/core/config/app_config.dart
#	lib/main.dart
#	test/config/server_endpoint_test.dart
2026-10-20 20:36:43 +08:00
Zhou Weidong 51c363540a merge: 合并 K68-A 锁定编译期接口地址 2026-10-20 20:21:00 +08:00
Zhou Weidong 9ebb54ecfd fix(config): 指定 API_BASE_URL 时跳过地址探测 2026-10-20 20:20:46 +08:00
Zhou Weidong 2c5ff3579c merge: 合并 K67-A 删除无引用代码与资源 2026-10-20 20:06:31 +08:00
Zhou Weidong 3fefd544fb chore(cleanup): 删除无引用代码与无用资源 2026-10-20 20:06:17 +08:00
Zhou Weidong d2174c5b74 merge: 合并 K66-A 启动页停留至首页就绪 2026-10-20 19:57:28 +08:00
Zhou Weidong 7871b0cfbb feat(home): 启动页停留至首页就绪 2026-10-20 19:57:13 +08:00
Zhou Weidong c76b388814 merge: 合并 K65-A 冷启动首页预取 2026-10-20 19:51:55 +08:00
Zhou Weidong 9b31d83e39 perf(home): 冷启动提前拉取首页数据 2026-10-20 19:51:41 +08:00
Zhou Weidong 1199db760f merge: 合并 K64-A 原生品牌启动图 2026-10-20 19:42:03 +08:00
Zhou Weidong 8f60046ec3 feat(splash): 原生开屏改为品牌启动图 2026-10-20 19:41:50 +08:00
Zhou Weidong 210ccc97cb merge: 合并 K63-A 首页 feed 跳转登录页暂停播放 2026-10-20 19:25:05 +08:00
Zhou Weidong 699fd427b8 fix(player): 路由覆盖时暂停播放 2026-10-20 19:24:49 +08:00
Zhou Weidong 911f13f464 merge: 合并 K62 安卓桌面图标资源 2026-10-20 19:16:42 +08:00
Zhou Weidong 4c8ae7bcec fix(android): 按设计源图重做桌面图标资源 2026-10-20 19:16:29 +08:00
Zhou Weidong 51e94029e4 merge: 合并 K61 导航栏闪白修复 2026-10-20 19:04:38 +08:00
Zhou Weidong 0aa5a99a22 fix(flutter): 修复详情与播放器底部闪白 2026-10-20 19:04:25 +08:00
Zhou Weidong 9eec9ed77a merge: 合并 K60 flutter_native_splash 原生开屏 2026-10-20 18:51:54 +08:00
Zhou Weidong 983e99a4b6 feat(flutter): 接入 flutter_native_splash 原生开屏 2026-10-20 18:51:28 +08:00
Zhou Weidong 7c2fd090e0 merge: 合并 K59 隐藏视频调试文字 2026-10-20 18:43:52 +08:00
Zhou Weidong a756faca6c fix(flutter): 隐藏视频常驻调试文字 2026-10-20 18:43:41 +08:00
Zhou Weidong da549492b0 merge: 合并 K58 浅色开屏 2026-10-20 18:38:10 +08:00
Zhou Weidong 398dc3f8b4 feat(flutter): 优化浅色开屏与原生过渡 2026-10-20 18:37:59 +08:00
Zhou Weidong 576f2b95f4 merge: 合并 K57 视频首帧防闪 2026-10-20 18:29:20 +08:00
Zhou Weidong 6b397015b1 fix(flutter): 修复 iOS 视频首帧闪屏 2026-10-20 18:29:10 +08:00
Zhou Weidong ca6776580f merge: 合并 K56 服务地址编辑器 2026-10-20 18:23:58 +08:00
Zhou Weidong c259b0b543 feat(flutter): 增加 debug 服务地址编辑器 2026-10-20 18:23:44 +08:00
Zhou Weidong eab5d3e230 merge: 合并 K55 目录加载诊断 2026-10-20 18:17:46 +08:00
Zhou Weidong 730a96559b feat(flutter): 增加目录加载诊断与前台重试 2026-10-20 18:17:36 +08:00
Zhou Weidong d68734cdab merge: 合并 iOS shared_preferences 兼容修复 2026-10-20 18:08:44 +08:00
Zhou Weidong aac9195e05 fix(flutter): 钉住 shared_preferences_foundation 2.5.6 兼容当前 Xcode。 2026-10-20 18:08:17 +08:00
Zhou Weidong ae11846b63 merge: 合并 K54 iOS 网络修复 2026-10-20 18:01:25 +08:00
Zhou Weidong a63334f5a5 fix(flutter): 修复 iOS 启动网络加载 2026-10-20 18:01:12 +08:00
Zhou Weidong 23059578d2 merge: 合并 K53 详情页首帧修复 2026-10-20 17:54:19 +08:00
Zhou Weidong bb92996db5 fix(flutter): 修复详情页首帧闪屏 2026-10-20 17:54:08 +08:00
Zhou Weidong a11ba50b13 merge: 合并 K52 原生开屏清理 2026-10-20 17:45:44 +08:00
Zhou Weidong fe5b6e6efa feat(flutter): 移除原生静态开屏 2026-10-20 17:45:33 +08:00
Zhou Weidong 53591abcd0 merge: 合并 K50 开屏品牌强化 2026-10-20 17:40:23 +08:00
Zhou Weidong c147645824 feat(flutter): 强化开屏品牌标识展示 2026-10-20 17:40:13 +08:00
Zhou Weidong fcf30ff39c merge: 合并 K50 图标最终资源 2026-10-20 17:36:55 +08:00
Zhou Weidong dfaff9714a fix(flutter): 同步水果短剧品牌图标最终资源 2026-10-20 17:36:49 +08:00
Zhou Weidong 04609ace7c merge: 合并 K50 品牌图标资源 2026-10-20 17:34:25 +08:00
Zhou Weidong 7e60e5ebc4 feat(flutter): 替换水果短剧品牌图标与开屏图 2026-10-20 17:34:15 +08:00
Vicky 375b013b1c merge: 合并 K51 详情页灰屏修复 2026-10-20 17:33:05 +08:00
Zhou Weidong 353196aaa3 fix(flutter): 修复详情页跳转灰屏 2026-10-20 17:32:53 +08:00
Zhou Weidong da78d8723d merge: 合并 K50 品牌开屏动画 2026-10-20 17:20:16 +08:00
Zhou Weidong 15570f117a feat(flutter): 重做品牌开屏动画 2026-10-20 17:20:06 +08:00
Zhou Weidong 499e42b511 merge: 合并 K49 加速态清屏按钮修正 2026-10-20 17:12:37 +08:00
Zhou Weidong 5034ac7670 fix(flutter): 隐藏首页加速态恢复清屏按钮 2026-10-20 17:12:28 +08:00
Zhou Weidong 030a0e7986 merge: 合并分类弧线方向修正 2026-10-20 17:09:46 +08:00
Zhou Weidong 420da29658 fix(flutter): 调整分类弧线开口方向 2026-10-20 17:09:37 +08:00
Zhou Weidong 3ce2599754 merge: 合并 K48 分类 tab 修正 2026-10-20 17:07:33 +08:00
Zhou Weidong 5af70c345a fix(flutter): 优化分类弧线与自动滚动 2026-10-20 17:07:25 +08:00
Zhou Weidong 3041849344 merge: 合并下拉刷新主色文案修正 2026-10-20 17:02:28 +08:00
Zhou Weidong 107ae40d2a fix(flutter): 统一下拉刷新主色文案 2026-10-20 17:02:20 +08:00
Zhou Weidong 8bcd882aa5 merge: 合并 K47 剧场搜索清空修正 2026-10-20 17:00:14 +08:00
Zhou Weidong 9870502901 fix(flutter): 增加剧场搜索清空按钮 2026-10-20 17:00:03 +08:00
Zhou Weidong fb0585c9cf merge: 合并下拉刷新页面跟随修正 2026-10-20 16:57:36 +08:00
Zhou Weidong 69eaacd74f fix(flutter): 修复下拉刷新页面跟随 2026-10-20 16:57:28 +08:00
Zhou Weidong 8f3049f374 merge: 合并 K46-A 榜单新剧分页与预约刷新 2026-10-20 16:53:15 +08:00
Zhou Weidong c4dcb714ab feat(flutter): 增加榜单新剧分页与预约刷新 2026-10-20 16:53:07 +08:00
Zhou Weidong 805762f761 merge: 合并 K45-A 文字下拉刷新 2026-10-20 16:47:20 +08:00
Zhou Weidong 5bdcb989bb feat(flutter): 统一文字下拉刷新 2026-10-20 16:47:13 +08:00
Zhou Weidong 0ad2cd5e95 merge: 合并 K44-A 个人资料页修正 2026-10-20 16:42:34 +08:00
Zhou Weidong b2a8bfa59f fix(flutter): 精简个人资料并修正保存文案 2026-10-20 16:42:27 +08:00
Zhou Weidong dc73f4e3bb merge: 合并 K43-A 首页冷启动灰屏修复 2026-10-20 16:39:55 +08:00
Zhou Weidong 0d2680e6c1 fix(flutter): 修复首页冷启动灰屏 2026-10-20 16:39:48 +08:00
Zhou Weidong 37802b24bf merge: 合并 K42-A 收藏页星标修正 2026-10-20 16:37:04 +08:00
Zhou Weidong 1d8582dfaa fix(flutter): 调整收藏页星标操作 2026-10-20 16:36:56 +08:00
Zhou Weidong 4c9bb18403 merge: 合并 K42-A 观看历史删除按钮修正 2026-10-20 16:34:38 +08:00
Zhou Weidong f62324084e fix(flutter): 优化观看历史删除按钮 2026-10-20 16:34:31 +08:00
Zhou Weidong 9f4dc93d11 merge: 合并 K42-A 收藏与观看历史页重做 2026-10-20 16:32:08 +08:00
Zhou Weidong 794dc87083 feat(flutter): 重做收藏与观看历史页 2026-10-20 16:31:59 +08:00
Zhou Weidong 7285a1cd4e merge: 合并 K41-A 登录页重设计 2026-10-20 16:21:14 +08:00
Zhou Weidong 81bb1543bb feat(flutter): 修复登录输入并升级登录页 2026-10-20 16:21:06 +08:00
Zhou Weidong f72cb41670 merge: 合并详情页顶部按钮统一 2026-10-20 16:16:01 +08:00
Zhou Weidong c1dd033581 fix(flutter): 统一详情页顶部圆形按钮 2026-10-20 16:15:53 +08:00
Zhou Weidong 230f16a44a merge: 合并 K40-B 评论入口登录闸 2026-10-20 16:12:34 +08:00
Zhou Weidong 870a170fad fix(flutter): 评论入口增加登录闸 2026-10-20 16:12:27 +08:00
Zhou Weidong a5c84b0297 merge: 合并 K39 返回按钮优化 2026-10-20 16:09:19 +08:00
Zhou Weidong 8493c4ef3b fix(flutter): 优化二级页面返回按钮样式 2026-10-20 16:09:10 +08:00
Zhou Weidong 457d2fa6f2 merge: 合并 K40-B 评论登录闸 2026-10-20 16:05:54 +08:00
Zhou Weidong d0c1a5f499 fix(flutter): 评论输入增加登录闸 2026-10-20 16:05:47 +08:00
Zhou Weidong 1eea9fafe7 merge: 合并 K40-A 播放加载修复 2026-10-20 15:59:32 +08:00
Zhou Weidong fa5fd124a4 fix(flutter): 优化播放加载占位与主题色 2026-10-20 15:59:23 +08:00
Zhou Weidong 9864c8e102 merge: 合并 K39-A 二级页头统一 2026-10-20 15:55:21 +08:00
Zhou Weidong 1c6c92170d feat(flutter): 统一二级页头并恢复新剧暖色 2026-10-20 15:55:12 +08:00
Zhou Weidong 7b2970d5c5 merge: 合并 K38-A 短剧详情页重做 2026-10-20 15:47:00 +08:00
Zhou Weidong 69faa7818c feat(flutter): 重做短剧详情页 2026-10-20 15:46:53 +08:00
Zhou Weidong 7250b5f0f9 merge: 合并 K37-A 剧场筛选页 2026-10-20 15:39:53 +08:00
Zhou Weidong 850f8e7c8c feat(flutter): 新增剧场筛选页 2026-10-20 15:39:46 +08:00
Zhou Weidong 85ba2a4fec merge: 合并 K36-A 首映厅与档期预约 2026-10-20 15:28:09 +08:00
Zhou Weidong 4f81bcf2b1 feat(flutter): 新剧首映厅与档期预约页 2026-10-20 15:28:09 +08:00
Zhou Weidong fdee89b345 merge: 合并 K34-H 吸顶闪动修复 2026-10-20 15:25:15 +08:00
Zhou Weidong 6463436a0d fix(flutter): 吸顶分类行背景常驻清除闪动 2026-10-20 15:25:15 +08:00
Zhou Weidong fcc1995d8b merge: 合并 K35-A 创作页重做 2026-10-20 15:21:54 +08:00
Zhou Weidong 6e9a93c920 feat(flutter): 去掉剧场封面角标并重做创作页 2026-10-20 15:21:54 +08:00
Zhou Weidong 72410b2a87 merge: 合并 K34-G 剧场吸顶修复 2026-10-20 15:17:46 +08:00
Zhou Weidong d1abfd47c7 fix(flutter): 修复剧场吸顶分类行闪烁并压缩间距 2026-10-20 15:17:46 +08:00
Zhou Weidong 963f376159 merge: 合并 K34-E 卡片直达播放追加 2026-10-20 15:15:30 +08:00
Zhou Weidong 720fd26935 fix(flutter): 剧场与榜单卡片直接进播放页 2026-10-20 15:15:27 +08:00
Zhou Weidong 9949b61560 merge: 合并 K34-E 榜单与吸顶优化 2026-10-20 15:13:12 +08:00
Zhou Weidong 04a1a4ee82 feat(flutter): 榜单前三铺满与剧场分类吸顶下划线 2026-10-20 15:13:12 +08:00
Zhou Weidong d80fff74ba merge: 合并 K34-D 榜单三强横排 2026-10-20 15:08:43 +08:00
Zhou Weidong 881088a428 feat(flutter): 榜单页三强横排与第四名起列表 2026-10-20 15:08:43 +08:00
Zhou Weidong ffedde0db5 merge: 合并 K34-C 榜单新剧预约页 2026-10-20 15:03:07 +08:00
Zhou Weidong bfba2b7c72 feat(flutter): 剧场搜索框改白底并新增榜单新剧预约页 2026-10-20 15:03:02 +08:00
Zhou Weidong 7a75ee6de8 merge: 合并 K34-B 剧场搜索框样式 2026-10-20 14:58:18 +08:00
Zhou Weidong eccd3c6dc1 fix(flutter): 剧场搜索框改为浅灰胶囊 2026-10-20 14:58:18 +08:00
Zhou Weidong 63ace3b574 merge: 合并 K34-A 剧场页精致化 2026-10-20 14:52:46 +08:00
Zhou Weidong 5283ab2549 feat(flutter): 剧场页搜索与吸顶筛选精致化 2026-10-20 14:52:41 +08:00
Zhou Weidong f9d4f982f0 merge: 合并 K33-K 首页 feed 固定顶栏 2026-10-20 14:46:13 +08:00
Zhou Weidong 9175a7f253 fix(flutter): 首页 feed 顶栏固定悬浮 2026-10-20 14:46:08 +08:00
Zhou Weidong 1cb4c56261 merge: 合并 K33 暂停按钮尺寸调整 2026-10-20 14:41:27 +08:00
Zhou Weidong 551da77e67 fix(flutter): 缩小播放暂停按钮 2026-10-20 14:41:27 +08:00
Zhou Weidong 33de925f65 merge: 合并 K33-J 清屏进度与长按恢复 2026-10-20 14:38:12 +08:00
Zhou Weidong b2c219d6bd fix(flutter): 清屏态进度可拖动并长按恢复播放 2026-10-20 14:38:09 +08:00
Zhou Weidong 8327c1dd13 merge: 合并 K33-I feed 清屏与寻址恢复 2026-10-20 14:33:39 +08:00
Zhou Weidong c87798fb16 fix(flutter): feed 清屏保留顶栏并寻址恢复播放 2026-10-20 14:33:35 +08:00
Zhou Weidong 9b99d96a07 merge: 合并 K33-H 画中画回前台复位 2026-10-20 14:29:02 +08:00
Zhou Weidong bc8f3bcab7 fix(flutter): 回前台复位残留画中画状态 2026-10-20 14:28:05 +08:00
Zhou Weidong 28ff9a7642 merge: 合并 K33-G feed rail 与更多弹窗 2026-10-20 14:23:40 +08:00
Zhou Weidong 7e415f6f4f fix(flutter): feed rail 底部对齐并复用播放页更多 2026-10-20 14:23:36 +08:00
Zhou Weidong e800f5971f merge: 合并 K33-F feed 长按与刷新 2026-10-20 14:19:20 +08:00
Zhou Weidong ea9ea6e606 fix(flutter): feed 长按加速与刷新转圈 2026-10-20 14:19:14 +08:00
Zhou Weidong 564c0dbbef merge: 合并 K33-E 首页 feed 刷新与进度优化 2026-10-20 14:11:46 +08:00
Zhou Weidong 82fe237c24 fix(flutter): 首页 feed 静默刷新与进度条细节 2026-10-20 14:11:42 +08:00
Zhou Weidong a3126723d2 merge: 合并 K33-D 首页 feed 精修 2026-10-20 14:05:28 +08:00
Zhou Weidong 636b369bd4 fix(flutter): 首页 feed 去底色并优化整集条 2026-10-20 14:05:24 +08:00
Zhou Weidong f94926cff5 merge: 合并 K33-C 弹幕文案清理 2026-10-20 13:58:27 +08:00
Zhou Weidong 521ebfc1ab fix(flutter): 清理未使用的弹幕文案 key 2026-10-20 13:58:26 +08:00
Zhou Weidong c3f5f43744 merge: 合并 K33-C 首页 feed 精修 2026-10-20 13:56:55 +08:00
Zhou Weidong 321c62664c fix(flutter): 精修首页 feed 交互与底色 2026-10-20 13:56:51 +08:00
Zhou Weidong 435fbbbbb1 merge: 合并 K33-B 深色首页导航追加 2026-10-20 13:48:45 +08:00
Zhou Weidong d36d62f6a6 fix(flutter): 去除关注 tab 并加深色首页导航 2026-10-20 13:48:42 +08:00
Zhou Weidong ea28420489 merge: 合并 K33-B 首页 feed 浮层重做 2026-10-20 13:46:24 +08:00
Zhou Weidong ee6f0d7e72 feat(flutter): 按参考图重做首页 feed 浮层 2026-10-20 13:46:20 +08:00
Zhou Weidong d6b055dc62 merge: 合并 K33-A 失效轮播用例清理 2026-10-20 13:35:40 +08:00
Zhou Weidong fe84244e7d test(flutter): 移除失效的首页轮播用例 2026-10-20 13:35:40 +08:00
Zhou Weidong 53cd2dbba1 merge: 合并 K33-A 首页沉浸式 feed 2026-10-20 13:35:13 +08:00
Zhou Weidong b7926d64a5 feat(flutter): 首页改为沉浸式 feed 流 2026-10-20 13:35:09 +08:00
Zhou Weidong 698fb12e07 merge: 合并 K32-A 识别入口移除 2026-10-20 13:27:07 +08:00
Zhou Weidong 5e2fba1746 fix(flutter): 移除搜索框识剧入口 2026-10-20 13:27:03 +08:00
Zhou Weidong edfef61ee5 merge: 合并 K32-A 剧场页重做 2026-10-20 13:24:24 +08:00
Zhou Weidong 5a51af4264 feat(flutter): 剧场页按参考图重做 2026-10-20 13:24:21 +08:00
Zhou Weidong 2abb8c7032 merge: 合并 K31-G 首页通知铃铛 2026-10-20 13:11:49 +08:00
Zhou Weidong 524b7f5b35 feat(flutter): 首页顶栏改为通知铃铛入口 2026-10-20 13:11:46 +08:00
Zhou Weidong e32fa58979 merge: 合并 K31-F 移除封面锁定角标 2026-10-20 13:08:07 +08:00
Zhou Weidong 1fc25ec5d1 fix(flutter): 首页剧卡移除封面锁定角标 2026-10-20 13:08:03 +08:00
Zhou Weidong 40e23793b4 merge: 合并 K31-E 首页剧卡压缩 2026-10-20 13:03:22 +08:00
Zhou Weidong 124ba75ab8 feat(flutter): 压缩首页剧卡并把播放次数上封面 2026-10-20 13:03:19 +08:00
Zhou Weidong 8e2cd5c838 merge: 合并 K31-D 首页频道吸顶 2026-10-20 12:56:17 +08:00
Zhou Weidong 7cc65b7b09 feat(flutter): 首页频道行滚动吸顶 2026-10-20 12:56:13 +08:00
Zhou Weidong cfc41efddc merge: 合并 K31-C 频道选中态主题色 2026-10-20 12:50:40 +08:00
Zhou Weidong 0e48c0773e fix(flutter): 首页频道选中态改用主题色 2026-10-20 12:50:37 +08:00
Zhou Weidong 290b92c6a7 merge: 合并 K31-B 首页频道与轮播改版 2026-10-20 12:45:16 +08:00
Zhou Weidong a24f1fa7c0 feat(flutter): 首页频道叠加轮播并改下划线选中 2026-10-20 12:45:13 +08:00
Zhou Weidong 36c71e63b4 merge: 合并 K31-A 加速态底部修正 2026-10-20 12:39:55 +08:00
Zhou Weidong 033ae5987c fix(flutter): 加速态隐藏底部选集与清屏按钮 2026-10-20 12:39:52 +08:00
Zhou Weidong 7a7bf0c207 merge: 合并 K31-A 播放页长按手势 2026-10-20 12:36:38 +08:00
Zhou Weidong 96074bc5e8 feat(flutter): 播放页长按手势与加速态 2026-10-20 12:36:35 +08:00
Zhou Weidong 7bb2714941 merge: 合并 K30-F 半透明播放按钮追加 2026-10-20 12:30:09 +08:00
Zhou Weidong a72adcfc3f fix(flutter): 放大播放图标并加半透明底 2026-10-20 12:30:06 +08:00
Zhou Weidong b8d2617873 merge: 合并 K30-F 播放按钮放大追加 2026-10-20 12:27:17 +08:00
Zhou Weidong 8013cc18ad fix(flutter): 放大透明播放按钮 2026-10-20 12:27:13 +08:00
Zhou Weidong ac73bf2208 merge: 合并 K30-F 简介灰字与透明暂停按钮 2026-10-20 12:24:41 +08:00
Zhou Weidong 02763c3dfb fix(flutter): 简介灰字统一并透明暂停按钮 2026-10-20 12:24:37 +08:00
Zhou Weidong 427e7a7897 merge: 合并 K30-E 选集条箭头靠右 2026-10-20 12:19:23 +08:00
Zhou Weidong 90769de3b7 fix(flutter): 选集条箭头靠右排列 2026-10-20 12:19:20 +08:00
Zhou Weidong ced0fae64a merge: 合并 K30-D 评论输入框尺寸修正 2026-10-20 12:16:06 +08:00
Zhou Weidong 8709b64dec fix(flutter): 修正评论输入框高度与圆角 2026-10-20 12:16:03 +08:00
Zhou Weidong edcd25fe68 merge: 合并 K30-C 评论面板视觉重做 2026-10-20 12:11:48 +08:00
Zhou Weidong 9493ad23bb fix(flutter): 重做评论面板视觉 2026-10-20 12:11:44 +08:00
Zhou Weidong ced1ec3d9e merge: 合并 K30-B 播放页数量与评论面板 2026-10-20 12:06:23 +08:00
Zhou Weidong bed6bbb75b feat(flutter): 播放页互动数量与评论面板收紧 2026-10-20 12:06:21 +08:00
Zhou Weidong e39aa766ac merge: 合并 K29.9 播放页底部细节 2026-10-20 11:55:31 +08:00
Zhou Weidong d25519bed7 fix(flutter): 调整播放页底部信息排版 2026-10-20 11:55:28 +08:00
Zhou Weidong 55d22a9940 merge: 合并 K29.8 更多面板小窗播放 2026-10-20 11:48:02 +08:00
Zhou Weidong 0af5de9641 fix(flutter): 更多面板增加小窗播放入口 2026-10-20 11:47:58 +08:00
Zhou Weidong 3b2bdb8e5a merge: 合并 K29.7 更多面板单行排列 2026-10-20 11:44:45 +08:00
Zhou Weidong f083a161e6 fix(flutter): 更多面板倍速清晰度改为单行排列 2026-10-20 11:44:42 +08:00
Zhou Weidong 1e3e5b64dc merge: 合并 K29.6 更多面板精简 2026-10-20 11:40:15 +08:00
Zhou Weidong c4797cd239 fix(flutter): 精简播放页更多面板 2026-10-20 11:40:15 +08:00
Zhou Weidong 08440b0160 merge: 合并 K29.5 进度条保留修正 2026-10-20 11:35:48 +08:00
Zhou Weidong d849834ce6 fix(flutter): 清屏时保留进度条 2026-10-20 11:35:48 +08:00
Zhou Weidong b26ca6cb9f merge: 合并 K29.5 清屏范围 2026-10-20 11:33:23 +08:00
Zhou Weidong 3068eb99da fix(flutter): 收窄清屏范围并保留底栏 2026-10-20 11:33:23 +08:00
Zhou Weidong a26b0071e4 merge: 合并 K29.4 透明播放页 2026-10-20 11:29:02 +08:00
Zhou Weidong 0af68f2455 fix(flutter): 播放页改为透明叠层 2026-10-20 11:29:02 +08:00
Zhou Weidong d762511412 merge: 合并 K29.3 深色播放器 2026-10-20 11:25:28 +08:00
Zhou Weidong 1dbe2cb8e9 fix(flutter): 恢复播放器深色外观 2026-10-20 11:25:28 +08:00
Zhou Weidong b2284b3a74 merge: 合并 K29.2 底栏清屏 2026-10-20 11:19:14 +08:00
Zhou Weidong 1666bc220e fix(flutter): 底栏右侧改为清屏 2026-10-20 11:19:14 +08:00
Zhou Weidong 9429257d62 merge: 合并 K29.1 浅色播放页底栏 2026-10-20 11:14:08 +08:00
Zhou Weidong e2162773e2 fix(flutter): 改为浅色播放页底栏 2026-10-20 11:14:08 +08:00
Zhou Weidong cc26c5472b merge: 合并 K29.1 播放页视觉收紧 2026-10-20 11:12:12 +08:00
Zhou Weidong cf93d8d794 fix(flutter): 收紧播放页底部与互动栏 2026-10-20 11:12:12 +08:00
Zhou Weidong 7b4a792c12 merge: 合并 K29 全屏胶囊双态切换 2026-10-20 11:03:53 +08:00
Zhou Weidong 6dbeb3e505 fix(flutter): 支持全屏胶囊双态切换 2026-10-20 11:03:53 +08:00
Zhou Weidong c4ad5d72d7 merge: 合并 K29 播放页与语言切换 2026-10-20 11:01:55 +08:00
Zhou Weidong 8c43e9e385 feat(flutter): 重构播放页并增加语言切换 2026-10-20 11:01:38 +08:00
Zhou Weidong f1efbc5be8 merge: 合并 K28-C 播放页与操作面板 2026-10-20 10:42:05 +08:00
Zhou Weidong 9a4269b847 feat(flutter): 重设计播放页与操作面板 2026-10-20 10:41:56 +08:00
Zhou Weidong 148fbc3c93 merge: 合并 K28-B 创作与我的页面 2026-10-20 10:28:38 +08:00
Zhou Weidong 03c3964353 feat(flutter): 重设计创作与我的页面 2026-10-20 10:28:31 +08:00
Zhou Weidong d50c6e6d9f merge: 合并 K28-A2 剧场与 Hero 重设计 2026-10-20 10:24:26 +08:00
Zhou Weidong 73aef4e44e feat(flutter): 重设计剧场与首页 Hero 2026-10-20 10:24:19 +08:00
Zhou Weidong 678b976ce1 merge: 合并 K28-S 四槽导航修正 2026-10-20 10:18:18 +08:00
Zhou Weidong 2f1a4c5c69 fix(flutter): 回退底部导航为四槽 2026-10-20 10:18:12 +08:00
Zhou Weidong e448003f1b merge: 合并 K28-S 浅色暖橙视觉主题 2026-10-20 10:15:12 +08:00
Zhou Weidong 634b63e16c feat(flutter): 应用浅色暖橙视觉主题 2026-10-20 10:15:03 +08:00
Zhou Weidong 0d2367431d merge: 合并 K28-A1 客户端四 Tab 首页 2026-10-20 09:57:17 +08:00
Zhou Weidong 49f3e63ed0 feat(flutter): 完成 K28-A1 四 Tab 首页 2026-10-20 09:57:10 +08:00
Zhou Weidong f981f88e0d merge: 合并 K28-A 客户端 UI 重构 2026-10-20 09:49:51 +08:00
Zhou Weidong 959edc0a32 feat(flutter): 重构四 Tab 与浏览页 2026-10-20 09:49:42 +08:00
Zhou Weidong 99c72e2c32 chore(config): 探测地址改为本机后端 127.0.0.1:8081 2026-10-20 09:26:12 +08:00
Zhou Weidong 6022d789ed chore(tools): 添加短剧资源批量上线脚本 2026-10-20 09:11:45 +08:00
Zhou Weidong 842e716ea6 merge: 红果式三 Tab 剧场与极简我的页 2026-10-20 05:29:54 +08:00
Zhou Weidong 12e57de3b3 chore(l10n): 清理已移除语言入口文案 2026-10-20 05:29:21 +08:00
Zhou Weidong 35338cbfc8 feat(app): 剧场页九宫格与极简我的页 2026-10-20 05:28:51 +08:00
Zhou Weidong dc8ca7b743 feat(app): 底部导航精简为三 Tab 2026-10-20 05:14:52 +08:00
Zhou Weidong 19caaa0a22 fix(app): 增加公网地址回退,远程/移动网络可连后端 2026-10-20 05:04:36 +08:00
Zhou Weidong 7ca7d20630 merge: 第二轮 App 接口对接 2026-10-20 04:39:51 +08:00
Zhou Weidong 7850dfc746 style(app): 格式化并拆分长函数 2026-10-20 04:38:36 +08:00
Zhou Weidong 2b2b7e9913 feat(app): 播放器增加清晰度档位入口 2026-10-20 04:37:42 +08:00
Zhou Weidong 30a5fa0776 feat(app): 剧场列表增加语种筛选 2026-10-20 04:35:48 +08:00
Zhou Weidong ade0cc3566 feat(app): 搜索改为调用后端并渲染卡片结果 2026-10-20 04:34:12 +08:00
Zhou Weidong 730e38622f feat(app): 接入金币充值与会员商品下单 2026-10-20 04:32:03 +08:00
Zhou Weidong ec2db6842e merge: Flutter P0-P2 App 对接完成 2026-10-19 23:58:11 +08:00
Zhou Weidong dfc650456a feat(app): 展示真实点赞与评论计数 2026-10-19 23:56:12 +08:00
Zhou Weidong 7b927c794e feat(app): 接入首页轮播 banner 2026-10-19 23:53:03 +08:00
Zhou Weidong 061aac6577 fix(app): 移除设置页调试服务器地址 2026-10-19 23:51:15 +08:00
Zhou Weidong 2b95c79931 feat(app): 增加首页榜单 tab 2026-10-19 23:50:03 +08:00
Zhou Weidong 136ee74618 feat(app): 完成 P0 钱包解锁、下载落盘与手机号登录 2026-10-19 23:46:47 +08:00
Zhou Weidong de941c841c fix(app): 对齐 release 修复(播放明文HTTP、shared_preferences 降级防崩溃、双网络访问) 2026-10-19 22:43:04 +08:00
297 changed files with 25424 additions and 8085 deletions

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_icon" />
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<!-- Android 12 already shows the icon in the system splash. This layer stays
the same color so the icon is not drawn a second time. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_icon" />
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
@@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -6,6 +6,8 @@
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/splash_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
</style>
</resources>
@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<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:windowLayoutInDisplayCutoutMode">shortEdges</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
@@ -6,6 +6,8 @@
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/splash_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
</style>
</resources>
+2 -3
View File
@@ -2,6 +2,5 @@
<!-- Author: fruit.com.cn -->
<resources>
<color name="splash_background">#FF100C0A</color>
<color name="ic_launcher_background">#C4322A</color>
</resources>
<color name="splash_background">#1F1B16</color>
</resources>
+4 -1
View File
@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Author: fruit.com.cn -->
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<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:windowLayoutInDisplayCutoutMode">shortEdges</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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

@@ -1,23 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 37 KiB

@@ -1,5 +0,0 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
+17 -6
View File
@@ -9,16 +9,26 @@
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="0.06274509803921569" green="0.047058823529411764" blue="0.0392156862745098" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="cx-launch"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="cy-launch"/>
<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>
@@ -28,6 +38,7 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="84" height="84"/>
<image name="LaunchImage" width="1152" height="1152"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>
+33
View File
@@ -28,8 +28,41 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>192.168.31.187</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>39.162.50.99</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>开发阶段需要访问同一局域网内的短剧内容服务。</string>
+59 -12
View File
@@ -6,13 +6,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'core/config/locale_controller.dart';
import 'core/config/app_locale.dart';
import 'core/data/remote/account_api.dart';
import 'core/data/remote/api_client.dart';
import 'core/data/remote/auth_api.dart';
import 'core/data/remote/catalog_api.dart';
import 'core/data/remote/category_api.dart';
import 'core/data/remote/comment_api.dart';
import 'core/data/remote/commerce_api.dart';
import 'core/data/remote/download_api.dart';
import 'core/data/remote/interaction_api.dart';
import 'core/data/remote/library_api.dart';
@@ -23,11 +24,14 @@ import 'core/data/repositories/catalog_repository.dart';
import 'core/data/repositories/account_repository.dart';
import 'core/data/repositories/category_repository.dart';
import 'core/data/repositories/comment_repository.dart';
import 'core/data/repositories/commerce_repository.dart';
import 'core/data/repositories/download_repository.dart';
import 'core/data/repositories/favorite_repository.dart';
import 'core/data/repositories/interaction_repository.dart';
import 'core/data/repositories/notice_repository.dart';
import 'core/data/repositories/playback_repository.dart';
import 'core/data/repositories/search_repository.dart';
import 'core/data/repositories/store_repository.dart';
import 'core/data/repositories/watch_history_repository.dart';
import 'core/data/services/auth_controller.dart';
import 'core/data/services/auth_session_store.dart';
@@ -36,14 +40,27 @@ import 'core/data/services/category_controller.dart';
import 'core/data/services/download_controller.dart';
import 'core/data/services/favorite_controller.dart';
import 'core/data/services/interaction_controller.dart';
import 'core/data/services/locale_controller.dart';
import 'core/data/services/playback_session.dart';
import 'core/data/services/video_player_port.dart';
import 'core/data/services/wallet_controller.dart';
import 'core/navigation/app_route_observer.dart';
import 'core/platform/haptics.dart';
import 'core/theme/app_theme.dart';
import 'core/theme/app_colors.dart';
import 'l10n/app_localizations.dart';
import 'pages/home/home_view.dart';
/// 全局系统栏样式:浅暖导航栏、关闭 Android 对比遮罩。
final SystemUiOverlayStyle appSystemOverlayStyle = SystemUiOverlayStyle.dark
.copyWith(
statusBarColor: Colors.transparent,
systemNavigationBarColor: AppColors.ink,
systemNavigationBarDividerColor: Colors.transparent,
systemNavigationBarIconBrightness: Brightness.dark,
systemNavigationBarContrastEnforced: false,
);
class LumaDramaApp extends StatefulWidget {
const LumaDramaApp({
super.key,
@@ -58,6 +75,9 @@ class LumaDramaApp extends StatefulWidget {
this.interactionRepository,
this.noticeRepository,
this.accountRepository,
this.commerceRepository,
this.storeRepository,
this.searchRepository,
this.haptics = const FlutterHaptics(),
});
@@ -74,6 +94,9 @@ class LumaDramaApp extends StatefulWidget {
final InteractionRepository? interactionRepository;
final NoticeRepository? noticeRepository;
final AccountRepository? accountRepository;
final CommerceRepository? commerceRepository;
final StoreRepository? storeRepository;
final SearchRepository? searchRepository;
final Haptics haptics;
@override
@@ -134,6 +157,20 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
_downloadRepository,
);
late final CommerceRepository _commerceRepository =
widget.commerceRepository ??
HttpCommerceRepository(CommerceApi(_apiClient));
late final WalletController _walletController = WalletController(
_commerceRepository,
);
late final SearchRepository _searchRepository =
widget.searchRepository ?? HttpSearchRepository(CatalogApi(_apiClient));
late final StoreRepository _storeRepository =
widget.storeRepository ?? HttpStoreRepository(CommerceApi(_apiClient));
late final CommentRepository _commentRepository =
widget.commentRepository ?? HttpCommentRepository(CommentApi(_apiClient));
@@ -150,14 +187,19 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
late final InteractionController _interactionController =
InteractionController(_interactionRepository);
late final LocaleController _localeController = LocaleController();
@override
void initState() {
super.initState();
// 恢复本地令牌与设备号;未登录也能正常浏览,不阻塞首屏
_auth.load();
unawaited(_localeController.load());
// 登录/登出时同步收藏状态:收藏属于账号,不能跨账号串联
_auth.addListener(_onAuthChanged);
if (_auth.isSignedIn) unawaited(_favoriteController.load());
if (_auth.isSignedIn) unawaited(_walletController.load());
unawaited(_downloadController.restore());
}
bool _wasSignedIn = false;
@@ -168,9 +210,11 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
_interactionController.clear();
if (_auth.isSignedIn) {
unawaited(_favoriteController.load());
unawaited(_walletController.load());
} else {
_favoriteController.clear();
_downloadController.reset();
_walletController.reset();
}
}
@@ -179,7 +223,9 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
_auth.removeListener(_onAuthChanged);
_favoriteController.dispose();
_downloadController.dispose();
_walletController.dispose();
_interactionController.dispose();
_localeController.dispose();
_apiClient.close();
super.dispose();
}
@@ -198,6 +244,9 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
Provider<InteractionRepository>.value(value: _interactionRepository),
Provider<NoticeRepository>.value(value: _noticeRepository),
Provider<AccountRepository>.value(value: _accountRepository),
Provider<CommerceRepository>.value(value: _commerceRepository),
Provider<StoreRepository>.value(value: _storeRepository),
Provider<SearchRepository>.value(value: _searchRepository),
Provider<Haptics>.value(value: widget.haptics),
ChangeNotifierProvider(
create: (_) => CatalogController(_catalogRepository)..load(),
@@ -205,30 +254,28 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
ChangeNotifierProvider(
create: (_) => CategoryController(_categoryRepository)..load(),
),
ChangeNotifierProvider(create: (_) => LocaleController()..load()),
ChangeNotifierProvider.value(value: _favoriteController),
ChangeNotifierProvider.value(value: _downloadController),
ChangeNotifierProvider.value(value: _walletController),
ChangeNotifierProvider.value(value: _interactionController),
ChangeNotifierProvider.value(value: _localeController),
ChangeNotifierProvider.value(value: _auth),
],
child: Consumer<LocaleController>(
builder: (context, locale, _) => MaterialApp(
builder: (context, localeController, _) => MaterialApp(
locale: localeController.locale,
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
locale: locale.locale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
navigatorObservers: [appRouteObserver],
localeResolutionCallback: (deviceLocale, _) =>
resolveAppLocale(locale.locale, deviceLocale),
resolveAppLocale(deviceLocale),
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.dark,
themeMode: ThemeMode.light,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
darkTheme: AppTheme.light(),
builder: (context, child) => AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light.copyWith(
statusBarColor: Colors.transparent,
systemNavigationBarColor: AppColors.bar,
systemNavigationBarIconBrightness: Brightness.light,
),
value: appSystemOverlayStyle,
child: child ?? const SizedBox.shrink(),
),
home: const HomePage(),
+107 -25
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import 'dart:io';
import 'package:flutter/foundation.dart';
@@ -9,20 +11,36 @@ import 'package:flutter/foundation.dart';
class AppConfig {
const AppConfig._();
/// 真机默认使用的服务地址。
/// 真机默认使用的开发机局域网地址。
///
/// 当前指向公网服务;换地址时改这一行。
static const String devHostBaseUrl = 'http://39.162.50.99:8081';
/// 这个地址跟着电脑所在的网络走,换网络后要用
/// `ipconfig getifaddr en0` 重新确认。默认值只改这一行。
static const String devHostBaseUrl = 'http://192.168.31.187:8081';
/// 双网络探测的首选地址,当前与默认地址一致(公网服务)。
static const String lanBaseUrl = 'http://39.162.50.99:8081';
/// 本机后端地址:通过 `adb reverse tcp:8081 tcp:8081` 映射到开发机,
/// 探测可达时 App 直连本机服务;不可达时回落到公网地址。
static const String lanBaseUrl = 'http://127.0.0.1:8081';
static const String _loopbackBaseUrl = 'http://127.0.0.1:8081';
/// 公网地址(远程/移动网络访问时使用)。
static const String publicBaseUrl = 'http://39.162.50.99:8081';
static const String _overrideBaseUrl = String.fromEnvironment('API_BASE_URL');
static String _runtimeBaseUrl = '';
static Future<void>? _ready;
/// 启动地址探测完成信号;默认已完成,避免影响既有测试与调用方。
static Future<void> get ready => _ready ?? Future<void>.value();
static set ready(Future<void> value) => _ready = value;
static const Duration _probeTimeout = Duration(seconds: 2);
static const List<String> _probeCandidates = [
lanBaseUrl,
devHostBaseUrl,
publicBaseUrl,
];
/// Build-time server URL supplied through `--dart-define=API_BASE_URL`.
static String get compiledBaseUrl => _overrideBaseUrl;
@@ -35,7 +53,7 @@ class AppConfig {
: (compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl);
static final String _defaultBaseUrl = _runsOnDevMachine
? _loopbackBaseUrl
? lanBaseUrl
: devHostBaseUrl;
/// 模拟器、桌面端与开发机同机,环回地址可用;iOS 真机不满足。
@@ -61,28 +79,85 @@ class AppConfig {
static const Duration requestTimeout = Duration(seconds: 8);
/// 探测内网地址是否可达,可达则切换为内网直连,否则回落到公网地址。
/// 按环回、开发机、公网顺序探测,写入首个返回 200 的地址。
///
/// Wi-Fi 下内网直连更快;移动数据下内网不可达,自动回落到公网端口映射。
static Future<void> probeAndApplyBaseUrl() async {
try {
final client = HttpClient()
..connectionTimeout = const Duration(seconds: 2);
final request = await client.getUrl(
Uri.parse('$lanBaseUrl/app/categories'),
);
final response = await request.close().timeout(const Duration(seconds: 2));
await response.drain<void>();
client.close(force: true);
if (response.statusCode == 200) {
applyRuntimeBaseUrl(lanBaseUrl);
/// 全部不可达时使用平台默认地址,真机保留开发机地址,避免退回公网。
static Future<void> probeAndApplyBaseUrl({
Future<bool> Function(String baseUrl)? probe,
String? fallbackBaseUrl,
/// 编译期地址测试注入;未传时读取 `API_BASE_URL`。
String? compiledOverride,
}) async {
final compiled = compiledOverride ?? compiledBaseUrl;
if (compiled.isNotEmpty) {
if (_runtimeBaseUrl.isEmpty) applyRuntimeBaseUrl(compiled);
return;
}
final candidates = <String>[
if (_runtimeBaseUrl.isNotEmpty) _runtimeBaseUrl,
for (final candidate in _probeCandidates)
if (candidate != _runtimeBaseUrl) candidate,
];
final results = await Future.wait([
for (final candidate in candidates)
probe == null
? _probeBaseUrl(candidate)
: _runInjectedProbe(candidate, probe),
]);
for (var index = 0; index < candidates.length; index++) {
final candidate = candidates[index];
final result = results[index];
if (kDebugMode) {
final status = result.ok
? 'ok'
: 'fail(${result.reason ?? 'request failed'})';
debugPrint('[probe] $candidate -> $status');
}
if (result.ok) {
applyRuntimeBaseUrl(candidate);
return;
}
} catch (_) {
// 内网不可达,回落到公网。
}
applyRuntimeBaseUrl(
compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl,
applyRuntimeBaseUrl(fallbackBaseUrl ?? defaultBaseUrl);
}
static Future<_ProbeResult> _runInjectedProbe(
String baseUrl,
Future<bool> Function(String baseUrl) probe,
) async {
try {
return _ProbeResult(ok: await probe(baseUrl));
} catch (error) {
return _ProbeResult(ok: false, reason: error.toString());
}
}
static Future<_ProbeResult> _probeBaseUrl(String baseUrl) async {
final client = HttpClient()..connectionTimeout = _probeTimeout;
try {
return await _requestCategories(
client,
Uri.parse('$baseUrl/app/categories'),
).timeout(_probeTimeout);
} catch (error) {
return _ProbeResult(ok: false, reason: error.toString());
} finally {
client.close(force: true);
}
}
static Future<_ProbeResult> _requestCategories(
HttpClient client,
Uri uri,
) async {
final request = await client.getUrl(uri);
final response = await request.close();
await response.drain<void>();
final ok = response.statusCode == 200;
return _ProbeResult(
ok: ok,
reason: ok ? null : 'HTTP ${response.statusCode}',
);
}
@@ -93,3 +168,10 @@ class AppConfig {
return '$apiBaseUrl$path';
}
}
class _ProbeResult {
const _ProbeResult({required this.ok, this.reason});
final bool ok;
final String? reason;
}
+12
View File
@@ -0,0 +1,12 @@
// Author: fruit.com.cn
import 'package:flutter/widgets.dart';
/// 将系统语言解析为受支持的语言;不支持时回退印尼语。
Locale resolveAppLocale(Locale? deviceLocale) {
return switch (deviceLocale?.languageCode) {
'zh' => const Locale('zh'),
'en' => const Locale('en'),
_ => const Locale('id'),
};
}
-56
View File
@@ -1,56 +0,0 @@
// Author: fruit.com.cn
import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Resolves the selected or system language to one of the supported app locales.
///
/// Indonesian is the launch-market fallback when a device uses an unsupported
/// language. The legacy `in` code is treated as Indonesian as well.
Locale resolveAppLocale(Locale? selected, Locale? deviceLocale) {
final code = (selected ?? deviceLocale)?.languageCode;
return switch (code) {
'zh' => const Locale('zh'),
'en' => const Locale('en'),
_ => const Locale('id'),
};
}
class LocaleController extends ChangeNotifier {
LocaleController({SharedPreferencesAsync? preferences})
: _preferences = preferences ?? SharedPreferencesAsync();
final SharedPreferencesAsync _preferences;
Locale? _locale;
Locale? get locale => _locale;
Future<void> load() async {
try {
final code = await _preferences.getString('ui.locale');
_locale = switch (code) {
'en' => const Locale('en'),
'zh' => const Locale('zh'),
'id' => const Locale('id'),
_ => null,
};
notifyListeners();
} catch (_) {
// The system locale remains usable if preferences are unavailable.
}
}
Future<void> select(String code) async {
_locale = switch (code) {
'en' => const Locale('en'),
'zh' => const Locale('zh'),
'id' => const Locale('id'),
_ => null,
};
notifyListeners();
try {
await _preferences.setString('ui.locale', code);
} catch (_) {
// Preference persistence is best effort for this prototype.
}
}
}
+1
View File
@@ -69,6 +69,7 @@ abstract final class ServerEndpoint {
preferenceKey,
normalized,
);
AppConfig.applyRuntimeBaseUrl(normalized);
return true;
} catch (_) {
return false;
+22
View File
@@ -65,6 +65,10 @@ class Drama {
this.freeEpisodes = 0,
this.isHot = false,
this.viewCount = 0,
this.likeCount,
this.favoriteCount,
this.commentCount,
this.language = '',
this.detailLoaded = false,
});
@@ -80,6 +84,14 @@ class Drama {
final bool isHot;
final int viewCount;
/// 详情下发的互动数量;卡片或旧接口没有这些字段时为 null(客户端不显示数字)。
final int? likeCount;
final int? favoriteCount;
final int? commentCount;
/// 卡片语种(如 zh-CN / id),用于剧场语言筛选。
final String language;
/// 详情接口是否已经取过完整剧集列表。
/// 列表卡片只带一集可播预览,需要完整剧集时再进详情。
final bool detailLoaded;
@@ -104,6 +116,10 @@ class Drama {
isHot: json['isHot'] == true,
// viewCount 后端是 Long,按字符串下发(见 json_numbers.dart)
viewCount: jsonToInt(json['viewCount']),
likeCount: _optionalInt(json['likeCount']),
favoriteCount: _optionalInt(json['favoriteCount']),
commentCount: _optionalInt(json['commentCount']),
language: json['language'] as String? ?? '',
);
}
@@ -120,6 +136,10 @@ class Drama {
freeEpisodes: (json['freeEpisodes'] as num?)?.toInt() ?? 0,
isHot: json['isHot'] == true,
viewCount: jsonToInt(json['viewCount']),
likeCount: _optionalInt(json['likeCount']),
favoriteCount: _optionalInt(json['favoriteCount']),
commentCount: _optionalInt(json['commentCount']),
language: json['language'] as String? ?? '',
detailLoaded: true,
episodes: (json['episodes'] as List? ?? const [])
.whereType<Map<String, dynamic>>()
@@ -128,3 +148,5 @@ class Drama {
);
}
}
int? _optionalInt(dynamic value) => value == null ? null : jsonToInt(value);
+49
View File
@@ -0,0 +1,49 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 充值订单:`POST /app/orders` 创建的 `CREATED` 订单,等待支付。
class AppOrder {
const AppOrder({
required this.id,
required this.orderNo,
required this.productName,
required this.coinAmount,
required this.status,
this.productId = '',
this.type = '',
this.amount = '',
this.currency = '',
this.createdAt,
});
static const String created = 'CREATED';
final String id;
final String orderNo;
final String productId;
final String productName;
final String type;
final int coinAmount;
final String amount;
final String currency;
final String status;
final DateTime? createdAt;
bool get isCreated => status == created;
factory AppOrder.fromJson(Map<String, dynamic> json) {
return AppOrder(
id: (json['id'] ?? '').toString(),
orderNo: json['orderNo'] as String? ?? '',
productId: (json['productId'] ?? '').toString(),
productName: json['productName'] as String? ?? '',
type: json['type'] as String? ?? '',
coinAmount: jsonToInt(json['coinAmount']),
amount: (json['amount'] ?? '').toString(),
currency: json['currency'] as String? ?? '',
status: json['status'] as String? ?? '',
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
);
}
}
+8
View File
@@ -13,6 +13,7 @@ class PlayTicket {
this.durationSec = 0,
this.unlockCoin = 0,
this.previewSeconds = 0,
this.qualities = const [],
});
static const String free = 'FREE';
@@ -35,6 +36,9 @@ class PlayTicket {
/// 试看秒数:仅 `COIN_REQUIRED` 时有意义(付费解锁在 B2 做)。
final int previewSeconds;
/// 后端下发的清晰度档位(如 AUTO / 720P / 1080P),HLS 由播放器自动切换。
final List<String> qualities;
bool get needsUnlock =>
accessType == coinRequired || accessType == vipRequired;
@@ -50,6 +54,10 @@ class PlayTicket {
durationSec: jsonToInt(json['durationSec']),
unlockCoin: jsonToInt(json['unlockCoin']),
previewSeconds: jsonToInt(json['previewSeconds']),
qualities: (json['qualities'] as List? ?? const [])
.whereType<String>()
.where((quality) => quality.isNotEmpty)
.toList(),
);
}
}
+54
View File
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 充值商品:`GET /app/products` 的 `COIN_PACK` / `VIP` 条目。
class AppProduct {
const AppProduct({
required this.id,
required this.code,
required this.name,
required this.type,
this.coinAmount = 0,
this.bonusCoin = 0,
this.totalCoin = 0,
this.displayPrice = '',
this.currency = '',
this.vipDays = 0,
});
static const String coinPackType = 'COIN_PACK';
static const String vipType = 'VIP';
final String id;
final String code;
final String name;
final String type;
final int coinAmount;
final int bonusCoin;
final int totalCoin;
final String displayPrice;
final String currency;
final int vipDays;
bool get isVip => type == vipType;
factory AppProduct.fromJson(Map<String, dynamic> json) {
final coinAmount = jsonToInt(json['coinAmount']);
final bonusCoin = jsonToInt(json['bonusCoin']);
return AppProduct(
id: (json['id'] ?? '').toString(),
code: json['code'] as String? ?? '',
name: json['name'] as String? ?? '',
type: json['type'] as String? ?? '',
coinAmount: coinAmount,
bonusCoin: bonusCoin,
totalCoin: json['totalCoin'] == null
? coinAmount + bonusCoin
: jsonToInt(json['totalCoin']),
displayPrice: json['displayPrice'] as String? ?? '',
currency: json['currency'] as String? ?? '',
vipDays: jsonToInt(json['vipDays']),
);
}
}
+22
View File
@@ -0,0 +1,22 @@
// Author: fruit.com.cn
import 'drama.dart';
import 'json_numbers.dart';
/// 搜索分页结果:`GET /app/search` 的 `{total, list}`。
class SearchResult {
const SearchResult({this.total = 0, this.list = const []});
final int total;
final List<Drama> list;
factory SearchResult.fromJson(Map<String, dynamic> json) {
return SearchResult(
total: jsonToInt(json['total']),
list: (json['list'] as List? ?? const [])
.whereType<Map<String, dynamic>>()
.map(Drama.fromCardJson)
.toList(),
);
}
}
+107
View File
@@ -0,0 +1,107 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 金币钱包与流水:对应 `GET /app/wallet` 的 `wallet` / `ledger`。
class CoinWallet {
const CoinWallet({
required this.coinBalance,
required this.totalRechargedCoin,
required this.totalConsumedCoin,
required this.totalRefundedCoin,
});
final int coinBalance;
final int totalRechargedCoin;
final int totalConsumedCoin;
final int totalRefundedCoin;
factory CoinWallet.fromJson(Map<String, dynamic> json) {
return CoinWallet(
coinBalance: jsonToInt(json['coinBalance']),
totalRechargedCoin: jsonToInt(json['totalRechargedCoin']),
totalConsumedCoin: jsonToInt(json['totalConsumedCoin']),
totalRefundedCoin: jsonToInt(json['totalRefundedCoin']),
);
}
}
/// 一笔金币变动;[changeAmount] 正数入账、负数出账。
class CoinLedgerEntry {
const CoinLedgerEntry({
required this.id,
required this.changeAmount,
required this.balanceAfter,
required this.bizType,
required this.remark,
required this.createdAt,
});
final String id;
final int changeAmount;
final int balanceAfter;
final String bizType;
final String remark;
final DateTime? createdAt;
factory CoinLedgerEntry.fromJson(Map<String, dynamic> json) {
return CoinLedgerEntry(
id: (json['id'] ?? '').toString(),
changeAmount: jsonToInt(json['changeAmount']),
balanceAfter: jsonToInt(json['balanceAfter']),
bizType: json['bizType'] as String? ?? '',
remark: json['remark'] as String? ?? '',
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
);
}
}
/// 钱包页一次渲染所需的余额与最近流水。
class WalletSnapshot {
const WalletSnapshot({required this.wallet, this.ledger = const []});
final CoinWallet wallet;
final List<CoinLedgerEntry> ledger;
factory WalletSnapshot.fromJson(Map<String, dynamic> json) {
final wallet = json['wallet'];
return WalletSnapshot(
wallet: wallet is Map<String, dynamic>
? CoinWallet.fromJson(wallet)
: const CoinWallet(
coinBalance: 0,
totalRechargedCoin: 0,
totalConsumedCoin: 0,
totalRefundedCoin: 0,
),
ledger: (json['ledger'] as List? ?? const [])
.whereType<Map<String, dynamic>>()
.map(CoinLedgerEntry.fromJson)
.toList(),
);
}
}
/// 金币解锁结果:`POST /app/episodes/{id}/unlock`。
class UnlockResult {
const UnlockResult({
required this.unlocked,
required this.accessType,
required this.coinCost,
required this.coinBalance,
});
final bool unlocked;
final String accessType;
final int coinCost;
final int coinBalance;
factory UnlockResult.fromJson(Map<String, dynamic> json) {
return UnlockResult(
unlocked: json['unlocked'] == true,
accessType: json['accessType'] as String? ?? '',
coinCost: jsonToInt(json['coinCost']),
coinBalance: jsonToInt(json['coinBalance']),
);
}
}
+11 -2
View File
@@ -21,7 +21,7 @@ class ApiClient {
this.tokenRefresher,
this.onUnauthorized,
this.deviceIdReader,
}) : baseUrl = baseUrl ?? AppConfig.apiBaseUrl {
}) : _fixedBaseUrl = baseUrl {
_dio = Dio(
BaseOptions(
baseUrl: this.baseUrl,
@@ -45,9 +45,12 @@ class ApiClient {
}
}
final String baseUrl;
final String? _fixedBaseUrl;
late final Dio _dio;
/// 当前请求使用的服务地址;未显式指定时跟随启动探测结果。
String get baseUrl => _fixedBaseUrl ?? AppConfig.apiBaseUrl;
/// 取当前 access token(未登录返回 null)
final String? Function()? accessTokenReader;
@@ -110,6 +113,7 @@ class ApiClient {
bool retried = false,
bool skipAuthRetry = false,
}) async {
_syncBaseUrl();
final disableAuthRetry = skipAuthRetry || tokenRefresher == null;
try {
final response = await _dio.request<Object?>(
@@ -150,5 +154,10 @@ class ApiClient {
}
}
void _syncBaseUrl() {
if (_fixedBaseUrl != null) return;
_dio.options.baseUrl = AppConfig.apiBaseUrl;
}
void close() => _dio.close(force: true);
}
+61
View File
@@ -10,6 +10,67 @@ class AuthApi {
final ApiClient _client;
/// 发送短信验证码;[scene] 为 REGISTER / LOGIN / RESET_PWD / BIND。
Future<void> sendOtp({
required String target,
required String scene,
required String deviceId,
}) async {
await _client.post(
'/app/auth/otp',
body: {
'scene': scene,
'targetType': 'PHONE',
'target': target,
'deviceId': deviceId,
},
);
}
/// 手机号 + 验证码登录。
Future<AuthSession> loginWithPhone({
required String target,
required String code,
required String deviceId,
String platform = '',
String appVersion = '',
}) async {
final data = await _client.post(
'/app/auth/login',
body: {
'targetType': 'PHONE',
'target': target,
'code': code,
'deviceId': deviceId,
'platform': platform,
'appVersion': appVersion,
},
);
return AuthSession.fromJson((data as Map).cast<String, dynamic>());
}
/// 手机号 + 验证码注册。
Future<AuthSession> registerWithPhone({
required String target,
required String code,
required String deviceId,
String platform = '',
String appVersion = '',
}) async {
final data = await _client.post(
'/app/auth/register',
body: {
'targetType': 'PHONE',
'target': target,
'code': code,
'deviceId': deviceId,
'platform': platform,
'appVersion': appVersion,
},
);
return AuthSession.fromJson((data as Map).cast<String, dynamic>());
}
Future<AuthSession> register({
required String email,
required String password,
+18
View File
@@ -1,5 +1,6 @@
import '../models/drama.dart';
import '../models/home_feed.dart';
import '../models/search_result.dart';
import 'api_client.dart';
/// 客户端目录接口,对应后端 `/app/**`。
@@ -16,6 +17,23 @@ class CatalogApi {
return HomeFeed.fromJson(_asMap(await _client.get('/app/discover/feed')));
}
/// 搜索已上线短剧:按标题/简介模糊匹配,分页返回卡片列表。
Future<SearchResult> search(
String keyword, {
int pageIndex = 1,
int pageSize = 20,
}) async {
final data = await _client.get(
'/app/search',
query: {
'keyword': keyword,
'pageIndex': '$pageIndex',
'pageSize': '$pageSize',
},
);
return SearchResult.fromJson(_asMap(data));
}
Future<Drama> dramaDetail(String dramaId) async {
return Drama.fromDetailJson(
_asMap(await _client.get('/app/dramas/$dramaId')),
+50
View File
@@ -0,0 +1,50 @@
// Author: fruit.com.cn
import '../models/order.dart';
import '../models/product.dart';
import '../models/wallet.dart';
import 'api_client.dart';
/// 金币与解锁接口,对应契约 §1.8。
class CommerceApi {
const CommerceApi(this._client);
final ApiClient _client;
Future<WalletSnapshot> wallet() async {
final data = await _client.get('/app/wallet');
if (data is Map<String, dynamic>) return WalletSnapshot.fromJson(data);
throw const ApiException(-1, '返回数据格式异常');
}
/// 上架商品列表;`type` 传 `COIN_PACK` 或 `VIP`。
Future<List<AppProduct>> products({String? type}) async {
final data = await _client.get(
'/app/products',
query: type == null ? null : {'type': type},
);
if (data is List) {
return data
.whereType<Map<String, dynamic>>()
.map(AppProduct.fromJson)
.toList();
}
throw const ApiException(-1, '返回数据格式异常');
}
/// 创建充值订单,返回 `CREATED` 状态订单。
Future<AppOrder> createOrder(String productId) async {
final data = await _client.post(
'/app/orders',
body: {'productId': productId},
);
if (data is Map<String, dynamic>) return AppOrder.fromJson(data);
throw const ApiException(-1, '返回数据格式异常');
}
Future<UnlockResult> unlock(String episodeId) async {
final data = await _client.post('/app/episodes/$episodeId/unlock');
if (data is Map<String, dynamic>) return UnlockResult.fromJson(data);
throw const ApiException(-1, '返回数据格式异常');
}
}
@@ -0,0 +1,23 @@
// Author: fruit.com.cn
import '../models/wallet.dart';
import '../remote/commerce_api.dart';
/// 钱包与金币解锁的数据来源。
abstract interface class CommerceRepository {
Future<WalletSnapshot> fetchWallet();
Future<UnlockResult> unlock(String episodeId);
}
class HttpCommerceRepository implements CommerceRepository {
const HttpCommerceRepository(this._api);
final CommerceApi _api;
@override
Future<WalletSnapshot> fetchWallet() => _api.wallet();
@override
Future<UnlockResult> unlock(String episodeId) => _api.unlock(episodeId);
}
@@ -59,7 +59,8 @@ class _CachedTicket {
final PlayTicket ticket;
final DateTime fetchedAt;
bool isExpired(DateTime now, Duration ttl) => now.difference(fetchedAt) >= ttl;
bool isExpired(DateTime now, Duration ttl) =>
now.difference(fetchedAt) >= ttl;
}
/// 测试与离线场景用:按集返回固定票据,并记录被请求过几次。
@@ -91,6 +92,7 @@ class FakePlaybackRepository implements PlaybackRepository {
accessible: true,
accessType: PlayTicket.free,
playUrl: '/files/episode/fake-$episodeId.mp4?e=1&s=abc',
qualities: const ['AUTO', '720P', '1080P'],
);
}
}
@@ -0,0 +1,26 @@
// Author: fruit.com.cn
import '../models/search_result.dart';
import '../remote/catalog_api.dart';
/// 搜索数据来源:只调用后端 `/app/search`,不在客户端做本地过滤。
abstract interface class SearchRepository {
Future<SearchResult> search(
String keyword, {
int pageIndex = 1,
int pageSize = 20,
});
}
class HttpSearchRepository implements SearchRepository {
const HttpSearchRepository(this._api);
final CatalogApi _api;
@override
Future<SearchResult> search(
String keyword, {
int pageIndex = 1,
int pageSize = 20,
}) => _api.search(keyword, pageIndex: pageIndex, pageSize: pageSize);
}
Loaded 100 of 297 files, more files were not shown because too many files have changed in this diff. Show more