Conversation
统一进度条调整的数字样式.
There was a problem hiding this comment.
Pull request overview
基于 PR 描述,本次改动旨在在现有 Flutter 工程内引入/对齐 MiuiX 设计风格,并开始推进 Android 端 Compose 化(新增 Compose 页面、ViewModel/Repository 与构建配置),同时对部分 Flutter 页面交互/样式做适配。
Changes:
- Flutter 侧:引入统一 SectionLabel、调整部分页面布局/样式,并新增卡片式 push 动画路由。
- Android 侧:启用 Compose + 引入 miuix kmp 依赖,新增 Apps/Channels/Blacklist/AI/Settings 等 Compose UI 与数据层。
- 工程侧:更新 manifest、主题样式与 Gradle 配置以支持 Compose 入口与构建。
Reviewed changes
Copilot reviewed 42 out of 46 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/widgets/batch_channel_settings_sheet.dart | 使用公共 SectionLabel、调整底部弹窗交互与按钮样式 |
| lib/widgets/app_list_widgets.dart | SearchHeader 支持自定义搜索框背景色 |
| lib/routes/card_push_route.dart | 新增卡片式转场的 RouteBuilder |
| lib/pages/whitelist_page.dart | 拆分说明文本与吸顶搜索栏;进入渠道页改用卡片式转场 |
| lib/pages/settings_page.dart | 页面跳转改用卡片式转场;部分 slider 文案样式调整 |
| lib/pages/blacklist_page.dart | AppBar 改为 pinned 的 SliverAppBar |
| lib/pages/app_channels_page.dart | AppBar 改 pinned;渠道列表 UI 结构调整为卡片容器 |
| lib/pages/ai_config_page.dart | AppBar 改 pinned;超时显示样式调整 |
| lib/main.dart | MaterialApp builder 中禁用 Hero 控制器作用域 |
| compose_migration_plan.md | 新增 Compose 迁移计划文档 |
| android/settings.gradle.kts | 增加 Kotlin Compose 插件声明 |
| android/gradle.properties | 新增 Gradle/JDK 与 compileSdk 抑制相关配置 |
| android/build.gradle.kts | Windows 跨盘构建目录处理逻辑调整 |
| android/app/src/main/res/values/styles.xml | 新增 ComposeTheme(亮色) |
| android/app/src/main/res/values-night/styles.xml | 新增 ComposeTheme(夜间) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/settings/SettingsViewModel.kt | 新增 Settings Compose 侧 ViewModel(含导入导出/桌面图标) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/home/HomeViewModel.kt | 新增 Home Compose 侧状态与重启作用域逻辑 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/home/HomeUiState.kt | 新增 Home UI 状态模型 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/FaIcon.kt | 新增 FontAwesome 字体图标封装 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/blacklist/GamePresets.kt | 新增游戏包名预置列表 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/blacklist/BlacklistViewModel.kt | 新增黑名单 ViewModel(筛选/批量操作/预置) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/blacklist/BlacklistUiState.kt | 新增黑名单 UI 状态模型 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/blacklist/BlacklistScreen.kt | 新增黑名单 Compose UI(含下拉刷新/空态) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/blacklist/BlacklistRepository.kt | 新增黑名单数据读写与应用列表加载 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppsViewModel.kt | 新增 Apps Compose 侧 ViewModel(含图标后台加载) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppsUiState.kt | 新增 Apps UI 状态与 AppItem 模型 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppsScreens.kt | 新增 Apps/Channels/ChannelSettings Compose UI(含批量弹窗等) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppFiltering.kt | 新增应用筛选排序逻辑 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppChannelsViewModel.kt | 新增渠道页 ViewModel(模板/超时/扩展项) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppChannelsUiState.kt | 新增渠道 UI 状态与 extras 模型 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/app/AppAdaptationRepository.kt | 新增适配仓库(应用/图标/渠道/偏好读写) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/ai/AiConfigViewModel.kt | 新增 AI 配置 ViewModel(测试连接/日志) |
| android/app/src/main/kotlin/io/github/hyperisland/ui/ai/AiConfigState.kt | 新增 AI 配置状态模型 |
| android/app/src/main/kotlin/io/github/hyperisland/ui/ai/AiConfigScreen.kt | 新增 AI 配置 Compose UI |
| android/app/src/main/kotlin/io/github/hyperisland/ui/ai/AiConfigRepository.kt | 新增 AI 配置偏好读写 |
| android/app/src/main/kotlin/io/github/hyperisland/NotificationChannelReader.kt | 新增 Root 读取并解析通知渠道的实现 |
| android/app/src/main/kotlin/io/github/hyperisland/data/prefs/SettingsState.kt | 新增 SettingsState 数据模型 |
| android/app/src/main/kotlin/io/github/hyperisland/data/prefs/SettingsRepository.kt | 新增设置仓库(与 FlutterSharedPreferences 兼容) |
| android/app/src/main/kotlin/io/github/hyperisland/data/prefs/PrefKeys.kt | 新增偏好 key 常量集合 |
| android/app/src/main/kotlin/io/github/hyperisland/data/config/ConfigIoManager.kt | 新增配置导入导出(文件/剪贴板/Uri) |
| android/app/src/main/AndroidManifest.xml | 新增 ComposeMainActivity 入口;alias 指向 Compose;保留 Flutter MainActivity |
| android/app/build.gradle.kts | 启用 Compose、引入 Compose/Miuix 依赖并调整 SDK 配置 |
| .gitignore | 忽略 tmp_* 文件 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError | ||
| org.gradle.java.home=C:/Program Files/Eclipse Adoptium/jdk-21.0.10.7-hotspot | ||
| org.gradle.caching=true |
There was a problem hiding this comment.
org.gradle.java.home points to a developer-specific absolute path. Committing this will break builds on other machines/CI. Move this to a local (unversioned) ~/.gradle/gradle.properties or rely on JAVA_HOME / toolchains instead.
| android.useAndroidX=true | ||
| android.enableJetifier=true | ||
| android.suppressUnsupportedCompileSdk=35 |
There was a problem hiding this comment.
android.suppressUnsupportedCompileSdk is set to 35, but the app module is compiling with compileSdk = 36. If this flag is needed, it should match the compileSdk being used (or be removed if no longer needed), otherwise the intended suppression may not apply and CI may still warn/fail.
| <style name="ComposeTheme" parent="@android:style/Theme.Light.NoTitleBar"> | ||
| <item name="android:windowBackground">@android:color/white</item> | ||
| <item name="android:windowDrawsSystemBarBackgrounds">true</item> | ||
| <item name="android:statusBarColor">@android:color/transparent</item> | ||
| <item name="android:navigationBarColor">@android:color/transparent</item> | ||
| <item name="android:enforceStatusBarContrast">false</item> | ||
| <item name="android:enforceNavigationBarContrast">false</item> | ||
| </style> |
There was a problem hiding this comment.
ComposeTheme in values-night uses a Light parent theme and a white windowBackground, which will make dark mode show a light window background (and can cause a white flash on launch). Use a dark parent (e.g. Theme.Black.NoTitleBar or a dark Material theme) and a dark windowBackground for night resources.
| defaultConfig { | ||
| applicationId = "io.github.hyperisland" | ||
| minSdk = 27 | ||
| targetSdk = flutter.targetSdkVersion | ||
| minSdk = 31 | ||
| targetSdk = 36 | ||
| versionCode = flutter.versionCode |
There was a problem hiding this comment.
minSdk was raised to 31. This drops support for Android 8.1–11 devices and is a breaking distribution change. If this isn’t strictly required, consider keeping the previous minSdk (or gate Compose-only features behind API checks) and document the rationale in release notes/README.
| tasks.configureEach { | ||
| if (name.contains("AarMetadata", ignoreCase = true)) { | ||
| enabled = false | ||
| } | ||
| } |
There was a problem hiding this comment.
Disabling all tasks whose name contains AarMetadata is very broad and can mask dependency minSdk/metadata issues (and make builds harder to diagnose). Prefer fixing the underlying metadata/minSdk mismatch or narrowly disabling the specific failing task/variant with a clear comment and condition.
| SliverPadding( | ||
| padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), | ||
| sliver: SliverList( | ||
| delegate: SliverChildBuilderDelegate( | ||
| (context, index) { | ||
| final ch = channels[index]; | ||
| final isFirst = index == 0; | ||
| final isLast = index == channels.length - 1; | ||
| final channelEnabled = _isEnabled(ch.id); | ||
| final template = | ||
| _channelTemplates[ch.id] ?? kTemplateNotificationIsland; | ||
| final extras = _channelExtras[ch.id] ?? {}; | ||
|
|
||
| return _ChannelTile( | ||
| channel: ch, | ||
| channelEnabled: channelEnabled, | ||
| appEnabled: _appEnabled, | ||
| template: template, | ||
| templateLabels: _templateLabels, | ||
| renderer: | ||
| extras['renderer'] ?? kRendererImageTextWithButtons4, | ||
| rendererLabels: _rendererLabels, | ||
| importanceLabel: _importanceLabel(ch.importance, l10n), | ||
| isFirst: isFirst, | ||
| isLast: isLast, | ||
| iconMode: extras['icon'] ?? kIconModeAuto, | ||
| focusIconMode: extras['focus_icon'] ?? kIconModeAuto, | ||
| focusNotif: extras['focus'] ?? kTriOptDefault, | ||
| preserveSmallIcon: | ||
| extras['preserve_small_icon'] ?? kTriOptDefault, | ||
| showIslandIcon: | ||
| extras['show_island_icon'] ?? kTriOptDefault, | ||
| firstFloat: extras['first_float'] ?? kTriOptDefault, | ||
| enableFloat: extras['enable_float'] ?? kTriOptDefault, | ||
| islandTimeout: extras['timeout'] ?? '5', | ||
| marquee: extras['marquee'] ?? kTriOptDefault, | ||
| restoreLockscreen: | ||
| extras['restore_lockscreen'] ?? kTriOptDefault, | ||
| highlightColor: extras['highlight_color'] ?? '', | ||
| showLeftHighlight: | ||
| extras['show_left_highlight'] ?? kTriOptOff, | ||
| showRightHighlight: | ||
| extras['show_right_highlight'] ?? kTriOptOff, | ||
| onToggle: (v) => _toggle(ch.id, v), | ||
| onSettingsApplied: (s) => _applyChannelSettings(ch.id, s), | ||
| ); | ||
| }, | ||
| childCount: channels.length, | ||
| addAutomaticKeepAlives: false, | ||
| sliver: SliverToBoxAdapter( | ||
| child: Material( | ||
| color: cs.surfaceContainerHighest, | ||
| borderRadius: BorderRadius.circular(16), | ||
| clipBehavior: Clip.antiAlias, | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: List.generate(channels.length, (index) { | ||
| final ch = channels[index]; | ||
| final isLast = index == channels.length - 1; | ||
| final channelEnabled = _isEnabled(ch.id); | ||
| final template = | ||
| _channelTemplates[ch.id] ?? | ||
| kTemplateNotificationIsland; | ||
| final extras = _channelExtras[ch.id] ?? {}; | ||
|
|
||
| return _ChannelTile( | ||
| channel: ch, | ||
| channelEnabled: channelEnabled, | ||
| appEnabled: _appEnabled, | ||
| template: template, | ||
| templateLabels: _templateLabels, | ||
| renderer: | ||
| extras['renderer'] ?? | ||
| kRendererImageTextWithButtons4, | ||
| rendererLabels: _rendererLabels, | ||
| importanceLabel: _importanceLabel(ch.importance, l10n), | ||
| isLast: isLast, | ||
| iconMode: extras['icon'] ?? kIconModeAuto, | ||
| focusIconMode: extras['focus_icon'] ?? kIconModeAuto, | ||
| focusNotif: extras['focus'] ?? kTriOptDefault, | ||
| preserveSmallIcon: | ||
| extras['preserve_small_icon'] ?? kTriOptDefault, | ||
| showIslandIcon: | ||
| extras['show_island_icon'] ?? kTriOptDefault, | ||
| firstFloat: extras['first_float'] ?? kTriOptDefault, | ||
| enableFloat: extras['enable_float'] ?? kTriOptDefault, | ||
| islandTimeout: extras['timeout'] ?? '5', | ||
| marquee: extras['marquee'] ?? kTriOptDefault, | ||
| restoreLockscreen: | ||
| extras['restore_lockscreen'] ?? kTriOptDefault, | ||
| highlightColor: extras['highlight_color'] ?? '', | ||
| showLeftHighlight: | ||
| extras['show_left_highlight'] ?? kTriOptOff, | ||
| showRightHighlight: | ||
| extras['show_right_highlight'] ?? kTriOptOff, | ||
| onToggle: (v) => _toggle(ch.id, v), | ||
| onSettingsApplied: (s) => | ||
| _applyChannelSettings(ch.id, s), | ||
| ); | ||
| }), | ||
| ), |
There was a problem hiding this comment.
This replaces a SliverList with a Column(List.generate(...)) inside a SliverToBoxAdapter, which eagerly builds all channel rows and loses sliver/lazy list virtualization. This can cause jank for apps with many channels. Consider keeping a SliverList/SliverChildBuilderDelegate and styling via a wrapping card + per-row dividers instead.
| val normalizedQuery = query.trim().lowercase() | ||
| return apps | ||
| .filter { app -> | ||
| val matchSystem = | ||
| showSystemApps || !app.isSystem || alwaysVisiblePackages.contains(app.packageName) | ||
| val matchQuery = | ||
| normalizedQuery.isBlank() || | ||
| app.appName.lowercase().contains(normalizedQuery) || | ||
| app.packageName.lowercase().contains(normalizedQuery) | ||
| matchSystem && matchQuery | ||
| } | ||
| .sortedWith( | ||
| compareByDescending<AppItem> { prioritizedPackages.contains(it.packageName) } | ||
| .thenBy { it.appName.lowercase() }, | ||
| ) |
There was a problem hiding this comment.
lowercase() without an explicit locale is locale-sensitive (e.g. Turkish casing rules) and can make search/sort behavior inconsistent across devices. For deterministic filtering/sorting, use lowercase(Locale.ROOT) (and import java.util.Locale).
| ) | ||
| }.getOrNull() | ||
| } | ||
| .sortedBy { it.appName.lowercase() } | ||
| .toList() |
There was a problem hiding this comment.
This sorts apps using it.appName.lowercase() which is locale-sensitive and can produce inconsistent ordering across locales. Prefer lowercase(Locale.ROOT) (import java.util.Locale) to keep ordering stable.
| ), | ||
| verticalArrangement = Arrangement.spacedBy(12.dp), | ||
| ) { | ||
| Text("AI 增强", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) | ||
| MiuixCard(modifier = Modifier.fillMaxWidth()) { | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(18.dp)).padding(16.dp), | ||
| horizontalArrangement = Arrangement.SpaceBetween, | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) { | ||
| Text("启用 AI 摘要") | ||
| Spacer(modifier = Modifier.height(4.dp)) | ||
| Text("由 AI 生成超级岛左右文本,超时或失败时自动回退", style = MaterialTheme.typography.bodySmall) | ||
| } | ||
| MiuixSwitch( | ||
| checked = state.enabled, | ||
| onCheckedChange = { onUpdate(state.copy(enabled = it)) }, | ||
| ) | ||
| } | ||
| } | ||
| Spacer(modifier = Modifier.height(4.dp)) | ||
|
|
||
| if (state.enabled) { | ||
| Text("API 参数", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) | ||
| MiuixCard(modifier = Modifier.fillMaxWidth()) { | ||
| Column(modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(18.dp)).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { | ||
| MiuixTextField( | ||
| value = state.url, | ||
| onValueChange = { onUpdate(state.copy(url = it)) }, | ||
| label = "API 地址(必须完整)", |
There was a problem hiding this comment.
New Compose UI strings are hardcoded in code. This makes localization difficult and diverges from existing Android code that uses R.string resources (e.g. MainActivity.kt). Please move user-visible text into res/values/strings.xml and use stringResource(...) / context.getString(...) so translations can be added consistently.
# Conflicts: # lib/pages/settings_page.dart
# Conflicts: # lib/pages/app_channels_page.dart # lib/widgets/batch_channel_settings_sheet.dart
基于Compose重构软件界面, 引入miuix设计风格.