diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..781add5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell scripts run under bash on every CI runner, including Windows checkouts with autocrlf. +*.sh text eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dfbb05f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +# Manual-only release build (PLAN §7.2): no push or pull_request trigger. +# Each RID job runs build -> test -> publish/pack -> upload; a failing step +# skips the rest of that job, so a RID whose tests fail produces no artifact. +# No GitHub Release is created. +name: Build + +on: + workflow_dispatch: + +permissions: + contents: read + +env: + DOTNET_NOLOGO: "true" + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + +jobs: + build: + name: ${{ matrix.rid }} + runs-on: ${{ matrix.os }} + strategy: + # Every RID stands on its own: one failure does not cancel the others. + fail-fast: false + matrix: + include: + - rid: win-x64 + os: windows-latest + - rid: linux-x64 + os: ubuntu-latest + - rid: osx-x64 + os: macos-15-intel + - rid: osx-arm64 + os: macos-15 + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Build + run: dotnet build YKanBan.slnx -c Release + + - name: Test + run: dotnet test YKanBan.slnx -c Release --no-build + + - name: Publish + run: > + dotnet publish YKanBan/YKanBan.csproj -c Release -r ${{ matrix.rid }} + --self-contained -o publish/${{ matrix.rid }} + + - name: Pack + id: pack + run: | + version="$(dotnet msbuild YKanBan/YKanBan.csproj -getProperty:Version)" + bash packaging/package.sh ${{ matrix.rid }} publish/${{ matrix.rid }} dist "$version" + echo "name=YKanBan-$version-${{ matrix.rid }}" >> "$GITHUB_OUTPUT" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.pack.outputs.name }} + path: dist/* + if-no-files-found: error diff --git a/README.md b/README.md index de12ab8..a70c893 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,30 @@ My personal used local/offline kanban. -WIP. +- Cross-platform desktop app (Windows / Linux / macOS) built with Avalonia. +- Git-like storage: the board lives in a `.ykanban` folder inside the project + folder it manages; start it with `ykanban `. +- One board per workspace with columns, cards and colored tags; strict search + expressions (`tag:bug OR title:login`); Markdown export. +- English and Simplified Chinese interface; light, dark or system theme. + +See **[USAGE.md](USAGE.md)** for installing, starting YKanBan, backups and +Git, and the full search syntax. + +## Building + +Requires the .NET 10 SDK. + +```sh +dotnet build +dotnet test +dotnet run --project YKanBan -- +``` + +Release archives for win-x64, linux-x64, osx-x64 and osx-arm64 are produced +by the manually triggered workflow in `.github/workflows/build.yml`; +`packaging/package.sh` packs one published RID. + +## License + +MIT diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..51d58cf --- /dev/null +++ b/USAGE.md @@ -0,0 +1,246 @@ +# YKanBan usage + +YKanBan is a local, offline kanban board. Like Git, it keeps its data in a +`.ykanban` folder inside the project folder it manages (the **workspace**). +Each YKanBan window manages exactly one workspace; several windows can run at +the same time on different workspaces. + +- [Installing](#installing) +- [Starting YKanBan](#starting-ykanban) +- [macOS](#macos) +- [Workspaces, Git and backups](#workspaces-git-and-backups) +- [Search syntax](#search-syntax) +- [Settings file](#settings-file) + +## Installing + +Each release build is a self-contained archive; no .NET runtime is needed. + +| Platform | Archive | Executable | +|---|---|---| +| Windows x64 | `YKanBan--win-x64.zip` | `YKanBan.exe` | +| Linux x64 | `YKanBan--linux-x64.tar.gz` | `YKanBan` | +| macOS Intel | `YKanBan--osx-x64.tar.gz` | `YKanBan.app` (see [macOS](#macos)) | +| macOS Apple silicon | `YKanBan--osx-arm64.tar.gz` | `YKanBan.app` (see [macOS](#macos)) | + +Extract the archive anywhere. To type `ykanban ` as shown below, put the +executable on your `PATH` under that name, for example: + +```sh +# Linux +ln -s /opt/YKanBan--linux-x64/YKanBan ~/.local/bin/ykanban +# macOS +ln -s /Applications/YKanBan.app/Contents/MacOS/YKanBan /usr/local/bin/ykanban +``` + +On Windows, add the extracted folder to `PATH`; `ykanban` then finds +`YKanBan.exe` because Windows ignores case. + +## Starting YKanBan + +The only way to start YKanBan is with the workspace path: + +```sh +ykanban +``` + +- Exactly one argument is accepted. A relative path is resolved against the + current working directory, so `ykanban .` opens the current folder. +- YKanBan looks for `.ykanban` **only in `` itself**; it does not search + parent folders the way Git does. +- There are no command-line options. An argument starting with `-` is never + treated as a path; to open a folder whose name starts with `-`, write it as + `./-name`. + +When the board cannot be shown, the whole window shows one of these pages +instead: + +| Page | When | +|---|---| +| Invalid arguments | No argument, more than one argument, an empty argument, or an argument starting with `-` (including `--help`). Shows the usage line. | +| Folder not found | `` does not exist or is not a folder. | +| No workspace here | `` exists but has no `.ykanban`. The **Initialize workspace** button creates one, with the columns To Do / In Progress / Done (named in the current interface language). | +| Workspace locked | Another YKanBan window already has this workspace open. Close that window first. | +| Cannot open workspace | The database was created by a newer YKanBan, is damaged or cannot be opened, or a file-system or permission error occurred (also when initializing fails). The English error details can be selected and copied. | + +The window title is ` - YKanBan` while a workspace is open and +just `YKanBan` on these pages. + +## macOS + +The app is not notarized. After extracting, remove the download quarantine +once, or macOS refuses to open it: + +```sh +xattr -dr com.apple.quarantine YKanBan.app +``` + +Double-clicking `YKanBan.app` starts it without a path, so it only shows the +**Invalid arguments** page. Start it from a terminal instead, either through +Launch Services or by running the executable inside the bundle: + +```sh +open -a YKanBan --args /path/to/project +/Applications/YKanBan.app/Contents/MacOS/YKanBan /path/to/project +``` + +`open -a YKanBan` finds the app once it is in `/Applications` (or another +folder Launch Services knows about). + +## Workspaces, Git and backups + +- **Do not commit `.ykanban` to Git.** Add it to your global ignore file once, + so no repository picks it up. If `git config --global core.excludesFile` + prints a file, add the line `.ykanban/` to that file; otherwise create one: + + ```sh + echo ".ykanban/" >> ~/.gitignore_global + git config --global core.excludesFile ~/.gitignore_global + ``` + +- **Back up or move all three database files together**: `ykanban.db`, + `ykanban.db-wal` and `ykanban.db-shm` in `.ykanban/`. Recent changes may + still be in the `-wal` file; copying only `ykanban.db` can lose them. The + simplest safe backup is to close YKanBan and copy the whole `.ykanban` + folder. +- **Network shares (SMB, NFS, …) are not supported.** The database and the + workspace lock rely on local file-system features that do not work reliably + over a network. YKanBan does not detect this; keep workspaces on a local + disk. +- `.ykanban/ykanban.lock` is held while a window has the workspace open and is + released automatically when YKanBan exits or crashes. + +## Search syntax + +Type an expression in the search box on the **Board** tab and click +**Search** (or press Enter). Searching only happens then; there is no search +while typing. The **×** button clears the box and shows every card again. + +- Columns are always shown, even when none of their cards match. +- An empty or blank expression shows every card. +- After any change (adding, editing, moving or deleting cards, columns or + tags), the board re-runs the **last expression that searched successfully**, + not whatever is in the box at that moment. Cards that stop matching + disappear; new cards that do not match are not shown. + +### Examples + +| Expression | Finds cards… | +|---|---| +| `login` | whose title or content contains "login" | +| `login crash` | containing both words (anywhere in title or content) | +| `login OR signup` | containing either word | +| `tag:bug title:login` | tagged `bug` whose title contains "login" | +| `tag:bug OR tag:ui` | tagged `bug` or `ui` | +| `(tag:bug OR tag:ui) column:Done` | tagged `bug` or `ui` in the column titled "Done" | +| `"dark mode"` | whose title or content contains "dark mode" | +| `tag:"needs review"` | tagged `needs review` | +| `id:61` or `id:#61` | number 61 | +| `"http://example.com"` | containing the URL (a `:` must be quoted) | +| `"AND"` | containing the text "AND" | + +### Grammar + +``` +query := orExpr +orExpr := andExpr ( OR andExpr )* +andExpr := operand ( (AND)? operand )* -- neighbours are joined by AND +operand := atom | '(' orExpr ')' +atom := word | phrase | qualifier +qualifier := key ':' value +key := tag | title | content | id | column +value := word | phrase -- tag / title / content / column + | '#'? digit+ -- id: a positive decimal integer +word := one or more characters, except whitespace and ( ) " : \ ; + not equal to AND or OR +phrase := '"' any text '"' -- escapes: \" and \\ +``` + +### Operators + +- `AND` and `OR` are operators **only in uppercase**. `and` and `or` are + ordinary words. +- Writing words next to each other means `AND`: `a b` is `a AND b`. +- `AND` binds tighter than `OR`: `a OR b c` is `a OR (b AND c)`. Use + parentheses to group differently: `(a OR b) c`. +- To search for the literal text `AND` or `OR`, quote it: `"AND"`, + `tag:"OR"`. +- The search keywords (`AND`, `OR`, `tag`, `title`, `content`, `id`, + `column`) are always English, whatever the interface language. Qualifier + keys are lowercase only: `Tag:bug` is an unknown qualifier. + +### What each atom matches + +| Atom | Matches when | +|---|---| +| word or phrase | the card title **or** content contains the text | +| `title:` value | the card title contains the text | +| `content:` value | the card content contains the text | +| `tag:` value | the card has a tag whose name **equals** the text | +| `column:` value | the card is in a column whose title **equals** the text | +| `id:` value | the card number equals the value | + +- A qualifier value can be a word (`tag:bug`) or a phrase + (`tag:"needs review"`). Use a phrase for values containing spaces, `(`, + `)`, `:` or `\`, and for the values `AND` / `OR`. +- `id:` takes an unquoted positive integer, optionally prefixed with `#`: + `id:61`, `id:#61`. + +### Quoting and escapes + +- A phrase is text in double quotes. Inside it, `\"` stands for a quote and + `\\` for a backslash; any other backslash is an error. +- Outside quotes, `\` is not allowed at all, and `:` always starts a + qualifier. Quote any text containing them: `"C:\\temp"`, `"a:b"`. + +### Case + +All matching ignores case, for letters of every language: `tag:BUG` finds +`bug`, and `ÄRGER` finds `ärger`. Text is otherwise compared literally: `%`, +`_` and `*` have no special meaning. + +Tag names and column titles must be unique ignoring case for the letters A–Z +only. Two tags such as `Ä` and `ä` can therefore both exist, and `tag:ä` +matches both. + +### Invalid expressions + +An invalid expression does not search. A dialog shows "The query expression is +invalid. Details: …" followed by the parser's message (always in English). +After closing it, the box keeps your text for correcting and the board keeps +the previous result. + +These are invalid: + +| Problem | Example | +|---|---| +| Unbalanced parentheses | `(tag:bug`, `tag:bug)` | +| An operator with a missing side | `bug AND`, `OR bug` | +| Consecutive operators | `a AND OR b` | +| A qualifier without a value | `tag:` | +| `AND` / `OR` as an unquoted qualifier value | `tag:AND` (write `tag:"AND"`) | +| An unterminated phrase | `"dark mode` | +| An invalid escape in a phrase | `tag:"a\x"` | +| A backslash outside a phrase | `C:\temp` | +| An unknown qualifier key | `label:bug`, `Tag:bug`, `http://x` | +| An unquoted value containing `:` | `title:a:b` | +| An `id:` value that is not an unquoted positive integer | `id:abc`, `id:0`, `id:"61"` | + +## Settings file + +Settings (language, theme, card sort order, column width and the confirmation +switches) are stored in `app.json`: + +| Platform | Location | +|---|---| +| Windows | `%APPDATA%\YKanBan\app.json` | +| Linux, macOS | `~/.config/YKanBan/app.json` (`$XDG_CONFIG_HOME/YKanBan/app.json` when that is set) | + +- The file is written when YKanBan exits, and only if a setting was changed in + that session. With several windows open, the last one closed after changing + a setting wins. +- A language change takes effect after restarting YKanBan; a theme change + applies immediately. +- If the file cannot be parsed, it is saved as `app.json.bak` and the defaults + are used. A single invalid value falls back to its default; the other + values are kept. diff --git a/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs b/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs index 48bfc53..079b7a7 100644 --- a/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs +++ b/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs @@ -30,7 +30,7 @@ public sealed class AboutDialogViewModel : ViewModelBase /// /// Gets the repository URL. /// - public string RepositoryUrl => "https://github.com/yyc12345/YKanBan"; + public string RepositoryUrl => "https://code.blumia.cn/yyc12345/YKanBan"; /// /// Gets the license name. diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md new file mode 100644 index 0000000..3721c8e --- /dev/null +++ b/docs/ACCEPTANCE.md @@ -0,0 +1,102 @@ +# YKanBan 人工验收清单 + +> 对应 PLAN §9 各里程碑的"人工"验证项。按计划 §7.1,窗口、模态、点击流、数据绑定、主题切换、状态页渲染不写自动化测试,一律按本清单人工验证。 +> 默认在本地 Windows 执行;Linux / macOS 行为验证按 §7.2 推迟至程序完工后,届时用同一清单逐项复核。 + +## 准备 + +- [ ] `dotnet build` 与 `dotnet test` 通过 +- [ ] 准备一个空文件夹 `` 作为演示工作区;以下 `ykanban` 均指 `dotnet run --project YKanBan --` 或发布产物中的可执行文件 +- [ ] 记下 `app.json` 位置(Windows:`%APPDATA%\YKanBan\app.json`),验收前备份或删除,保证从默认值开始 + +## A. 计划指定的四项(M7 必验) + +### A1 语言切换重启生效 + +- [ ] 设置 → 语言改为简体中文 → 显示"重启后生效"提示;当前界面**不**变化 +- [ ] 确认关闭后,退出程序 → `app.json` 中 `Language` 为 `zh-Hans` +- [ ] 重启 → 全部界面文本(顶部区、两个 tab、各模态、错误页、排序项、确认对话框)为中文,无残留英文硬编码 +- [ ] 切回英文 → 重启后恢复英文 +- [ ] 中文下新初始化的工作区预置列为 待办 / 进行中 / 已完成;切回英文后这些列名**不变**(已是数据) + +### A2 导出语言跟随界面 + +- [ ] 英文界面导出:默认文件名 `<工作区名>-export-yyyyMMdd-HHmmss.md`(精确到秒);文件内模板文字为英文,时间格式 `yyyy-MM-dd HH:mm:ss` +- [ ] 中文界面导出:默认文件名 `<工作区名>-导出-yyyyMMdd-HHmmss.md`;模板文字为中文,时间格式 `yyyy年M月d日 HH:mm` +- [ ] 保存对话框的文件类型名随语言(Markdown document / Markdown 文档) +- [ ] 看板处于搜索筛选、非默认排序时导出:文件仍含**全部**列与卡片,列与卡片均按 id 升序;空列保留;无标题卡片为 `### #id` +- [ ] 标签总表含全部标签(含未使用的),使用数正确;标签名含 `` ` ``、`|`、换行时转义符合 §5.8 +- [ ] 文件为 UTF-8 无 BOM;取消保存对话框不写任何文件;保存到无权限位置时弹"导出失败"消息框 + +### A3 主题三态切换 + +- [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:**即时生效**,无重启提示 +- [ ] 每种主题下检查:主窗口、看板列与卡片、标签 tab、全部模态、全部错误页颜色完整切换,无亮底暗字/暗底亮字残留 +- [ ] 标签徽章在两种主题下均显示原色(§5.9 例外),文字颜色保持可读 +- [ ] 跟随系统:程序运行中切换 Windows 深浅色 → 程序跟随变化 +- [ ] 重启后主题保持上次选择 + +### A4 app.json 多实例脏写 + +- [ ] 同时打开两个实例(不同工作区)。实例 1 改排序,实例 2 改列宽;先关实例 1、后关实例 2 → `app.json` 为实例 2 的完整设置(后退出者覆盖,实例 1 的排序改动丢失——已接受) +- [ ] 打开两个实例,只在实例 1 改设置;先关实例 1,再关**未改过设置**的实例 2 → `app.json` 保持实例 1 写入的内容(未改设置的实例不写盘) +- [ ] 处于错误页(例如参数错误页)的实例退出时不写 `app.json`(可先删除 `app.json`,退出后确认文件未被创建) +- [ ] `app.json` 写成非法 JSON → 启动正常、使用默认值,生成 `app.json.bak`;改一次设置退出后 `app.json` 恢复为合法内容 +- [ ] `app.json` 中单个字段非法(如 `"Theme": "pink"`、列宽 `9999`)→ 仅该字段回退默认,其余设置保留,不生成 `.bak` + +## B. 启动与错误页(M4 / M4R) + +- [ ] 无参数、两个参数、`--help`、`-x`、空字符串参数 → 参数错误页,显示 `Usage: ykanban ` +- [ ] 不存在的路径、指向文件的路径 → 目录不存在页 +- [ ] 相对路径(如 `ykanban .`)按当前目录解析 +- [ ] 无 `.ykanban` 的文件夹 → 未初始化页;点"初始化"后进入看板,预置三列 +- [ ] 初始化失败(如只读文件夹)→ 工作区打开失败页 +- [ ] 同一工作区开第二个实例 → 锁冲突页,无任何操作按钮;关闭第一个实例后可正常打开 +- [ ] 工作区数据库 `user_version` 改为更高值 → 打开失败页,提示版本过新,英文详情可选中复制 +- [ ] 把 `ykanban.db` 换成垃圾内容 → 打开失败页(数据库无法打开/损坏) +- [ ] 窗口标题:正常时 `工作区名 - YKanBan`,错误页时仅 `YKanBan` +- [ ] 顶部区:工作区名 + 小一号完整路径;右侧 导出 / 设置 / 关于 同排 + +## C. 看板 tab(M5) + +- [ ] 新建列、编辑列(标题 + 描述);空标题、重名(含仅大小写不同,如 `Done` / `done`)→ 模态内提示错误,模态不关、输入保留 +- [ ] 删除列:确认框提示将删除 N 张卡片;确认后列与卡片一并删除 +- [ ] 新建卡片、点击卡片编辑:标题可空;内容多行;`#id` / 创建时间 / 修改时间只读显示(新建时不显示) +- [ ] 编辑确认但内容未变 → 修改时间不变;改动后确认 → 修改时间刷新 +- [ ] 有未保存改动时取消或点标题栏关闭 → 丢弃确认(开关开启时) +- [ ] 卡片菜单"移动到列":卡片移动,修改时间**不**变 +- [ ] 删除卡片:确认框(开关开启时) +- [ ] 卡片条目:`#id` 加粗与标题同行;无标题只显示 `#id`;内容约 200 字截断;标签彩色徽章;不显示时间 +- [ ] 标签区:× 移除;"添加标签"嵌套模态可筛选、选中即添加、可反复打开;嵌套模态中新建标签**立即写库**——外层取消后该标签仍出现在标签 tab +- [ ] 列宽:窄 240 / 标准 320 / 宽 480 / 手动 160–720;全局生效,重启保持 +- [ ] 排序:id 升序 / 最后修改时间降序 / 标题升序(空标题在前);只影响卡片,列始终 id 序;重启保持 +- [ ] 大数据量:用脚本向一列灌入约 10000 张卡片,滚动流畅、打开工作区无明显卡顿(虚拟化) +- [ ] 所有模态在父窗口居中;使用系统原生标题栏 +- [ ] 打开带输入框的模态时,第一个输入框自动获得焦点并全选 + +## D. 标签 tab 与搜索(M6) + +- [ ] 标签 tab 条目:颜色徽章 + 名称 + 描述 + 使用数;顶部显示标签总数;无标签时显示空状态文字 +- [ ] 新建标签:预置色块或手输 `#RRGGBB`;非法颜色、空名、重名(仅大小写不同也算)→ 模态内提示 +- [ ] 重命名、改色、改描述后,看板上的徽章同步更新 +- [ ] 删除标签:确认框提示"将从 N 张卡片移除该标签";删除后卡片保留、只是去掉该标签 +- [ ] 看板写库(编辑/移动/删除卡片等)后切到标签 tab,使用数已更新 +- [ ] 搜索仅在点"搜索"或按回车时执行;输入过程中不执行、不报错 +- [ ] 筛选后空列保留显示;× 清空并显示全部;空白表达式 = 全部 +- [ ] 非法表达式(如 `bug AND`、`tag:`、`(a`、`id:0`)→ 错误对话框"查询表达式有误,详情:…"+ 英文详情,父窗口居中;关闭后搜索框保留原文,看板保持上次结果 +- [ ] 筛选状态下写库:按**上次成功**的表达式刷新(即使搜索框已改成别的文字);新建但不匹配的卡片不显示 +- [ ] USAGE.md 搜索语法示例表逐条实测结果与文档一致 + +## E. 设置与关于(M4) + +- [ ] 四个确认开关分别关闭后,对应操作不再弹确认;重启保持 +- [ ] 关于:应用名、版本号、仓库链接(可点击打开浏览器)、许可证 + +## F. 发布(M7,视网络条件推迟) + +- [ ] 在代码托管平台手动触发 `Build` workflow;四个 RID 作业均 build → test → 上传产物 +- [ ] 产物:`YKanBan-<版本>-win-x64.zip`、`-linux-x64.tar.gz`、`-osx-x64.tar.gz`、`-osx-arm64.tar.gz` +- [ ] Windows:解压后 `YKanBan.exe ` 可直接运行(自包含,无需安装 .NET) +- [ ] Linux:解压后 `./YKanBan ` 可运行(可执行位保留) +- [ ] macOS:解除隔离后 `open -a YKanBan --args ` 与直接运行包内可执行文件均可用;双击 `.app` 只显示参数错误页 +- [ ] 故意让某平台测试失败 → 该 RID 无产物,其余 RID 不受影响 diff --git a/docs/PLAN.md b/docs/PLAN.md index 1617db1..303e371 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -375,7 +375,7 @@ phrase := '"' 任意文本 '"' (内部支持转义:\" → 字面引号, | **M4R v1.9 回补** | 已完成 | 见下方清单 | 本地 Windows build + test 通过;新增页面人工 | | M5 看板界面 | 已完成(待人工验收) | 看板 tab 全部交互与模态(含约束冲突模态内提示、卡片列表虚拟化、嵌套模态新建标签立即写库、写库后按最近成功表达式刷新) | 人工 | | M6 标签与搜索集成 | 已完成(待人工验收) | 标签 tab、搜索执行与空列保留、导出接线(含文件名/时间戳 i18n 模式) | 人工 | -| M7 发布与文档 | 待办 | 单一 workflow(4 RID 矩阵 build→test→artifact,osx-x64 用 Intel 运行器)文件就绪(结构自查)、根目录 USAGE.md、README(引用 USAGE.md)、人工验收清单(含语言切换重启生效、导出语言跟随界面、主题三态切换、app.json 多实例脏写四项) | workflow 结构自查通过;实际触发与产物验证视网络条件推迟 | +| M7 发布与文档 | 已完成(待人工验收;workflow 实际触发推迟) | 单一 workflow(4 RID 矩阵 build→test→artifact,osx-x64 用 Intel 运行器)文件就绪(结构自查)、根目录 USAGE.md、README(引用 USAGE.md)、人工验收清单(含语言切换重启生效、导出语言跟随界面、主题三态切换、app.json 多实例脏写四项) | workflow 结构自查通过;实际触发与产物验证视网络条件推迟 | **M4R 清单**(v1.9 对已完成里程碑的回补,进入 M5 前完成): diff --git a/packaging/macos/Info.plist b/packaging/macos/Info.plist new file mode 100644 index 0000000..c7bf2bd --- /dev/null +++ b/packaging/macos/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleName + YKanBan + CFBundleDisplayName + YKanBan + CFBundleIdentifier + cn.blumia.code.yyc12345.ykanban + CFBundleExecutable + YKanBan + CFBundlePackageType + APPL + CFBundleShortVersionString + @VERSION@ + CFBundleVersion + @VERSION@ + CFBundleInfoDictionaryVersion + 6.0 + NSHighResolutionCapable + + + diff --git a/packaging/package.sh b/packaging/package.sh new file mode 100755 index 0000000..03fbfbf --- /dev/null +++ b/packaging/package.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Packs one published RID into a release archive. +# +# packaging/package.sh +# +# win-x64 -> YKanBan--win-x64.zip (folder YKanBan--win-x64/) +# linux-x64 -> YKanBan--linux-x64.tar.gz (folder YKanBan--linux-x64/) +# osx-* -> YKanBan--.tar.gz (YKanBan.app bundle, ad-hoc signed when codesign exists) +# +# tar.gz keeps the executable bit, which a plain zip artifact upload would lose. +set -euo pipefail + +if [ "$#" -ne 4 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +rid="$1" +publish_dir="$(cd "$2" && pwd)" +mkdir -p "$3" +output_dir="$(cd "$3" && pwd)" +version="$4" +script_dir="$(cd "$(dirname "$0")" && pwd)" +name="YKanBan-${version}-${rid}" +staging="$(mktemp -d)" +trap 'rm -rf "$staging"' EXIT + +case "$rid" in + win-*) + cp -R "$publish_dir" "$staging/$name" + # Compress-Archive is available on every Windows host, unlike zip. + powershell -NoProfile -Command \ + "Compress-Archive -Path '$(cygpath -w "$staging/$name" 2>/dev/null || echo "$staging/$name")' -DestinationPath '$(cygpath -w "$output_dir/$name.zip" 2>/dev/null || echo "$output_dir/$name.zip")' -Force" + ;; + linux-*) + cp -R "$publish_dir" "$staging/$name" + chmod +x "$staging/$name/YKanBan" + tar -C "$staging" -czf "$output_dir/$name.tar.gz" "$name" + ;; + osx-*) + app="$staging/YKanBan.app" + mkdir -p "$app/Contents/MacOS" + cp -R "$publish_dir/." "$app/Contents/MacOS/" + chmod +x "$app/Contents/MacOS/YKanBan" + sed "s/@VERSION@/${version}/g" "$script_dir/macos/Info.plist" > "$app/Contents/Info.plist" + if command -v codesign > /dev/null 2>&1; then + # Ad-hoc signature: unsigned arm64 code does not run at all. + codesign --force --deep --sign - "$app" + fi + tar -C "$staging" -czf "$output_dir/$name.tar.gz" YKanBan.app + ;; + *) + echo "unsupported RID: $rid" >&2 + exit 2 + ;; +esac + +echo "packed $rid -> $output_dir"