diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 00000000..c7ffc6ba --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,11 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "FlowVision" + +[setup] +script = "" + +[[actions]] +name = "Run" +icon = "run" +command = "./script/build_and_run.sh" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..19ee614c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,139 @@ +name: Build and Release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version, for example v1.7.2" + required: true + type: string + draft: + description: "Create release as draft" + required: false + default: false + type: boolean + prerelease: + description: "Mark release as prerelease" + required: false + default: false + type: boolean + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + name: Build macOS DMG and publish GitHub Release + runs-on: macos-latest + + steps: + - name: Checkout FlowVision + uses: actions/checkout@v4 + with: + path: FlowVision + + - name: Checkout Settings dependency + uses: actions/checkout@v4 + with: + repository: sindresorhus/Settings + path: Settings + + - name: Checkout BTree dependency + uses: actions/checkout@v4 + with: + repository: attaswift/BTree + path: BTree + + - name: Download FFmpegKit XCFrameworks + shell: bash + env: + FFMPEG_KIT_URL: https://github.com/netdcy/ffmpeg-kit/releases/download/v6.0/ffmpeg-kit-full-gpl-6.0-macos-xcframework.zip + run: | + set -euo pipefail + mkdir -p ffmpeg-kit-build/bundle-apple-xcframework-macos + curl --fail --location --retry 3 --output ffmpeg-kit-build/ffmpeg-kit-full-gpl-6.0-macos-xcframework.zip "$FFMPEG_KIT_URL" + unzip -q ffmpeg-kit-build/ffmpeg-kit-full-gpl-6.0-macos-xcframework.zip -d ffmpeg-kit-build/bundle-apple-xcframework-macos + + - name: Resolve release version + id: version + shell: bash + run: | + set -euo pipefail + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + VERSION="${GITHUB_REF_NAME}" + else + VERSION="${{ inputs.version }}" + fi + VERSION="${VERSION#refs/tags/}" + if [[ -z "$VERSION" ]]; then + echo "Release version is required." >&2 + exit 1 + fi + if [[ "$VERSION" != v* ]]; then + VERSION="v$VERSION" + fi + SHORT_VERSION="${VERSION#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "short_version=$SHORT_VERSION" >> "$GITHUB_OUTPUT" + echo "dmg_name=FlowVision-${VERSION}-macOS" >> "$GITHUB_OUTPUT" + + - name: Build DMG + working-directory: FlowVision + env: + ENABLE_CODESIGN: "0" + CONFIGURATION: Release + APP_NAME: FlowVision + DMG_NAME: ${{ steps.version.outputs.dmg_name }} + XCODEBUILD_EXTRA_ARGS: "MARKETING_VERSION=${{ steps.version.outputs.short_version }} CURRENT_PROJECT_VERSION=${{ github.run_number }} CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=" + run: ./build_dmg.sh + + - name: Create ZIP + working-directory: FlowVision + shell: bash + run: | + set -euo pipefail + APP_PATH="$(find build/DerivedData/Build/Products/Release -maxdepth 1 -type d -name '*.app' | head -n 1)" + if [[ -z "${APP_PATH:-}" ]]; then + echo "App bundle not found." >&2 + exit 1 + fi + mkdir -p dist + ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "dist/${{ steps.version.outputs.dmg_name }}.zip" + cp "dist/${{ steps.version.outputs.dmg_name }}.zip" "dist/FlowVision-macOS.zip" + + - name: Upload workflow artifacts + uses: actions/upload-artifact@v4 + with: + name: FlowVision-${{ steps.version.outputs.version }}-macOS + path: | + FlowVision/dist/*.dmg + FlowVision/dist/*.zip + + - name: Publish GitHub Release + working-directory: FlowVision + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + DRAFT: ${{ inputs.draft || false }} + PRERELEASE: ${{ inputs.prerelease || false }} + shell: bash + run: | + set -euo pipefail + + FLAGS="" + if [[ "$DRAFT" == "true" ]]; then + FLAGS="$FLAGS --draft" + fi + if [[ "$PRERELEASE" == "true" ]]; then + FLAGS="$FLAGS --prerelease" + fi + + if gh release view "$VERSION" >/dev/null 2>&1; then + gh release upload "$VERSION" dist/*.dmg dist/*.zip --clobber + else + # shellcheck disable=SC2086 + gh release create "$VERSION" dist/*.dmg dist/*.zip --title "FlowVision $VERSION" --generate-notes $FLAGS + fi diff --git a/.gitignore b/.gitignore index cdd0b098..2b28f42d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ xcuserdata/ -LocalDev.xcconfig \ No newline at end of file +LocalDev.xcconfig +.DS_Store +.mindfs/ +dist/ +build/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ff6fde1e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# FlowVision Agent Guide + +本文件是仓库的低 token 导航入口。先读本文件,再按任务只打开 +[`public/doc/ARCHITECTURE.md`](public/doc/ARCHITECTURE.md) 中对应的小节和代码锚点;不要默认通读大型 Swift 文件。 + +## 仓库与构建 + +- 工程:`FlowVision.xcodeproj`;Scheme:`FlowVision`;目标平台:macOS。 +- 源码:`FlowVision/Sources/`;详细架构:`public/doc/ARCHITECTURE.md`。 +- 本地依赖通常位于相邻目录(如 `../Settings`、`../BTree`),另有 Xcode Package 依赖。 +- `build/`、`dist/` 是生成物,不属于源码,不应提交。 +- 统一构建/运行入口:`./script/build_and_run.sh`;可用 `--verify`、`--debug`、`--logs`、`--telemetry`。 +- 无签名 Release 验证: + +```bash +xcodebuild -quiet -project FlowVision.xcodeproj -scheme FlowVision \ + -configuration Release -destination 'platform=macOS' \ + -derivedDataPath build/DerivedData \ + CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build +``` + +## 快速定位 + +优先使用 `rg -n '函数名|状态名' FlowVision/Sources`,按下表读取最少文件。 + +| 任务 | 首选文件 / 锚点 | +|---|---| +| 重命名、复制、移动、删除 | `ViewControllerExtension/FileOperation.swift` | +| 批量重命名入口与预览 | `handleBatchRenameSelectedItems`、`BatchRenamePreviewDataSource` | +| 当前目录快捷重命名 | `handleQuickRenameInCurrentFolder` | +| 重命名执行、回滚、Undo | `executeFileRenameMappings`、`executeFileRenameMappingsAsync` | +| 重命名后无闪烁更新 | `applyRenameMappingsInPlace` | +| 图片/视频复制到配置目录 | `handleCopyToConfiguredFolder` | +| 缩略图右键菜单 | `Views/CustomCollectionViewItem.swift` | +| 左侧目录树右键菜单 | `Views/CustomOutlineView.swift` | +| 快捷键分发 | `ViewControllerExtension/KeyShortcut.swift`、`WindowController.swift` | +| 文件扫描、刷新、刷新后定位 | `ViewControllerExtension/FileSystem.swift` / `selectItemsNewChanged` | +| 跨操作状态 | `ViewController.swift` / `PublicVar` | +| 排序键与文件模型 | `Common/DataModel.swift` / `SortKeyFile`、`FileModel` | +| 视频播放连续性 | `Views/LargeImageView.swift`、`Views/CustomCollectionViewItem.swift` | +| 邻近媒体 / SMB 预热 | `Common/VideoProcess.swift` / `MediaPreheatManager`、`ViewControllerExtension/LargeImage.swift` / `preloadLargeImage` | +| 进度 UI | `Views/CoreAreaView.swift`、`ViewControllerExtension/ProgressBar.swift` | +| Enhanced Index / Finder 元数据 | `Common/FinderTag.swift` | +| FFmpeg 调用 | `Common/FFmpegKit.swift`;剪切规则见架构文档 | + +## 文件操作不变量 + +1. 大批量磁盘 I/O、冲突检查、索引更新放后台队列;AppKit、集合视图、窗口标题、Undo 注册只在主线程。 +2. 文件操作期间正确维护 `publicVar.isInFileOperation`,避免文件系统监听器插入竞争刷新;所有成功、失败和提前返回路径都必须复位。 +3. 重命名使用“源文件 → 临时名 → 最终名”的两阶段移动。执行前检查重复目标和外部占用;部分失败必须回滚。 +4. `BTree.Map` 中的 `SortKeyFile` 是排序键,不能原地修改路径;应复制键并重建 Map,同时复用现有 `FileModel`。 +5. 纯重命名优先调用 `applyRenameMappingsInPlace`:只更新路径、名称、排序位置和标题,不重新配置缩略图/播放器,不调用 `reloadData()` 或 `scheduledRefresh()`。 +6. 原位更新条件不满足时才允许完整刷新;刷新前保存 `collectionScrollRestoreAfterRefresh`,最终在 `selectItemsNewChanged` 恢复可见位置。 +7. 重命名不得重启或停止视频。同步维护 `currentPlayingURL`、`restorePlayURL`、大图路径和窗口标题,保留播放进度。 +8. `filesForLocateAfterChange` 用于复制、创建、移动后的新目标定位;快捷重命名传空定位目标,不能把视图滚到第一个改名文件。 +9. 图片目录 1 和视频目录 2 的复制共用 `handleCopyToConfiguredFolder`。复制在后台执行,自动避让重名,完成后回主线程刷新/定位。 +10. 不要通过 Finder 剪贴板模拟应用内复制;直接使用 `FileManager`,错误和进度要可见。 + +## 邻近媒体缓存不变量 + +1. 大图浏览以当前媒体为中心维护前后各 5 个媒体的预热窗口;数量按图片/视频媒体计算,不按目录中的所有文件计算。 +2. 图片继续写入 `LargeImageProcessor` 解码缓存;图片预热最多并发 2,尺寸与元数据读取也不能阻塞主线程。 +3. 邻近视频只预读前 5 秒压缩视频样本到 macOS Unified Buffer Cache,并保留解析后的 `AVURLAsset`;不要生成需要和原片拼接的临时短视频。 +4. 视频预热串行执行。切换媒体时 `beginWindow` 必须取消旧队列并递增 generation,运行中的任务须检查 generation 后尽快退出。 +5. 当前视频不与邻项预热器重复读取:mpv/AVPlayer 自己维持 5 秒前向缓冲,开始播放后继续顺序预读。 +6. mpv 缓存必须有上限;当前约束为 5 秒目标缓冲、128 MiB 前向 demuxer 上限、32 MiB回看上限。禁止无界占用内存或磁盘。 +7. AVPlayer 路径优先复用 `mediaPreheatManager.preheatedAsset(for:)`,避免再次解析 SMB 媒体头。 +8. 放大/旋转等同一媒体内部刷新不能重建预热窗口;只有浏览位置变化时才调度。 + +## FFmpeg 剪切约束 + +- `-ss` 位于输入端且配合 `-c copy` 时只能从附近关键帧无损起切,不保证精确起点。 +- 不要默认 `-map 0:0 ...` 复制 iPhone MOV 的全部 data/metadata 流;这些流可能保留原始时间戳,造成黑屏、时长异常。 +- 只需音视频时优先 `-map 0:v:0 -map 0:a?`。要求帧精确时重编码视频,并显式重置时间戳;不要把 stream copy 描述成精确剪切。 + +## 修改与验证 + +- 保留工作区内用户已有改动;不要清理或覆盖无关 diff。 +- 编辑使用小范围补丁;禁止顺手重构与任务无关的大文件。 +- 最少验证:`git diff --check`,再按风险运行 Release 构建。 +- 重命名手工回归:多选预览、变量替换、冲突提示、Undo、排序变化、滚动位置、缩略图无闪烁、视频播放进度不丢。 +- 异步复制手工回归:图片目录 1、选中视频目录 2、当前视频目录 2、重名避让、失败提示、操作期间界面响应。 +- SMB 缓存手工回归:连续前后切换图片和视频、快速连翻后旧任务停止、首帧等待降低、播放 5 秒后持续加载、内存和网络读取保持有界。 +- 发布前检查 `git status --short`,只提交任务相关文件;push/tag 必须在用户明确要求后执行,tag不应该直接检查actions,需要用户确认才能执行。 diff --git a/FlowVision-Release-2026-04-19-v2.dmg b/FlowVision-Release-2026-04-19-v2.dmg new file mode 100644 index 00000000..7c310f60 Binary files /dev/null and b/FlowVision-Release-2026-04-19-v2.dmg differ diff --git a/FlowVision-Release-2026-04-19-v2.zip b/FlowVision-Release-2026-04-19-v2.zip new file mode 100644 index 00000000..cdff809f Binary files /dev/null and b/FlowVision-Release-2026-04-19-v2.zip differ diff --git a/FlowVision-Release-2026-04-19.dmg b/FlowVision-Release-2026-04-19.dmg new file mode 100644 index 00000000..763f16ea Binary files /dev/null and b/FlowVision-Release-2026-04-19.dmg differ diff --git a/FlowVision-Release-2026-04-19.zip b/FlowVision-Release-2026-04-19.zip new file mode 100644 index 00000000..d0f32037 Binary files /dev/null and b/FlowVision-Release-2026-04-19.zip differ diff --git a/FlowVision-Release-20260419.zip b/FlowVision-Release-20260419.zip new file mode 100644 index 00000000..8f9e77fe Binary files /dev/null and b/FlowVision-Release-20260419.zip differ diff --git a/FlowVision.xcodeproj/project.pbxproj b/FlowVision.xcodeproj/project.pbxproj index d8075ea2..9ec97570 100644 --- a/FlowVision.xcodeproj/project.pbxproj +++ b/FlowVision.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A1B2C3D42F90000100ABCDEF /* UpdateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32F90000100ABCDEF /* UpdateManager.swift */; }; F70368E82BF609F300155F36 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F70368E72BF609F300155F36 /* Localizable.xcstrings */; }; F70F9FC12BAFE7B300C50CDD /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70F9FC02BAFE7B300C50CDD /* Common.swift */; }; F70F9FC32BAFE85D00C50CDD /* CustomCollectionViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70F9FC22BAFE85D00C50CDD /* CustomCollectionViewManager.swift */; }; @@ -72,6 +73,7 @@ F7A075582BA1D717009C47A6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7A075572BA1D717009C47A6 /* Assets.xcassets */; }; F7A0755B2BA1D717009C47A6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7A075592BA1D717009C47A6 /* Main.storyboard */; }; F7AA00012F9E000000AA0001 /* VideoPlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AA00002F9E000000AA0001 /* VideoPlayerControlsView.swift */; }; + 40694BBD27B5D5464D319BAE /* MPVPlayerBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = B513CF896032F495D8769284 /* MPVPlayerBackend.swift */; }; F7AAA8BD2D9BD1F2007CE330 /* VideoProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AAA8BC2D9BD1ED007CE330 /* VideoProcess.swift */; }; F7AC30052D37E26B00F48AEF /* CustomPathControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AC30042D37E25800F48AEF /* CustomPathControl.swift */; }; F7C2DEB82C4E6AE9003DF765 /* Settings in Frameworks */ = {isa = PBXBuildFile; productRef = F7C2DEB72C4E6AE9003DF765 /* Settings */; }; @@ -114,6 +116,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + A1B2C3D32F90000100ABCDEF /* UpdateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateManager.swift; sourceTree = ""; }; E00E13C3056A4910B242187C /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; F70368E72BF609F300155F36 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; F70F9FC02BAFE7B300C50CDD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; @@ -175,6 +178,7 @@ F7790ED52BA5FB6A00406D35 /* CustomCollectionViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCollectionViewItem.swift; sourceTree = ""; }; F7790ED62BA5FB6A00406D35 /* CustomCollectionViewItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CustomCollectionViewItem.xib; sourceTree = ""; }; F78960F12BDCC26B00C2571B /* LargeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageView.swift; sourceTree = ""; }; + B513CF896032F495D8769284 /* MPVPlayerBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVPlayerBackend.swift; sourceTree = ""; }; F78960F52BDFFE9200C2571B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F78D1A692D432DCE00741908 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Main.xcstrings; sourceTree = ""; }; F78D1A872D4CF31D00741908 /* GeneralSettingsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = GeneralSettingsViewController.xib; path = Base.lproj/CustomSettingsViewController.xib; sourceTree = ""; }; @@ -216,6 +220,7 @@ F73865DC2C37BD7F00837FE4 /* Common */ = { isa = PBXGroup; children = ( + A1B2C3D32F90000100ABCDEF /* UpdateManager.swift */, F76F6A622F4FE58000D798DC /* TempVariable.swift */, F73865D82C37BBCB00837FE4 /* GlobalVariable.swift */, F70F9FC02BAFE7B300C50CDD /* Common.swift */, @@ -251,6 +256,7 @@ F7FAE0002F6BA00000FAE000 /* FavoritesPopoverViewController.swift */, F78960F12BDCC26B00C2571B /* LargeImageView.swift */, F73865D42C37B9B800837FE4 /* Layout.swift */, + B513CF896032F495D8769284 /* MPVPlayerBackend.swift */, F7AA00002F9E000000AA0001 /* VideoPlayerControlsView.swift */, ); path = Views; @@ -473,6 +479,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1B2C3D42F90000100ABCDEF /* UpdateManager.swift in Sources */, F78960F22BDCC26B00C2571B /* LargeImageView.swift in Sources */, F78FAB0D2C0EE94D00FE66CC /* CustomImageView.swift in Sources */, F76E2EFE2C3B765C0031B0B4 /* Log.swift in Sources */, @@ -485,6 +492,7 @@ F7790ED72BA5FB6A00406D35 /* CustomCollectionViewItem.swift in Sources */, F7663C162F222AAC0028DF35 /* ImageEditingView.swift in Sources */, F7AA00012F9E000000AA0001 /* VideoPlayerControlsView.swift in Sources */, + 40694BBD27B5D5464D319BAE /* MPVPlayerBackend.swift in Sources */, F74D555C2F0E501200C9AB85 /* EventHandler.swift in Sources */, F74D55522F0E46D200C9AB85 /* Search.swift in Sources */, F7FED5F32F7D0B1100E35164 /* Tagging.swift in Sources */, @@ -729,7 +737,8 @@ CODE_SIGN_ENTITLEMENTS = FlowVision/FlowVision.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20260422; + CURRENT_PROJECT_VERSION = 20260809; + DEVELOPMENT_TEAM = M6WQA48PJ2; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FlowVision/Info.plist; @@ -744,7 +753,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.7.1; + MARKETING_VERSION = 1.7.6; MERGED_BINARY_TYPE = automatic; PRODUCT_BUNDLE_IDENTIFIER = netdcy.FlowVisionDbg; PRODUCT_NAME = "$(TARGET_NAME)Dbg"; @@ -762,8 +771,8 @@ CODE_SIGN_ENTITLEMENTS = FlowVision/FlowVision.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20260422; - DEVELOPMENT_TEAM = M9PR3WG2FN; + CURRENT_PROJECT_VERSION = 20260809; + DEVELOPMENT_TEAM = M6WQA48PJ2; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FlowVision/Info.plist; @@ -778,7 +787,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.7.1; + MARKETING_VERSION = 1.7.6; MERGED_BINARY_TYPE = automatic; PRODUCT_BUNDLE_IDENTIFIER = netdcy.FlowVision; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/FlowVision/Resources/Base.lproj/Main.storyboard b/FlowVision/Resources/Base.lproj/Main.storyboard index 4edffd50..0807d896 100644 --- a/FlowVision/Resources/Base.lproj/Main.storyboard +++ b/FlowVision/Resources/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -1201,11 +1201,11 @@ Gw -