Skip to content

Commit abeb548

Browse files
committed
feat: surface per-app accent discovery
1 parent f9af997 commit abeb548

19 files changed

Lines changed: 562 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,52 @@
22

33
All SwiftFloris release history is consolidated here. This replaces the former root-level `RELEASE_NOTES_v*.md` file-per-release pattern.
44

5+
<a id="v1.8.210"></a>
6+
## v1.8.210
7+
8+
Released: 2026-06-04
9+
10+
### Per-app accent discovery and preview
11+
12+
F6 makes the existing opt-in per-app accent feature visible without changing its privacy posture. Settings -> Theme now includes an illustrative preview that compares the active theme accent with app-tinted samples for Slack, WhatsApp, Discord, and Telegram. The preview is static: runtime tinting still computes locally from the focused editor app's launcher icon.
13+
14+
The Smartbar can now show a one-time opt-in hint after SwiftFloris sees three distinct editor packages in the current IME process. Package names are not persisted for the hint; SwiftFloris stores only the discovery state (`COLLECTING`, `READY`, or `DISMISSED`). Enabling the feature or tapping Skip dismisses the hint permanently.
15+
16+
### Changes
17+
18+
- **`PerAppAccentPreview.kt`** (new) - compact Theme settings preview with selectable app-color samples and side-by-side keyboard illustrations.
19+
- **`PerAppAccentDiscoveryHint.kt`** (new, theme policy) - process-local three-app threshold tracker plus persisted hint-state enum.
20+
- **`PerAppAccentController.kt`** - observes editor package focus, promotes the hint state asynchronously, and suppresses future hints once per-app accent is enabled.
21+
- **`PerAppAccentDiscoveryHint.kt`** (new, Smartbar UI) / **`Smartbar.kt`** - renders the one-time enable/skip hint in the suggestion slot when the hint reaches `READY`.
22+
- **`ThemeScreen.kt` / `SettingsSearchIndex.kt`** - surfaces the preview, resource-backed switch copy, and searchable per-app accent entry.
23+
- **`PRIVACY_AND_AI.md`** - documents that discovery tracking does not persist package names.
24+
25+
### Verification
26+
27+
- `./gradlew.bat :app:compileDebugKotlin :app:compileDebugUnitTestKotlin` -> green with `JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot`.
28+
- `./gradlew.bat :app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug` -> green with `JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot`.
29+
- `./gradlew.bat :app:verifyRoborazziDebug` -> green with `JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot`.
30+
- `git diff --check` -> green; CRLF normalization warnings only.
31+
- `bash scripts/check-fastlane-metadata.sh` -> green for versionCode 2010.
32+
- `bash scripts/check-repo-hygiene.sh` -> green.
33+
34+
### Files Touched
35+
36+
- `app/src/main/kotlin/dev/patrickgold/florisboard/app/FlorisPreferenceModelImpl.kt`
37+
- `app/src/main/kotlin/dev/patrickgold/florisboard/app/prefs/ThemePrefs.kt`
38+
- `app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/search/SettingsSearchIndex.kt`
39+
- `app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/theme/ThemeScreen.kt`
40+
- `app/src/main/kotlin/dev/patrickgold/florisboard/ime/smartbar/PerAppAccentDiscoveryHint.kt` (new)
41+
- `app/src/main/kotlin/dev/patrickgold/florisboard/ime/smartbar/Smartbar.kt`
42+
- `app/src/main/kotlin/dev/patrickgold/florisboard/ime/theme/PerAppAccentController.kt`
43+
- `app/src/main/kotlin/dev/patrickgold/florisboard/ime/theme/PerAppAccentDiscoveryHint.kt` (new)
44+
- `app/src/main/kotlin/dev/patrickgold/florisboard/ime/theme/PerAppAccentPreview.kt` (new)
45+
- `app/src/main/res/values/strings.xml`
46+
- `app/src/test/kotlin/dev/patrickgold/florisboard/app/AppPrefsPartitionTest.kt`
47+
- `app/src/test/kotlin/dev/patrickgold/florisboard/ime/theme/PerAppAccentDiscoveryHintTrackerTest.kt` (new)
48+
- `docs/PRIVACY_AND_AI.md`
49+
- `fastlane/metadata/android/en-US/changelogs/2010.txt` (new)
50+
551
<a id="v1.8.209"></a>
652
## v1.8.209
753

