Compare commits
17
Commits
0f77c9cf10
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f74a9d9b4 | ||
|
|
b8b088c1ab | ||
|
|
51e8cdbfe0 | ||
|
|
f8e7bb99b5 | ||
|
|
457a924a51 | ||
|
|
341259d09f | ||
|
|
f0374a08c7 | ||
|
|
64f309c144 | ||
|
|
4fc0de4518 | ||
|
|
c212d90408 | ||
|
|
004fd7646b | ||
|
|
154a81cafa | ||
|
|
8f2573fcbb | ||
|
|
e6e7ada855 | ||
|
|
c0f69160b9 | ||
|
|
158ddee6c3 | ||
|
|
81e4c77644 |
@@ -308,5 +308,6 @@ fabric.properties
|
|||||||
.idea/deploymentTargetSelector.xml
|
.idea/deploymentTargetSelector.xml
|
||||||
.idea/copilot
|
.idea/copilot
|
||||||
.idea/other.xml
|
.idea/other.xml
|
||||||
|
.idea/studiobot.xml
|
||||||
|
|
||||||
.kotlin
|
.kotlin
|
||||||
+1
@@ -73,5 +73,6 @@
|
|||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
android:fullBackupContent="true"
|
android:fullBackupContent="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
android:stateNotNeeded="true"
|
android:stateNotNeeded="true"
|
||||||
android:theme="@style/LauncherTheme"
|
android:theme="@style/LauncherTheme"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|screenLayout|layoutDirection|navigation|orientation|uiMode"
|
||||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
@@ -24,7 +24,7 @@ fun ProvideAppWidgetHost(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
|
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
|
||||||
LaunchedEffect(null) {
|
LaunchedEffect(null) {
|
||||||
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
widgetHost.startListening()
|
widgetHost.startListening()
|
||||||
try {
|
try {
|
||||||
awaitCancellation()
|
awaitCancellation()
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package de.mm20.launcher2.ui.launcher
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.util.Log
|
||||||
import com.android.launcher3.GestureNavContract
|
import com.android.launcher3.GestureNavContract
|
||||||
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
|
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
|
||||||
|
|
||||||
@@ -19,6 +21,12 @@ class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
Log.e("LauncherActivity","newConfig.keyboard ${newConfig.keyboard}")
|
||||||
|
Log.e("LauncherActivity","newConfig.keyboardHidden ${newConfig.keyboardHidden}")
|
||||||
|
Log.e("LauncherActivity","newConfig.hardKeyboardHidden ${newConfig.hardKeyboardHidden}")
|
||||||
|
}
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
enterHomeTransitionManager.clear()
|
enterHomeTransitionManager.clear()
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ import de.mm20.launcher2.ui.locals.LocalWindowSize
|
|||||||
import de.mm20.launcher2.ui.overlays.OverlayHost
|
import de.mm20.launcher2.ui.overlays.OverlayHost
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||||
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
|
|
||||||
abstract class SharedLauncherActivity(
|
abstract class SharedLauncherActivity(
|
||||||
@@ -195,7 +194,10 @@ abstract class SharedLauncherActivity(
|
|||||||
if (it != null) {
|
if (it != null) {
|
||||||
enterTransitionProgress.value = 0f
|
enterTransitionProgress.value = 0f
|
||||||
enterTransition = it
|
enterTransition = it
|
||||||
enterTransitionProgress.animateTo(100f, animationSpec = animMotionSpec)
|
enterTransitionProgress.animateTo(
|
||||||
|
100f,
|
||||||
|
animationSpec = animMotionSpec
|
||||||
|
)
|
||||||
enterTransition = null
|
enterTransition = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,10 +277,15 @@ abstract class SharedLauncherActivity(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
is GestureAction.Widgets -> ScaffoldGesture(
|
is GestureAction.Widgets ->
|
||||||
component = widgetComponent,
|
if (widgetsOnHomeScreen == true) {
|
||||||
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
null
|
||||||
)
|
} else {
|
||||||
|
ScaffoldGesture(
|
||||||
|
component = widgetComponent,
|
||||||
|
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is GestureAction.Notifications -> ScaffoldGesture(
|
is GestureAction.Notifications -> ScaffoldGesture(
|
||||||
component = NotificationsComponent,
|
component = NotificationsComponent,
|
||||||
|
|||||||
@@ -250,10 +250,10 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
previousResults = SearchResults(apps = apps)
|
previousResults = SearchResults(apps = apps)
|
||||||
|
|
||||||
searchActionResults.clear()
|
searchActionResults.clear()
|
||||||
appResults.mergeWith(apps)
|
appResults.updateItems(apps)
|
||||||
workAppResults.mergeWith(workApps)
|
workAppResults.updateItems(workApps)
|
||||||
privateSpaceAppResults.mergeWith(privateApps)
|
privateSpaceAppResults.updateItems(privateApps)
|
||||||
hiddenResults.mergeWith(hiddenItems)
|
hiddenResults.updateItems(hiddenItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -273,19 +273,31 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
workAppResults.clear()
|
workAppResults.clear()
|
||||||
privateSpaceAppResults.clear()
|
privateSpaceAppResults.clear()
|
||||||
|
|
||||||
appResults.mergeWith(results.apps, hiddenKeys, query)
|
appResults.updateItems(
|
||||||
appShortcutResults.mergeWith(results.shortcuts, hiddenKeys, query)
|
results.apps
|
||||||
fileResults.mergeWith(results.files, hiddenKeys, query)
|
?.filterNot { hiddenKeys.contains(it.key) }
|
||||||
|
?.applyRanking(query)
|
||||||
|
)
|
||||||
|
appShortcutResults.updateItems(
|
||||||
|
results.shortcuts
|
||||||
|
?.filterNot { hiddenKeys.contains(it.key) }
|
||||||
|
?.applyRanking(query)
|
||||||
|
)
|
||||||
|
fileResults.updateItems(
|
||||||
|
results.files
|
||||||
|
?.filterNot { hiddenKeys.contains(it.key) }
|
||||||
|
?.applyRanking(query)
|
||||||
|
)
|
||||||
|
|
||||||
contactResults.mergeWith(
|
contactResults.updateItems(
|
||||||
results.contacts?.filterNot { hiddenKeys.contains(it.key) }
|
results.contacts?.filterNot { hiddenKeys.contains(it.key) }
|
||||||
?.applyRanking(query)
|
?.applyRanking(query)
|
||||||
)
|
)
|
||||||
calendarResults.mergeWith(
|
calendarResults.updateItems(
|
||||||
results.calendars?.filterNot { hiddenKeys.contains(it.key) }
|
results.calendars?.filterNot { hiddenKeys.contains(it.key) }
|
||||||
?.applyRanking(query)
|
?.applyRanking(query)
|
||||||
)
|
)
|
||||||
locationResults.mergeWith(
|
locationResults.updateItems(
|
||||||
results.locations?.filterNot { hiddenKeys.contains(it.key) }
|
results.locations?.filterNot { hiddenKeys.contains(it.key) }
|
||||||
?.let { locations ->
|
?.let { locations ->
|
||||||
devicePoseProvider.lastCachedLocation?.let {
|
devicePoseProvider.lastCachedLocation?.let {
|
||||||
@@ -298,17 +310,17 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
} ?: locations.applyRanking(query)
|
} ?: locations.applyRanking(query)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
articleResults.mergeWith(
|
articleResults.updateItems(
|
||||||
results.wikipedia?.applyRanking(query)
|
results.wikipedia?.applyRanking(query)
|
||||||
)
|
)
|
||||||
websiteResults.mergeWith(
|
websiteResults.updateItems(
|
||||||
results.websites?.applyRanking(query)
|
results.websites?.applyRanking(query)
|
||||||
)
|
)
|
||||||
calculatorResults.mergeWith(results.calculators)
|
calculatorResults.updateItems(results.calculators)
|
||||||
unitConverterResults.mergeWith(results.unitConverters)
|
unitConverterResults.updateItems(results.unitConverters)
|
||||||
|
|
||||||
if (results.searchActions != null) {
|
if (results.searchActions != null) {
|
||||||
searchActionResults.mergeWith(results.searchActions!!)
|
searchActionResults.updateItems(results.searchActions!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (launchOnEnter.value) {
|
if (launchOnEnter.value) {
|
||||||
@@ -429,24 +441,18 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
return sorted.distinctBy { it.key }.toList()
|
return sorted.distinctBy { it.key }.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> SnapshotStateList<T>.mergeWith(newItems: List<T>?) {
|
/**
|
||||||
val items = newItems ?: emptyList()
|
* Merges a list of new items into the current SnapshotStateList.
|
||||||
val diff = toSet() subtract items.toSet()
|
* It removes items that are in the current list but not in the new list.
|
||||||
removeAll(diff)
|
* Then, it updates existing items or adds new items from the new list.
|
||||||
for ((i, item) in items.withIndex()) {
|
*
|
||||||
if (i < size)
|
* @param T The type of items in the list.
|
||||||
set(i, item)
|
* @param newItems The list of new items to merge with. If null, an empty list is used.
|
||||||
else
|
*/
|
||||||
add(item)
|
private fun <T> SnapshotStateList<T>.updateItems(newItems: List<T>?) {
|
||||||
}
|
clear()
|
||||||
|
addAll(newItems ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T : SavableSearchable> SnapshotStateList<T>.mergeWith(
|
|
||||||
newItems: List<T>?,
|
|
||||||
hiddenKeys: List<String>,
|
|
||||||
query: String
|
|
||||||
) = this.mergeWith((newItems ?: emptyList()).filterNot { hiddenKeys.contains(it.key) }
|
|
||||||
.applyRanking(query))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -226,11 +226,19 @@ fun LocationItem(
|
|||||||
this@AnimatedContent
|
this@AnimatedContent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// val rateing =
|
||||||
val formattedDistance =
|
val formattedDistance =
|
||||||
distance?.metersToLocalizedString(context, imperialUnits)
|
distance?.metersToLocalizedString(context, imperialUnits)
|
||||||
val sublabel = listOf(location.category, formattedDistance)
|
val sublabel = if (location.category?.contains("|") == true) {
|
||||||
.fastFilterNotNull()
|
listOf(location.category!!.split("|")!!.first(), formattedDistance,location.userRating)
|
||||||
.joinToString(" • ")
|
.fastFilterNotNull()
|
||||||
|
.joinToString(" • ")
|
||||||
|
} else {
|
||||||
|
listOf(location.category, formattedDistance,location.userRating)
|
||||||
|
.fastFilterNotNull()
|
||||||
|
.joinToString(" • ")
|
||||||
|
}
|
||||||
val isOpenString = location.openingSchedule?.isOpen()
|
val isOpenString = location.openingSchedule?.isOpen()
|
||||||
?.let { stringResource(if (it) R.string.location_open else R.string.location_closed) }
|
?.let { stringResource(if (it) R.string.location_open else R.string.location_closed) }
|
||||||
|
|
||||||
|
|||||||
@@ -119,10 +119,13 @@ fun EditFavoritesSheet(
|
|||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (createShortcutTarget != null) {
|
|
||||||
ShortcutPicker(viewModel, it)
|
|
||||||
} else {
|
} else {
|
||||||
ReorderFavoritesGrid(viewModel, it)
|
ReorderFavoritesGrid(viewModel, it)
|
||||||
|
if (createShortcutTarget != null) {
|
||||||
|
BottomSheetDialog({viewModel.cancelPickShortcut()}) {
|
||||||
|
ShortcutPicker(viewModel, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ fun WidgetColumn(
|
|||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
Column {
|
Column {
|
||||||
|
|||||||
+3
-2
@@ -52,6 +52,7 @@ fun GestureSettingsScreen() {
|
|||||||
val viewModel: GestureSettingsScreenVM = viewModel()
|
val viewModel: GestureSettingsScreenVM = viewModel()
|
||||||
|
|
||||||
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
|
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
|
||||||
|
val allowWidgetGesture by viewModel.allowWidgetGesture.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
val options = buildList {
|
val options = buildList {
|
||||||
add(stringResource(R.string.gesture_action_none) to GestureAction.NoAction)
|
add(stringResource(R.string.gesture_action_none) to GestureAction.NoAction)
|
||||||
@@ -61,7 +62,7 @@ fun GestureSettingsScreen() {
|
|||||||
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents)
|
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents)
|
||||||
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
|
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
|
||||||
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search)
|
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search)
|
||||||
add(stringResource(R.string.gesture_action_widgets) to GestureAction.Widgets)
|
if (allowWidgetGesture == true) add(stringResource(R.string.gesture_action_widgets) to GestureAction.Widgets)
|
||||||
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
|
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +266,7 @@ fun GesturePreference(
|
|||||||
icon = icon,
|
icon = icon,
|
||||||
items = options,
|
items = options,
|
||||||
value = value,
|
value = value,
|
||||||
summary = options.find { value?.javaClass == it.second.javaClass }?.first,
|
summary = options.find { value?.javaClass == it.second.javaClass }?.first ?: stringResource(R.string.gesture_action_none),
|
||||||
onValueChanged = { if (it != null) onValueChanged(it) }
|
onValueChanged = { if (it != null) onValueChanged(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
|
val allowWidgetGesture = uiSettings.homeScreenWidgets.map { it == false }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
val swipeDown = gestureSettings.swipeDown
|
val swipeDown = gestureSettings.swipeDown
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
val swipeLeft = gestureSettings.swipeLeft
|
val swipeLeft = gestureSettings.swipeLeft
|
||||||
|
|||||||
-32
@@ -157,38 +157,6 @@ class HomescreenSettingsScreenVM(
|
|||||||
|
|
||||||
fun setWidgetsOnHomeScreen(widgetsOnHomeScreen: Boolean) {
|
fun setWidgetsOnHomeScreen(widgetsOnHomeScreen: Boolean) {
|
||||||
uiSettings.setHomeScreenWidgets(widgetsOnHomeScreen)
|
uiSettings.setHomeScreenWidgets(widgetsOnHomeScreen)
|
||||||
viewModelScope.launch {
|
|
||||||
val gestures = gestureSettings.first()
|
|
||||||
if (widgetsOnHomeScreen) {
|
|
||||||
if (gestures.swipeUp is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeUp(GestureAction.NoAction)
|
|
||||||
} else if (gestures.swipeRight is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeUp(GestureAction.NoAction)
|
|
||||||
} else if (gestures.swipeLeft is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeUp(GestureAction.NoAction)
|
|
||||||
} else if (gestures.swipeDown is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeUp(GestureAction.NoAction)
|
|
||||||
} else if (gestures.longPress is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setLongPress(GestureAction.NoAction)
|
|
||||||
} else if (gestures.doubleTap is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setDoubleTap(GestureAction.NoAction)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (gestures.swipeUp is GestureAction.NoAction || gestures.swipeUp is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeUp(GestureAction.Widgets)
|
|
||||||
} else if (gestures.swipeRight is GestureAction.NoAction || gestures.swipeRight is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeRight(GestureAction.Widgets)
|
|
||||||
} else if (gestures.swipeLeft is GestureAction.NoAction || gestures.swipeLeft is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeLeft(GestureAction.Widgets)
|
|
||||||
} else if (gestures.swipeDown is GestureAction.NoAction || gestures.swipeDown is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setSwipeDown(GestureAction.Widgets)
|
|
||||||
} else if (gestures.longPress is GestureAction.NoAction || gestures.longPress is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setLongPress(GestureAction.Widgets)
|
|
||||||
} else if (gestures.doubleTap is GestureAction.NoAction || gestures.doubleTap is GestureAction.Widgets) {
|
|
||||||
gestureSettings.setDoubleTap(GestureAction.Widgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : KoinComponent {
|
companion object : KoinComponent {
|
||||||
|
|||||||
@@ -589,7 +589,7 @@
|
|||||||
<string name="clock_style_digital2">Semplice</string>
|
<string name="clock_style_digital2">Semplice</string>
|
||||||
<string name="clock_style_orbit">Orbita</string>
|
<string name="clock_style_orbit">Orbita</string>
|
||||||
<string name="clock_style_binary">Binario</string>
|
<string name="clock_style_binary">Binario</string>
|
||||||
<string name="clock_style_analog">Mani</string>
|
<string name="clock_style_analog">Lancette</string>
|
||||||
<string name="clock_style_segment">7 segmenti</string>
|
<string name="clock_style_segment">7 segmenti</string>
|
||||||
<string name="clock_style_empty">Nessun orologio</string>
|
<string name="clock_style_empty">Nessun orologio</string>
|
||||||
<string name="clock_variant_outlined">Delineato</string>
|
<string name="clock_variant_outlined">Delineato</string>
|
||||||
|
|||||||
@@ -442,7 +442,7 @@
|
|||||||
<string name="poi_category_books">서점</string>
|
<string name="poi_category_books">서점</string>
|
||||||
<string name="poi_category_florist">꽃집</string>
|
<string name="poi_category_florist">꽃집</string>
|
||||||
<string name="poi_category_church">교회</string>
|
<string name="poi_category_church">교회</string>
|
||||||
<string name="poi_category_restaurant">레스토랑</string>
|
<string name="poi_category_restaurant">레스토랑|음식점|식당|밥집|분식</string>
|
||||||
<string name="poi_category_fast_food">패스트푸드</string>
|
<string name="poi_category_fast_food">패스트푸드</string>
|
||||||
<string name="poi_category_hotel">호텔</string>
|
<string name="poi_category_hotel">호텔</string>
|
||||||
<string name="poi_category_cafe">카페</string>
|
<string name="poi_category_cafe">카페</string>
|
||||||
|
|||||||
@@ -554,7 +554,7 @@
|
|||||||
<string name="search_action_app">Aplicação</string>
|
<string name="search_action_app">Aplicação</string>
|
||||||
<string name="search_action_websearch_url">Modelo de URL</string>
|
<string name="search_action_websearch_url">Modelo de URL</string>
|
||||||
<string name="preference_plugin_enable">Ativar plugin</string>
|
<string name="preference_plugin_enable">Ativar plugin</string>
|
||||||
<string name="plugin_type_filesearch">Pesquisa de ficheiros</string>
|
<string name="plugin_type_filesearch">Ficheiros</string>
|
||||||
<string name="experimental_feature">Experimental</string>
|
<string name="experimental_feature">Experimental</string>
|
||||||
<string name="tag_exists_error">Já existe uma etiqueta com este nome.</string>
|
<string name="tag_exists_error">Já existe uma etiqueta com este nome.</string>
|
||||||
<string name="tag_exists_message">Já existe uma etiqueta com este nome. Se continuar, as etiquetas serão combinadas.</string>
|
<string name="tag_exists_message">Já existe uma etiqueta com este nome. Se continuar, as etiquetas serão combinadas.</string>
|
||||||
@@ -799,4 +799,63 @@
|
|||||||
<string name="preference_clock_widget_time_format_24h">24 horas</string>
|
<string name="preference_clock_widget_time_format_24h">24 horas</string>
|
||||||
<string name="preference_clock_widget_time_format_12h">12 horas</string>
|
<string name="preference_clock_widget_time_format_12h">12 horas</string>
|
||||||
<string name="preference_clock_widget_time_format_system">Padrão do sistema</string>
|
<string name="preference_clock_widget_time_format_system">Padrão do sistema</string>
|
||||||
|
<string name="theme_import_title">Importar tema</string>
|
||||||
|
<string name="import_theme_exists">O tema já existe e será atualizado</string>
|
||||||
|
<string name="preference_search_bar_style_transparent">Transparente</string>
|
||||||
|
<string name="preference_search_bar_style_solid">Sólida</string>
|
||||||
|
<string name="preference_search_bar_style_hidden">Oculta</string>
|
||||||
|
<string name="import_theme_contents">Conteúdo</string>
|
||||||
|
<string name="clock_variant_analog_ticks">Ponteiros 2</string>
|
||||||
|
<string name="theme_export_title">Exportar tema</string>
|
||||||
|
<string name="preference_colors_high_contrast">Alto contraste</string>
|
||||||
|
<string name="theme_bundle_name">Nome</string>
|
||||||
|
<string name="theme_bundle_author">Autor</string>
|
||||||
|
<string name="preference_shapes_default">Padrão</string>
|
||||||
|
<string name="preference_shapes_extra_round">Super redonda</string>
|
||||||
|
<string name="preference_screen_shapes">Formas</string>
|
||||||
|
<string name="preference_shapes_base">Forma base</string>
|
||||||
|
<string name="preference_shapes_rect">Retangular</string>
|
||||||
|
<string name="confirmation_delete_shapes_scheme">Tem a certeza de que deseja eliminar o esquema de formas %1$s?</string>
|
||||||
|
<string name="bad_configuration_title">Parabéns, acabou de se bloquear!</string>
|
||||||
|
<string name="preference_launch_breezyweather_app">Abrir Breezy Weather</string>
|
||||||
|
<string name="preference_screen_transparencies">Transparência</string>
|
||||||
|
<string name="preference_transparencies_default">Padrão</string>
|
||||||
|
<string name="preference_transparencies_semi_transparent">Semi-transparente</string>
|
||||||
|
<string name="provider_breezy">Breezy Weather</string>
|
||||||
|
<string name="new_theme_name">(sem nome)</string>
|
||||||
|
<string name="preference_breezyweather_integration_instructions">Para utilizar Breezy Weather como serviço meteorológico:\n\n1. Aceda a Breezy Weather > Definições > Widgets & Papel de parede > Enviara dados Gadgetbridge > Ativar%1$s\n\n2. Em %1$s, selecione Breezy Weather como serviço de meteorologia.</string>
|
||||||
|
<string name="music_widget_interactive_progress_bar">Barra de progresso interativa</string>
|
||||||
|
<string name="missing_permission_tasks_integration">A permissão Tasks é necessária para a integração Tasks</string>
|
||||||
|
<string name="preference_location_breezy">Gerir localização em Breezy Weather</string>
|
||||||
|
<string name="no_selection">Nada selecionado</string>
|
||||||
|
<string name="save_as_file">Guardar como ficheiro</string>
|
||||||
|
<string name="preference_breezyweather_integration">Breezy Weather</string>
|
||||||
|
<string name="plugin_type_contacts">Contactos</string>
|
||||||
|
<string name="confirmation_delete_transparencies_scheme">Tem a certeza de que deseja eliminar o esquema de transparência %1$s?</string>
|
||||||
|
<string name="action_install">Instalar</string>
|
||||||
|
<string name="missing_permission_tasks_search_settings">A permissão Tasks é necessária para pesquisar tarefas</string>
|
||||||
|
<string name="preference_search_tasks">Tarefas</string>
|
||||||
|
<string name="preference_search_tasks_summary">Pesquisar tarefas na aplicação Tasks</string>
|
||||||
|
<string name="preference_tasks_integration">Tasks</string>
|
||||||
|
<string name="preference_tasks_integration_description">Tasks é uma aplicação open souce para gerir tarefas, listas e lembretes. Se instalada, %1$s pode mostrar e pesquisar as tarefas na aplicação.</string>
|
||||||
|
<string name="preference_tasks_integration_ready">Integração com Tasks finalizada e pronta para utilização.</string>
|
||||||
|
<plurals name="calendar_widget_running_tasks">
|
||||||
|
<item quantity="one">+%1$d tarefa por realizar</item>
|
||||||
|
<item quantity="many">+%1$d tarefas por realizar</item>
|
||||||
|
<item quantity="other">+%1$d tarefas por realizar</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="preference_screen_typography">Tipografia</string>
|
||||||
|
<string name="preference_breezyweather_integration_description">Breezy Weather é uma aplicação open source de meteorologia. Se instalada, %1$s pode utilizar Breezy Weather com a fonte de dados meteorológicos.</string>
|
||||||
|
<string name="confirmation_delete_typography_scheme">Tem a certeza de que deseja eliminar o esquema tipográfico %1$s?</string>
|
||||||
|
<string name="preference_typography_fonts">Tipo de letra</string>
|
||||||
|
<string name="font_category_device_default">Definições do dispositivo</string>
|
||||||
|
<string name="font_name_device_headline">Tipo de letra padrão</string>
|
||||||
|
<string name="font_name_device_body">Tipo de letra do dispositivo</string>
|
||||||
|
<string name="font_category_generic">Genérica</string>
|
||||||
|
<string name="preference_widgets_on_home_screen">Widgets no ecrã principal</string>
|
||||||
|
<string name="preference_gesture_swipe_up">Deslize acima</string>
|
||||||
|
<string name="preference_widgets_on_home_screen_summary">Mostrar widgets no ecrã principal e não em página secundária</string>
|
||||||
|
<string name="gesture_action_widgets">Widgets</string>
|
||||||
|
<string name="bad_configuration_summary">Você conseguir encontrar uma combinação de definições em não é possível aceder às pesquisas e às definições. Acabou de se bloquear.</string>
|
||||||
|
<string name="preference_launch_tasks_app">Abrir Tasks</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -575,7 +575,7 @@
|
|||||||
<string name="location_opens_other_day">开启于 %1$s, %2$s</string>
|
<string name="location_opens_other_day">开启于 %1$s, %2$s</string>
|
||||||
<string name="menu_map">在地图上查看</string>
|
<string name="menu_map">在地图上查看</string>
|
||||||
<string name="menu_website">打开网站</string>
|
<string name="menu_website">打开网站</string>
|
||||||
<string name="plugin_state_setup_required">您需要先设置该插件</string>
|
<string name="plugin_state_setup_required">您需要先设置此插件</string>
|
||||||
<string name="plugin_state_error">此插件未正常运行</string>
|
<string name="plugin_state_error">此插件未正常运行</string>
|
||||||
<string name="menu_bugreport">Bug 报告</string>
|
<string name="menu_bugreport">Bug 报告</string>
|
||||||
<string name="length_unit">长度单位</string>
|
<string name="length_unit">长度单位</string>
|
||||||
@@ -613,7 +613,7 @@
|
|||||||
<string name="preference_search_supportedunits">支持的单位</string>
|
<string name="preference_search_supportedunits">支持的单位</string>
|
||||||
<string name="note_widget_conflict_description">链接的文件不为空,且其内容与此笔记的上次保存版本不匹配。您想保留哪个版本?</string>
|
<string name="note_widget_conflict_description">链接的文件不为空,且其内容与此笔记的上次保存版本不匹配。您想保留哪个版本?</string>
|
||||||
<string name="note_widget_file_read_error">错误读取注释</string>
|
<string name="note_widget_file_read_error">错误读取注释</string>
|
||||||
<string name="note_widget_file_read_error_description">无法读取链接文件。可能已被移动或删除。已从启动器的内部存储中恢复副本。如果您编辑笔记,链接文件可能会被覆盖</string>
|
<string name="note_widget_file_read_error_description">无法读取链接的文件,可能其已被移动或删除。已从启动器的存储中恢复副本。如果您编辑笔记,链接的文件可能会被覆盖。</string>
|
||||||
<string name="note_widget_file_write_error">保存笔记时出错</string>
|
<string name="note_widget_file_write_error">保存笔记时出错</string>
|
||||||
<string name="theme_color_scheme_autogenerate">从原色开始</string>
|
<string name="theme_color_scheme_autogenerate">从原色开始</string>
|
||||||
<string name="import_theme_error">无法读取所选文件。请确保您选择的是有效的主题文件 (*.kvtheme),并且该文件未损坏。</string>
|
<string name="import_theme_error">无法读取所选文件。请确保您选择的是有效的主题文件 (*.kvtheme),并且该文件未损坏。</string>
|
||||||
@@ -793,4 +793,63 @@
|
|||||||
<string name="preference_clock_widget_time_format_12h">12小时</string>
|
<string name="preference_clock_widget_time_format_12h">12小时</string>
|
||||||
<string name="preference_clock_widget_time_format_system">系统默认</string>
|
<string name="preference_clock_widget_time_format_system">系统默认</string>
|
||||||
<string name="missing_permission_call_contacts_settings">需要通话权限才能开始通话</string>
|
<string name="missing_permission_call_contacts_settings">需要通话权限才能开始通话</string>
|
||||||
|
<string name="import_theme_exists">已有的样式将会被覆盖</string>
|
||||||
|
<string name="preference_search_bar_style_transparent">透明</string>
|
||||||
|
<string name="preference_search_bar_style_hidden">隐藏</string>
|
||||||
|
<string name="preference_search_bar_style_solid">填充</string>
|
||||||
|
<string name="import_theme_contents">文件包含</string>
|
||||||
|
<string name="font_name_device_headline">标题字体</string>
|
||||||
|
<string name="theme_import_title">导入样式</string>
|
||||||
|
<string name="clock_variant_analog_ticks">刻度</string>
|
||||||
|
<string name="theme_export_title">导出样式</string>
|
||||||
|
<string name="preference_colors_high_contrast">高对比度</string>
|
||||||
|
<string name="preference_screen_shapes">形状</string>
|
||||||
|
<string name="preference_shapes_default">默认</string>
|
||||||
|
<string name="preference_shapes_rect">直角</string>
|
||||||
|
<string name="preference_shapes_extra_round">大圆角</string>
|
||||||
|
<string name="preference_shapes_base">基础形状</string>
|
||||||
|
<string name="preference_gesture_swipe_up">向上滑动</string>
|
||||||
|
<string name="gesture_action_widgets">组件页</string>
|
||||||
|
<string name="confirmation_delete_shapes_scheme">删除形状预设“%1$s”吗?</string>
|
||||||
|
<string name="bad_configuration_summary">你探索出了一套访问不了搜索和组件页的配置,使你无法进入设置 — 让启动器桌面变成了启动弃。</string>
|
||||||
|
<string name="preference_transparencies_default">默认</string>
|
||||||
|
<string name="preference_screen_transparencies">透明度</string>
|
||||||
|
<string name="preference_transparencies_semi_transparent">半透明</string>
|
||||||
|
<string name="confirmation_delete_transparencies_scheme">删除透明度预设“%1$s”吗?</string>
|
||||||
|
<string name="weather_condition_thunder">雷雨</string>
|
||||||
|
<string name="weather_condition_haze">雾霾</string>
|
||||||
|
<string name="preference_search_tasks">任务</string>
|
||||||
|
<string name="preference_launch_breezyweather_app">打开 Breezy Weather</string>
|
||||||
|
<string name="no_selection">未选</string>
|
||||||
|
<string name="save_as_file">保存为文件</string>
|
||||||
|
<string name="preference_breezyweather_integration">Breezy Weather</string>
|
||||||
|
<string name="theme_bundle_name">名称</string>
|
||||||
|
<string name="theme_bundle_author">作者</string>
|
||||||
|
<string name="provider_breezy">Breezy Weather</string>
|
||||||
|
<string name="preference_location_breezy">在 Breezy Weather 中管理位置</string>
|
||||||
|
<string name="preference_search_tasks_summary">搜索在 Tasks 应用中的任务</string>
|
||||||
|
<string name="new_theme_name">(新建)</string>
|
||||||
|
<string name="font_category_device_default">设备默认的</string>
|
||||||
|
<string name="music_widget_interactive_progress_bar">可交互进度条</string>
|
||||||
|
<string name="confirmation_delete_typography_scheme">删除字体预设“%1$s”吗?</string>
|
||||||
|
<string name="bad_configuration_title">恭喜!你给你自个儿锁里头了!</string>
|
||||||
|
<string name="missing_permission_tasks_integration">需要 Tasks 权限来接入 Tasks 应用</string>
|
||||||
|
<string name="preference_breezyweather_integration_instructions">要使用 Breezy Weather 作为天气数据源:\n\n1. 启用 Breezy Weather > 设置 > 微件与动态壁纸 > 发送天气数据到 Gadgetbridge 中的 %1$s\n\n2. 在 %1$s 中将 Breezy Weather 设为天气数据源</string>
|
||||||
|
<string name="action_install">安装</string>
|
||||||
|
<string name="preference_tasks_integration">Tasks</string>
|
||||||
|
<string name="preference_tasks_integration_ready">已成功接入 Tasks 并可以开始使用了。</string>
|
||||||
|
<string name="preference_tasks_integration_description">Tasks 是一个免费的开源 to-do 及提醒事项应用。如果已经安装,%1$s 可以显示并搜索在 Tasks 应用中的任务。</string>
|
||||||
|
<plurals name="calendar_widget_running_tasks">
|
||||||
|
<item quantity="other">+%1$d 个未完成任务</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="missing_permission_tasks_search_settings">需要 Tasks 应用权限来搜索任务</string>
|
||||||
|
<string name="preference_screen_typography">文字</string>
|
||||||
|
<string name="preference_typography_fonts">字体</string>
|
||||||
|
<string name="preference_breezyweather_integration_description">Breezy Weather 是一款免费的开源天气应用。安装后 %1$s 可将其作为天气数据的数据源。</string>
|
||||||
|
<string name="font_name_device_body">正文字体</string>
|
||||||
|
<string name="font_category_generic">常规字体</string>
|
||||||
|
<string name="preference_widgets_on_home_screen">直达组件</string>
|
||||||
|
<string name="preference_widgets_on_home_screen_summary">在主屏幕而不是组件页显示组件</string>
|
||||||
|
<string name="tag_empty_name">标签必须拥有一个名字。如果继续设为空,这个标签会被删除。</string>
|
||||||
|
<string name="preference_launch_tasks_app">打开 Tasks 应用</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<item quantity="other">米</item>
|
<item quantity="other">米</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="unit_kilometer">
|
<plurals name="unit_kilometer">
|
||||||
<item quantity="other">公里</item>
|
<item quantity="other">千米</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="unit_centimeter">
|
<plurals name="unit_centimeter">
|
||||||
<item quantity="other">厘米</item>
|
<item quantity="other">厘米</item>
|
||||||
@@ -119,28 +119,28 @@
|
|||||||
<item quantity="other">吉位字节</item>
|
<item quantity="other">吉位字节</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="unit_tebibyte">
|
<plurals name="unit_tebibyte">
|
||||||
<item quantity="other">万亿位字节</item>
|
<item quantity="other">太位字节</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="unit_kilobit">
|
<plurals name="unit_kilobit">
|
||||||
<item quantity="other">千比特</item>
|
<item quantity="other">千比特</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_kilobit_symbol">kbit</string>
|
<string name="unit_kilobit_symbol">kb</string>
|
||||||
<string name="unit_megabit_symbol">Mbit</string>
|
<string name="unit_megabit_symbol">Mb</string>
|
||||||
<plurals name="unit_megabit">
|
<plurals name="unit_megabit">
|
||||||
<item quantity="other">兆比特</item>
|
<item quantity="other">兆比特</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_gigabit_symbol">Gbit</string>
|
<string name="unit_gigabit_symbol">Gb</string>
|
||||||
<plurals name="unit_gigabit">
|
<plurals name="unit_gigabit">
|
||||||
<item quantity="other">吉比特</item>
|
<item quantity="other">吉比特</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_terabit_symbol">万亿比特</string>
|
<string name="unit_terabit_symbol">TB</string>
|
||||||
<plurals name="unit_terabit">
|
<plurals name="unit_terabit">
|
||||||
<item quantity="other">terabits</item>
|
<item quantity="other">太比特</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_meter_per_second_symbol">m/s</string>
|
<string name="unit_meter_per_second_symbol">m/s</string>
|
||||||
<string name="unit_kilometer_per_hour_symbol">km/h</string>
|
<string name="unit_kilometer_per_hour_symbol">km/h</string>
|
||||||
<plurals name="unit_kilometer_per_hour">
|
<plurals name="unit_kilometer_per_hour">
|
||||||
<item quantity="other">公里每小时</item>
|
<item quantity="other">千米每小时</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_mile_per_hour_symbol">mph</string>
|
<string name="unit_mile_per_hour_symbol">mph</string>
|
||||||
<plurals name="unit_mile_per_hour">
|
<plurals name="unit_mile_per_hour">
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_metric_ton_symbol">t</string>
|
<string name="unit_metric_ton_symbol">t</string>
|
||||||
<plurals name="unit_metric_ton">
|
<plurals name="unit_metric_ton">
|
||||||
<item quantity="other">公吨</item>
|
<item quantity="other">吨</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_long_ton_symbol">tn.l.</string>
|
<string name="unit_long_ton_symbol">tn.l.</string>
|
||||||
<plurals name="unit_long_ton">
|
<plurals name="unit_long_ton">
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
<item quantity="other">开</item>
|
<item quantity="other">开</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="unit_terabyte">
|
<plurals name="unit_terabyte">
|
||||||
<item quantity="other">万亿字节</item>
|
<item quantity="other">太字节</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="unit_kibibyte_symbol">kiB</string>
|
<string name="unit_kibibyte_symbol">kiB</string>
|
||||||
<plurals name="unit_kibibyte">
|
<plurals name="unit_kibibyte">
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<string name="wind_west_north_west">西北偏西</string>
|
<string name="wind_west_north_west">西北偏西</string>
|
||||||
<string name="wind_north_west">西北</string>
|
<string name="wind_north_west">西北</string>
|
||||||
<string name="wind_north_north_west">西北偏北</string>
|
<string name="wind_north_north_west">西北偏北</string>
|
||||||
<string name="wikipedia_url">https://en.wikipedia.org</string>
|
<string name="wikipedia_url">https://zh.wikipedia.org/zh-tw/</string>
|
||||||
<string name="wikipedia_source">來自維基百科</string>
|
<string name="wikipedia_source">來自維基百科</string>
|
||||||
<string name="file_meta_title">標題:%1$s</string>
|
<string name="file_meta_title">標題:%1$s</string>
|
||||||
<string name="file_meta_artist">藝術家:%1$s</string>
|
<string name="file_meta_artist">藝術家:%1$s</string>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
<string name="search_filter_online">線上結果</string>
|
<string name="search_filter_online">線上結果</string>
|
||||||
<string name="search_filter_apps">應用程式</string>
|
<string name="search_filter_apps">應用程式</string>
|
||||||
<string name="preference_clock_widget_alignment_center">中間</string>
|
<string name="preference_clock_widget_alignment_center">中間</string>
|
||||||
<string name="preference_clock_widget_show_seconds">顯示秒</string>
|
<string name="preference_clock_widget_show_seconds">顯示秒數</string>
|
||||||
<string name="preference_clock_widget_alignment_bottom">底部</string>
|
<string name="preference_clock_widget_alignment_bottom">底部</string>
|
||||||
<string name="preference_clockwidget_dynamic_zone">動態區域</string>
|
<string name="preference_clockwidget_dynamic_zone">動態區域</string>
|
||||||
<string name="preference_customize_filter_bar">自訂篩選器列</string>
|
<string name="preference_customize_filter_bar">自訂篩選器列</string>
|
||||||
@@ -845,4 +845,9 @@
|
|||||||
<string name="preference_gesture_swipe_up">向上滑動</string>
|
<string name="preference_gesture_swipe_up">向上滑動</string>
|
||||||
<string name="gesture_action_widgets">小工具</string>
|
<string name="gesture_action_widgets">小工具</string>
|
||||||
<string name="preference_launch_tasks_app">開啟 Tasks 應用程式</string>
|
<string name="preference_launch_tasks_app">開啟 Tasks 應用程式</string>
|
||||||
|
<string name="preference_search_bar_style_solid">不透明</string>
|
||||||
|
<string name="preference_search_bar_style_hidden">隱藏</string>
|
||||||
|
<string name="preference_search_bar_style_transparent">透明</string>
|
||||||
|
<string name="clock_variant_analog_ticks">時鐘標記</string>
|
||||||
|
<string name="music_widget_interactive_progress_bar">互動式播放器進度條</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -249,7 +249,7 @@ internal class AppRepositoryImpl(
|
|||||||
appResults.addAll(apps.mapNotNull {
|
appResults.addAll(apps.mapNotNull {
|
||||||
val score = ResultScore(
|
val score = ResultScore(
|
||||||
query = query,
|
query = query,
|
||||||
primaryFields = listOf(it.label),
|
primaryFields = listOf(it.label,it.componentName.packageName),
|
||||||
)
|
)
|
||||||
if (score.score < 0.8f) return@mapNotNull null
|
if (score.score < 0.8f) return@mapNotNull null
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|||||||
@@ -54,5 +54,6 @@ dependencies {
|
|||||||
implementation(project(":core:ktx"))
|
implementation(project(":core:ktx"))
|
||||||
implementation(project(":core:crashreporter"))
|
implementation(project(":core:crashreporter"))
|
||||||
implementation(project(":core:preferences"))
|
implementation(project(":core:preferences"))
|
||||||
|
implementation(project(":core:profiles"))
|
||||||
|
|
||||||
}
|
}
|
||||||
+5
-2
@@ -13,6 +13,7 @@ import de.mm20.launcher2.ktx.normalize
|
|||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
||||||
|
import de.mm20.launcher2.profiles.ProfileManager
|
||||||
import de.mm20.launcher2.search.AppShortcut
|
import de.mm20.launcher2.search.AppShortcut
|
||||||
import de.mm20.launcher2.search.ResultScore
|
import de.mm20.launcher2.search.ResultScore
|
||||||
import de.mm20.launcher2.search.SearchableRepository
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
@@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -54,6 +56,7 @@ internal class AppShortcutRepositoryImpl(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val permissionsManager: PermissionsManager,
|
private val permissionsManager: PermissionsManager,
|
||||||
private val settings: ShortcutSearchSettings,
|
private val settings: ShortcutSearchSettings,
|
||||||
|
private val profileManager: ProfileManager,
|
||||||
) : AppShortcutRepository {
|
) : AppShortcutRepository {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||||
@@ -213,9 +216,9 @@ internal class AppShortcutRepositoryImpl(
|
|||||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
if (!launcherApps.hasShortcutHostPermission()) return emptyList()
|
if (!launcherApps.hasShortcutHostPermission()) return emptyList()
|
||||||
val results = mutableListOf<AppShortcutConfigActivity>()
|
val results = mutableListOf<AppShortcutConfigActivity>()
|
||||||
val profiles = launcherApps.profiles
|
val profiles = profileManager.activeProfiles.first()
|
||||||
for (profile in profiles) {
|
for (profile in profiles) {
|
||||||
val activities = launcherApps.getShortcutConfigActivityList(null, profile)
|
val activities = launcherApps.getShortcutConfigActivityList(null, profile.userHandle)
|
||||||
results.addAll(
|
results.addAll(
|
||||||
activities.map {
|
activities.map {
|
||||||
AppShortcutConfigActivity(it)
|
AppShortcutConfigActivity(it)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.koin.core.qualifier.named
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val appShortcutsModule = module {
|
val appShortcutsModule = module {
|
||||||
factory<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get(), get()) }
|
factory<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||||
factory<SearchableRepository<AppShortcut>>(named<AppShortcut>()) { get<AppShortcutRepository>() }
|
factory<SearchableRepository<AppShortcut>>(named<AppShortcut>()) { get<AppShortcutRepository>() }
|
||||||
factory<SearchableDeserializer>(named(LauncherShortcut.Domain)) { LauncherShortcutDeserializer(androidContext()) }
|
factory<SearchableDeserializer>(named(LauncherShortcut.Domain)) { LauncherShortcutDeserializer(androidContext()) }
|
||||||
factory<SearchableDeserializer>(named(LegacyShortcut.Domain)) { LegacyShortcutDeserializer(androidContext()) }
|
factory<SearchableDeserializer>(named(LegacyShortcut.Domain)) { LegacyShortcutDeserializer(androidContext()) }
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ internal class ContactRepository(
|
|||||||
override fun search(query: String, allowNetwork: Boolean): Flow<List<Contact>> {
|
override fun search(query: String, allowNetwork: Boolean): Flow<List<Contact>> {
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 1) {
|
||||||
return flow {
|
return flow {
|
||||||
emit(persistentListOf())
|
emit(persistentListOf())
|
||||||
}
|
}
|
||||||
|
|||||||
+579
@@ -0,0 +1,579 @@
|
|||||||
|
package de.mm20.launcher2.locations.providers.googleapi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.ktx.ifNullOrEmpty
|
||||||
|
import de.mm20.launcher2.ktx.into
|
||||||
|
import de.mm20.launcher2.ktx.map
|
||||||
|
import de.mm20.launcher2.ktx.stripStartOrNull
|
||||||
|
import de.mm20.launcher2.locations.OsmLocationSerializer
|
||||||
|
import de.mm20.launcher2.openstreetmaps.R
|
||||||
|
import de.mm20.launcher2.search.Location
|
||||||
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
|
import de.mm20.launcher2.search.UpdatableSearchable
|
||||||
|
import de.mm20.launcher2.search.UpdateResult
|
||||||
|
import de.mm20.launcher2.search.location.Address
|
||||||
|
import de.mm20.launcher2.search.location.Departure
|
||||||
|
import de.mm20.launcher2.search.location.LocationIcon
|
||||||
|
import de.mm20.launcher2.search.location.OpeningHours
|
||||||
|
import de.mm20.launcher2.search.location.OpeningSchedule
|
||||||
|
import de.mm20.launcher2.search.location.PaymentMethod
|
||||||
|
import de.westnordost.osm_opening_hours.model.ClockTime
|
||||||
|
import de.westnordost.osm_opening_hours.model.ExtendedClockTime
|
||||||
|
import de.westnordost.osm_opening_hours.model.LastNth
|
||||||
|
import de.westnordost.osm_opening_hours.model.MonthRange
|
||||||
|
import de.westnordost.osm_opening_hours.model.Nth
|
||||||
|
import de.westnordost.osm_opening_hours.model.NthRange
|
||||||
|
import de.westnordost.osm_opening_hours.model.Range
|
||||||
|
import de.westnordost.osm_opening_hours.model.SingleMonth
|
||||||
|
import de.westnordost.osm_opening_hours.model.SpecificWeekdays
|
||||||
|
import de.westnordost.osm_opening_hours.model.StartingAtYear
|
||||||
|
import de.westnordost.osm_opening_hours.model.TimeSpan
|
||||||
|
import de.westnordost.osm_opening_hours.model.TimesSelector
|
||||||
|
import de.westnordost.osm_opening_hours.model.TwentyFourSeven
|
||||||
|
import de.westnordost.osm_opening_hours.model.Weekday
|
||||||
|
import de.westnordost.osm_opening_hours.model.WeekdayRange
|
||||||
|
import de.westnordost.osm_opening_hours.model.WeekdaysSelector
|
||||||
|
import de.westnordost.osm_opening_hours.model.Year
|
||||||
|
import de.westnordost.osm_opening_hours.model.YearRange
|
||||||
|
import de.westnordost.osm_opening_hours.parser.toOpeningHoursOrNull
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import org.woheller69.AndroidAddressFormatter.OsmAddressFormatter
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.time.temporal.TemporalAdjusters
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
internal data class OsmLocation(
|
||||||
|
internal val id: Long,
|
||||||
|
override val label: String,
|
||||||
|
override val icon: LocationIcon?,
|
||||||
|
override val category: String?,
|
||||||
|
override val latitude: Double,
|
||||||
|
override val longitude: Double,
|
||||||
|
override val address: Address?,
|
||||||
|
override val openingSchedule: OpeningSchedule?,
|
||||||
|
override val websiteUrl: String?,
|
||||||
|
override val phoneNumber: String?,
|
||||||
|
override val emailAddress: String? = null,
|
||||||
|
override val labelOverride: String? = null,
|
||||||
|
override val timestamp: Long,
|
||||||
|
override var updatedSelf: (suspend (SavableSearchable) -> UpdateResult<Location>)? = null,
|
||||||
|
override val userRating: Float?,
|
||||||
|
override val acceptedPaymentMethods: Map<PaymentMethod, Boolean>?
|
||||||
|
) : Location, UpdatableSearchable<Location> {
|
||||||
|
|
||||||
|
override val domain: String
|
||||||
|
get() = DOMAIN
|
||||||
|
override val key: String = "$domain://$id"
|
||||||
|
override val fixMeUrl: String
|
||||||
|
get() = FIXMEURL
|
||||||
|
|
||||||
|
override val userRatingCount: Int? = null
|
||||||
|
override val departures: List<Departure>? = null
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): OsmLocation {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSerializer(): SearchableSerializer {
|
||||||
|
return OsmLocationSerializer()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
internal const val DOMAIN = "osm"
|
||||||
|
internal const val FIXMEURL = "https://www.openstreetmap.org/fixthemap"
|
||||||
|
|
||||||
|
internal val addressFormatter =
|
||||||
|
OsmAddressFormatter(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun fromOverpassResponse(
|
||||||
|
result: OverpassResponse,
|
||||||
|
context: Context
|
||||||
|
): List<OsmLocation> = result.elements.mapNotNull {
|
||||||
|
it.tags ?: return@mapNotNull null
|
||||||
|
val (category, icon) = it.tags.categorize(context)
|
||||||
|
icon ?: return@mapNotNull null
|
||||||
|
OsmLocation(
|
||||||
|
id = it.id,
|
||||||
|
label = it.tags["name"] ?: it.tags["brand"] ?: return@mapNotNull null,
|
||||||
|
icon = icon,
|
||||||
|
category = category,
|
||||||
|
latitude = it.lat ?: it.center?.lat ?: return@mapNotNull null,
|
||||||
|
longitude = it.lon ?: it.center?.lon ?: return@mapNotNull null,
|
||||||
|
address = it.tags.toAddress(),
|
||||||
|
openingSchedule = it.tags["opening_hours"]?.let { ot -> parseOpeningSchedule(ot) },
|
||||||
|
websiteUrl = it.tags["website"] ?: it.tags["contact:website"],
|
||||||
|
phoneNumber = it.tags["phone"] ?: it.tags["contact:phone"],
|
||||||
|
emailAddress = it.tags["email"] ?: it.tags["contact:email"],
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
userRating = it.tags["stars"]?.runCatching { this.toInt() }?.getOrNull()
|
||||||
|
?.let { min(it, 5) / 5.0f },
|
||||||
|
acceptedPaymentMethods = with(
|
||||||
|
it.tags.mapNotNull { (key, value) ->
|
||||||
|
(key.stripStartOrNull("payment:") ?: return@mapNotNull null) to value
|
||||||
|
}.toMap()
|
||||||
|
) {
|
||||||
|
// best-effort way to take any method payment as it being available,
|
||||||
|
// otherwise as being unavailable, or undefined
|
||||||
|
mapOf(
|
||||||
|
PaymentMethod.Card to listOf("credit_cards", "debit_cards", "cards"),
|
||||||
|
PaymentMethod.Cash to listOf("cash")
|
||||||
|
).mapNotNull { (method, values) ->
|
||||||
|
when {
|
||||||
|
values.any { this[it] in listOf("yes", "only") } -> method to true
|
||||||
|
values.any { this[it] == "no" } -> method to false
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.toMap().takeUnless { it.isEmpty() }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, String>.firstOfAlso(vararg strs: String, also: (String) -> Unit): String? {
|
||||||
|
for (str in strs) {
|
||||||
|
if (str in this) {
|
||||||
|
also(str)
|
||||||
|
return this[str]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, String>.toAddress(): Address? {
|
||||||
|
val formatAddrKeys = this.keys.filter { it.contains("addr") }.toMutableSet()
|
||||||
|
if (formatAddrKeys.isEmpty()) return null
|
||||||
|
|
||||||
|
val addr = Address(
|
||||||
|
city = firstOfAlso("addr:city", "addr:suburb", "addr:hamlet") { formatAddrKeys.remove(it) },
|
||||||
|
state = firstOfAlso("addr:state", "addr:province") { formatAddrKeys.remove(it) },
|
||||||
|
postalCode = firstOfAlso("addr:postcode") { formatAddrKeys.remove(it) },
|
||||||
|
country = firstOfAlso("addr:country") { formatAddrKeys.remove(it) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val formattedRest = buildJsonObject {
|
||||||
|
formatAddrKeys.mapNotNull {
|
||||||
|
val (_, subkey) = it.split(':', limit = 2).takeIf { it.size == 2 }
|
||||||
|
?: return@mapNotNull null
|
||||||
|
put(subkey, this@toAddress[it])
|
||||||
|
}
|
||||||
|
}.takeIf { it.isNotEmpty() }?.toString()?.runCatching {
|
||||||
|
OsmLocation.addressFormatter.format(
|
||||||
|
this,
|
||||||
|
this@toAddress["addr:country"] ?: Locale.getDefault().country
|
||||||
|
)
|
||||||
|
}?.getOrNull() ?: return addr
|
||||||
|
|
||||||
|
val lines = formattedRest.lines().filter { it.isNotBlank() }
|
||||||
|
return addr.copy(
|
||||||
|
address = lines.getOrNull(0),
|
||||||
|
address2 = lines.getOrNull(1),
|
||||||
|
address3 = lines.getOrNull(2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MatchAnyReceiverScope<T, A, B> {
|
||||||
|
private val pairs = mutableMapOf<T, Pair<A, B>>()
|
||||||
|
operator fun get(key: T): Pair<A, B>? = pairs[key]
|
||||||
|
infix fun T.with(pair: Pair<A, B>) = pairs.put(this, pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <A, B> Map<String, String>.matchAnyTag(
|
||||||
|
key: String,
|
||||||
|
block: MatchAnyReceiverScope<String, A, B>.() -> Unit
|
||||||
|
): Pair<A, B>? {
|
||||||
|
val scope = MatchAnyReceiverScope<String, A, B>()
|
||||||
|
scope.block()
|
||||||
|
return this[key]?.split(' ', ',', '.', ';')?.map { it.trim() }
|
||||||
|
?.firstNotNullOfOrNull { scope[it] }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, String>.categorize(context: Context): Pair<String?, LocationIcon?> {
|
||||||
|
val category = this.firstNotNullOfOrNull { (tag, value) ->
|
||||||
|
val values = value.split(' ', ',', '.', ';').map { it.trim() }.toSet()
|
||||||
|
when (tag.lowercase()) {
|
||||||
|
|
||||||
|
"shop" -> values.firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
"florist" -> R.string.poi_category_florist to LocationIcon.Florist
|
||||||
|
"kiosk" -> R.string.poi_category_kiosk to LocationIcon.Kiosk
|
||||||
|
"furniture" -> R.string.poi_category_furniture to LocationIcon.FurnitureStore
|
||||||
|
"cell_phones", "mobile_phone" -> R.string.poi_category_mobile_phone to LocationIcon.CellPhoneStore
|
||||||
|
"books" -> R.string.poi_category_books to LocationIcon.BookStore
|
||||||
|
"clothes" -> R.string.poi_category_clothes to LocationIcon.ClothingStore
|
||||||
|
"convenience" -> R.string.poi_category_convenience to LocationIcon.ConvenienceStore
|
||||||
|
"discount" -> R.string.poi_category_discount_store to LocationIcon.DiscountStore
|
||||||
|
"jewelry" -> R.string.poi_category_jewelry to LocationIcon.JewelryStore
|
||||||
|
"alcohol" -> R.string.poi_category_alcohol to LocationIcon.LiquorStore
|
||||||
|
"pet", "pet_grooming" -> R.string.poi_category_pet to LocationIcon.PetStore
|
||||||
|
"mall", "shopping_centre", "department_store" -> R.string.poi_category_mall to LocationIcon.ShoppingMall
|
||||||
|
"supermarket" -> R.string.poi_category_supermarket to LocationIcon.Supermarket
|
||||||
|
"bakery" -> R.string.poi_category_bakery to LocationIcon.Bakery
|
||||||
|
"optician" -> R.string.poi_category_optician to LocationIcon.Optician
|
||||||
|
"hairdresser" -> R.string.poi_category_hairdresser to LocationIcon.HairSalon
|
||||||
|
"laundry" -> R.string.poi_category_laundry to LocationIcon.Laundromat
|
||||||
|
|
||||||
|
else -> R.string.poi_category_shopping to LocationIcon.Shopping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"amenity" -> values.firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
"place_of_worship" -> matchAnyTag<Int, LocationIcon>("religion") {
|
||||||
|
"christian" with (R.string.poi_category_church to LocationIcon.Church)
|
||||||
|
"muslim" with (R.string.poi_category_mosque to LocationIcon.Mosque)
|
||||||
|
"buddhist" with (R.string.poi_category_buddhist_temple to LocationIcon.BuddhistTemple)
|
||||||
|
"hindu" with (R.string.poi_category_hindu_temple to LocationIcon.HinduTemple)
|
||||||
|
"jewish" with (R.string.poi_category_synagogue to LocationIcon.Synagogue)
|
||||||
|
} ?: (R.string.poi_category_place_of_worship to LocationIcon.PlaceOfWorship)
|
||||||
|
|
||||||
|
"fast_food" -> R.string.poi_category_fast_food to LocationIcon.FastFood
|
||||||
|
"cafe" -> R.string.poi_category_cafe to LocationIcon.Cafe
|
||||||
|
"ice_cream" -> R.string.poi_category_ice_cream to LocationIcon.IceCream
|
||||||
|
"bar" -> R.string.poi_category_bar to LocationIcon.Bar
|
||||||
|
"pub" -> R.string.poi_category_pub to LocationIcon.Pub
|
||||||
|
"restaurant" -> matchAnyTag<Int, LocationIcon>("cuisine") {
|
||||||
|
"pizza" with (R.string.poi_category_pizza_restaurant to LocationIcon.Pizza)
|
||||||
|
"burger" with (R.string.poi_category_burger_restaurant to LocationIcon.Burger)
|
||||||
|
"chinese" with (R.string.poi_category_chinese_restaurant to LocationIcon.Ramen)
|
||||||
|
"ramen" with (R.string.poi_category_ramen_restaurant to LocationIcon.Ramen)
|
||||||
|
"japanese" with (R.string.poi_category_japanese_restaurant to LocationIcon.JapaneseCuisine)
|
||||||
|
"kebab" with (R.string.poi_category_kebab_restaurant to LocationIcon.Kebab)
|
||||||
|
"asian" with (R.string.poi_category_asian_restaurant to LocationIcon.AsianCuisine)
|
||||||
|
"soup" with (R.string.poi_category_soup_restaurant to LocationIcon.Soup)
|
||||||
|
"coffee_shop" with (R.string.poi_category_cafe to LocationIcon.Cafe)
|
||||||
|
"brunch" with (R.string.poi_category_brunch_restaurant to LocationIcon.Brunch)
|
||||||
|
} ?: (R.string.poi_category_restaurant to LocationIcon.Restaurant)
|
||||||
|
|
||||||
|
"fuel" -> R.string.poi_category_fuel to LocationIcon.GasStation
|
||||||
|
"car_rental" -> R.string.poi_category_car_rental to LocationIcon.CarRental
|
||||||
|
"car_sharing" -> R.string.poi_category_car_sharing to LocationIcon.CarRental
|
||||||
|
"car_wash" -> R.string.poi_category_car_wash to LocationIcon.CarWash
|
||||||
|
"charging_station" -> R.string.poi_category_charging_station to LocationIcon.ChargingStation
|
||||||
|
"parking", "parking_space", "motorcycle_parking" -> R.string.poi_category_parking to LocationIcon.Parking
|
||||||
|
"motorcycle_rental" -> R.string.poi_category_motorcycle_rental to LocationIcon.Motorcycle
|
||||||
|
|
||||||
|
"theatre" -> R.string.poi_category_theater to LocationIcon.Theater
|
||||||
|
"cinema" -> R.string.poi_category_cinema to LocationIcon.MovieTheater
|
||||||
|
"nightclub" -> R.string.poi_category_nightclub to LocationIcon.NightClub
|
||||||
|
"concert_hall" -> R.string.poi_category_concert_hall to LocationIcon.ConcertHall
|
||||||
|
"casino" -> R.string.poi_category_casino to LocationIcon.Casino
|
||||||
|
|
||||||
|
"pharmacy" -> R.string.poi_category_pharmacy to LocationIcon.Pharmacy
|
||||||
|
"bank" -> R.string.poi_category_bank to LocationIcon.Bank
|
||||||
|
"atm" -> R.string.poi_category_atm to LocationIcon.Atm
|
||||||
|
"doctors" -> R.string.poi_category_doctors to LocationIcon.Physician
|
||||||
|
"dentist" -> R.string.poi_category_dentist to LocationIcon.Dentist
|
||||||
|
"hospital" -> R.string.poi_category_hospital to LocationIcon.Hospital
|
||||||
|
"clinic" -> R.string.poi_category_clinic to LocationIcon.Clinic
|
||||||
|
|
||||||
|
"police" -> R.string.poi_category_police to LocationIcon.Police
|
||||||
|
"fire_station" -> R.string.poi_category_fire_station to LocationIcon.FireDepartment
|
||||||
|
"courthouse" -> R.string.poi_category_courthouse to LocationIcon.Courthouse
|
||||||
|
"post_office" -> R.string.poi_category_post_office to LocationIcon.PostOffice
|
||||||
|
"library" -> R.string.poi_category_library to LocationIcon.Library
|
||||||
|
"school" -> R.string.poi_category_school to LocationIcon.School
|
||||||
|
"university" -> R.string.poi_category_university to LocationIcon.University
|
||||||
|
"toilets" -> R.string.poi_category_toilets to LocationIcon.PublicBathroom
|
||||||
|
"townhall" -> R.string.poi_category_townhall to LocationIcon.GovernmentBuilding
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"tourism" -> values.firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
"gallery" -> R.string.poi_category_gallery to LocationIcon.ArtGallery
|
||||||
|
"museum" -> R.string.poi_category_museum to LocationIcon.Museum
|
||||||
|
"theme_park" -> R.string.poi_category_amusement_park to LocationIcon.AmusementPark
|
||||||
|
"hotel" -> R.string.poi_category_hotel to LocationIcon.Hotel
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"leisure" -> values.firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
"stadium" -> R.string.poi_category_stadium to LocationIcon.Stadium
|
||||||
|
"fitness_centre" -> R.string.poi_category_fitness_center to LocationIcon.FitnessCenter
|
||||||
|
"swimming_pool" -> R.string.poi_category_swimming to LocationIcon.Swimming
|
||||||
|
"pitch", "sports_centre" -> matchAnyTag<Int, LocationIcon>("sport") {
|
||||||
|
"soccer" with (R.string.poi_category_soccer to LocationIcon.Soccer)
|
||||||
|
"tennis" with (R.string.poi_category_tennis to LocationIcon.Tennis)
|
||||||
|
"basketball" with (R.string.poi_category_basketball to LocationIcon.Basketball)
|
||||||
|
"gymnastics" with (R.string.poi_category_gymnastics to LocationIcon.Gymnastics)
|
||||||
|
"martial_arts" with (R.string.poi_category_martial_arts to LocationIcon.MartialArts)
|
||||||
|
"ice_hockey" with (R.string.poi_category_ice_hockey to LocationIcon.Hockey)
|
||||||
|
"baseball" with (R.string.poi_category_baseball to LocationIcon.Baseball)
|
||||||
|
"american_football" with (R.string.poi_category_american_football to LocationIcon.AmericanFootball)
|
||||||
|
"handball" with (R.string.poi_category_handball to LocationIcon.Handball)
|
||||||
|
"volleyball" with (R.string.poi_category_volleyball to LocationIcon.Volleyball)
|
||||||
|
"skiing" with (R.string.poi_category_skiing to LocationIcon.Skiing)
|
||||||
|
"cricket" with (R.string.poi_category_cricket to LocationIcon.Cricket)
|
||||||
|
}
|
||||||
|
|
||||||
|
"golf_course" -> R.string.poi_category_golf to LocationIcon.Golf
|
||||||
|
"park" -> R.string.poi_category_park to LocationIcon.Park
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"historic" -> values.firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
"monument" -> R.string.poi_category_monument to LocationIcon.Monument
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"building" -> values.firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
"government" -> R.string.poi_category_government_building to LocationIcon.GovernmentBuilding
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val (rid, icon) = category ?: return null to null
|
||||||
|
return context.resources.getString(rid) to icon
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun parseOpeningSchedule(
|
||||||
|
openingHours: String?,
|
||||||
|
localTime: LocalDateTime = LocalDateTime.now()
|
||||||
|
): OpeningSchedule? {
|
||||||
|
val parsed = openingHours?.toOpeningHoursOrNull(lenient = true) ?: return null
|
||||||
|
|
||||||
|
if (parsed.rules.singleOrNull()?.selector is TwentyFourSeven) {
|
||||||
|
return OpeningSchedule.TwentyFourSeven
|
||||||
|
}
|
||||||
|
|
||||||
|
val rangeRules = parsed.rules.mapNotNull { it.selector as? Range }
|
||||||
|
|
||||||
|
// Group rules by the weekdays they apply to. Rules can apply to multiple weekdays.
|
||||||
|
val rulesMap = mutableMapOf<Weekday, MutableList<Range>>(
|
||||||
|
Weekday.Monday to mutableListOf(),
|
||||||
|
Weekday.Tuesday to mutableListOf(),
|
||||||
|
Weekday.Wednesday to mutableListOf(),
|
||||||
|
Weekday.Thursday to mutableListOf(),
|
||||||
|
Weekday.Friday to mutableListOf(),
|
||||||
|
Weekday.Saturday to mutableListOf(),
|
||||||
|
Weekday.Sunday to mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
for (rule in rangeRules) {
|
||||||
|
if (rule.weekdays != null) {
|
||||||
|
for (selector in rule.weekdays!!) {
|
||||||
|
when (selector) {
|
||||||
|
is Weekday -> rulesMap[selector]!!.add(rule)
|
||||||
|
is WeekdayRange -> {
|
||||||
|
for (weekday in selector.start.ordinal..selector.end.ordinal) {
|
||||||
|
rulesMap[Weekday.entries[weekday]]!!.add(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is SpecificWeekdays -> {
|
||||||
|
rulesMap[selector.weekday]!!.add(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!rule.holidays.isNullOrEmpty()) {
|
||||||
|
continue // skip PH and SH entries
|
||||||
|
} else if (!rule.times.isNullOrEmpty() || !rule.months.isNullOrEmpty()) {
|
||||||
|
rulesMap.forEach { _, it ->
|
||||||
|
it.add(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out rules that are not valid for the current year, month, and week.
|
||||||
|
val applicableRules = rulesMap.flatMap { (day, rules) ->
|
||||||
|
rules.filterYears(localTime)
|
||||||
|
.filterMonths(localTime)
|
||||||
|
.filterNthDays(localTime)
|
||||||
|
.map { it.copy(weekdays = listOf(day)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val hours = mutableSetOf<OpeningHours>()
|
||||||
|
|
||||||
|
for (range in applicableRules) {
|
||||||
|
|
||||||
|
val localTimesWithDuration =
|
||||||
|
range.times?.mapNotNull { it.toLocalTimeWithDuration() } ?: continue
|
||||||
|
val daysOfWeek = range.weekdays
|
||||||
|
.ifNullOrEmpty { Weekday.entries.toList() }
|
||||||
|
.flatMap { it.toDaysOfWeek() }
|
||||||
|
|
||||||
|
hours += daysOfWeek.flatMap { dow ->
|
||||||
|
localTimesWithDuration.map {
|
||||||
|
val (start, dur) = it
|
||||||
|
OpeningHours(dow, start, dur)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OpeningSchedule.Hours(hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only the rules that are valid for the given year.
|
||||||
|
*/
|
||||||
|
private fun List<Range>.filterYears(localTime: LocalDateTime): List<Range> {
|
||||||
|
if (all { it.years.isNullOrEmpty() }) return this
|
||||||
|
|
||||||
|
val thisYear = filter {
|
||||||
|
it.years?.any {
|
||||||
|
when (it) {
|
||||||
|
is Year -> it.year == localTime.year
|
||||||
|
is StartingAtYear -> it.start <= localTime.year
|
||||||
|
is YearRange -> localTime.year in it.start..it.end step (it.step ?: 1)
|
||||||
|
}
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thisYear.isEmpty()) return thisYear
|
||||||
|
|
||||||
|
return filter { it.years.isNullOrEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only the rules that are valid for the given month.
|
||||||
|
*/
|
||||||
|
private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> {
|
||||||
|
if (all { it.months.isNullOrEmpty() }) return this
|
||||||
|
|
||||||
|
val thisMonth = filter {
|
||||||
|
it.months?.any {
|
||||||
|
when (it) {
|
||||||
|
is MonthRange -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal in it.start.ordinal..it.end.ordinal
|
||||||
|
|
||||||
|
is SingleMonth -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal == it.month.ordinal
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thisMonth.isEmpty()) return thisMonth
|
||||||
|
|
||||||
|
return filter { it.months.isNullOrEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only the rules that are valid for the given week.
|
||||||
|
* (i.e. if the given week is the 2nd week of the month, it will return only the rules that are
|
||||||
|
* valid for the 2nd week of the month)
|
||||||
|
*/
|
||||||
|
private fun List<Range>.filterNthDays(localTime: LocalDateTime): List<Range> {
|
||||||
|
if (none { it.weekdays?.any { it is SpecificWeekdays } == true }) return this
|
||||||
|
|
||||||
|
val currentWeek = localTime.getNthWeekdaysOfCurrentWeek()
|
||||||
|
|
||||||
|
val specific = mapNotNull { range ->
|
||||||
|
(range.weekdays?.singleOrNull() as? SpecificWeekdays)?.let {
|
||||||
|
range to it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rule = specific.firstOrNull { (_, specific) ->
|
||||||
|
currentWeek.any { (dow, nthFwd, nthBwd) ->
|
||||||
|
specific.weekday.ordinal == dow.ordinal && specific.nths.any {
|
||||||
|
when (it) {
|
||||||
|
is Nth -> it.nth == nthFwd
|
||||||
|
is NthRange -> nthFwd in it.start..it.end
|
||||||
|
is LastNth -> it.nth == nthBwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule != null) return listOf(rule.first)
|
||||||
|
|
||||||
|
return listOfNotNull(
|
||||||
|
singleOrNull { it.weekdays?.singleOrNull() !is SpecificWeekdays }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> {
|
||||||
|
return when (this) {
|
||||||
|
is Weekday -> listOf(
|
||||||
|
when (this) {
|
||||||
|
Weekday.Monday -> DayOfWeek.MONDAY
|
||||||
|
Weekday.Tuesday -> DayOfWeek.TUESDAY
|
||||||
|
Weekday.Wednesday -> DayOfWeek.WEDNESDAY
|
||||||
|
Weekday.Thursday -> DayOfWeek.THURSDAY
|
||||||
|
Weekday.Friday -> DayOfWeek.FRIDAY
|
||||||
|
Weekday.Saturday -> DayOfWeek.SATURDAY
|
||||||
|
Weekday.Sunday -> DayOfWeek.SUNDAY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is WeekdayRange -> (start to end).map { it.toDaysOfWeek().single().value }
|
||||||
|
.into { start, end -> (start..end).map { DayOfWeek.of(it) } }
|
||||||
|
|
||||||
|
is SpecificWeekdays -> weekday.toDaysOfWeek()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun TimesSelector.toLocalTimeWithDuration(): Pair<LocalTime, Duration>? {
|
||||||
|
if (this !is TimeSpan) return null
|
||||||
|
|
||||||
|
val start = start as? ClockTime ?: return null
|
||||||
|
val end = end as? ExtendedClockTime ?: return null
|
||||||
|
|
||||||
|
return LocalTime.of(
|
||||||
|
start.hour,
|
||||||
|
start.minutes
|
||||||
|
) to Duration.ofMinutes(
|
||||||
|
(Math.floorMod(
|
||||||
|
end.hour - start.hour,
|
||||||
|
24
|
||||||
|
) * 60 + end.minutes - start.minutes).toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the ordinal position of each day of the current week (Monday to Sunday) within the month.
|
||||||
|
*
|
||||||
|
* @receiver LocalDateTime The current date and time.
|
||||||
|
* @return A list of triples, where each triple contains:
|
||||||
|
* - The `DayOfWeek` (e.g., Monday, Tuesday, etc.).
|
||||||
|
* - The forward ordinal position of the day in the month (e.g., 1st Monday, 2nd Tuesday, etc.).
|
||||||
|
* - The backward ordinal position of the day in the month (e.g., last Monday, 2nd last Tuesday, etc.).
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* If today is the 15th of a month (Wednesday), the function will return:
|
||||||
|
* - For Monday: `(DayOfWeek.MONDAY, 3, 2)` (3rd Monday of the month, 2nd last Monday of the month).
|
||||||
|
* - For Wednesday: `(DayOfWeek.WEDNESDAY, 3, 2)` (3rd Wednesday of the month, 2nd last Wednesday of the month).
|
||||||
|
*/
|
||||||
|
private fun LocalDateTime.getNthWeekdaysOfCurrentWeek(): List<Triple<DayOfWeek, Int, Int>> {
|
||||||
|
val monday = with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||||
|
return (0 until 7).map { i ->
|
||||||
|
val (nth, nthLast) = monday.plusDays(i.toLong()).let { weekday ->
|
||||||
|
(
|
||||||
|
ChronoUnit.WEEKS.between(
|
||||||
|
with(TemporalAdjusters.firstInMonth(weekday.dayOfWeek)),
|
||||||
|
weekday
|
||||||
|
).toInt() + 1
|
||||||
|
) to (
|
||||||
|
ChronoUnit.WEEKS.between(
|
||||||
|
weekday,
|
||||||
|
with(TemporalAdjusters.lastInMonth(weekday.dayOfWeek))
|
||||||
|
).toInt() + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Triple(DayOfWeek.entries[i], nth, nthLast)
|
||||||
|
}
|
||||||
|
}
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
package de.mm20.launcher2.locations.providers.googleapi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
|
import de.mm20.launcher2.locations.providers.AndroidLocation
|
||||||
|
import de.mm20.launcher2.locations.providers.LocationProvider
|
||||||
|
import de.mm20.launcher2.openstreetmaps.R
|
||||||
|
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||||
|
import de.mm20.launcher2.search.Location
|
||||||
|
import de.mm20.launcher2.search.ResultScore
|
||||||
|
import de.mm20.launcher2.search.UpdateResult
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
private val Scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
private val HttpClient = OkHttpClient()
|
||||||
|
|
||||||
|
internal class OsmLocationProvider(
|
||||||
|
private val context: Context,
|
||||||
|
settings: LocationSearchSettings
|
||||||
|
) : LocationProvider<Long> {
|
||||||
|
|
||||||
|
private val overpassApi = settings.overpassUrl.map {
|
||||||
|
try {
|
||||||
|
Retrofit.Builder()
|
||||||
|
.client(HttpClient)
|
||||||
|
.baseUrl(it?.takeIf { it.isNotBlank() }
|
||||||
|
?: LocationSearchSettings.DefaultOverpassUrl)
|
||||||
|
.addConverterFactory(OverpassQueryConverterFactory())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(OverpassApi::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
CrashReporter.logException(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.stateIn(Scope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun update(
|
||||||
|
id: Long
|
||||||
|
): UpdateResult<Location> = overpassApi.first()?.runCatching {
|
||||||
|
this.search(
|
||||||
|
OverpassIdQuery(
|
||||||
|
id = id
|
||||||
|
)
|
||||||
|
).let {
|
||||||
|
OsmLocation.fromOverpassResponse(it, context)
|
||||||
|
}.first().apply {
|
||||||
|
updatedSelf = { update(id) }
|
||||||
|
}
|
||||||
|
}?.fold(
|
||||||
|
onSuccess = { UpdateResult.Success(it) },
|
||||||
|
onFailure = {
|
||||||
|
when (it) {
|
||||||
|
is CancellationException, is UnknownHostException -> {
|
||||||
|
// network
|
||||||
|
UpdateResult.TemporarilyUnavailable(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
is HttpException -> when (it.code()) {
|
||||||
|
in 400..499 -> UpdateResult.PermanentlyUnavailable(it)
|
||||||
|
else -> UpdateResult.TemporarilyUnavailable(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NoSuchElementException -> {
|
||||||
|
// empty response
|
||||||
|
UpdateResult.PermanentlyUnavailable(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
if (it is Exception) {
|
||||||
|
CrashReporter.logException(it)
|
||||||
|
}
|
||||||
|
UpdateResult.TemporarilyUnavailable(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) ?: let {
|
||||||
|
Log.e("OsmProvider", "overpassApi was not initialized")
|
||||||
|
UpdateResult.TemporarilyUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(
|
||||||
|
query: String,
|
||||||
|
userLocation: AndroidLocation,
|
||||||
|
allowNetwork: Boolean,
|
||||||
|
searchRadiusMeters: Int,
|
||||||
|
hideUncategorized: Boolean,
|
||||||
|
): List<Location> {
|
||||||
|
if (!allowNetwork || query.length < 2) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
HttpClient.dispatcher.cancelAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
return overpassApi.first()?.runCatching {
|
||||||
|
search(
|
||||||
|
OverpassFuzzyRadiusQuery(
|
||||||
|
query = query,
|
||||||
|
tagGroups = delocalizeToQueryableTags(query),
|
||||||
|
radius = searchRadiusMeters,
|
||||||
|
latitude = userLocation.latitude,
|
||||||
|
longitude = userLocation.longitude
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}?.onFailure {
|
||||||
|
if (it !is HttpException && it !is CancellationException) {
|
||||||
|
Log.e("OsmLocationProvider", "Failed to search for: $query", it)
|
||||||
|
}
|
||||||
|
}?.getOrNull()?.let {
|
||||||
|
OsmLocation.fromOverpassResponse(it, context)
|
||||||
|
}?.asSequence()?.filter {
|
||||||
|
(!hideUncategorized || (it.category != null)) && it.distanceTo(userLocation) < searchRadiusMeters
|
||||||
|
}?.groupBy {
|
||||||
|
it.label.lowercase()
|
||||||
|
}?.flatMap { (_, duplicates) ->
|
||||||
|
// deduplicate results with same labels, if
|
||||||
|
// - same category
|
||||||
|
// - distance is less than 100m
|
||||||
|
if (duplicates.size < 2) duplicates
|
||||||
|
else {
|
||||||
|
val luckyFirst = duplicates.first()
|
||||||
|
duplicates
|
||||||
|
.drop(1)
|
||||||
|
.filter {
|
||||||
|
it.category != luckyFirst.category ||
|
||||||
|
it.distanceTo(luckyFirst) > 100.0
|
||||||
|
} + luckyFirst
|
||||||
|
}
|
||||||
|
}?.sortedBy {
|
||||||
|
it.distanceTo(userLocation)
|
||||||
|
}?.take(9)?.toImmutableList() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val poiCategories = mapOf(
|
||||||
|
R.string.poi_category_restaurant to "amenity=restaurant",
|
||||||
|
R.string.poi_category_fast_food to "amenity=fast_food",
|
||||||
|
R.string.poi_category_bar to "amenity=bar",
|
||||||
|
R.string.poi_category_cafe to "amenity=cafe",
|
||||||
|
R.string.poi_category_hotel to "tourism=hotel",
|
||||||
|
R.string.poi_category_supermarket to "shop=supermarket",
|
||||||
|
R.string.poi_category_school to "amenity=school",
|
||||||
|
R.string.poi_category_parking to "amenity=parking",
|
||||||
|
R.string.poi_category_fuel to "amenity=fuel",
|
||||||
|
R.string.poi_category_toilets to "amenity=toilets",
|
||||||
|
R.string.poi_category_pharmacy to "amenity=pharmacy",
|
||||||
|
R.string.poi_category_hospital to "amenity=hospital",
|
||||||
|
R.string.poi_category_post_office to "amenity=post_office",
|
||||||
|
R.string.poi_category_pub to "amenity=pub",
|
||||||
|
R.string.poi_category_doctors to "amenity=doctors",
|
||||||
|
R.string.poi_category_police to "amenity=police",
|
||||||
|
R.string.poi_category_dentist to "amenity=dentist",
|
||||||
|
R.string.poi_category_library to "amenity=library",
|
||||||
|
R.string.poi_category_ice_cream to "amenity=ice_cream",
|
||||||
|
R.string.poi_category_theater to "amenity=theatre",
|
||||||
|
R.string.poi_category_cinema to "amenity=cinema",
|
||||||
|
R.string.poi_category_nightclub to "amenity=nightclub",
|
||||||
|
R.string.poi_category_clinic to "amenity=clinic",
|
||||||
|
R.string.poi_category_university to "amenity=university",
|
||||||
|
R.string.poi_category_clothes to "shop=clothes",
|
||||||
|
R.string.poi_category_convenience to "shop=convenience",
|
||||||
|
R.string.poi_category_hairdresser to "shop=hairdresser",
|
||||||
|
R.string.poi_category_books to "shop=books",
|
||||||
|
R.string.poi_category_bakery to "shop=bakery",
|
||||||
|
R.string.poi_category_car_rental to "amenity=car_rental",
|
||||||
|
R.string.poi_category_car_sharing to "amenity=car_sharing",
|
||||||
|
R.string.poi_category_mobile_phone to "shop=mobile_phone",
|
||||||
|
R.string.poi_category_furniture to "shop=furniture",
|
||||||
|
R.string.poi_category_alcohol to "shop=alcohol",
|
||||||
|
R.string.poi_category_florist to "shop=florist",
|
||||||
|
R.string.poi_category_mall to "shop=mall",
|
||||||
|
R.string.poi_category_optician to "shop=optician",
|
||||||
|
R.string.poi_category_jewelry to "shop=jewelry",
|
||||||
|
R.string.poi_category_laundry to "amenity=laundry",
|
||||||
|
R.string.poi_category_bank to "amenity=bank",
|
||||||
|
R.string.poi_category_soccer to "leisure=pitch,sport=soccer",
|
||||||
|
R.string.poi_category_basketball to "leisure=pitch,sport=basketball",
|
||||||
|
R.string.poi_category_tennis to "leisure=pitch,sport=tennis",
|
||||||
|
R.string.poi_category_atm to "amenity=atm",
|
||||||
|
R.string.poi_category_kiosk to "shop=kiosk",
|
||||||
|
R.string.poi_category_museum to "tourism=museum",
|
||||||
|
R.string.poi_category_fitness_center to "leisure=fitness_centre",
|
||||||
|
R.string.poi_category_church to "amenity=place_of_worship,religion=christian",
|
||||||
|
R.string.poi_category_mosque to "amenity=place_of_worship,religion=muslim",
|
||||||
|
R.string.poi_category_buddhist_temple to "amenity=place_of_worship,religion=buddhist",
|
||||||
|
R.string.poi_category_hindu_temple to "amenity=place_of_worship,religion=hindu",
|
||||||
|
R.string.poi_category_synagogue to "amenity=place_of_worship,religion=jewish",
|
||||||
|
R.string.poi_category_pizza_restaurant to "amenity=restaurant,cuisine=pizza",
|
||||||
|
R.string.poi_category_burger_restaurant to "amenity=restaurant,cuisine=burger",
|
||||||
|
R.string.poi_category_place_of_worship to "amenity=place_of_worship",
|
||||||
|
R.string.poi_category_chinese_restaurant to "amenity=restaurant,cuisine=chinese",
|
||||||
|
R.string.poi_category_japanese_restaurant to "amenity=restaurant,cuisine=japanese",
|
||||||
|
R.string.poi_category_kebab_restaurant to "amenity=restaurant,cuisine=kebab",
|
||||||
|
R.string.poi_category_asian_restaurant to "amenity=restaurant,cuisine=asian",
|
||||||
|
R.string.poi_category_ramen_restaurant to "amenity=restaurant,cuisine=ramen",
|
||||||
|
R.string.poi_category_soup_restaurant to "amenity=restaurant,cuisine=soup",
|
||||||
|
R.string.poi_category_brunch_restaurant to "amenity=restaurant,cuisine=brunch",
|
||||||
|
R.string.poi_category_car_wash to "amenity=car_wash",
|
||||||
|
R.string.poi_category_charging_station to "amenity=charging_station",
|
||||||
|
R.string.poi_category_motorcycle_rental to "amenity=motorcycle_rental",
|
||||||
|
R.string.poi_category_gallery to "tourism=gallery",
|
||||||
|
R.string.poi_category_amusement_park to "tourism=theme_park",
|
||||||
|
R.string.poi_category_concert_hall to "amenity=concert_hall",
|
||||||
|
R.string.poi_category_stadium to "leisure=stadium",
|
||||||
|
R.string.poi_category_casino to "amenity=casino",
|
||||||
|
R.string.poi_category_discount_store to "shop=discount",
|
||||||
|
R.string.poi_category_pet to "shop=pet",
|
||||||
|
R.string.poi_category_shopping to "shop=mall",
|
||||||
|
R.string.poi_category_swimming to "leisure=swimming_pool",
|
||||||
|
R.string.poi_category_martial_arts to "leisure=sports_centre,sport=martial_arts",
|
||||||
|
R.string.poi_category_golf to "leisure=golf_course",
|
||||||
|
R.string.poi_category_gymnastics to "leisure=sports_hall,sport=gymnastics",
|
||||||
|
R.string.poi_category_ice_hockey to "leisure=sports_centre,sport=ice_hockey",
|
||||||
|
R.string.poi_category_baseball to "leisure=pitch,sport=baseball",
|
||||||
|
R.string.poi_category_american_football to "leisure=pitch,sport=american_football",
|
||||||
|
R.string.poi_category_handball to "leisure=pitch,sport=handball",
|
||||||
|
R.string.poi_category_volleyball to "leisure=pitch,sport=volleyball",
|
||||||
|
R.string.poi_category_skiing to "leisure=piste",
|
||||||
|
R.string.poi_category_cricket to "leisure=pitch,sport=cricket",
|
||||||
|
R.string.poi_category_park to "leisure=park",
|
||||||
|
R.string.poi_category_monument to "historic=monument",
|
||||||
|
R.string.poi_category_government_building to "building=government",
|
||||||
|
R.string.poi_category_fire_station to "amenity=fire_station",
|
||||||
|
R.string.poi_category_courthouse to "amenity=courthouse",
|
||||||
|
R.string.poi_category_townhall to "amenity=townhall"
|
||||||
|
).mapKeys { context.getString(it.key) }
|
||||||
|
|
||||||
|
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
||||||
|
poiCategories.mapNotNull { (string, tags) ->
|
||||||
|
val score = ResultScore(
|
||||||
|
localizedQuery,
|
||||||
|
primaryFields = listOf(string)
|
||||||
|
)
|
||||||
|
if (score.score > 0.8f) tags else null
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package de.mm20.launcher2.locations.providers.googleapi
|
||||||
|
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import retrofit2.Converter
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import kotlin.math.cos
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overpass API query builder
|
||||||
|
* Searches for nodes and ways that at least:
|
||||||
|
* - match the query string in their name or brand tag
|
||||||
|
* - match one of the given tag groups
|
||||||
|
*/
|
||||||
|
data class OverpassFuzzyRadiusQuery(
|
||||||
|
/**
|
||||||
|
* Free text query to search for.
|
||||||
|
*/
|
||||||
|
val query: String,
|
||||||
|
/**
|
||||||
|
* Tags groups to search for. Each item represents a group of tags, separated by commas.
|
||||||
|
* The query matches if all tags in a group are present in the element.
|
||||||
|
* For example:
|
||||||
|
* ["amenity=restaurant,cuisine=italian", "amenity=cafe"]
|
||||||
|
* This query will match elements that are either a restaurant with italian cuisine or a cafe.
|
||||||
|
*/
|
||||||
|
val tagGroups: List<String>,
|
||||||
|
val radius: Int,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OverpassIdQuery(
|
||||||
|
val id: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OverpassResponse(
|
||||||
|
val elements: List<OverpassResponseElement>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OverpassResponseElementCenter(
|
||||||
|
val lat: Double,
|
||||||
|
val lon: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OverpassResponseElement(
|
||||||
|
val type: String,
|
||||||
|
val id: Long,
|
||||||
|
val lat: Double?,
|
||||||
|
val lon: Double?,
|
||||||
|
val center: OverpassResponseElementCenter?,
|
||||||
|
val tags: Map<String, String>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface OverpassApi {
|
||||||
|
@POST("api/interpreter")
|
||||||
|
suspend fun search(@Body data: OverpassFuzzyRadiusQuery): OverpassResponse
|
||||||
|
|
||||||
|
@POST("api/interpreter")
|
||||||
|
suspend fun search(@Body data: OverpassIdQuery): OverpassResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, RequestBody> {
|
||||||
|
override fun convert(value: OverpassFuzzyRadiusQuery): RequestBody {
|
||||||
|
val encodedQuery = value.query.split(' ')
|
||||||
|
.joinToString(
|
||||||
|
separator = ".*",
|
||||||
|
prefix = "\"",
|
||||||
|
postfix = "\""
|
||||||
|
) { Regex.escapeReplacement(it) }
|
||||||
|
|
||||||
|
val overpassQlBuilder = StringBuilder()
|
||||||
|
val latDegreeChange = value.radius * 0.00001 / 1.11
|
||||||
|
val lonDegreeChange = latDegreeChange / cos(Math.toRadians(value.latitude))
|
||||||
|
val boundingBox = arrayOf(
|
||||||
|
value.latitude - latDegreeChange, value.longitude - lonDegreeChange,
|
||||||
|
value.latitude + latDegreeChange, value.longitude + lonDegreeChange
|
||||||
|
)
|
||||||
|
overpassQlBuilder.append("[out:json][timeout:10][bbox:" + boundingBox.joinToString(",") + "];")
|
||||||
|
|
||||||
|
overpassQlBuilder.append("(")
|
||||||
|
overpassQlBuilder.append("nw[name~$encodedQuery,i];")
|
||||||
|
overpassQlBuilder.append("nw[brand~$encodedQuery,i];")
|
||||||
|
for (tag in value.tagGroups) {
|
||||||
|
val tags = tag.split(',')
|
||||||
|
|
||||||
|
if (tags.isEmpty()) continue
|
||||||
|
|
||||||
|
overpassQlBuilder.append("nw[${tags.joinToString("][")}];")
|
||||||
|
}
|
||||||
|
overpassQlBuilder.append(");")
|
||||||
|
// center to add the center coordinate of a way to the result, if applicable
|
||||||
|
overpassQlBuilder.append("out center;")
|
||||||
|
|
||||||
|
return overpassQlBuilder.toString().toRequestBody()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverpassIdQueryConverter : Converter<OverpassIdQuery, RequestBody> {
|
||||||
|
override fun convert(value: OverpassIdQuery): RequestBody = """
|
||||||
|
[out:json];
|
||||||
|
nw(${value.id});
|
||||||
|
out center;
|
||||||
|
""".trimIndent().toRequestBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverpassQueryConverterFactory : Converter.Factory() {
|
||||||
|
override fun requestBodyConverter(
|
||||||
|
type: Type,
|
||||||
|
parameterAnnotations: Array<out Annotation>,
|
||||||
|
methodAnnotations: Array<out Annotation>,
|
||||||
|
retrofit: Retrofit
|
||||||
|
): Converter<*, RequestBody>? {
|
||||||
|
if (type == OverpassFuzzyRadiusQuery::class.java)
|
||||||
|
return OverpassFuzzyRadiusQueryConverter()
|
||||||
|
|
||||||
|
if (type == OverpassIdQuery::class.java)
|
||||||
|
return OverpassIdQueryConverter()
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+14
-5
@@ -102,7 +102,7 @@ internal class OsmLocationProvider(
|
|||||||
searchRadiusMeters: Int,
|
searchRadiusMeters: Int,
|
||||||
hideUncategorized: Boolean,
|
hideUncategorized: Boolean,
|
||||||
): List<Location> {
|
): List<Location> {
|
||||||
if (!allowNetwork || query.length < 2) {
|
if (!allowNetwork || query.length < 1) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,11 +243,20 @@ internal class OsmLocationProvider(
|
|||||||
).mapKeys { context.getString(it.key) }
|
).mapKeys { context.getString(it.key) }
|
||||||
|
|
||||||
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
||||||
|
|
||||||
poiCategories.mapNotNull { (string, tags) ->
|
poiCategories.mapNotNull { (string, tags) ->
|
||||||
val score = ResultScore(
|
val score = if (string.contains("|")) {
|
||||||
localizedQuery,
|
ResultScore(
|
||||||
primaryFields = listOf(string)
|
localizedQuery,
|
||||||
)
|
primaryFields = string.split("|")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ResultScore(
|
||||||
|
localizedQuery,
|
||||||
|
primaryFields = listOf(string)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (score.score > 0.8f) tags else null
|
if (score.score > 0.8f) tags else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ dependencies {
|
|||||||
implementation(libs.bundles.retrofit)
|
implementation(libs.bundles.retrofit)
|
||||||
implementation(libs.suncalc)
|
implementation(libs.suncalc)
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:4.5.0")
|
implementation(libs.logging.interceptor)
|
||||||
|
|
||||||
implementation(project(":data:database"))
|
implementation(project(":data:database"))
|
||||||
implementation(project(":core:base"))
|
implementation(project(":core:base"))
|
||||||
|
|||||||
+1
-1
@@ -23,6 +23,6 @@ hero:
|
|||||||
<img src="/img/screenshot-5.png"></img>
|
<img src="/img/screenshot-5.png"></img>
|
||||||
<img src="/img/screenshot-6.png"></img>
|
<img src="/img/screenshot-6.png"></img>
|
||||||
|
|
||||||
<div class="credits">Wallpaper by Allec Gomes on <a href="https://unsplash.com/de/fotos/ein-grunes-blatt-das-auf-einem-gewasser-schwimmt-UcWUMqIsld8" target="_blank">Unspash.com</a></div>
|
<div class="credits">Wallpaper by Allec Gomes on <a href="https://unsplash.com/de/fotos/ein-grunes-blatt-das-auf-einem-gewasser-schwimmt-UcWUMqIsld8" target="_blank">Unsplash.com</a></div>
|
||||||
</div>
|
</div>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
loggingInterceptor = "4.5.0"
|
||||||
mustache = "0.9.14"
|
mustache = "0.9.14"
|
||||||
jacksonCore = "2.19.0"
|
jacksonCore = "2.19.0"
|
||||||
jacksonDataformatYaml = "2.19.0"
|
jacksonDataformatYaml = "2.19.0"
|
||||||
minSdk = "26"
|
minSdk = "26"
|
||||||
compileSdk = "36"
|
compileSdk = "35"
|
||||||
targetSdk = "36"
|
targetSdk = "34"
|
||||||
|
|
||||||
pluginSdk = "2.2.0"
|
pluginSdk = "2.2.0"
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ espressoCore = "3.6.1"
|
|||||||
osmOpeningHours = "0.2.0"
|
osmOpeningHours = "0.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
|
||||||
mustache-compiler = { module = "com.github.spullara.mustache.java:compiler", version.ref = "mustache" }
|
mustache-compiler = { module = "com.github.spullara.mustache.java:compiler", version.ref = "mustache" }
|
||||||
|
|
||||||
jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jacksonCore" }
|
jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jacksonCore" }
|
||||||
|
|||||||
@@ -231,8 +231,12 @@ class NextcloudApiHelper(val context: Context) {
|
|||||||
.url("$server/ocs/v2.php/core/apppassword")
|
.url("$server/ocs/v2.php/core/apppassword")
|
||||||
.build()
|
.build()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val response = httpClient.newCall(request).execute()
|
try {
|
||||||
response
|
val response = httpClient.newCall(request).execute()
|
||||||
|
response
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("NextcloudApiHelper", "Error during Nextcloud logout", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putString("server", null)
|
putString("server", null)
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ internal class SearchServiceImpl(
|
|||||||
}
|
}
|
||||||
if (filters.places) {
|
if (filters.places) {
|
||||||
launch {
|
launch {
|
||||||
|
Log.e("SearchService","filters.places >>> $query ${filters.allowNetwork}")
|
||||||
locationRepository.search(query, filters.allowNetwork)
|
locationRepository.search(query, filters.allowNetwork)
|
||||||
.combine(customAttrResults) { locations, customAttrs ->
|
.combine(customAttrResults) { locations, customAttrs ->
|
||||||
if (customAttrs.locations != null) locations + customAttrs.locations
|
if (customAttrs.locations != null) locations + customAttrs.locations
|
||||||
|
|||||||
+10
-1
@@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
|
|||||||
import android.appwidget.AppWidgetProviderInfo
|
import android.appwidget.AppWidgetProviderInfo
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
|
import android.os.Build
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.widgets.CalendarWidget
|
import de.mm20.launcher2.widgets.CalendarWidget
|
||||||
import de.mm20.launcher2.widgets.FavoritesWidget
|
import de.mm20.launcher2.widgets.FavoritesWidget
|
||||||
@@ -32,7 +33,15 @@ class WidgetsService(
|
|||||||
for (profile in profiles) {
|
for (profile in profiles) {
|
||||||
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
|
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
|
||||||
}
|
}
|
||||||
widgets
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
// Ignore widgets that the launcher is not supposed to access
|
||||||
|
widgets.filter {
|
||||||
|
it.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER == 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widgets
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAvailableBuiltInWidgets(): Flow<List<BuiltInWidgetInfo>> {
|
fun getAvailableBuiltInWidgets(): Flow<List<BuiltInWidgetInfo>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user