commit a223798c25220d98a12783616c72ee2e86aebe10 Author: Codex Date: Mon Oct 12 22:30:13 2026 +0800 docs: record approved app direction and Android setup scripts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d26c0e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.tools/ +.dart_tool/ +.flutter-plugins-dependencies +build/ +android/local.properties +*.jks +*.keystore +*.log diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5f4e59f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".tools/flutter" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..94e5e3f --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# 河畔 Everyday + +电子科技大学研究生课表 APP。采用 Flutter,优先 Android,后续支持 iOS。 + +当前阶段:配置 Android 开发环境并审核首版设计;尚未生成业务代码或 APP 安装包。 + +## 开发环境 + +项目工具与缓存放在 `.tools/`,不会进入版本控制。使用官方发行版及 SHA-256 校验: + +- Flutter 3.47.7 稳定版,Dart 3.13.5。 +- Microsoft OpenJDK 17.0.20.1。 +- Android 命令行工具 22.0、Android API 36、Build Tools 36.0.0、Platform Tools。 +- Flutter 配套 NDK 28.2.13676358、CMake 3.22.1。 +- 可选 Android Emulator 与 Android 16(API 36)Google APIs x86_64 镜像。 + +在 Windows PowerShell 中,进入本目录后,安装与检查环境: + +```powershell +./scripts/setup-android.ps1 -WithEmulator +``` + +安装时需要访问官方下载源和软件包服务,并接受 Android SDK 开发工具许可。安装日志与许可文本保留在 `.tools/`。安装脚本只在项目内放置工具;Flutter 自身会保存 SDK 路径配置。 + +每次打开新的 PowerShell 终端后激活环境: + +```powershell +. ./scripts/android-env.ps1 +flutter doctor -v +flutter devices +``` + +真机调试:在 Android 手机打开开发者选项和 USB 调试,通过 USB 连接电脑,在手机上允许电脑调试,然后检查 `flutter devices`。如果 Windows 无法识别手机,根据手机厂商说明安装官方 USB 驱动。不要在聊天中提供学校账号密码。 + +模拟器需 Windows 支持的硬件虚拟化环境;环境检查与设备启动结果记录在 `docs/android-setup-status.md`。安装完成后可使用: + +```powershell +. ./scripts/android-env.ps1 +./scripts/create-android-emulator.ps1 +emulator -avd hepan_everyday +``` + +已创建的虚拟设备会重复使用,不覆盖其数据。 + +## 首版设计 + +见 [首版设计文件](docs/superpowers/specs/2026-10-12-hepan-everyday-design.md)。聊天中的方案已确认,完整设计文件待审核。学校真实课表解析需要在用户本人认证后的页面上验证。 diff --git a/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md b/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md new file mode 100644 index 0000000..7c14d16 --- /dev/null +++ b/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md @@ -0,0 +1,73 @@ +# 河畔 Everyday 首版设计 + +日期:2026-10-12。用户已确认聊天中展示的设计,并指定优先配置 Android;此文件待用户审核。采用 Flutter,为后续 iOS 保留共用界面与业务代码。首轮交付 Android 可安装测试包,iOS 打包另行安排。 + +## 目标与范围 + +面向电子科技大学研究生,把学校课表导入手机,按周离线查看课程,并在课前收到提醒。首版只有课表和我的两个标签页。课程、设置和同步结果保存在设备上。示例课程必须显著标注,不能被当作学校导入结果,也不能默认生成真实提醒或日历事件。 + +## 界面 + +整体采用浅色、河水蓝绿色点缀、留白与圆角。底部 Dock 使用 Flutter 模糊滤镜、半透明渐变、细边框和高光模拟液态玻璃,包含日历与个人两个图标,不显示文字;保留无障碍标签。Dock 避开系统手势区域,不遮挡最后一节课程。 + +课表页顶部显示学期、本周日期范围和当前周次,支持上一周、下一周与回到本周。周一到周日以七列呈现;左侧显示节次和时间。小屏上课程列采用最低可读宽度并支持横向滚动,顶部日期与课程列同步。今天列高亮。晨间、午间与晚间分组,休息间隔不形成课程节次。连续多节课程呈现一个跨节次的课程条,显示课程名、教师和教室,点击详情展示完整内容及上课周次。同一时段冲突的课程并排呈现并提示冲突,不静默覆盖。 + +我的页:未认证时显示 APP 自有登录界面和学校认证说明;已认证时显示连接状态、最近成功导入时间、课程数量、重新同步和退出登录。学校认证成功与课表导入成功分开表示。没有验证用户姓名时不生成虚假个人资料。设置区在未登录时也可访问。 + +设置:单色/多彩课程配色,单色时可选择主题色;提醒可关闭或选择提前 5、10、15、30、60 分钟,默认 15 分钟;手机日历导入开关;学期开始日、学期总周数(1–30,首版初始值 20,需用户确认)。首次使用要求选择真实学期开始日,不推断学校校历。 + +## 节次时间 + +时间规则以用户提供的信息为准,采用 Asia/Shanghai。节次: + +|节次|开始|结束| +|---|---|---| +|1|08:30|09:15| +|2|09:20|10:05| +|3|10:20|11:05| +|4|11:10|11:55| +|5|14:30|15:15| +|6|15:20|16:05| +|7|16:20|17:05| +|8|17:10|17:55| +|9|19:30|20:15| +|10|20:20|21:05| +|11|21:10|21:55| + +学期开始日所在的周一至周日为第 1 周;首周开始日前不生成课程实例。课表需处理上课周范围、不连续周次与单双周。每条课表记录保存明确的上课周次集合,不把描述字符串直接用于提醒计算。 + +## 学校认证与导入 + +入口为 https://idas.uestc.edu.cn/authserver/login ,课表目标为 https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1 。已读取公开认证页面;目标课表页面尚未在认证会话中核实。不能宣称已经完成真实接入,也不编造学校接口或页面字段。 + +选定方案是设备本地 WebView 会话,搭配 Flutter 自有登录输入界面。只有在学校官方 HTTPS 页面且所需表单与提交逻辑经过验证时,才能把用户输入交给学校原有表单处理。密码仅在本次认证时使用,用后清除,不写入磁盘、日志、分析服务或自建后端。验证码、二次验证或页面变动时显示原学校认证页供用户完成认证;不绕过学校认证机制。学校跳转到新的认证域名时停止自动注入凭据并展示跳转信息。 + +登录后沿学校实际 SSO 路径进入课表页,同一 WebView 保留学校管理的会话 Cookie。只读取经确认的学校课表页面数据。解析层从页面提取名称、教师、教室、星期、节次、上课周次与学期信息,转换为结构化记录;将解析结果展示给用户确认后替换本地课表。若页面不是预期结构、字段缺失或课程数为零,保留旧课表并显示原因,不能用空结果覆盖。零课程仅在页面明确表示无课程且用户确认时接受。 + +在无法取得登录后页面样本时,首版完成认证入口与解析适配边界,明确标记真实课表识别尚待联调。提供示例体验和结构化文件导入以检验完整本地流程;它们不伪装成自动学校导入。后续需由用户在本人设备登录以核实学校页面,账号密码不要求在聊天中提供。 + +退出登录清理学校 WebView 会话,同时让用户选择是否保留已导入课表。保留课表时继续按本地设置工作;清除课表时一并取消对应通知与本 APP 创建的日历事件。 + +## 数据与模块 + +课程记录包含稳定 ID、名称、教师、教室、星期(1–7)、起止节次(1–11)、上课周次集合和来源。设置包含学期开始日、总周数、颜色模式与主题色、提醒提前量、日历导入状态。导入结果保存来源、成功时间和解析版本。 + +模块分为 UI(课表/我的/设置/认证)、纯 Dart 课程与学期计算、学校认证与解析适配器、本地存储、通知和日历服务。状态由单一应用状态层管理;平台服务用接口隔离,便于测试和后续 iOS 实现。暂不增加服务器、账号注册、社交功能或云同步。 + +## 提醒与手机日历 + +每次连续课程只在开始前提醒一次;通知内容包含课程名、教室和教师。只安排未来且在实际上课周次内的实例。修改课表、学期或提醒设置后,用稳定标识取消旧任务再重建,避免重复。设备重启后的恢复通过 Android 原生支持处理。只有用户开启提醒时申请通知权限;需要精确提醒时根据系统要求申请精确闹钟访问,若不获准则明确告知时间可能延迟。设备厂商省电设置仍需真机验证。 + +手机日历默认关闭。开启后请求日历权限并让用户选择日历;仅创建本 APP 课表对应的事件,保存事件 ID 以便同步修改和去重。关闭时询问是否删除本 APP 创建的事件,不影响其他日历事件。事件包含正确的日期、Asia/Shanghai 时区、连续节次起止时间、教师和教室。APP 通知与日历提醒分别管理,默认不为日历事件额外添加提醒,以免双重提醒。 + +## 异常处理与验收 + +网络不可达、校内网络限制、认证过期、验证码和解析失败均展示具体状态;已导入课表仍可离线查看。权限拒绝时对应功能显示未启用,不假装已生效。导入文件与网页数据都校验节次、日期、周次和必填字段。 + +验收包含:11 节时间计算;学期开始日前、学期外、单双周与不连续周次;课程冲突;跨节次显示;七天可达与 Dock 安全区域;设置重启后保持;导入失败保留旧数据;通知去重与关闭清理;日历去重与只清理本 APP 事件。真实认证、学校解析、系统后台提醒与日历写入必须在 Android 设备上核实,测试包编译成功不能替代这些验证。 + +## Android 环境与交付 + +Windows 上先配置项目本地 Flutter 稳定版、Android SDK、兼容 JDK。工具与缓存位于 .tools 并从版本控制排除;使用 scripts/android-env.ps1 激活当前终端环境。先确保 flutter doctor 的 Android 工具链检查通过,再进入产品实现和构建。没有连接真机或模拟器时,报告设备验证尚未完成。 + +交付源代码、启动与构建说明、测试结果及能够成功构建的 Android 测试 APK。未完成的学校联调以明确的状态和后续步骤记录,不包装为已经可用。 diff --git a/scripts/android-env.ps1 b/scripts/android-env.ps1 new file mode 100644 index 0000000..b3f4a54 --- /dev/null +++ b/scripts/android-env.ps1 @@ -0,0 +1,21 @@ +# Dot-source this file before using Flutter: . ./scripts/android-env.ps1 +$taskProjectRoot = Split-Path -Parent $PSScriptRoot +$taskToolRoot = Join-Path $taskProjectRoot '.tools' +$env:JAVA_HOME = Join-Path $taskToolRoot 'jdk-17' +$env:ANDROID_HOME = Join-Path $taskToolRoot 'android-sdk' +$env:ANDROID_SDK_ROOT = $env:ANDROID_HOME +$env:ANDROID_USER_HOME = Join-Path $taskToolRoot 'android-user' +$env:ANDROID_EMULATOR_HOME = Join-Path $taskToolRoot 'android-user' +$env:ANDROID_AVD_HOME = Join-Path $taskToolRoot 'avd' +$env:GRADLE_USER_HOME = Join-Path $taskToolRoot 'gradle-cache' +$env:PUB_CACHE = Join-Path $taskToolRoot 'pub-cache' +$env:FLUTTER_SUPPRESS_ANALYTICS = 'true' +$taskPathEntries = @( + (Join-Path $taskToolRoot 'flutter/bin'), + (Join-Path $env:JAVA_HOME 'bin'), + (Join-Path $env:ANDROID_HOME 'platform-tools'), + (Join-Path $env:ANDROID_HOME 'cmdline-tools/latest/bin'), + (Join-Path $env:ANDROID_HOME 'emulator') +) +$taskOldPath = @($env:PATH -split ';' | Where-Object { $_ -and $_ -notin $taskPathEntries }) +$env:PATH = ($taskPathEntries + $taskOldPath) -join ';' diff --git a/scripts/create-android-emulator.ps1 b/scripts/create-android-emulator.ps1 new file mode 100644 index 0000000..502d8af --- /dev/null +++ b/scripts/create-android-emulator.ps1 @@ -0,0 +1,14 @@ +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'android-env.ps1') +$taskImagePath = Join-Path $env:ANDROID_HOME 'system-images/android-36/google_apis/x86_64/system.img' +if (-not (Test-Path -LiteralPath $taskImagePath)) { throw 'Run setup-android-sdk.ps1 -WithEmulator first.' } +$taskAvdName = 'hepan_everyday' +$taskAvdConfig = Join-Path $env:ANDROID_AVD_HOME "$taskAvdName.avd/config.ini" +if (Test-Path -LiteralPath $taskAvdConfig) { + Write-Output "Emulator $taskAvdName already exists." + exit 0 +} +New-Item -ItemType Directory -Force -Path $env:ANDROID_AVD_HOME | Out-Null +'no' | & avdmanager.bat create avd --name $taskAvdName --package 'system-images;android-36;google_apis;x86_64' --device 'pixel_7' +if ($LASTEXITCODE -ne 0) { throw 'Android virtual device creation failed.' } +Write-Output "Created $taskAvdName (Pixel 7, Android 16)." diff --git a/scripts/install-tool.ps1 b/scripts/install-tool.ps1 new file mode 100644 index 0000000..554498e --- /dev/null +++ b/scripts/install-tool.ps1 @@ -0,0 +1,69 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet('flutter', 'java', 'android-cli')] + [string]$Tool +) +$ErrorActionPreference = 'Stop' +$taskProjectRoot = Split-Path -Parent $PSScriptRoot +$taskToolRoot = Join-Path $taskProjectRoot '.tools' +$taskDownloads = Join-Path $taskToolRoot 'downloads' +New-Item -ItemType Directory -Force -Path $taskDownloads | Out-Null +$taskPackages = @{ + 'flutter' = @{ + Url = 'https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.47.7-stable.zip' + Sha256 = 'c2c875da25a1a1448d18ee4e8f398b2f6fce888b4775d8f9130538e16d826cb4' + Zip = 'flutter-3.47.7.zip' + Marker = 'flutter/bin/flutter.bat' + } + 'java' = @{ + Url = 'https://aka.ms/download-jdk/microsoft-jdk-17.0.20.1-windows-x64.zip' + Sha256 = '3d9006956fc8af5601cd24ffc4f468bef48279c7ebd8171b9bdf90d0aabfbf1f' + Zip = 'jdk-17.zip' + Marker = 'jdk-17/bin/java.exe' + } + 'android-cli' = @{ + Url = 'https://dl.google.com/android/repository/commandlinetools-win-15859902_latest.zip' + Sha256 = '90ae805d20434428bffcb699c290860f19bb5f66a67e6b330067e3de801fb04a' + Zip = 'android-commandline.zip' + Marker = 'android-sdk/cmdline-tools/latest/bin/sdkmanager.bat' + } +} +$taskPackage = $taskPackages[$Tool] +if (Test-Path -LiteralPath (Join-Path $taskToolRoot $taskPackage.Marker)) { + Write-Output "$Tool is already installed." + exit 0 +} +$taskArchive = Join-Path $taskDownloads $taskPackage.Zip +Write-Output "Downloading $Tool from the official distribution." +& curl.exe --fail --location --silent --show-error --retry 3 --retry-all-errors --connect-timeout 30 --speed-limit 1024 --speed-time 60 --continue-at - --output $taskArchive $taskPackage.Url +if ($LASTEXITCODE -ne 0) { throw "Download failed: $Tool" } +$taskActualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $taskArchive).Hash.ToLowerInvariant() +if ($taskActualHash -ne $taskPackage.Sha256) { throw "SHA-256 verification failed: $Tool" } +Write-Output "Verified SHA-256 for $Tool; extracting." +if ($Tool -eq 'flutter') { + & tar.exe -xf $taskArchive -C $taskToolRoot + if ($LASTEXITCODE -ne 0) { throw 'Flutter extraction failed.' } +} else { + $taskStage = Join-Path $taskToolRoot "staging/$Tool" + New-Item -ItemType Directory -Force -Path $taskStage | Out-Null + & tar.exe -xf $taskArchive -C $taskStage + if ($LASTEXITCODE -ne 0) { throw "$Tool extraction failed." } + if ($Tool -eq 'java') { + $taskSource = (Get-ChildItem -LiteralPath $taskStage -Directory | Where-Object Name -Like 'jdk-*' | Select-Object -First 1).FullName + $taskDestination = Join-Path $taskToolRoot 'jdk-17' + } else { + $taskSource = Join-Path $taskStage 'cmdline-tools' + $taskDestination = Join-Path $taskToolRoot 'android-sdk/cmdline-tools/latest' + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $taskDestination) | Out-Null + } + # Validate the resolved paths before moving the unpacked tool directory. + $taskResolvedRoot = [IO.Path]::GetFullPath($taskToolRoot).TrimEnd('\') + '\' + foreach ($taskMovePath in @($taskSource, $taskDestination)) { + if (-not $taskMovePath -or -not [IO.Path]::GetFullPath($taskMovePath).StartsWith($taskResolvedRoot, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Tool path is outside the project tool directory.' + } + } + Move-Item -LiteralPath $taskSource -Destination $taskDestination +} +if (-not (Test-Path -LiteralPath (Join-Path $taskToolRoot $taskPackage.Marker))) { throw "$Tool installation is incomplete." } +Write-Output "Installed $Tool." diff --git a/scripts/setup-android-sdk.ps1 b/scripts/setup-android-sdk.ps1 new file mode 100644 index 0000000..7d0d882 --- /dev/null +++ b/scripts/setup-android-sdk.ps1 @@ -0,0 +1,19 @@ +param([switch]$WithEmulator) +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'android-env.ps1') +foreach ($taskRequiredPath in @((Join-Path $env:JAVA_HOME 'bin/java.exe'), (Join-Path $env:ANDROID_HOME 'cmdline-tools/latest/bin/sdkmanager.bat'))) { + if (-not (Test-Path -LiteralPath $taskRequiredPath)) { throw "Missing tool: $taskRequiredPath" } +} +$taskLicenseLog = Join-Path $taskToolRoot 'android-license-review.txt' +1..100 | ForEach-Object { 'y' } | & sdkmanager.bat "--sdk_root=$env:ANDROID_HOME" --licenses *> $taskLicenseLog +if ($LASTEXITCODE -ne 0) { throw "SDK license setup failed; inspect $taskLicenseLog" } +Get-Content -LiteralPath $taskLicenseLog -Tail 3 +$taskPackages = @('platform-tools', 'platforms/android-36', 'build-tools/36.0.0', 'cmake/3.22.1', 'ndk/28.2.13676358') +if ($WithEmulator) { $taskPackages += @('emulator', 'system-images/android-36/google_apis/x86_64') } +$taskInstallLog = Join-Path $taskToolRoot 'android-sdk-install.txt' +Write-Output ('Installing Android SDK components: ' + ($taskPackages -join ', ')) +# Google's current native CLI avoids the legacy Java downloader's stalled socket. +& android.exe --no-metrics "--sdk=$env:ANDROID_HOME" sdk install @taskPackages *> $taskInstallLog +if ($LASTEXITCODE -ne 0) { Get-Content -LiteralPath $taskInstallLog -Tail 20; throw 'Android SDK installation failed.' } +Get-Content -LiteralPath $taskInstallLog -Tail 5 +Write-Output 'Android SDK components installed.' diff --git a/scripts/setup-android.ps1 b/scripts/setup-android.ps1 new file mode 100644 index 0000000..de3aa39 --- /dev/null +++ b/scripts/setup-android.ps1 @@ -0,0 +1,19 @@ +param([switch]$WithEmulator) +$ErrorActionPreference = 'Stop' +foreach ($taskToolName in @('flutter', 'java', 'android-cli')) { + & (Join-Path $PSScriptRoot 'install-tool.ps1') -Tool $taskToolName + if ($LASTEXITCODE -ne 0) { throw "Tool setup failed: $taskToolName" } +} +. (Join-Path $PSScriptRoot 'android-env.ps1') +& (Join-Path $PSScriptRoot 'setup-android-sdk.ps1') -WithEmulator:$WithEmulator +if ($WithEmulator) { & (Join-Path $PSScriptRoot 'create-android-emulator.ps1') } +& flutter.bat config --android-sdk $env:ANDROID_HOME --jdk-dir $env:JAVA_HOME --no-analytics +if ($LASTEXITCODE -ne 0) { throw 'Flutter configuration failed.' } +$taskDoctorLog = Join-Path $taskToolRoot 'flutter-doctor.txt' +& flutter.bat doctor -v *> $taskDoctorLog +$taskDoctorExit = $LASTEXITCODE +$taskDoctorText = Get-Content -Raw -LiteralPath $taskDoctorLog +Write-Output $taskDoctorText +if ($taskDoctorExit -ne 0 -or $taskDoctorText -notmatch '\[√\] Android toolchain') { + throw 'Android toolchain verification has not passed; inspect the doctor output.' +}