COMPLETED.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Consolidated from the archived open-work checklist (closed items). Full per-rele
4848
- [x] EI7 (P1, reframed) — Voice input empty state now explains FUTO as a separate offline Android voice keyboard and keeps the F-Droid install action visible without migration-window framing. Shipped v1.8.207. — *Source: TODO_2026-06-03.md*
4949
- [x] F3 (P0, reframed) — First-run "Import your dictionary" setup hint for post-cutoff local keyboard exports, routing SwiftKey JSON, Gboard XML/ZIP, FlorisBoard CSV/.flbackup/.fldic, and SwiftFloris exports into the existing personal-dictionary importer. Shipped v1.8.208. — *Source: TODO_2026-06-03.md*
5050
- [x] EI3 (P2) — Personal dictionary bulk-import preview for modular imports, with first-50-row review, row exclusion before commit, excluded-row summary counts, and persisted skip-preview opt-out. Shipped v1.8.209. — *Source: TODO_2026-06-03.md*
51+
- [x] F6 (P2) — Per-app accent opt-in discovery hint and Settings preview, with process-local three-app threshold tracking, persisted hint state only, Settings search coverage, and privacy docs. Shipped v1.8.210. — *Source: TODO_2026-06-03.md*
5152
- [x] F1, F2, F15, F16, F17, F19, F20, F25, F26, F32, F34, F35, F36, F41, F42, EI8, EI11, EI4 (doc) — Closed across v1.8.174 -> v1.8.187. — *Source: TODO_2026-06-03.md*
5253
- [x] IMPROVEMENT_PLAN Workstreams 1, 3, 4, 5, 6 complete; Workstream 2 (lint) monotonically decreasing. — *Source: IMPROVEMENT_PLAN_2026-05-18.md*
5354

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SwiftFloris
22

