diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fde2d7..64d4e9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,13 +2,13 @@ name: Build app packages on: push: - branches: [main, master] + branches: [main, master, 'feat/macos-*'] tags: ['v*'] pull_request: workflow_dispatch: permissions: - contents: write + contents: read concurrency: group: app-build-${{ github.workflow }}-${{ github.ref }} @@ -138,26 +138,9 @@ jobs: path: dist/android/* if-no-files-found: error retention-days: 14 - windows: - if: false + macos: needs: checks - runs-on: windows-latest - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - include: - - edition: hongguojian - arguments: '' - - edition: zhenguojian - arguments: '--all-sources' - steps: - - uses: actions/checkout@v4 - - ios: - if: false - needs: checks - runs-on: macos-latest + runs-on: macos-26 timeout-minutes: 60 strategy: fail-fast: false @@ -169,10 +152,55 @@ jobs: arguments: '--all-sources' steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: actions/setup-go@v5 + with: + go-version-file: native/go.mod + cache-dependency-path: native/go.sum + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.47.4' + channel: stable + cache: true + - name: 检查构建工具并启用通用架构 + run: | + xcodebuild -version + pod --version + flutter config --enable-macos-desktop --no-enable-macos-arm64-only + - name: 构建临时签名的 macOS 预览包 + run: python scripts/build_macos.py ${{ matrix.arguments }} + - name: 校验架构、签名和压缩包摘要 + run: | + set -euo pipefail + app=build/macos/Build/Products/Release/duanju_app.app + for arch in arm64 x86_64; do + lipo "$app/Contents/MacOS/duanju_app" -verify_arch "$arch" + lipo "$app/Contents/Frameworks/libduanju_core.dylib" -verify_arch "$arch" + done + codesign --verify --deep --strict "$app" + cd dist/macos + shasum -a 256 -c ./*.zip.sha256 + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.edition }}-macos + path: dist/macos/* + if-no-files-found: error + retention-days: 14 release: - needs: [android] - if: always() && needs.android.result == 'success' + needs: [checks, android, macos] + if: >- + !cancelled() && + ((github.event_name == 'push' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || + startsWith(github.ref, 'refs/tags/v'))) || + (github.event_name == 'workflow_dispatch' && + github.ref_name == github.event.repository.default_branch)) && + needs.checks.result == 'success' && + needs.android.result == 'success' && + needs.macos.result == 'success' runs-on: ubuntu-latest timeout-minutes: 20 permissions: @@ -222,11 +250,9 @@ jobs: printf '%s\n' "- *-arm64-v8a.apk:绝大多数手机选这个" printf '%s\n' "- *-armeabi-v7a.apk:较老的 32 位手机" printf '%s\n' "- *-x86_64.apk:模拟器" - printf '\n%s\n' "iOS(未签名,需自行签名后才能安装):" - printf '%s\n' "- *-ios-unsigned.ipa:未签名 IPA,用 AltStore / Sideloadly / TrollStore 等工具自签后安装" - printf '%s\n' "- *-ios-unsigned-app.zip:未签名 Runner.app 压缩包,供自行签名 / 打包使用" - printf '\n%s\n' "Windows(解压后运行 hongguojian.exe 或 zhenguojian.exe):" - printf '%s\n' "- *-windows-x64.zip:Windows 10/11 x64 完整包;构建机没有音频和图形设备,播放自检未通过,未做设备验收" + printf '\n%s\n' "macOS(通用架构预览包,临时签名、未公证):" + printf '%s\n' "- *-macos-preview.zip:解压后得到 .app;尚未完成平台验收,可能需要自行签名" + printf '%s\n' "- Windows / iOS 自动构建暂未启用,本次不提供对应附件" printf '\n%s\n' "校验:SHA256SUMS.txt" if printf '%s\n' "${{ vars.ANDROID_CERT_SHA256 }}" | grep -q .; then printf '%s\n' "签名证书 SHA-256:${{ vars.ANDROID_CERT_SHA256 }}" diff --git a/.gitignore b/.gitignore index 0b5530d..51b48c0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ ios/Flutter/flutter_export_environment.sh ios/Runner/GeneratedPluginRegistrant.* ios/DuanjuCore/*.xcframework/ **/xcuserdata/ +macos/Pods/ +macos/.symlinks/ +macos/Flutter/ephemeral/ +macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/.metadata b/.metadata index 69408cb..8302dba 100644 --- a/.metadata +++ b/.metadata @@ -19,6 +19,10 @@ migration: create_revision: 9584c6713b324636289d067944a46fd6b49df14b base_revision: 9584c6713b324636289d067944a46fd6b49df14b + - platform: macos + create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a + # User provided section # List of Local paths (relative to this file) that should be @@ -28,3 +32,4 @@ migration: unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' + - 'macos/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md index d3d312a..7ce50ec 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,15 @@ > ⚠️ **免责声明**:本项目源码来自网上大名鼎鼎的**鱼佬**(原作者)。我只是把它拿来打包、测试着玩,方便自己用,**不保证任何可用性,随时可能删库**。 -Flutter 多端独立短剧 / 影视应用,原名「短剧库 APP」。站源请求、解析、下载和播放均在设备上完成,不依赖自建服务。当前源码版本:**0.2.51+57**。 +Flutter 多端独立短剧 / 影视应用,原名「短剧库 APP」。站源请求、解析、下载和播放均在设备上完成,不依赖自建服务。当前源码版本:**0.2.67+74(开发快照,未完成全量验收)**。 -本轮在 0.2.50 基线上新增两个原生站源:**韩小圈**(`hanxiaoquan`,韩剧 / 韩国电影 / 韩国综艺 / 韩国动漫)与**鬼片网**(`guipian`,鬼片 / 电视剧 / 动漫)。默认可见站源顺序为:**红果 → 韩小圈 → 鬼片 → 青空**。 +本分支新增 macOS 工程、Go 动态库编译嵌入、桌面窗口 / 全屏、系统代理、设备识别、本机签名入口及合成媒体集成测试,并接入双版本 GitHub Actions 构建与预览包发布。 -> 说明:应用长期按「只维护源码与定向测试,集中验证另行安排」的方式推进。未完成集中验证与真机验收的项目保持未验收状态;历史版本的检查记录不能作为后续新增功能的验收结论。 +共享 Go 核心的 MP4 / HLS 误判修复单独提交于 [播放修复 PR #3](https://github.com/zhoufuweigg/guoapp/pull/3),不包含在此平台分支中。两者都从上游 `main` 分出;建议先合入播放修复,再对 macOS 做完整媒体验收。下文已通过的播放集成结果来自同时包含该修复的本地组合版本,不能当作当前独立分支已通过播放验收的证明。 + +默认可见站源顺序为:**红果 → 韩小圈 → 鬼片 → 青空**。 + +> 说明:本轮按用户要求执行 macOS 构建与验证;其他平台沿用暂停集中回归的安排。未完成集中验证与真机验收的项目保持未验收状态;历史版本的检查记录不能作为后续新增功能的验收结论。 | 编译方式 | 应用名称 | 可用站源 | | --- | --- | --- | @@ -15,7 +19,7 @@ Flutter 多端独立短剧 / 影视应用,原名「短剧库 APP」。站源 默认可见站源(红果、韩小圈、鬼片、青空)无需密码;其余站源默认隐藏,在站源管理里通过密码锁解锁后显示。已有受限用户不会自动获得新增站源权限,管理员可在用户管理中勾选。 -这是**编译选项**,应用内不能切换版本。标题、Android 桌面名称与电视横幅、Windows 窗口与分发文件名、iOS 显示名随编译选项变化。界面、站源调用、原生下载调度同时限制可用站源;红果版不会访问或继续执行其他站源的旧任务。 +这是**编译选项**,应用内不能切换版本。标题、Android 桌面名称与电视横幅、Windows 窗口与分发文件名、iOS / macOS 显示名随编译选项变化。界面、站源调用、原生下载调度同时限制可用站源;红果版不会访问或继续执行其他站源的旧任务。 两版保留原 Android / iOS 应用标识及数据目录;Android 使用同一签名可相互覆盖升级,不能作为两个独立正式应用并排安装。切换版本保留追剧、观看记录、用户权限和下载记录;备份格式保持兼容。 @@ -71,20 +75,39 @@ Flutter 多端独立短剧 / 影视应用,原名「短剧库 APP」。站源 | Windows 10/11 x64 | 完整 ZIP 解压后运行 `hongguojian.exe` / `zhenguojian.exe`,保留所有 DLL 与 `data`;局域网原生发现依赖 Windows 10 1903+ | | Android TV | 与手机共用源码,自动识别电视模式并保持横屏;待电视 / 盒子实机验收 | | iOS 15.1+ | 已加入工程、Go 核心链接、媒体依赖、文件管理与构建脚本;iOS 播放页禁用 media_kit_video 硬件纹理加速以规避 libmpv 渲染崩溃;待 Xcode 构建与真机验收,无已签名 IPA | +| macOS 12.0+(工程目标) | 独立分支的两版 Release 已构建并通过临时签名校验;主程序与 Go 核心包含 arm64 / x86_64。此前组合版本另已验证 Debug 和本机 Apple Development 证书签名。Apple Silicon 组合版本(含播放修复)已通过合成媒体集成测试;Intel 执行、最低系统版本及真实站源完整闭环待验证,未公证 | + +macOS 当前按独立桌面应用接入,关闭 App Sandbox,以沿用用户选择的下载 / 导出目录与持久化路径;保留网络访问、监听及局域网用途声明。关闭窗口会退出应用并终止后台任务,最小化不等同于退出。画质增强仍只支持 Windows,macOS 暂未接入画中画;macOS 已加入 GitHub Actions 构建与预览包发布,首次云端执行状态以 Actions 为准。 + +本轮后续待办按顺序执行: + +- [x] 安装要求的 Flutter SDK,完成 macOS Debug、Release 与本机签名构建。 +- [x] 在包含播放修复的组合版本中禁用站源图片,以合成媒体验证目录进入播放、MP4 / AES HLS / CENC 解码、跳转与窗口全屏。 +- [x] 在包含播放修复的组合版本中验证真实 Go 下载 MP4 / HLS、离线 Emby 导出,以及原生 FFmpeg 探测、封装和拼接。 +- [ ] 验收真实站源搜索、详情、选集、错误重试与长期播放,修复全量回归失败。 +- [ ] 验收代理切换、局域网互联、完整合并队列与退出时任务恢复。 +- [x] 配置两版 macOS 通用架构构建、签名 / 摘要校验、Actions 产物和受信任事件的预览包发布。 +- [ ] 合入播放修复后复验 macOS 媒体集成,并观察首次云端 CI。 +- [ ] 补 Intel 实机、最低系统版本、应用图标和公证;当前工程使用 Flutter 模板图标。 + +2026-10-07 本地组合版本的验证环境为 Flutter 3.47.4 / Dart 3.13.3、Xcode 27.0、CocoaPods 1.15.2、Go 1.27.1。Python 脚本测试 19 项通过;Go 播放与下载相关 race 测试通过;macOS 集成测试通过,并输出 `build/device-test/results/playback.json`。Computer Use 已确认合成 MP4 / HLS / CENC 的实际画面、选集及进入 / 退出全屏。Flutter 全量单元测试 132 项通过、23 项失败,涉及焦点、目录、下载、媒体合并等现有行为与断言;Go 全量测试在目录缓存 / 游标测试失败并触发 panic;Dart 全量分析无 error,有 105 项 warning / info,新增 macOS 验证文件分析无问题。上述结果不代表全项目通过验收,Android / Windows / iOS 本轮未构建。 `INSTALL_FAILED_NO_MATCHING_ABIS` 表示 APK 与设备架构不匹配,请更换对应架构安装包。 ### GitHub Actions -推送 `main` / `master`、`v*` 标签、提交 PR,或手动运行 **Build app packages**,会先检查再构建两版(默认与 `--all-sources`): +推送 `main` / `master` / `feat/macos-*` 分支、`v*` 标签、提交 PR,或手动运行 **Build app packages**,会先检查再构建两版(默认与 `--all-sources`): | 产物 | 内容 | | --- | --- | | `*-android` | 三种架构 APK 和 SHA256 | -| `*-windows` | 完整 ZIP 和 SHA256 | -| `*-ios-unsigned` | 未签名 `.app` ZIP 和 SHA256,不能直接当已签名 IPA 安装 | +| `*-macos` | arm64 / x86_64 通用 `.app` ZIP 和 SHA256,临时签名、未公证 | -推送 `main` 且 android / ios / windows 全部构建成功时,自动创建 / 更新 GitHub Release(tag `app-v{version}`)。发布新版本前需先在 `pubspec.yaml` 提升 `version`,否则会覆盖同名 tag 的 Release。 +macOS 在 `macos-26` runner 上分别构建红果版和全站源版,固定 Flutter 3.47.4,校验主程序 / Go dylib 的两种架构、嵌套签名和 ZIP 摘要。PR 与功能分支推送只执行检查、构建和上传 Actions 产物,不创建 Release;Windows / iOS 的占位构建原本处于关闭状态,本轮未启用。 + +当 `checks`、Android、macOS 作业均成功,且事件为 `main` / `master` / `v*` 标签推送,或在默认分支手动触发时,自动创建 / 更新 GitHub Release(tag `app-v{version}`,版本号中的 `+` 转为 `-`)。工作流默认只有 `contents: read`,发布作业单独获得写权限。全量 Dart / Go 回归仍沿用现有非阻断策略,作业成功不表示全量测试通过。发布新版本前需先提升 `pubspec.yaml` 的版本,否则会更新同名 Release。 + +云端 macOS 使用 ad-hoc 临时签名,不需要 Apple 证书 Secrets,也不包含本机 Apple Development 证书;正式分发签名和公证尚未接入。工作流配置已通过 actionlint;独立 worktree 的两版 Release 构建、签名 / 摘要校验、19 项 Python 测试及新增 Dart 验证文件分析通过。本地结果不代替 GitHub runner 的首次实跑。 Android 正式发布使用同一签名并递增构建号,在仓库 Secrets 配置:`ANDROID_KEYSTORE_BASE64`、`ANDROID_KEYSTORE_PASSWORD`、`ANDROID_KEY_ALIAS`、`ANDROID_KEY_PASSWORD`。未配置时生成 debug 签名预览包。 @@ -99,7 +122,7 @@ keyPassword=你的密码 ## 开发与构建 -Flutter `3.47.4`、Dart `3.12+`、Go `1.24.1+`、Python `3.10+`。Android 需要 JDK 17、SDK 36、NDK `28.2.13676358`;Windows 需要 Visual Studio C++ 桌面组件及 MinGW-w64 x64;iOS 需要 macOS、完整 Xcode 和 CocoaPods。 +Flutter `3.47.4`、Dart `3.12+`、Go `1.24.1+`、Python `3.10+`(建议 `3.12+`)。Android 需要 JDK 17、SDK 36、NDK `28.2.13676358`;Windows 需要 Visual Studio C++ 桌面组件及 MinGW-w64 x64;iOS / macOS 需要 macOS、完整 Xcode 和 CocoaPods。macOS 已使用 Flutter 3.47.4 构建,媒体插件当前通过 CocoaPods 集成。 构建脚本对子进程默认设置 `GOPROXY=https://goproxy.cn,direct`、`GOSUMDB=off`,同名环境变量可覆盖。 @@ -123,7 +146,33 @@ python3 scripts/build_ios.py --core-only [--simulator] python3 scripts/build_ios.py --export-options /path/to/ExportOptions.plist ~~~ -产物在 `dist/android`、`dist/windows`、`dist/ios`,红果版以 `hongguojian-` 开头,全站源版以 `zhenguojian-` 开头。 +macOS 构建入口: + +~~~sh +python3 scripts/build_macos.py # 红果鉴开发快照 ZIP +python3 scripts/build_macos.py --all-sources # 真果鉴开发快照 ZIP +python3 scripts/build_macos.py --debug # 只生成 Debug .app +python3 scripts/build_macos.py --all-sources --sign-identity 'Apple Development: 你的证书名称' +~~~ + +可用 `FLUTTER_BIN=/绝对路径/flutter` 指定符合版本要求的 SDK。脚本沿用锁文件安装依赖,由 Flutter 驱动 CocoaPods 与 Xcode;Xcode 构建阶段自动按 `DART_DEFINES` 与 `ARCHS` 编译 Go 核心,放入 `.app/Contents/Frameworks/libduanju_core.dylib` 并随应用签名,无需手工复制。终端的 PATH 需包含 Go 与 CocoaPods;直接从 Xcode 构建时也需配置可访问的工具路径。 + +调试可执行 `flutter pub get --enforce-lockfile`,再 `flutter run -d macos`;全站源版加 `--dart-define=ALL_SOURCES=true`。Xcode 会同步选择核心版本,避免混装。单独编译 Go 核心可使用 `python3 scripts/build_native.py --platform darwin`,需要多架构时追加 `--darwin-arch arm64 --darwin-arch x86_64`。 + +产物在 `dist/android`、`dist/windows`、`dist/ios`、`dist/macos`,红果版以 `hongguojian-` 开头,全站源版以 `zhenguojian-` 开头。macOS 脚本生成 `*-macos-preview.zip` 与同名 `.zip.sha256`;默认临时签名,传入 `--sign-identity` 可使用钥匙串中的本机证书重签所有嵌套原生库。开发证书签名不等于公证或面向其他电脑的正式分发签名。验证包可添加 `--disable-remote-images`,文件名追加 `-no-images`。 + +macOS 合成媒体验证(需要本机 `ffmpeg`,验证期间不请求站源图片): + +~~~sh +python3 scripts/create_test_media.py +python3 scripts/serve_test_media.py +# 另一个终端执行 +flutter drive -d macos --driver=test_driver/playback.dart --target=integration_test/macos_test.dart --dart-define=ALL_SOURCES=true --dart-define=DISABLE_REMOTE_IMAGES=true +# 手动查看合成画面 +flutter run -d macos --target=test_driver/macos_preview.dart --dart-define=ALL_SOURCES=true --dart-define=DISABLE_REMOTE_IMAGES=true +~~~ + +验证会使用应用数据目录,创建并清理专用合成剧集的下载与导出文件;不要同时运行正式应用或其他集成测试。 首次 Android 调试先编译对应架构核心,再运行: @@ -177,7 +226,7 @@ python3 scripts/sync_source.py --check | --- | --- | | `lib` | 页面、播放器、本地用户、FFI、下载和媒体处理 | | `native/core`、`native/bridge` | 独立站源核心、缓存、下载、目录迁移及 C ABI | -| `android`、`windows`、`ios` | 平台工程与必要资源 | +| `android`、`windows`、`ios`、`macos` | 平台工程与必要资源 | | `assets/video_enhancement`、`packages/media_kit_libs_windows_video` | 增强 Shader 与许可、固定 Windows 媒体依赖插件 | | `scripts`、`.github/workflows` | 构建、签名、验证、同步和版本快照 | | `test`、`integration_test` | 自动化与设备回归 | diff --git a/analysis_options.yaml b/analysis_options.yaml index c5b17ee..96ba1c0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,3 +8,4 @@ analyzer: - android/** - windows/** - ios/** + - macos/** diff --git a/integration_test/macos_test.dart b/integration_test/macos_test.dart new file mode 100644 index 0000000..bec78ac --- /dev/null +++ b/integration_test/macos_test.dart @@ -0,0 +1,223 @@ +import 'dart:io'; + +import 'package:duanju_app/local_store.dart'; +import 'package:duanju_app/main.dart'; +import 'package:duanju_app/media_library.dart'; +import 'package:duanju_app/media_pipeline.dart'; +import 'package:duanju_app/player_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit_config.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../test/macos_fixtures.dart'; + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets( + 'macOS native core, MP4, encrypted HLS, CENC, fullscreen and FFmpeg', + (tester) async { + expect(Platform.isMacOS, isTrue); + expect(const bool.fromEnvironment('DISABLE_REMOTE_IMAGES'), isTrue); + MediaKit.ensureInitialized(); + await windowManager.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + final store = LocalStore(await SharedPreferences.getInstance()); + final repository = MacosFixtureRepository(); + FFmpegKitConfig.enableLogCallback((log) { + if (log.getLevel() >= 0 && log.getLevel() <= 16) { + debugPrintSynchronously('FFmpeg fixture: ${log.getMessage()}'); + } + }); + await repository.initialize(); + final detail = await repository.detail(MacosFixtureRepository.drama); + final evidence = >[]; + + Future until(bool Function() ready, String step) async { + final watch = Stopwatch()..start(); + while (!ready()) { + if (watch.elapsed > const Duration(seconds: 40)) { + fail('Timed out: $step'); + } + await tester.pump(const Duration(milliseconds: 200)); + } + } + + await tester.pumpWidget(DuanjuApp(repository: repository, store: store)); + await until( + () => + find.text(MacosFixtureRepository.drama.title).evaluate().isNotEmpty, + 'catalog', + ); + await tester.tap(find.text(MacosFixtureRepository.drama.title).first); + await until( + () => find.byType(Video).evaluate().isNotEmpty, + 'open player', + ); + var player = tester + .widget