feat(infrared): expand remote library and creation flow (#13)

## Summary

- replace the crowded add-remote dialog with a full-screen three-step flow
- make the first step explicitly choose between adding from the infrared library and creating through learning
- add a full-screen remote editor with read-only library configuration, editable custom layouts, and delete confirmation
- append newly created remotes to the bottom of the management list and right side of the remote switcher
- remove the fan emoji from the device type label
- add a curated CC0 Flipper-IRDB preset subset with real NEC/NECext protocol codes
- add projector, audio/receiver, set-top box, streaming device, and lighting categories
- require a test power command and explicit device-response confirmation before creating library remotes
- include source attribution and a repeatable converter for parsed NEC/NECext signals

## Compatibility scope

- imported formats: parsed NEC and NECext only
- excluded: raw pulse data and other protocols because the ABox irCode encoding contract is not publicly specified for them
- existing devices, remotes, and scenes are not migrated or renamed

## Verification

- ./gradlew.bat :android:app:assembleDebug --no-daemon
- converter reproduced

Reviewed-on: #13
Co-authored-by: KremeCN <3265494849@qq.com>
This commit was merged in pull request #13.
This commit is contained in:
2026-08-28 09:23:06 +08:00
committed by KremeCN
parent 387ecacef5
commit ad0dd2cd5b
12 changed files with 966 additions and 280 deletions
@@ -0,0 +1,77 @@
package com.flagship.abox.manager.domain.infrared
internal data class OpenSourceIrPreset(
val option: PresetBrandOption,
val buttons: List<PresetCodeLibrary.PresetButtonTemplate>,
)
internal object OpenSourceIrCatalog {
val presets = listOf(
preset("firetv_omni_4k", "Amazon Fire TV Omni 4K", RemoteDeviceType.TV, "TVs/Amazon/FireTV_Omni_Series_4K.ir",
"POWER" to "电源" to "0x027D46B9", "HOME" to "主页" to "0x027D9F60", "MENU" to "菜单" to "0x027D45BA",
"BACK" to "返回" to "0x027D0DF2", "VOL_UP" to "音量 +" to "0x027D0CF3", "VOL_DOWN" to "音量 -" to "0x027D19E6",
"MUTE" to "静音" to "0x027D4CB3", "UP" to "上" to "0x027D48B7", "DOWN" to "下" to "0x027D4DB2",
"LEFT" to "左" to "0x027D4EB1", "RIGHT" to "右" to "0x027D49B6", "OK" to "确定" to "0x027D4AB5"),
preset("akai_ate_22y604w", "Akai ATE-22Y604W", RemoteDeviceType.TV, "TVs/Akai/AKAI_ATE_22Y604W.ir",
"POWER" to "电源" to "0x01721EE1", "MUTE" to "静音" to "0x01725CA3", "VOL_UP" to "音量 +" to "0x01720AF5",
"VOL_DOWN" to "音量 -" to "0x017206F9", "CH_UP" to "频道 +" to "0x017248B7", "CH_DOWN" to "频道 -" to "0x017244BB",
"SOURCE" to "信号源" to "0x017207F8", "OK" to "确定" to "0x01725FA0", "UP" to "上" to "0x017203FC", "DOWN" to "下" to "0x01725EA1"),
preset("acer_h6517st", "Acer H6517ST", RemoteDeviceType.PROJECTOR, "Projectors/Acer/Acer_H6517ST.ir",
"POWER" to "电源" to "0x08138778", "SOURCE" to "信号源" to "0x08138C73", "SETUP" to "设置" to "0x08133BC4",
"MODE" to "模式" to "0x0813807F", "ZOOM" to "缩放" to "0x08138B74", "MENU" to "菜单" to "0x0813847B",
"UP" to "上" to "0x0813827D", "DOWN" to "下" to "0x0813857A", "LEFT" to "左" to "0x0813837C", "RIGHT" to "右" to "0x0813817E"),
preset("nebula_capsule", "Anker Nebula Capsule", RemoteDeviceType.PROJECTOR, "Projectors/Anker/Nebula_Capsule_Mini_Projector.ir",
"POWER" to "电源" to "0x807F51AE", "OK" to "确定" to "0x807F0DF2", "UP" to "上" to "0x807F26D9",
"DOWN" to "下" to "0x807F28D7", "LEFT" to "左" to "0x807F25DA", "RIGHT" to "右" to "0x807F27D8",
"MENU" to "菜单" to "0x807F49B6", "BACK" to "返回" to "0x807F1BE4", "HOME" to "主页" to "0x807F53AC"),
preset("bose_solo_5", "Bose Solo 5", RemoteDeviceType.AUDIO, "SoundBars/Bose/Bose_Solo_5.ir",
"POWER" to "电源" to "0xBAA04CB3", "MUTE" to "静音" to "0xBAA001FE", "VOL_UP" to "音量 +" to "0xBAA003FC",
"VOL_DOWN" to "音量 -" to "0xBAA002FD", "PLAY_PAUSE" to "播放/暂停" to "0xBAA055AA", "NEXT" to "下一曲" to "0xBAA05AA5",
"PREVIOUS" to "上一曲" to "0xBAA059A6", "BLUETOOTH" to "蓝牙" to "0xBAA0817E", "BASS" to "低音" to "0xBAA0827D"),
preset("fosi_zd3", "Fosi Audio ZD3", RemoteDeviceType.AUDIO, "Audio_and_Video_Receivers/Fosi_Audio/Fosi_Audio_ZD3.ir",
"POWER" to "电源" to "0xFA0105FA", "MUTE" to "静音" to "0xFA0102FD", "VOL_UP" to "音量 +" to "0xFA0103FC",
"VOL_DOWN" to "音量 -" to "0xFA0107F8", "PLAY_PAUSE" to "播放/暂停" to "0xFA010AF5", "NEXT" to "下一曲" to "0xFA0106F9",
"PREVIOUS" to "上一曲" to "0xFA0104FB", "OPTICAL" to "光纤" to "0xFA0108F7", "HDMI" to "HDMI" to "0xFA0113EC", "BLUETOOTH" to "蓝牙" to "0xFA0115EA"),
preset("humax_generic", "Humax Cable Box", RemoteDeviceType.SET_TOP_BOX, "Cable_Boxes/Humax/Humax_generic_cable_box.ir",
"POWER" to "电源" to "0x001000FF", "TV_RADIO" to "电视/广播" to "0x00100DF2", "SOURCE" to "信号源" to "0x001002FD",
"VOL_UP" to "音量 +" to "0x00101FE0", "VOL_DOWN" to "音量 -" to "0x001040BF", "CH_UP" to "频道 +" to "0x001010EF", "CH_DOWN" to "频道 -" to "0x00100FF0"),
preset("huawei_dn300", "Huawei DN300", RemoteDeviceType.SET_TOP_BOX, "Cable_Boxes/Huawei/Huawei_dn300_mgts(mts).ir",
"POWER" to "电源" to "0x00BF43BC", "VOL_UP" to "音量 +" to "0x00BF52AD", "VOL_DOWN" to "音量 -" to "0x00BF53AC",
"CH_UP" to "频道 +" to "0x00BF4BB4", "CH_DOWN" to "频道 -" to "0x00BF4CB3", "UP" to "上" to "0x00BF15EA",
"DOWN" to "下" to "0x00BF16E9", "LEFT" to "左" to "0x00BF17E8", "RIGHT" to "右" to "0x00BF18E7", "BACK" to "返回" to "0x00BF51AE", "OK" to "确定" to "0x00BF19E6"),
preset("firetv_stick_4k_max", "Amazon Fire TV Stick 4K Max", RemoteDeviceType.STREAMING_DEVICE, "Streaming_Devices/Amazon/FireTV_Stick_4K_Max.ir",
"POWER" to "电源" to "0x03FC1DE2", "MUTE" to "静音" to "0x007F4EB1", "VOL_UP" to "音量 +" to "0x03FC11EE", "VOL_DOWN" to "音量 -" to "0x03FC15EA"),
preset("apple_tv_4k_gen2", "Apple TV 4K Gen 2", RemoteDeviceType.STREAMING_DEVICE, "Streaming_Devices/Apple/Apple_TV_4K_Gen2.ir",
"POWER_OFF" to "关机" to "0xE5872AFE", "POWER_ON" to "开机" to "0xE5872CFE", "TV" to "电视" to "0xE58740FE",
"OK" to "确定" to "0xEE875C8C", "UP" to "上" to "0xEE870A8C", "DOWN" to "下" to "0xEE870C8C",
"LEFT" to "左" to "0xEE87098C", "RIGHT" to "右" to "0xEE87068C", "BACK" to "返回" to "0xEE87038C", "PLAY_PAUSE" to "播放/暂停" to "0xEE875F8C"),
preset("atomberg_fan", "Atomberg Fan", RemoteDeviceType.FAN, "Fans/Atomberg/Atomberg_fan.ir",
"POWER" to "电源" to "0x00F3916E", "SPEED_1" to "风速 1" to "0x00F38B74", "SPEED_2" to "风速 2" to "0x00F3906F",
"SPEED_3" to "风速 3" to "0x00F38A75", "SPEED_4" to "风速 4" to "0x00F3936C", "SPEED_5" to "风速 5" to "0x00F38877",
"BOOST" to "强劲" to "0x00F38F70", "TIMER" to "定时" to "0x00F39669", "SLEEP" to "睡眠" to "0x00F38E71"),
preset("daewoo_ddv200", "Daewoo DDV200", RemoteDeviceType.FAN, "Fans/Daewoo/Daewoo_DDV200.ir",
"POWER" to "电源" to "0x00FF45BA", "SPEED" to "风速" to "0x00FF47B8", "WIND" to "风类" to "0x00FF40BF",
"TIMER" to "定时" to "0x00FF19E6", "OSCILLATE" to "摇头" to "0x00FF1CE3"),
preset("adj_uc_ir", "ADJ UC-IR Lighting", RemoteDeviceType.LIGHT, "LED_Lighting/ADJ/ADJ_UC-IR.ir",
"POWER" to "电源" to "0x00FF47B8", "BRIGHTER" to "调亮" to "0x00FF43BC", "DIMMER" to "调暗" to "0x00FF09F6",
"FULL_ON" to "全亮" to "0x00FF44BB", "FADE" to "渐变" to "0x00FF40BF", "STROBE" to "频闪" to "0x00FF07F8", "COLOR" to "颜色" to "0x00FF15EA"),
preset("antizer_candles", "Antizer Flameless Candles", RemoteDeviceType.LIGHT, "LED_Lighting/Antizer_Flameless_Candles/Antizer_Flameless_Candles.ir",
"ON" to "开启" to "0x08B700FF", "OFF" to "关闭" to "0x08B702FD", "DIMMER" to "调暗" to "0x08B710EF",
"BRIGHTER" to "调亮" to "0x08B712ED", "CANDLE" to "烛光模式" to "0x08B70CF3", "LIGHT" to "常亮模式" to "0x08B70EF1"),
)
fun preset(id: String?): OpenSourceIrPreset? = presets.firstOrNull { it.option.id == id }
private fun preset(
id: String,
name: String,
type: RemoteDeviceType,
sourcePath: String,
vararg buttons: Pair<Pair<String, String>, String>,
) = OpenSourceIrPreset(
PresetBrandOption(id, name, type, sourcePath),
buttons.map { (identity, code) -> PresetCodeLibrary.PresetButtonTemplate(identity.first, identity.second, code) },
)
private infix fun Pair<String, String>.to(code: String): Pair<Pair<String, String>, String> = Pair(this, code)
}
@@ -4,30 +4,21 @@ data class PresetBrandOption(
val id: String,
val displayName: String,
val deviceType: RemoteDeviceType,
val sourcePath: String? = null,
)
object PresetCodeLibrary {
val availablePresets: List<PresetBrandOption> = listOf(
// 电视预设
PresetBrandOption("generic_tv", "通用电视 (Generic TV)", RemoteDeviceType.TV),
PresetBrandOption("xiaomi_tv", "小米电视 (Xiaomi TV)", RemoteDeviceType.TV),
PresetBrandOption("sony_tv", "索尼电视 (Sony TV)", RemoteDeviceType.TV),
PresetBrandOption("samsung_tv", "三星电视 (Samsung TV)", RemoteDeviceType.TV),
// 空调预设
PresetBrandOption("gree_ac", "格力空调 (Gree AC)", RemoteDeviceType.AIR_CONDITIONER),
PresetBrandOption("midea_ac", "美的空调 (Midea AC)", RemoteDeviceType.AIR_CONDITIONER),
PresetBrandOption("haier_ac", "海尔空调 (Haier AC)", RemoteDeviceType.AIR_CONDITIONER),
PresetBrandOption("aux_ac", "奥克斯空调 (AUX AC)", RemoteDeviceType.AIR_CONDITIONER),
// 风扇预设
PresetBrandOption("midea_fan", "美的电风扇 (Midea Fan)", RemoteDeviceType.FAN),
PresetBrandOption("generic_fan", "通用风扇 (Generic Fan)", RemoteDeviceType.FAN),
// 自定义
PresetBrandOption("custom_blank", "自定义遥控器 (Custom)", RemoteDeviceType.CUSTOM),
)
) + OpenSourceIrCatalog.presets.map { it.option }
data class PresetButtonTemplate(
val buttonId: String,
@@ -39,8 +30,8 @@ object PresetCodeLibrary {
type: RemoteDeviceType,
presetId: String? = null,
layoutStyle: PanelLayoutStyle? = null,
): List<PresetButtonTemplate> = when (type) {
RemoteDeviceType.TV -> getTvTemplates(hasHexCode = true)
): List<PresetButtonTemplate> = OpenSourceIrCatalog.preset(presetId)?.buttons ?: when (type) {
RemoteDeviceType.TV -> emptyList()
RemoteDeviceType.AIR_CONDITIONER -> listOf(
PresetButtonTemplate("AC_POWER", "电源"),
PresetButtonTemplate("AC_TEMP_UP", "升温"),
@@ -51,13 +42,12 @@ object PresetCodeLibrary {
PresetButtonTemplate("AC_SWING_H", "左右扫风"),
PresetButtonTemplate("AC_ECO", "节能"),
)
RemoteDeviceType.FAN -> listOf(
PresetButtonTemplate("FAN_POWER", "电源", "0x01FE02FD"),
PresetButtonTemplate("FAN_SPEED", "风速", "0x01FE04FB"),
PresetButtonTemplate("FAN_SWING", "摇头", "0x01FE06F9"),
PresetButtonTemplate("FAN_TIMER", "定时", "0x01FE08F7"),
PresetButtonTemplate("FAN_MODE", "模式", "0x01FE0AF5"),
)
RemoteDeviceType.FAN,
RemoteDeviceType.PROJECTOR,
RemoteDeviceType.AUDIO,
RemoteDeviceType.SET_TOP_BOX,
RemoteDeviceType.STREAMING_DEVICE,
RemoteDeviceType.LIGHT -> emptyList()
RemoteDeviceType.CUSTOM -> when (layoutStyle) {
PanelLayoutStyle.TV_NAV -> getTvTemplates(hasHexCode = false)
PanelLayoutStyle.MEDIA_PLAYER -> getMediaPlayerTemplates(hasHexCode = false)
@@ -0,0 +1,31 @@
package com.flagship.abox.manager.domain.infrared
import com.flagship.abox.manager.domain.DeviceName
import com.flagship.abox.manager.domain.DomainError
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.InfraredCode
import com.flagship.abox.manager.domain.InfraredGateway
import com.flagship.abox.manager.domain.ProviderProfileId
class TestPresetInfraredCode(
private val gateway: InfraredGateway,
) {
suspend operator fun invoke(
profileId: ProviderProfileId,
deviceName: DeviceName,
presetId: String,
): DomainResult<Unit> {
val preset = OpenSourceIrCatalog.preset(presetId)
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Preset is not testable"))
val testButton = preset.buttons.firstOrNull { button ->
button.buttonId.contains("POWER") || button.buttonId == "ON" || button.buttonId == "OFF"
} ?: return DomainResult.Failure(DomainError.NoData(detail = "Preset has no test button"))
val code = testButton.defaultHexCode
?: return DomainResult.Failure(DomainError.NoData(detail = "Preset test code is unavailable"))
val scratchSlot = VirtualRemote.DEFAULT_DYNAMIC_SCRATCH_SLOT
return when (val download = gateway.downloadCodes(profileId, listOf(InfraredCode(deviceName, scratchSlot, code)))) {
is DomainResult.Failure -> download
is DomainResult.Success -> gateway.sendKey(profileId, deviceName, scratchSlot)
}
}
}
@@ -10,6 +10,11 @@ enum class RemoteDeviceType {
TV,
AIR_CONDITIONER,
FAN,
PROJECTOR,
AUDIO,
SET_TOP_BOX,
STREAMING_DEVICE,
LIGHT,
CUSTOM,
}
@@ -70,7 +75,12 @@ data class VirtualRemote(
fun defaultLayoutFor(type: RemoteDeviceType): PanelLayoutStyle = when (type) {
RemoteDeviceType.TV -> PanelLayoutStyle.TV_NAV
RemoteDeviceType.AIR_CONDITIONER -> PanelLayoutStyle.AC_COMPACT
RemoteDeviceType.FAN -> PanelLayoutStyle.GRID
RemoteDeviceType.FAN,
RemoteDeviceType.PROJECTOR,
RemoteDeviceType.AUDIO,
RemoteDeviceType.SET_TOP_BOX,
RemoteDeviceType.STREAMING_DEVICE,
RemoteDeviceType.LIGHT,
RemoteDeviceType.CUSTOM -> PanelLayoutStyle.GRID
}
}