Compare commits
11 Commits
release/0.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e2b361ac3d | |||
|
|
5562d49546 | ||
|
|
5891c53cf6 | ||
|
|
5466d00037 | ||
|
|
8a06d764bb | ||
|
|
a800c7b230 | ||
|
|
67e99aeca3 | ||
|
|
ac14d92192 | ||
|
|
3392f8f212 | ||
|
|
8c7cafad61 | ||
|
|
721b25c349 |
39
Android.bp
Normal file
39
Android.bp
Normal file
@@ -0,0 +1,39 @@
|
||||
android_app {
|
||||
name: "FlorisBoard",
|
||||
srcs: [
|
||||
"app/src/main/kotlin/**/*.kt",
|
||||
"app/src/main/java/**/*.java",
|
||||
],
|
||||
resource_dirs: ["app/src/main/res"],
|
||||
manifest: "app/src/main/AndroidManifest.xml",
|
||||
certificate: "platform",
|
||||
system_ext_specific: true,
|
||||
platform_apis: true,
|
||||
optimize: {
|
||||
enabled: true,
|
||||
},
|
||||
overrides: ["FlorisBoard"],
|
||||
static_libs: [
|
||||
"androidx.core_core",
|
||||
"androidx.emoji2_emoji2",
|
||||
"androidx.emoji2_emoji2-views",
|
||||
"androidx.startup_startup",
|
||||
"androidx.appcompat_appcompat",
|
||||
"androidx.preference_preference",
|
||||
"androidx.recyclerview_recyclerview",
|
||||
"androidx.constraintlayout_constraintlayout",
|
||||
],
|
||||
required: ["android.permission.VIBRATE"],
|
||||
optional: ["android.permission.POST_NOTIFICATIONS"],
|
||||
allow_backup: true,
|
||||
backup_config: "res/xml/backup_rules.xml",
|
||||
full_backup_content: "res/xml/backup_rules.xml",
|
||||
enable_on_back_invoked_callback: true,
|
||||
profileable: true,
|
||||
package: "dev.patrickgold.florisboard",
|
||||
privileged: false,
|
||||
dex_preopt: {
|
||||
enabled: false,
|
||||
},
|
||||
asset_dirs: ["app/src/main/assets"],
|
||||
}
|
||||
@@ -274,6 +274,12 @@
|
||||
"authors": [ "patrickgold", "Hayleia" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "korean_phonetic",
|
||||
"label": "South Korean Phonetic",
|
||||
"authors": [ "Shunnuo" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "kurdish",
|
||||
"label": "کوردی (قوەرتی نوێ)",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
[
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 12615, "label": "ㅇ"},
|
||||
{ "$": "auto_text_key", "code": 12641, "label": "ㅡ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12628, "label": "ㅔ" },
|
||||
"upper": { "code": 12630, "label": "ㅖ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12601, "label": "ㄹ"},
|
||||
{ "$": "auto_text_key", "code": 12620, "label": "ㅌ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12624, "label": "ㅐ" },
|
||||
"upper": { "code": 12626, "label": "ㅒ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12636, "label": "ㅜ"},
|
||||
{ "$": "auto_text_key", "code": 12643, "label": "ㅣ"},
|
||||
{ "$": "auto_text_key", "code": 12631, "label": "ㅗ"},
|
||||
{ "$": "auto_text_key", "code": 12621, "label": "ㅍ"}
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 12623, "label": "ㅏ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12613, "label": "ㅅ" },
|
||||
"upper": { "code": 12614, "label": "ㅆ" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12599, "label": "ㄷ" },
|
||||
"upper": { "code": 12600, "label": "ㄸ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12625, "label": "ㅑ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12593, "label": "ㄱ" },
|
||||
"upper": { "code": 12594, "label": "ㄲ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12622, "label": "ㅎ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12616, "label": "ㅈ" },
|
||||
"upper": { "code": 12617, "label": "ㅉ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12619, "label": "ㅋ"},
|
||||
{ "$": "auto_text_key", "code": 12635, "label": "ㅛ"}
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 12629, "label": "ㅕ"},
|
||||
{ "$": "auto_text_key", "code": 12640, "label": "ㅠ"},
|
||||
{ "$": "auto_text_key", "code": 12618, "label": "ㅊ"},
|
||||
{ "$": "auto_text_key", "code": 12627, "label": "ㅓ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12610, "label": "ㅂ" },
|
||||
"upper": { "code": 12611, "label": "ㅃ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12596, "label": "ㄴ"},
|
||||
{ "$": "auto_text_key", "code": 12609, "label": "ㅁ"}
|
||||
]
|
||||
]
|
||||
@@ -59,6 +59,7 @@
|
||||
{ "id": "ja-JP-jis", "authors": [ "waelwindows" ] },
|
||||
{ "id": "kab", "authors": [ "yanis867" ] },
|
||||
{ "id": "ko", "authors": [ "patrickgold", "Hayleia" ] },
|
||||
{ "id": "ko-KR", "authors": [ "Shunnuo" ] },
|
||||
{ "id": "ku", "authors": [ "GoRaN" ] },
|
||||
{ "id": "lt", "authors": [ "patrickgold" ] },
|
||||
{ "id": "lv", "authors": [ "patrickgold", "eandersons" ] },
|
||||
@@ -606,6 +607,15 @@
|
||||
"characters": "org.florisboard.layouts:korean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"languageTag": "ko-KR",
|
||||
"composer": "org.florisboard.composers:hangul-unicode",
|
||||
"currencySet": "org.florisboard.currencysets:south_korean_won",
|
||||
"popupMapping": "org.florisboard.localization:ko",
|
||||
"preferred": {
|
||||
"characters": "org.florisboard.layouts:korean_phonetic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"languageTag": "lt-LT",
|
||||
"composer": "org.florisboard.composers:appender",
|
||||
|
||||
@@ -360,7 +360,9 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
if (info == null) return
|
||||
val editorInfo = FlorisEditorInfo.wrap(info)
|
||||
activeState.batchEdit {
|
||||
activeState.imeUiMode = ImeUiMode.TEXT
|
||||
if (activeState.imeUiMode != ImeUiMode.CLIPBOARD || prefs.clipboard.historyHideOnNextTextField.get()) {
|
||||
activeState.imeUiMode = ImeUiMode.TEXT
|
||||
}
|
||||
activeState.isSelectionMode = editorInfo.initialSelection.isSelectionMode
|
||||
editorInstance.handleStartInputView(editorInfo, isRestart = restarting)
|
||||
}
|
||||
@@ -428,6 +430,7 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
}
|
||||
isWindowShown = false
|
||||
activeState.batchEdit {
|
||||
activeState.imeUiMode = ImeUiMode.TEXT
|
||||
activeState.isActionsOverflowVisible = false
|
||||
activeState.isActionsEditorVisible = false
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
|
||||
import dev.patrickgold.florisboard.app.settings.theme.SnyggLevel
|
||||
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
|
||||
import dev.patrickgold.florisboard.ime.clipboard.CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO
|
||||
import dev.patrickgold.florisboard.ime.clipboard.ClipboardSyncBehavior
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.input.CapitalizationBehavior
|
||||
@@ -63,6 +64,7 @@ import dev.patrickgold.jetpref.datastore.model.LocalTime
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceData
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceType
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -84,62 +86,13 @@ abstract class FlorisPreferenceModel : PreferenceModel() {
|
||||
key = "clipboard__use_internal_clipboard",
|
||||
default = false,
|
||||
)
|
||||
val syncToFloris = boolean(
|
||||
val syncToFloris = enum(
|
||||
key = "clipboard__sync_to_floris",
|
||||
default = true,
|
||||
default = ClipboardSyncBehavior.ALL_EVENTS,
|
||||
)
|
||||
val syncToSystem = boolean(
|
||||
val syncToSystem = enum(
|
||||
key = "clipboard__sync_to_system",
|
||||
default = false,
|
||||
)
|
||||
val historyEnabled = boolean(
|
||||
key = "clipboard__history_enabled",
|
||||
default = false,
|
||||
)
|
||||
val numHistoryGridColumnsPortrait = int(
|
||||
key = "clipboard__num_history_grid_columns_portrait",
|
||||
default = CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO,
|
||||
)
|
||||
val numHistoryGridColumnsLandscape = int(
|
||||
key = "clipboard__num_history_grid_columns_landscape",
|
||||
default = CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO,
|
||||
)
|
||||
@Composable
|
||||
fun numHistoryGridColumns(): PreferenceData<Int> {
|
||||
val configuration = LocalConfiguration.current
|
||||
return if (configuration.isOrientationPortrait()) {
|
||||
numHistoryGridColumnsPortrait
|
||||
} else {
|
||||
numHistoryGridColumnsLandscape
|
||||
}
|
||||
}
|
||||
val cleanUpOld = boolean(
|
||||
key = "clipboard__clean_up_old",
|
||||
default = false,
|
||||
)
|
||||
val cleanUpAfter = int(
|
||||
key = "clipboard__clean_up_after",
|
||||
default = 20,
|
||||
)
|
||||
val autoCleanSensitive = boolean(
|
||||
key = "clipboard__auto_clean_sensitive",
|
||||
default = false,
|
||||
)
|
||||
val autoCleanSensitiveAfter = int(
|
||||
key = "clipboard__auto_clean_sensitive_after",
|
||||
default = 20,
|
||||
)
|
||||
val limitHistorySize = boolean(
|
||||
key = "clipboard__limit_history_size",
|
||||
default = true,
|
||||
)
|
||||
val maxHistorySize = int(
|
||||
key = "clipboard__max_history_size",
|
||||
default = 20,
|
||||
)
|
||||
val clearPrimaryClipDeletesLastItem = boolean(
|
||||
key = "clipboard__clear_primary_clip_deletes_last_item",
|
||||
default = true,
|
||||
default = ClipboardSyncBehavior.NO_EVENTS,
|
||||
)
|
||||
val suggestionEnabled = boolean(
|
||||
key = "clipboard__suggestion_enabled",
|
||||
@@ -149,6 +102,63 @@ abstract class FlorisPreferenceModel : PreferenceModel() {
|
||||
key = "clipboard__suggestion_timeout",
|
||||
default = 60,
|
||||
)
|
||||
val historyEnabled = boolean(
|
||||
key = "clipboard__history_enabled",
|
||||
default = false,
|
||||
)
|
||||
val historyNumGridColumnsPortrait = int(
|
||||
key = "clipboard__history_num_grid_columns_portrait",
|
||||
default = CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO,
|
||||
)
|
||||
val historyNumGridColumnsLandscape = int(
|
||||
key = "clipboard__history_num_grid_columns_landscape",
|
||||
default = CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO,
|
||||
)
|
||||
@Composable
|
||||
fun historyNumGridColumns(): PreferenceData<Int> {
|
||||
val configuration = LocalConfiguration.current
|
||||
return if (configuration.isOrientationPortrait()) {
|
||||
historyNumGridColumnsPortrait
|
||||
} else {
|
||||
historyNumGridColumnsLandscape
|
||||
}
|
||||
}
|
||||
val historyAutoCleanOldEnabled = boolean(
|
||||
key = "clipboard__history_auto_clean_old_enabled",
|
||||
default = false,
|
||||
)
|
||||
val historyAutoCleanOldAfter = int(
|
||||
key = "clipboard__history_auto_clean_old_after",
|
||||
default = 20,
|
||||
)
|
||||
val historyAutoCleanSensitiveEnabled = boolean(
|
||||
key = "clipboard__history_auto_clean_sensitive_enabled",
|
||||
default = false,
|
||||
)
|
||||
val historyAutoCleanSensitiveAfter = int(
|
||||
key = "clipboard__history_auto_clean_sensitive_after",
|
||||
default = 20,
|
||||
)
|
||||
val historySizeLimitEnabled = boolean(
|
||||
key = "clipboard__history_size_limit_enabled",
|
||||
default = true,
|
||||
)
|
||||
val historySizeLimit = int(
|
||||
key = "clipboard__history_size_limit",
|
||||
default = 20,
|
||||
)
|
||||
val historyHideOnPaste = boolean(
|
||||
key = "clipboard__history_hide_on_paste",
|
||||
default = false,
|
||||
)
|
||||
val historyHideOnNextTextField = boolean(
|
||||
key = "clipboard__history_hide_on_next_text_field",
|
||||
default = true,
|
||||
)
|
||||
val clearPrimaryClipAffectsHistoryIfUnpinned = boolean(
|
||||
key = "clipboard__clear_primary_clip_affects_history_if_unpinned",
|
||||
default = true,
|
||||
)
|
||||
}
|
||||
|
||||
val correction = Correction()
|
||||
@@ -882,6 +892,44 @@ abstract class FlorisPreferenceModel : PreferenceModel() {
|
||||
)
|
||||
}
|
||||
|
||||
// Migrate clipboard history pref names
|
||||
// Keep migration rules until: 0.7 dev cycle
|
||||
"clipboard__sync_to_floris", "clipboard__sync_to_system" -> {
|
||||
entry.transform(
|
||||
type = PreferenceType.string(),
|
||||
rawValue = when (entry.rawValue) {
|
||||
"true" -> ClipboardSyncBehavior.ALL_EVENTS
|
||||
else -> ClipboardSyncBehavior.NO_EVENTS
|
||||
}.name,
|
||||
)
|
||||
}
|
||||
"clipboard__num_history_grid_columns_portrait" -> {
|
||||
entry.transform(key = "clipboard__history_num_grid_columns_portrait")
|
||||
}
|
||||
"clipboard__num_history_grid_columns_landscape" -> {
|
||||
entry.transform(key = "clipboard__history_num_grid_columns_landscape")
|
||||
}
|
||||
"clipboard__clean_up_old" -> {
|
||||
entry.transform(key = "clipboard__history_auto_clean_old_enabled")
|
||||
}
|
||||
"clipboard__clean_up_after" -> {
|
||||
entry.transform(key = "clipboard__history_auto_clean_old_after")
|
||||
}
|
||||
"clipboard__auto_clean_sensitive" -> {
|
||||
entry.transform(key = "clipboard__history_auto_clean_sensitive_enabled")
|
||||
}
|
||||
"clipboard__auto_clean_sensitive_after" -> {
|
||||
entry.transform(key = "clipboard__history_auto_clean_sensitive_after")
|
||||
}
|
||||
"clipboard__limit_history_size" -> {
|
||||
entry.transform(key = "clipboard__history_size_limit_enabled")
|
||||
}
|
||||
"clipboard__max_history_size" -> {
|
||||
entry.transform(key = "clipboard__history_size_limit")
|
||||
}
|
||||
"clipboard__clear_primary_clip_deletes_last_item" -> {
|
||||
entry.transform(key = "clipboard__clear_primary_clip_affects_history_if_unpinned")
|
||||
}
|
||||
|
||||
// Default: keep entry
|
||||
else -> entry.keepAsIs()
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
|
||||
import dev.patrickgold.florisboard.app.settings.theme.SnyggLevel
|
||||
import dev.patrickgold.florisboard.ime.clipboard.ClipboardSyncBehavior
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.ime.input.CapitalizationBehavior
|
||||
import dev.patrickgold.florisboard.ime.input.HapticVibrationMode
|
||||
@@ -104,6 +105,30 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
|
||||
)
|
||||
}
|
||||
},
|
||||
ClipboardSyncBehavior::class to DEFAULT to {
|
||||
listPrefEntries {
|
||||
entry(
|
||||
key = ClipboardSyncBehavior.NO_EVENTS,
|
||||
label = stringRes(R.string.enum__clipboard_sync_behavior__no_events),
|
||||
description = stringRes(R.string.enum__clipboard_sync_behavior__no_events__description),
|
||||
)
|
||||
entry(
|
||||
key = ClipboardSyncBehavior.ONLY_CLEAR_EVENTS,
|
||||
label = stringRes(R.string.enum__clipboard_sync_behavior__only_clear_events),
|
||||
description = stringRes(R.string.enum__clipboard_sync_behavior__only_clear_events__description),
|
||||
)
|
||||
entry(
|
||||
key = ClipboardSyncBehavior.ONLY_SET_EVENTS,
|
||||
label = stringRes(R.string.enum__clipboard_sync_behavior__only_set_events),
|
||||
description = stringRes(R.string.enum__clipboard_sync_behavior__only_set_events__description),
|
||||
)
|
||||
entry(
|
||||
key = ClipboardSyncBehavior.ALL_EVENTS,
|
||||
label = stringRes(R.string.enum__clipboard_sync_behavior__all_events),
|
||||
description = stringRes(R.string.enum__clipboard_sync_behavior__all_events__description),
|
||||
)
|
||||
}
|
||||
},
|
||||
ColorRepresentation::class to DEFAULT to {
|
||||
listPrefEntries {
|
||||
entry(
|
||||
|
||||
@@ -194,7 +194,8 @@ fun BackupScreen() = FlorisScreen {
|
||||
}
|
||||
|
||||
if (backupFilesSelector.provideClipboardItems()) {
|
||||
val clipboardHistory = context.clipboardManager().value.history().all
|
||||
val clipboardManager by context.clipboardManager()
|
||||
val clipboardHistory = clipboardManager.currentHistory.all
|
||||
val clipboardFilesDir = workspace.inputDir.subDir("clipboard")
|
||||
clipboardFilesDir.mkdir()
|
||||
if (backupFilesSelector.clipboardTextItems) {
|
||||
|
||||
@@ -18,10 +18,13 @@ package dev.patrickgold.florisboard.app.settings.clipboard
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
|
||||
import dev.patrickgold.florisboard.ime.clipboard.CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO
|
||||
import dev.patrickgold.florisboard.ime.clipboard.ClipboardSyncBehavior
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
import org.florisboard.lib.android.AndroidVersion
|
||||
@@ -40,16 +43,16 @@ fun ClipboardScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.pref__clipboard__use_internal_clipboard__label),
|
||||
summary = stringRes(R.string.pref__clipboard__use_internal_clipboard__summary),
|
||||
)
|
||||
SwitchPreference(
|
||||
ListPreference(
|
||||
prefs.clipboard.syncToFloris,
|
||||
title = stringRes(R.string.pref__clipboard__sync_from_system_clipboard__label),
|
||||
summary = stringRes(R.string.pref__clipboard__sync_from_system_clipboard__summary),
|
||||
entries = enumDisplayEntriesOf(ClipboardSyncBehavior::class),
|
||||
enabledIf = { prefs.clipboard.useInternalClipboard isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
ListPreference(
|
||||
prefs.clipboard.syncToSystem,
|
||||
title = stringRes(R.string.pref__clipboard__sync_to_system_clipboard__label),
|
||||
summary = stringRes(R.string.pref__clipboard__sync_to_system_clipboard__summary),
|
||||
entries = enumDisplayEntriesOf(ClipboardSyncBehavior::class),
|
||||
enabledIf = { prefs.clipboard.useInternalClipboard isEqualTo true },
|
||||
)
|
||||
|
||||
@@ -77,8 +80,8 @@ fun ClipboardScreen() = FlorisScreen {
|
||||
summary = stringRes(R.string.pref__clipboard__enable_clipboard_history__summary),
|
||||
)
|
||||
DialogSliderPreference(
|
||||
primaryPref = prefs.clipboard.numHistoryGridColumnsPortrait,
|
||||
secondaryPref = prefs.clipboard.numHistoryGridColumnsLandscape,
|
||||
primaryPref = prefs.clipboard.historyNumGridColumnsPortrait,
|
||||
secondaryPref = prefs.clipboard.historyNumGridColumnsLandscape,
|
||||
title = stringRes(R.string.pref__clipboard__num_history_grid_columns__label),
|
||||
primaryLabel = stringRes(R.string.screen_orientation__portrait),
|
||||
secondaryLabel = stringRes(R.string.screen_orientation__landscape),
|
||||
@@ -95,53 +98,65 @@ fun ClipboardScreen() = FlorisScreen {
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.clipboard.cleanUpOld,
|
||||
prefs.clipboard.historyAutoCleanOldEnabled,
|
||||
title = stringRes(R.string.pref__clipboard__clean_up_old__label),
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
|
||||
)
|
||||
DialogSliderPreference(
|
||||
prefs.clipboard.cleanUpAfter,
|
||||
prefs.clipboard.historyAutoCleanOldAfter,
|
||||
title = stringRes(R.string.pref__clipboard__clean_up_after__label),
|
||||
valueLabel = { pluralsRes(R.plurals.unit__minutes__written, it, "v" to it) },
|
||||
min = 0,
|
||||
max = 120,
|
||||
stepIncrement = 5,
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.cleanUpOld isEqualTo true },
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.historyAutoCleanOldEnabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.clipboard.autoCleanSensitive,
|
||||
prefs.clipboard.historyAutoCleanSensitiveEnabled,
|
||||
title = stringRes(R.string.pref__clipboard__auto_clean_sensitive__label),
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
|
||||
visibleIf = { AndroidVersion.ATLEAST_API33_T },
|
||||
)
|
||||
DialogSliderPreference(
|
||||
prefs.clipboard.autoCleanSensitiveAfter,
|
||||
prefs.clipboard.historyAutoCleanSensitiveAfter,
|
||||
title = stringRes(R.string.pref__clipboard__auto_clean_sensitive_after__label),
|
||||
valueLabel = { pluralsRes(R.plurals.unit__seconds__written, it, "v" to it) },
|
||||
min = 0,
|
||||
max = 300,
|
||||
stepIncrement = 10,
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.autoCleanSensitive isEqualTo true },
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.historyAutoCleanSensitiveEnabled isEqualTo true },
|
||||
visibleIf = { AndroidVersion.ATLEAST_API33_T },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.clipboard.limitHistorySize,
|
||||
prefs.clipboard.historySizeLimitEnabled,
|
||||
title = stringRes(R.string.pref__clipboard__limit_history_size__label),
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
|
||||
)
|
||||
DialogSliderPreference(
|
||||
prefs.clipboard.maxHistorySize,
|
||||
prefs.clipboard.historySizeLimit,
|
||||
title = stringRes(R.string.pref__clipboard__max_history_size__label),
|
||||
valueLabel = { pluralsRes(R.plurals.unit__items__written, it, "v" to it) },
|
||||
min = 5,
|
||||
max = 100,
|
||||
stepIncrement = 5,
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.limitHistorySize isEqualTo true },
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.historySizeLimitEnabled isEqualTo true },
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
prefs.clipboard.historyHideOnPaste,
|
||||
title = stringRes(R.string.pref__clipboard__history_hide_on_paste__label),
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true }
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.clipboard.clearPrimaryClipDeletesLastItem,
|
||||
title = stringRes(R.string.pref__clipboard__clear_primary_clip_deletes_last_item__label),
|
||||
summary = stringRes(R.string.pref__clipboard__clear_primary_clip_deletes_last_item__summary),
|
||||
prefs.clipboard.historyHideOnNextTextField,
|
||||
title = stringRes(R.string.pref__clipboard__history_hide_on_next_text_field__label),
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true }
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
prefs.clipboard.clearPrimaryClipAffectsHistoryIfUnpinned,
|
||||
title = stringRes(R.string.pref__clipboard__clear_primary_clip_affects_history_if_unpinned__label),
|
||||
summary = stringRes(R.string.pref__clipboard__clear_primary_clip_affects_history_if_unpinned__summary),
|
||||
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The FlorisBoard Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.clipboard
|
||||
|
||||
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
|
||||
|
||||
data class ClipboardHistory(val all: List<ClipboardItem>) {
|
||||
companion object {
|
||||
private const val RECENT_TIMESPAN_MS = 300_000 // 300 sec = 5 min
|
||||
|
||||
val EMPTY = ClipboardHistory(emptyList())
|
||||
}
|
||||
|
||||
private val now = System.currentTimeMillis()
|
||||
|
||||
val pinned = all.filter { it.isPinned }
|
||||
val unpinned = all.filter { !it.isPinned }
|
||||
val recent = unpinned.filter { (now - it.creationTimestampMs) < RECENT_TIMESPAN_MS }
|
||||
val other = unpinned.filter { (now - it.creationTimestampMs) >= RECENT_TIMESPAN_MS }
|
||||
}
|
||||
@@ -63,13 +63,14 @@ import androidx.compose.material.icons.filled.TextFields
|
||||
import androidx.compose.material.icons.filled.ToggleOff
|
||||
import androidx.compose.material.icons.filled.ToggleOn
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material.icons.outlined.ContentPaste
|
||||
import androidx.compose.material.icons.outlined.ContentPasteGo
|
||||
import androidx.compose.material.icons.outlined.Email
|
||||
import androidx.compose.material.icons.outlined.PushPin
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateSetOf
|
||||
@@ -102,7 +103,6 @@ import dev.patrickgold.florisboard.ime.smartbar.VerticalExitTransition
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.keyboardManager
|
||||
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||
import dev.patrickgold.florisboard.lib.observeAsTransformingState
|
||||
import dev.patrickgold.florisboard.lib.util.NetworkUtils
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
@@ -151,14 +151,14 @@ fun ClipboardInputLayout(
|
||||
var isFilterRowShown by remember { mutableStateOf(false) }
|
||||
val activeFilterTypes = remember { mutableStateSetOf<ItemType>() }
|
||||
|
||||
val unfilteredHistory by clipboardManager.history.observeAsNonNullState()
|
||||
val unfilteredHistory by clipboardManager.historyFlow.collectAsState()
|
||||
val filteredHistory = remember(unfilteredHistory, activeFilterTypes.toSet()) {
|
||||
if (activeFilterTypes.isEmpty()) {
|
||||
unfilteredHistory
|
||||
} else {
|
||||
unfilteredHistory.all
|
||||
.filter { activeFilterTypes.contains(it.type) }
|
||||
.let { ClipboardManager.ClipboardHistory(it) }
|
||||
.let { ClipboardHistory(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ fun ClipboardInputLayout(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val historyAlpha by animateFloatAsState(targetValue = if (isPopupSurfaceActive()) 0.12f else 1f)
|
||||
val staggeredGridCells by prefs.clipboard.numHistoryGridColumns()
|
||||
val staggeredGridCells by prefs.clipboard.historyNumGridColumns()
|
||||
.observeAsTransformingState { numGridColumns ->
|
||||
if (numGridColumns == CLIPBOARD_HISTORY_NUM_GRID_COLUMNS_AUTO) {
|
||||
StaggeredGridCells.Adaptive(160.dp)
|
||||
@@ -498,7 +498,9 @@ fun ClipboardInputLayout(
|
||||
SnyggColumn(modifier = Modifier.weight(0.5f)) {
|
||||
ClipItemView(
|
||||
elementName = FlorisImeUi.ClipboardItemPopup.elementName,
|
||||
modifier = Modifier.widthIn(max = ItemWidth),
|
||||
modifier = Modifier
|
||||
.widthIn(max = ItemWidth)
|
||||
.weight(1f, fill = false),
|
||||
item = popupItem!!,
|
||||
contentScrollInsteadOfClip = true,
|
||||
)
|
||||
@@ -531,11 +533,11 @@ fun ClipboardInputLayout(
|
||||
icon = Icons.Default.Delete,
|
||||
text = stringRes(R.string.clip__delete_item),
|
||||
) {
|
||||
clipboardManager.deleteClip(popupItem!!)
|
||||
clipboardManager.deleteClip(popupItem!!, onlyIfUnpinned = false)
|
||||
popupItem = null
|
||||
}
|
||||
PopupAction(
|
||||
icon = Icons.Outlined.ContentPaste,
|
||||
icon = Icons.Outlined.ContentPasteGo,
|
||||
text = stringRes(R.string.clip__paste_item),
|
||||
) {
|
||||
clipboardManager.pasteItem(popupItem!!)
|
||||
|
||||
@@ -18,8 +18,6 @@ package dev.patrickgold.florisboard.ime.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dev.patrickgold.florisboard.app.FlorisPreferenceStore
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.editorInstance
|
||||
@@ -41,8 +39,8 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.florisboard.lib.android.AndroidClipboardManager
|
||||
import org.florisboard.lib.android.AndroidClipboardManager_OnPrimaryClipChangedListener
|
||||
import org.florisboard.lib.android.clearPrimaryClipAnyApi
|
||||
import org.florisboard.lib.android.setOrClearPrimaryClip
|
||||
import org.florisboard.lib.android.showShortToast
|
||||
import org.florisboard.lib.android.showShortToastSync
|
||||
import org.florisboard.lib.android.systemService
|
||||
import org.florisboard.lib.kotlin.tryOrNull
|
||||
@@ -62,7 +60,6 @@ class ClipboardManager(
|
||||
companion object {
|
||||
// 1 minute
|
||||
private const val INTERVAL = 60 * 1000L
|
||||
private const val RECENT_TIMESPAN_MS = 300_000 // 300 sec = 5 min
|
||||
|
||||
/**
|
||||
* Taken from ClipboardDescription.java from the AOSP
|
||||
@@ -101,8 +98,10 @@ class ClipboardManager(
|
||||
private var clipHistoryDb: ClipboardHistoryDatabase? = null
|
||||
private val clipHistoryDao: ClipboardHistoryDao? get() = clipHistoryDb?.clipboardItemDao()
|
||||
|
||||
private val _history = MutableLiveData(ClipboardHistory.Empty)
|
||||
val history: LiveData<ClipboardHistory> get() = _history
|
||||
private val _historyFlow = MutableStateFlow(ClipboardHistory.EMPTY)
|
||||
val historyFlow = _historyFlow.asStateFlow()
|
||||
val currentHistory: ClipboardHistory
|
||||
get() = historyFlow.value
|
||||
|
||||
private val primaryClipLastFromCallbackGuard = Mutex(locked = false)
|
||||
private var primaryClipLastFromCallback: ClipData? = null
|
||||
@@ -119,7 +118,7 @@ class ClipboardManager(
|
||||
cleanUpJob = ioScope.launch {
|
||||
while (isActive) {
|
||||
delay(INTERVAL)
|
||||
enforceExpiryDate(history())
|
||||
enforceExpiryDate(currentHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,20 +140,21 @@ class ClipboardManager(
|
||||
val itemsSorted = items.sortedByDescending { it.creationTimestampMs }
|
||||
val clipHistory = ClipboardHistory(itemsSorted)
|
||||
enforceHistoryLimit(clipHistory)
|
||||
_history.postValue(clipHistory)
|
||||
_historyFlow.value = clipHistory
|
||||
}
|
||||
|
||||
fun history(): ClipboardHistory = history.value!!
|
||||
|
||||
/**
|
||||
* Sets the current primary clip without updating the internal clipboard history.
|
||||
*/
|
||||
fun updatePrimaryClip(item: ClipboardItem?) {
|
||||
primaryClip = item
|
||||
if (prefs.clipboard.useInternalClipboard.get()) {
|
||||
// Purposely do not sync to system if disabled in prefs
|
||||
if (prefs.clipboard.syncToSystem.get()) {
|
||||
systemClipboardManager.setOrClearPrimaryClip(item?.toClipData(appContext))
|
||||
val syncBehavior = prefs.clipboard.syncToSystem.get()
|
||||
val clipData = item?.toClipData(appContext)
|
||||
if (clipData != null && syncBehavior.shouldSyncSet) {
|
||||
systemClipboardManager.setPrimaryClip(clipData)
|
||||
} else if (clipData == null && syncBehavior.shouldSyncClear) {
|
||||
systemClipboardManager.clearPrimaryClipAnyApi()
|
||||
}
|
||||
} else {
|
||||
systemClipboardManager.setOrClearPrimaryClip(item?.toClipData(appContext))
|
||||
@@ -165,7 +165,8 @@ class ClipboardManager(
|
||||
* Called by system clipboard when the system primary clip has changed.
|
||||
*/
|
||||
override fun onPrimaryClipChanged() {
|
||||
if (!prefs.clipboard.useInternalClipboard.get() || prefs.clipboard.syncToFloris.get()) {
|
||||
val syncBehavior = prefs.clipboard.syncToFloris.get()
|
||||
if (!prefs.clipboard.useInternalClipboard.get() || syncBehavior != ClipboardSyncBehavior.NO_EVENTS) {
|
||||
val systemPrimaryClip = systemClipboardManager.primaryClip
|
||||
ioScope.launch {
|
||||
val isDuplicate: Boolean
|
||||
@@ -173,7 +174,7 @@ class ClipboardManager(
|
||||
val a = primaryClipLastFromCallback?.getItemAt(0)
|
||||
val b = systemPrimaryClip?.getItemAt(0)
|
||||
isDuplicate = when {
|
||||
a === b || a == null && b == null -> true
|
||||
a === b -> true
|
||||
a == null || b == null -> false
|
||||
else -> a.text == b.text && a.uri == b.uri
|
||||
}
|
||||
@@ -184,12 +185,20 @@ class ClipboardManager(
|
||||
val internalPrimaryClip = primaryClip
|
||||
|
||||
if (systemPrimaryClip == null) {
|
||||
primaryClip = null
|
||||
if (syncBehavior.shouldSyncClear) {
|
||||
primaryClip = null
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (systemPrimaryClip.getItemAt(0).let { it.text == null && it.uri == null }) {
|
||||
primaryClip = null
|
||||
if (syncBehavior.shouldSyncClear) {
|
||||
primaryClip = null
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!syncBehavior.shouldSyncSet) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -224,7 +233,7 @@ class ClipboardManager(
|
||||
*/
|
||||
private fun insertOrMoveBeginning(newItem: ClipboardItem) {
|
||||
if (prefs.clipboard.historyEnabled.get()) {
|
||||
val historyElement = history().all.firstOrNull { it.type == ItemType.TEXT && it.text == newItem.text }
|
||||
val historyElement = currentHistory.all.firstOrNull { it.type == ItemType.TEXT && it.text == newItem.text }
|
||||
if (historyElement != null) {
|
||||
moveToTheBeginning(
|
||||
oldItem = historyElement,
|
||||
@@ -241,9 +250,9 @@ class ClipboardManager(
|
||||
}
|
||||
|
||||
private fun enforceHistoryLimit(clipHistory: ClipboardHistory) {
|
||||
if (prefs.clipboard.limitHistorySize.get()) {
|
||||
if (prefs.clipboard.historySizeLimitEnabled.get()) {
|
||||
val nonPinnedItems = clipHistory.recent + clipHistory.other
|
||||
val nToRemove = nonPinnedItems.size - prefs.clipboard.maxHistorySize.get()
|
||||
val nToRemove = nonPinnedItems.size - prefs.clipboard.historySizeLimit.get()
|
||||
if (nToRemove > 0) {
|
||||
val itemsToRemove = nonPinnedItems.asReversed().filterIndexed { n, _ -> n < nToRemove }
|
||||
ioScope.launch {
|
||||
@@ -255,14 +264,14 @@ class ClipboardManager(
|
||||
|
||||
private fun enforceExpiryDate(clipHistory: ClipboardHistory) {
|
||||
val itemsToRemove = mutableSetOf<ClipboardItem>()
|
||||
if (prefs.clipboard.cleanUpOld.get()) {
|
||||
if (prefs.clipboard.historyAutoCleanOldEnabled.get()) {
|
||||
val nonPinnedItems = clipHistory.recent + clipHistory.other
|
||||
val expiryTime = System.currentTimeMillis() - (prefs.clipboard.cleanUpAfter.get() * 60 * 1000)
|
||||
val expiryTime = System.currentTimeMillis() - (prefs.clipboard.historyAutoCleanOldAfter.get() * 60 * 1000)
|
||||
itemsToRemove.addAll(nonPinnedItems.filter { it.creationTimestampMs < expiryTime })
|
||||
}
|
||||
if (prefs.clipboard.autoCleanSensitive.get()) {
|
||||
if (prefs.clipboard.historyAutoCleanSensitiveEnabled.get()) {
|
||||
val sensitiveData = clipHistory.all.filter { it.isSensitive }
|
||||
val expiryTime = System.currentTimeMillis() - (prefs.clipboard.autoCleanSensitiveAfter.get() * 1000)
|
||||
val expiryTime = System.currentTimeMillis() - (prefs.clipboard.historyAutoCleanSensitiveAfter.get() * 1000)
|
||||
itemsToRemove.addAll(sensitiveData.filter { it.creationTimestampMs < expiryTime })
|
||||
}
|
||||
if (itemsToRemove.isNotEmpty()) {
|
||||
@@ -274,7 +283,7 @@ class ClipboardManager(
|
||||
|
||||
private fun moveToTheBeginning(oldItem: ClipboardItem, newItem: ClipboardItem) {
|
||||
ioScope.launch {
|
||||
clipHistoryDao?.delete(oldItem)
|
||||
clipHistoryDao?.delete(oldItem.id)
|
||||
clipHistoryDao?.insert(newItem)
|
||||
}
|
||||
}
|
||||
@@ -300,7 +309,7 @@ class ClipboardManager(
|
||||
*/
|
||||
fun clearHistory() {
|
||||
ioScope.launch {
|
||||
for (item in history().all) {
|
||||
for (item in currentHistory.all) {
|
||||
item.close(appContext)
|
||||
}
|
||||
clipHistoryDao?.deleteAllUnpinned()
|
||||
@@ -312,7 +321,7 @@ class ClipboardManager(
|
||||
*/
|
||||
fun clearFullHistory() {
|
||||
ioScope.launch {
|
||||
for (item in history().all) {
|
||||
for (item in currentHistory.all) {
|
||||
item.close(appContext)
|
||||
}
|
||||
clipHistoryDao?.deleteAll()
|
||||
@@ -327,18 +336,22 @@ class ClipboardManager(
|
||||
*/
|
||||
fun restoreHistory(items: List<ClipboardItem>) {
|
||||
ioScope.launch {
|
||||
val currentHistory = this@ClipboardManager.history().all
|
||||
val currentHistory = currentHistory.all
|
||||
for (item in items) {
|
||||
if (!currentHistory.map { it.copy(id = 0) }.contains(item.copy(id = 0))) {
|
||||
this@ClipboardManager.insertClip(item.copy(id = 0))
|
||||
insertClip(item.copy(id = 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteClip(item: ClipboardItem) {
|
||||
fun deleteClip(item: ClipboardItem, onlyIfUnpinned: Boolean) {
|
||||
ioScope.launch {
|
||||
clipHistoryDao?.delete(item)
|
||||
if (onlyIfUnpinned) {
|
||||
clipHistoryDao?.deleteIfUnpinned(item.id)
|
||||
} else {
|
||||
clipHistoryDao?.delete(item.id)
|
||||
}
|
||||
tryOrNull {
|
||||
val uri = item.uri
|
||||
if (uri != null) {
|
||||
@@ -391,17 +404,4 @@ class ClipboardManager(
|
||||
systemClipboardManager.removePrimaryClipChangedListener(this)
|
||||
cleanUpJob.cancel()
|
||||
}
|
||||
|
||||
class ClipboardHistory(val all: List<ClipboardItem>) {
|
||||
companion object {
|
||||
val Empty = ClipboardHistory(emptyList())
|
||||
}
|
||||
|
||||
private val now = System.currentTimeMillis()
|
||||
|
||||
val pinned = all.filter { it.isPinned }
|
||||
val unpinned = all.filter { !it.isPinned }
|
||||
val recent = unpinned.filter { (now - it.creationTimestampMs) < RECENT_TIMESPAN_MS }
|
||||
val other = unpinned.filter { (now - it.creationTimestampMs) >= RECENT_TIMESPAN_MS }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The FlorisBoard Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.clipboard
|
||||
|
||||
enum class ClipboardSyncBehavior(val shouldSyncSet: Boolean, val shouldSyncClear: Boolean) {
|
||||
NO_EVENTS(shouldSyncSet = false, shouldSyncClear = false),
|
||||
ONLY_CLEAR_EVENTS(shouldSyncSet = false, shouldSyncClear = true),
|
||||
ONLY_SET_EVENTS(shouldSyncSet = true, shouldSyncClear = false),
|
||||
ALL_EVENTS(shouldSyncSet = true, shouldSyncClear = true);
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import org.florisboard.lib.android.UriSerializer
|
||||
import org.florisboard.lib.android.query
|
||||
import org.florisboard.lib.android.stringRes
|
||||
import org.florisboard.lib.kotlin.tryOrNull
|
||||
import androidx.core.net.toUri
|
||||
|
||||
private const val CLIPBOARD_HISTORY_TABLE = "clipboard_history"
|
||||
private const val CLIPBOARD_FILES_TABLE = "clipboard_files"
|
||||
@@ -89,7 +90,7 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
|
||||
val uri: Uri?,
|
||||
val creationTimestampMs: Long,
|
||||
val isPinned: Boolean,
|
||||
val mimeTypes: Array<String>,
|
||||
val mimeTypes: List<String>,
|
||||
@EncodeDefault
|
||||
@ColumnInfo(name = "is_sensitive", defaultValue = "0")
|
||||
val isSensitive: Boolean = false,
|
||||
@@ -101,7 +102,7 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
|
||||
/**
|
||||
* So that every item doesn't have to allocate its own array.
|
||||
*/
|
||||
private val TEXT_PLAIN = arrayOf("text/plain")
|
||||
private val TEXT_PLAIN = listOf("text/plain")
|
||||
private val MEDIA_PROJECTION = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
|
||||
const val FLORIS_CLIP_LABEL = "florisboard/clipboard_item"
|
||||
@@ -177,7 +178,7 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
|
||||
val mimeTypes = when (type) {
|
||||
ItemType.TEXT -> TEXT_PLAIN
|
||||
ItemType.IMAGE, ItemType.VIDEO -> {
|
||||
Array(data.description.mimeTypeCount) { data.description.getMimeType(it) }
|
||||
List(data.description.mimeTypeCount) { data.description.getMimeType(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,34 +231,6 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ClipboardItem
|
||||
|
||||
if (id != other.id) return false
|
||||
if (type != other.type) return false
|
||||
if (text != other.text) return false
|
||||
if (uri != other.uri) return false
|
||||
if (creationTimestampMs != other.creationTimestampMs) return false
|
||||
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
|
||||
if (isSensitive != other.isSensitive) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + type.hashCode()
|
||||
result = 31 * result + (text?.hashCode() ?: 0)
|
||||
result = 31 * result + (uri?.hashCode() ?: 0)
|
||||
result = 31 * result + creationTimestampMs.hashCode()
|
||||
result = 31 * result + mimeTypes.contentHashCode()
|
||||
result = 31 * result + isSensitive.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun stringRepresentation(): String {
|
||||
return when {
|
||||
text != null -> text
|
||||
@@ -270,7 +243,7 @@ data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun uriFromString(value: String?): Uri? {
|
||||
return Uri.parse(value)
|
||||
return value?.toUri()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@@ -293,13 +266,13 @@ class Converters {
|
||||
* DOES NOT USE A GENERALIZED FORMAT.
|
||||
*/
|
||||
@TypeConverter
|
||||
fun mimeTypesToString(mimeTypes: Array<String>): String {
|
||||
fun mimeTypesToString(mimeTypes: List<String>): String {
|
||||
return mimeTypes.joinToString(",")
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToMimeTypes(value: String): Array<String> {
|
||||
return value.split(",").toTypedArray()
|
||||
fun stringToMimeTypes(value: String): List<String> {
|
||||
return value.split(",")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,12 +293,12 @@ interface ClipboardHistoryDao {
|
||||
@Update
|
||||
fun update(items: List<ClipboardItem>)
|
||||
|
||||
@Delete
|
||||
fun delete(item: ClipboardItem)
|
||||
|
||||
@Query("DELETE FROM $CLIPBOARD_HISTORY_TABLE WHERE ${BaseColumns._ID} = :id")
|
||||
fun delete(id: Long)
|
||||
|
||||
@Query("DELETE FROM $CLIPBOARD_HISTORY_TABLE WHERE ${BaseColumns._ID} = :id AND not isPinned ")
|
||||
fun deleteIfUnpinned(id: Long)
|
||||
|
||||
@Delete
|
||||
fun delete(items: List<ClipboardItem>)
|
||||
|
||||
@@ -382,30 +355,8 @@ data class ClipboardFileInfo(
|
||||
@ColumnInfo(name=OpenableColumns.DISPLAY_NAME) val displayName: String,
|
||||
@ColumnInfo(name=OpenableColumns.SIZE) val size: Long,
|
||||
@ColumnInfo(name=Media.ORIENTATION) val orientation: Int,
|
||||
val mimeTypes: Array<String>,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ClipboardFileInfo
|
||||
|
||||
if (id != other.id) return false
|
||||
if (displayName != other.displayName) return false
|
||||
if (size != other.size) return false
|
||||
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + displayName.hashCode()
|
||||
result = 31 * result + size.hashCode()
|
||||
result = 31 * result + mimeTypes.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
val mimeTypes: List<String>,
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface ClipboardFilesDao {
|
||||
|
||||
@@ -35,6 +35,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.florisboard.lib.kotlin.tryOrNull
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* Allows apps to access images and videos on the clipboard.
|
||||
@@ -49,8 +50,8 @@ class ClipboardMediaProvider : ContentProvider() {
|
||||
|
||||
companion object {
|
||||
const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider.clipboard"
|
||||
val IMAGE_CLIPS_URI: Uri = Uri.parse("content://$AUTHORITY/clips/images")
|
||||
val VIDEO_CLIPS_URI: Uri = Uri.parse("content://$AUTHORITY/clips/videos")
|
||||
val IMAGE_CLIPS_URI: Uri = "content://$AUTHORITY/clips/images".toUri()
|
||||
val VIDEO_CLIPS_URI: Uri = "content://$AUTHORITY/clips/videos".toUri()
|
||||
|
||||
private const val IMAGE_CLIP_ITEM = 0
|
||||
private const val IMAGE_CLIPS_TABLE = 1
|
||||
@@ -118,7 +119,7 @@ class ClipboardMediaProvider : ContentProvider() {
|
||||
override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? {
|
||||
return when (Matcher.match(uri)) {
|
||||
IMAGE_CLIP_ITEM, VIDEO_CLIP_ITEM -> {
|
||||
cachedFileInfos.getOrDefault(ContentUris.parseId(uri), null)?.mimeTypes
|
||||
cachedFileInfos.getOrDefault(ContentUris.parseId(uri), null)?.mimeTypes?.toTypedArray()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -152,7 +153,7 @@ class ClipboardMediaProvider : ContentProvider() {
|
||||
}
|
||||
val id = ClipboardFileStorage.cloneUri(context!!, mediaUri)
|
||||
val size = ClipboardFileStorage.getFileForId(context!!, id).length()
|
||||
val mimeTypes = values.getAsString(Columns.MimeTypes).split(",").toTypedArray()
|
||||
val mimeTypes = values.getAsString(Columns.MimeTypes).split(",")
|
||||
val displayName = values.getAsString(OpenableColumns.DISPLAY_NAME)
|
||||
val fileInfo = ClipboardFileInfo(id, displayName, size, rotation, mimeTypes)
|
||||
cachedFileInfos[id] = fileInfo
|
||||
|
||||
@@ -26,6 +26,7 @@ import dev.patrickgold.florisboard.FlorisImeService
|
||||
import dev.patrickgold.florisboard.app.FlorisPreferenceStore
|
||||
import dev.patrickgold.florisboard.appContext
|
||||
import dev.patrickgold.florisboard.clipboardManager
|
||||
import dev.patrickgold.florisboard.ime.ImeUiMode
|
||||
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardFileStorage
|
||||
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
|
||||
@@ -317,7 +318,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
if (!file.exists()) return false
|
||||
val inputContentInfo = InputContentInfoCompat(
|
||||
item.uri,
|
||||
ClipDescription("clipboard media file", mimeTypes),
|
||||
ClipDescription("clipboard media file", mimeTypes.toTypedArray()),
|
||||
null,
|
||||
)
|
||||
val ic = currentInputConnection() ?: return false
|
||||
@@ -325,6 +326,10 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
val flags = InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION
|
||||
InputConnectionCompat.commitContent(ic, activeInfo.base, inputContentInfo, flags, null)
|
||||
}
|
||||
}.also {
|
||||
if (prefs.clipboard.historyHideOnPaste.get()) {
|
||||
keyboardManager.activeState.imeUiMode = ImeUiMode.TEXT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,8 @@ package dev.patrickgold.florisboard.ime.keyboard
|
||||
import android.content.Context
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowRightAlt
|
||||
import androidx.compose.material.icons.automirrored.filled.Backspace
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardBackspace
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardReturn
|
||||
import androidx.compose.material.icons.automirrored.filled.Redo
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
@@ -32,7 +30,7 @@ import androidx.compose.material.icons.automirrored.outlined.Backspace
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.ContentCut
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.ContentPasteGo
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.FontDownload
|
||||
@@ -200,7 +198,7 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
|
||||
Icons.Default.ContentCut
|
||||
}
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
Icons.Default.ContentPaste
|
||||
Icons.Default.ContentPasteGo
|
||||
}
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
Icons.Default.SelectAll
|
||||
|
||||
@@ -717,8 +717,8 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
|
||||
KeyCode.CLIPBOARD_CLEAR_HISTORY -> clipboardManager.clearHistory()
|
||||
KeyCode.CLIPBOARD_CLEAR_FULL_HISTORY -> clipboardManager.clearFullHistory()
|
||||
KeyCode.CLIPBOARD_CLEAR_PRIMARY_CLIP -> {
|
||||
if (prefs.clipboard.clearPrimaryClipDeletesLastItem.get()) {
|
||||
clipboardManager.primaryClip?.let { clipboardManager.deleteClip(it) }
|
||||
if (prefs.clipboard.clearPrimaryClipAffectsHistoryIfUnpinned.get()) {
|
||||
clipboardManager.primaryClip?.let { clipboardManager.deleteClip(it, onlyIfUnpinned = true) }
|
||||
}
|
||||
clipboardManager.updatePrimaryClip(null)
|
||||
appContext.showShortToastSync(R.string.clipboard__cleared_primary_clip)
|
||||
|
||||
@@ -701,9 +701,7 @@
|
||||
<string name="pref__clipboard__use_internal_clipboard__label">Use internal clipboard</string>
|
||||
<string name="pref__clipboard__use_internal_clipboard__summary">Use an internal clipboard instead of the system clipboard</string>
|
||||
<string name="pref__clipboard__sync_from_system_clipboard__label">Sync from system clipboard</string>
|
||||
<string name="pref__clipboard__sync_from_system_clipboard__summary">System clipboard updates also update Floris clipboard</string>
|
||||
<string name="pref__clipboard__sync_to_system_clipboard__label">Sync to system clipboard</string>
|
||||
<string name="pref__clipboard__sync_to_system_clipboard__summary">Floris clipboard updates also update system clipboard</string>
|
||||
<string name="pref__clipboard__group_clipboard_suggestion__label">Clipboard suggestions</string>
|
||||
<string name="pref__clipboard__suggestion_enabled__label" comment="Preference title">Clipboard content suggestions</string>
|
||||
<string name="pref__clipboard__suggestion_enabled__summary" comment="Preference summary">Suggest previously copied clipboard content</string>
|
||||
@@ -719,8 +717,10 @@
|
||||
<string name="pref__clipboard__auto_clean_sensitive_after__label">Auto clean sensitive items after</string>
|
||||
<string name="pref__clipboard__limit_history_size__label">Limit history size</string>
|
||||
<string name="pref__clipboard__max_history_size__label">Max history size</string>
|
||||
<string name="pref__clipboard__clear_primary_clip_deletes_last_item__label">Clear primary clip affects history</string>
|
||||
<string name="pref__clipboard__clear_primary_clip_deletes_last_item__summary">Clearing the primary clip also deletes the latest history entry</string>
|
||||
<string name="pref__clipboard__history_hide_on_paste__label">Hide history on paste</string>
|
||||
<string name="pref__clipboard__history_hide_on_next_text_field__label">Hide history on next text field</string>
|
||||
<string name="pref__clipboard__clear_primary_clip_affects_history_if_unpinned__label">Clear primary clip affects history</string>
|
||||
<string name="pref__clipboard__clear_primary_clip_affects_history_if_unpinned__summary">Clearing the primary clip also removes it from the history, if not pinned</string>
|
||||
<string name="send_to_clipboard__unknown_error">An unknown error occurred. Please try again!</string>
|
||||
<string name="send_to_clipboard__type_not_supported_error">This media type is not supported.</string>
|
||||
<string name="send_to_clipboard__description__copied_image_to_clipboard">Copied below image to clipboard.</string>
|
||||
@@ -928,6 +928,15 @@
|
||||
<string name="enum__capitalization_behavior__capslock_by_double_tap" comment="Enum value label">Enable Capslock by double tapping shift</string>
|
||||
<string name="enum__capitalization_behavior__capslock_by_cycle" comment="Enum value label">Switch to the next capitalization step each time the shift key is pressed</string>
|
||||
|
||||
<string name="enum__clipboard_sync_behavior__no_events" comment="Enum value label">No events</string>
|
||||
<string name="enum__clipboard_sync_behavior__no_events__description" comment="Enum value description">No events are synced in this direction. In this mode, sync excludes text content.</string>
|
||||
<string name="enum__clipboard_sync_behavior__only_clear_events" comment="Enum value label">Only clear events</string>
|
||||
<string name="enum__clipboard_sync_behavior__only_clear_events__description" comment="Enum value description">Only events clearing clip data are synced in this direction. In this mode, sync excludes text content.</string>
|
||||
<string name="enum__clipboard_sync_behavior__only_set_events" comment="Enum value label">Only set events</string>
|
||||
<string name="enum__clipboard_sync_behavior__only_set_events__description" comment="Enum value description">Only events setting clip data are synced in this direction. In this mode, sync includes text content.</string>
|
||||
<string name="enum__clipboard_sync_behavior__all_events" comment="Enum value label">All events</string>
|
||||
<string name="enum__clipboard_sync_behavior__all_events__description" comment="Enum value description">Both events setting and clearing clip data are synced in this direction. In this mode, sync includes text content.</string>
|
||||
|
||||
<string name="enum__color_representation__hex" comment="Enum value label">Hexadecimal</string>
|
||||
<string name="enum__color_representation__rgb" comment="Enum value label">Red Green Blue</string>
|
||||
<string name="enum__color_representation__hsv" comment="Enum value label">Hue Saturation Value</string>
|
||||
|
||||
@@ -11,5 +11,5 @@ projectMinSdk=26
|
||||
projectTargetSdk=35
|
||||
projectCompileSdk=35
|
||||
|
||||
projectVersionCode=116
|
||||
projectVersionName=0.5.1
|
||||
projectVersionCode=114
|
||||
projectVersionName=0.5.0-rc02
|
||||
|
||||
Reference in New Issue
Block a user