3-
![Version](https://img.shields.io/badge/version-v1.8.209-blue) ![License](https://img.shields.io/badge/license-Apache%202.0-green) ![Platform](https://img.shields.io/badge/platform-Android%208.0+-orange) ![Network](https://img.shields.io/badge/network-none-lightgrey) ![Dictionary imports](https://img.shields.io/badge/dictionary%20imports-local%20files-green)
3+
![Version](https://img.shields.io/badge/version-v1.8.210-blue) ![License](https://img.shields.io/badge/license-Apache%202.0-green) ![Platform](https://img.shields.io/badge/platform-Android%208.0+-orange) ![Network](https://img.shields.io/badge/network-none-lightgrey) ![Dictionary imports](https://img.shields.io/badge/dictionary%20imports-local%20files-green)
44

55
**SwiftFloris** is a privacy-first Android keyboard, forked from FlorisBoard and pushed toward SwiftKey-class multilingual typing without the cloud. It ships under Apache-2.0, holds no `INTERNET` permission, and binds zero accounts.
66

@@ -37,7 +37,7 @@
3737
3838
## Highlights
3939

40-
| Area | What's in v1.8.209 | Privacy posture |
40+
| Area | What's in v1.8.210 | Privacy posture |
4141
|------|-------------------|-----------------|
4242
| **Autocorrect / prediction** | SCOWL 117k English dictionary, SymSpell d1+d2, bigram + trigram next-word, capitalization-aware completions, contraction handling, instant-remember user-dictionary overlay | On-device |
4343
| **Multilingual typing** | Bilingual subtype presets (EN+ES / EN+FR / EN+DE), per-token Latin language identification, top-two straddle guard, sentence-local context scoring, and opt-in remembered keyboard language per app | On-device |
@@ -47,7 +47,7 @@
4747
| **Emoji & stickers** | Emoji search/history/pinned groups with an in-keyboard pin-to-group sheet, bundled local sticker packs, and user-imported SAF sticker folders for PNG / WebP / JPEG / GIF files | Local folder URI only |
4848
| **Clipboard** | Room-backed history with pinning + per-app source tag, media/provider metadata, sensitive-item gates, and startup/restore reconciliation | On-device |
4949
| **Productivity** | Calendar quick-insert reads local agenda entries for today + next 7 days; task quick-insert sends selected text to user-chosen task / note apps | Calendar permission is explicit opt-in; no network |
50-
| **Themes** | 21 bundled themes — SwiftKey Pure (Light/Dark + M3 Expressive), SwiftKey High Contrast (AAA), Aurora Animated, Floris Day/Night, Swift Glacier, Swift Slate, M3E Nord (light + dark), Tokyo Night, Dracula, Catppuccin Mocha; borderless variants where applicable; Snygg theme engine; per-app accent | No telemetry |
50+
| **Themes** | 21 bundled themes — SwiftKey Pure (Light/Dark + M3 Expressive), SwiftKey High Contrast (AAA), Aurora Animated, Floris Day/Night, Swift Glacier, Swift Slate, M3E Nord (light + dark), Tokyo Night, Dracula, Catppuccin Mocha; borderless variants where applicable; Snygg theme engine; per-app accent with Settings preview and one-time opt-in hint | No telemetry |
5151
| **MCP daemon bridge** | AIDL bridge to user-installed MCP daemons with per-daemon enable / disable in Settings → MCP daemon bridge | Local-only binder, no network |
5252
| **Addon packs** | Addon manifest/enumerator contracts, IME-startup registry reconciliation, Settings -> Addons status/rescan, trust reset/changed-certificate controls, dictionary-pack catalog details, persisted signing-certificate pins, descriptor validation, provenance reports, typed dictionary-pack catalog, and addon APK dictionary asset mounting | No-network addon rejection |
5353
| **Settings UX** | Clearer empty states for voice setup, selected user-dictionary languages, extension categories, language packs, filtered clipboard history, and theme-manager recovery; surfaced keyboard preview field with ready/active state feedback | Local UI only |
@@ -442,7 +442,7 @@ limitations under the License.
442442

443443
## Status
444444

445-
🚀 **Active development.** Current release: **v1.8.209** (2026-06-04). The SwiftKey account export window closed on **2026-05-31**; local/on-device migration paths remain documented above.
445+
🚀 **Active development.** Current release: **v1.8.210** (2026-06-04). The SwiftKey account export window closed on **2026-05-31**; local/on-device migration paths remain documented above.
446446

447447
---
448448

ROADMAP.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Single source of truth for all planned work. Items above the --- are existing plans; items below are research conducted 2026-06-03.
44
5-
**Current release:** v1.8.209 (versionCode 2009). **Baseline green:** `:app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug`.
5+
**Current release:** v1.8.210 (versionCode 2010). **Baseline green:** `:app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug`.
66

77
Hard rules still apply (see `AGENTS.md`): no `INTERNET` permission in `:app`; Apache-2.0 ceiling on `:app`; no closed-source blobs; one logical change per commit; every shipped release bumps `gradle.properties` version, writes a `CHANGELOG.md` section, and adds a `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` (draft <=480 chars for headroom).
88

@@ -12,11 +12,6 @@ Item IDs trace to their origin research: `F#`/`EI#` from the archived 2026-05-25
1212

1313
### Settings & UX surfaces
1414

15-
- [ ] P2 — Per-app accent opt-in discovery hint + preview (F6)
16-
- Why: Per-app accent feature is undiscoverable.
17-
- Touches: single-fire discovery hint + Settings preview.
18-
- Acceptance: hint fires once; Settings shows a preview.
19-
- Source: TODO.md A3 / research feature plan F6.
2015
- [ ] P3 — Settings home cosmetic re-bucket (EI2)
2116
- Why: Settings home is already grouped into four labelled sections (Essentials / Experience / Data / System); the research's "15 sub-screens at one level" premise is stale. Only residual value is a cosmetic re-bucket into the 5 research groups.
2217
- Touches: `HomeScreen.kt` grouping.

app/src/main/kotlin/dev/patrickgold/florisboard/app/FlorisPreferenceModelImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ class FlorisPreferenceModelImpl : FlorisPreferenceModel() {
212212
theme.nightThemeId to "theme.nightThemeId",
213213
theme.accentColor to "theme.accentColor",
214214
theme.perAppAccentEnabled to "theme.perAppAccentEnabled",
215+
theme.perAppAccentDiscoveryHintState to "theme.perAppAccentDiscoveryHintState",
215216
theme.sunriseTime to "theme.sunriseTime",
216217
theme.sunsetTime to "theme.sunsetTime",
217218
theme.editorColorRepresentation to "theme.editorColorRepresentation",

app/src/main/kotlin/dev/patrickgold/florisboard/app/prefs/ThemePrefs.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
5353
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
5454
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
5555
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
56+
import dev.patrickgold.florisboard.ime.theme.PerAppAccentDiscoveryHintState
5657
import dev.patrickgold.florisboard.ime.theme.ThemeMode
5758
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
5859
import dev.patrickgold.florisboard.ime.voice.VoiceCommandCustomCommands
@@ -100,6 +101,10 @@ open class ThemePrefs : PreferenceModel() {
100101
key = "theme__per_app_accent_enabled",
101102
default = false,
102103
)
104+
val perAppAccentDiscoveryHintState = enum(
105+
key = "theme__per_app_accent_discovery_hint_state",
106+
default = PerAppAccentDiscoveryHintState.COLLECTING,
107+
)
103108
val sunriseTime = localTime(
104109
key = "theme__sunrise_time",
105110
default = LocalTime(6, 0),

app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/search/SettingsSearchIndex.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ object SettingsSearchIndex {
156156
entry("theme.day", R.string.settings__theme__title, R.string.pref__theme__day, destination = SettingsSearchDestination.THEME),
157157
entry("theme.night", R.string.settings__theme__title, R.string.pref__theme__night, destination = SettingsSearchDestination.THEME),
158158
entry("theme.accent", R.string.settings__theme__title, R.string.pref__theme__theme_accent_color__label, destination = SettingsSearchDestination.THEME),
159+
entry("theme.per-app-accent", R.string.settings__theme__title, R.string.pref__theme__per_app_accent_enabled__label, R.string.pref__theme__per_app_accent_enabled__summary, SettingsSearchDestination.THEME),
159160
entry("theme.customization", R.string.settings__theme__title, R.string.pref__theme__customization__label, R.string.pref__theme__customization__summary, SettingsSearchDestination.THEME),
160161
entry("theme.manager", R.string.ext__list__ext_theme, R.string.settings__theme_manager__title_manage, destination = SettingsSearchDestination.THEME_MANAGER, keywords = "installed themes"),
161162

app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/theme/ThemeScreen.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.LightMode
2525
import androidx.compose.material.icons.filled.Tune
2626
import androidx.compose.material.icons.filled.WbTwilight
2727
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.LaunchedEffect
2829
import androidx.compose.runtime.collectAsState
2930
import androidx.compose.runtime.getValue
3031
import androidx.compose.ui.graphics.Color
@@ -34,6 +35,8 @@ import dev.patrickgold.florisboard.app.LocalNavController
3435
import dev.patrickgold.florisboard.app.Routes
3536
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
3637
import dev.patrickgold.florisboard.app.ext.ExtensionListScreenType
38+
import dev.patrickgold.florisboard.ime.theme.PerAppAccentDiscoveryHintState
39+
import dev.patrickgold.florisboard.ime.theme.PerAppAccentPreview
3740
import dev.patrickgold.florisboard.ime.theme.ThemeManager
3841
import dev.patrickgold.florisboard.ime.theme.ThemeMode
3942
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
@@ -69,6 +72,15 @@ fun ThemeScreen() = FlorisScreen {
6972
content {
7073
val dayThemeId by prefs.theme.dayThemeId.collectAsState()
7174
val nightThemeId by prefs.theme.nightThemeId.collectAsState()
75+
val perAppAccentEnabled by prefs.theme.perAppAccentEnabled.collectAsState()
76+
77+
LaunchedEffect(perAppAccentEnabled) {
78+
if (perAppAccentEnabled &&
79+
prefs.theme.perAppAccentDiscoveryHintState.get() != PerAppAccentDiscoveryHintState.DISMISSED
80+
) {
81+
prefs.theme.perAppAccentDiscoveryHintState.set(PerAppAccentDiscoveryHintState.DISMISSED)
82+
}
83+
}
7284

7385
PreferenceGroup(title = stringRes(R.string.pref__theme__group_schedule__label)) {
7486
ListPreference(
@@ -145,11 +157,12 @@ fun ThemeScreen() = FlorisScreen {
145157
// and fall through to the active Snygg theme's `--primary` token
146158
// when the toggle is off (or when the resolver finds no usable
147159
// accent in the active editor's app icon).
160+
PerAppAccentPreview()
148161
SwitchPreference(
149162
pref = prefs.theme.perAppAccentEnabled,
150163
icon = Icons.Default.ColorLens,
151-
title = "Tint to active app's icon",
152-
summary = "When typing in an app, tint the keyboard accent to match the dominant color of that app's launcher icon. All on-device — no extra permissions, no network.",
164+
title = stringRes(R.string.pref__theme__per_app_accent_enabled__label),
165+
summary = stringRes(R.string.pref__theme__per_app_accent_enabled__summary),
153166
)
154167
}
155168
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (C) 2026 SwiftFloris Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dev.patrickgold.florisboard.ime.smartbar
18+
19+
import androidx.compose.foundation.clickable
20+
import androidx.compose.foundation.layout.Arrangement
21+
import androidx.compose.foundation.layout.fillMaxSize
22+
import androidx.compose.foundation.layout.padding
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.rememberCoroutineScope
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.semantics.Role
28+
import androidx.compose.ui.unit.dp
29+
import dev.patrickgold.florisboard.R
30+
import dev.patrickgold.florisboard.app.FlorisPreferenceStore
31+
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
32+
import dev.patrickgold.florisboard.ime.theme.PerAppAccentDiscoveryHintState
33+
import kotlinx.coroutines.launch
34+
import org.florisboard.lib.compose.stringRes
35+
import org.florisboard.lib.snygg.ui.SnyggRow
36+
import org.florisboard.lib.snygg.ui.SnyggText
37+
38+
@Composable
39+
fun PerAppAccentDiscoveryHint(modifier: Modifier = Modifier) {
40+
val prefs by FlorisPreferenceStore
41+
val scope = rememberCoroutineScope()
42+
43+
fun dismiss() {
44+
scope.launch {
45+
prefs.theme.perAppAccentDiscoveryHintState.set(PerAppAccentDiscoveryHintState.DISMISSED)
46+
}
47+
}
48+
49+
SnyggRow(
50+
elementName = FlorisImeUi.SmartbarCandidatesRow.elementName,
51+
modifier = modifier
52+
.fillMaxSize()
53+
.padding(horizontal = 8.dp),
54+
horizontalArrangement = Arrangement.spacedBy(10.dp),
55+
verticalAlignment = Alignment.CenterVertically,
56+
) {
57+
SnyggText(
58+
elementName = FlorisImeUi.SmartbarCandidateWordText.elementName,
59+
modifier = Modifier.weight(1f),
60+
text = stringRes(R.string.ime__per_app_accent_hint__message),
61+
)
62+
SnyggText(
63+
elementName = FlorisImeUi.SmartbarCandidateWordText.elementName,
64+
modifier = Modifier
65+
.clickable(role = Role.Button) {
66+
scope.launch {
67+
prefs.theme.perAppAccentEnabled.set(true)
68+
prefs.theme.perAppAccentDiscoveryHintState.set(PerAppAccentDiscoveryHintState.DISMISSED)
69+
}
70+
}
71+
.padding(8.dp),
72+
text = stringRes(R.string.ime__per_app_accent_hint__enable),
73+
)
74+
SnyggText(
75+
elementName = FlorisImeUi.SmartbarCandidateWordSecondaryText.elementName,
76+
modifier = Modifier
77+
.clickable(role = Role.Button, onClick = ::dismiss)
78+
.padding(8.dp),
79+
text = stringRes(R.string.ime__per_app_accent_hint__dismiss),
80+
)
81+
}
82+
}

0 commit comments

Comments
 (0)