docs: record approved app direction and Android setup scripts

This commit is contained in:
Codex
2026-10-12 22:30:13 +08:00
commit a223798c25
9 changed files with 273 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.tools/
.dart_tool/
.flutter-plugins-dependencies
build/
android/local.properties
*.jks
*.keystore
*.log
+3
View File
@@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".tools/flutter"
}
+47
View File
@@ -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)。聊天中的方案已确认,完整设计文件待审核。学校真实课表解析需要在用户本人认证后的页面上验证。
@@ -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。未完成的学校联调以明确的状态和后续步骤记录,不包装为已经可用。
+21
View File
@@ -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 ';'
+14
View File
@@ -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)."
+69
View File
@@ -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."
+19
View File
@@ -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.'
+19
View File
@@ -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.'
}