Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .codex/environments/environment.toml
Original file line number Diff line number Diff line change
@@ -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"
139 changes: 139 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
xcuserdata/
LocalDev.xcconfig
LocalDev.xcconfig
.DS_Store
.mindfs/
dist/
build/
84 changes: 84 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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,需要用户确认才能执行。
Binary file added FlowVision-Release-2026-04-19-v2.dmg
Binary file not shown.
Binary file added FlowVision-Release-2026-04-19-v2.zip
Binary file not shown.
Binary file added FlowVision-Release-2026-04-19.dmg
Binary file not shown.
Binary file added FlowVision-Release-2026-04-19.zip
Binary file not shown.
Binary file added FlowVision-Release-20260419.zip
Binary file not shown.
19 changes: 14 additions & 5 deletions FlowVision.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -114,6 +116,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
A1B2C3D32F90000100ABCDEF /* UpdateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateManager.swift; sourceTree = "<group>"; };
E00E13C3056A4910B242187C /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; };
F70368E72BF609F300155F36 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
F70F9FC02BAFE7B300C50CDD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -175,6 +178,7 @@
F7790ED52BA5FB6A00406D35 /* CustomCollectionViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCollectionViewItem.swift; sourceTree = "<group>"; };
F7790ED62BA5FB6A00406D35 /* CustomCollectionViewItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CustomCollectionViewItem.xib; sourceTree = "<group>"; };
F78960F12BDCC26B00C2571B /* LargeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageView.swift; sourceTree = "<group>"; };
B513CF896032F495D8769284 /* MPVPlayerBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVPlayerBackend.swift; sourceTree = "<group>"; };
F78960F52BDFFE9200C2571B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F78D1A692D432DCE00741908 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Main.xcstrings; sourceTree = "<group>"; };
F78D1A872D4CF31D00741908 /* GeneralSettingsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = GeneralSettingsViewController.xib; path = Base.lproj/CustomSettingsViewController.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -216,6 +220,7 @@
F73865DC2C37BD7F00837FE4 /* Common */ = {
isa = PBXGroup;
children = (
A1B2C3D32F90000100ABCDEF /* UpdateManager.swift */,
F76F6A622F4FE58000D798DC /* TempVariable.swift */,
F73865D82C37BBCB00837FE4 /* GlobalVariable.swift */,
F70F9FC02BAFE7B300C50CDD /* Common.swift */,
Expand Down Expand Up @@ -251,6 +256,7 @@
F7FAE0002F6BA00000FAE000 /* FavoritesPopoverViewController.swift */,
F78960F12BDCC26B00C2571B /* LargeImageView.swift */,
F73865D42C37B9B800837FE4 /* Layout.swift */,
B513CF896032F495D8769284 /* MPVPlayerBackend.swift */,
F7AA00002F9E000000AA0001 /* VideoPlayerControlsView.swift */,
);
path = Views;
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -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;
Expand All @@ -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)";
Expand Down
Loading