[WIP] Add experimental feed support
This commit is contained in:
@@ -10,6 +10,7 @@ import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
@@ -27,6 +28,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
@@ -48,6 +50,7 @@ import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.ClockAndWidgetsHomeComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.ClockHomeComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.DismissComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.FeedComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.LaunchComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffold
|
||||
@@ -168,7 +171,7 @@ abstract class SharedLauncherActivity(
|
||||
val darkSearchBar = LocalPreferDarkContentOverWallpaper.current
|
||||
&& searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark
|
||||
|
||||
LaunchedEffect(dimBackground && darkTheme) {
|
||||
/*LaunchedEffect(dimBackground && darkTheme) {
|
||||
if (dimBackground && darkTheme) {
|
||||
val windowAttributes = window.attributes
|
||||
windowAttributes.flags =
|
||||
@@ -182,7 +185,7 @@ abstract class SharedLauncherActivity(
|
||||
window.attributes = windowAttributes
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
val enterTransitionProgress = remember { mutableStateOf(100f) }
|
||||
var enterTransition by remember {
|
||||
@@ -211,6 +214,10 @@ abstract class SharedLauncherActivity(
|
||||
|
||||
OverlayHost(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (dimBackground && darkTheme) Color( 0f, 0f, 0f, 0.3f)
|
||||
else Color.Transparent
|
||||
)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
@@ -266,6 +273,7 @@ abstract class SharedLauncherActivity(
|
||||
openKeyboard = searchBarAutofocus,
|
||||
)
|
||||
val widgetComponent by lazy { WidgetsComponent }
|
||||
val feedComponent by lazy { FeedComponent(this@SharedLauncherActivity) }
|
||||
|
||||
fun getScaffoldGesture(
|
||||
action: GestureAction?,
|
||||
@@ -318,6 +326,11 @@ abstract class SharedLauncherActivity(
|
||||
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
||||
)
|
||||
|
||||
is GestureAction.Feed -> ScaffoldGesture(
|
||||
component = FeedComponent(this@SharedLauncherActivity),
|
||||
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
||||
)
|
||||
|
||||
is GestureAction.Launch if (searchable != null) -> ScaffoldGesture(
|
||||
component = LaunchComponent(
|
||||
this@SharedLauncherActivity,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.mm20.launcher2.ui.launcher.scaffold
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import de.mm20.launcher2.feed.FeedConnection
|
||||
import de.mm20.launcher2.feed.FeedService
|
||||
import de.mm20.launcher2.preferences.feed.FeedSettings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.ktx.toIntOffset
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
internal class FeedComponent(
|
||||
private val context: Context,
|
||||
) : ScaffoldComponent(), KoinComponent {
|
||||
|
||||
private val feedSettings: FeedSettings by inject()
|
||||
private val feedService: FeedService by inject()
|
||||
|
||||
override val permanent: Boolean = true
|
||||
|
||||
@Composable
|
||||
override fun Component(
|
||||
modifier: Modifier,
|
||||
insets: PaddingValues,
|
||||
state: LauncherScaffoldState
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val progress = state.currentProgress
|
||||
val feedProgress = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val feedProviderPackage by remember { feedSettings.providerPackage }.collectAsState(null)
|
||||
|
||||
var feedConnection by remember { mutableStateOf<FeedConnection?>(null) }
|
||||
val feedReady = feedConnection?.ready?.collectAsState(false)
|
||||
val feedAvailable = feedConnection?.available?.collectAsState(null)
|
||||
|
||||
|
||||
DisposableEffect(feedProviderPackage) {
|
||||
val conn = feedProviderPackage?.let {
|
||||
feedService.createFeedInstance(activity as AppCompatActivity, it) { p ->
|
||||
feedProgress.floatValue = p
|
||||
}
|
||||
}
|
||||
feedConnection = conn
|
||||
|
||||
onDispose {
|
||||
conn?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
val enableScroll = state.currentComponent == this && progress > 0f && progress < 1f
|
||||
|
||||
LaunchedEffect(enableScroll) {
|
||||
if (enableScroll) {
|
||||
feedConnection?.startScroll()
|
||||
} else {
|
||||
feedConnection?.endScroll()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(progress) {
|
||||
feedConnection?.onScroll(progress)
|
||||
}
|
||||
|
||||
LaunchedEffect(feedProgress.floatValue) {
|
||||
if (isActive) {
|
||||
state.setProgress(feedProgress.floatValue)
|
||||
if (feedProgress.floatValue <= 0f) {
|
||||
state.onPredictiveBackEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (feedAvailable?.value == false) {
|
||||
Icon(
|
||||
painterResource(R.drawable.error_48px),
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
"Feed could not be loaded.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
override fun homePageModifier(
|
||||
state: LauncherScaffoldState,
|
||||
defaultModifier: Modifier
|
||||
): Modifier {
|
||||
return defaultModifier.alpha(1f - state.currentProgress)
|
||||
}
|
||||
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
override fun searchBarModifier(
|
||||
state: LauncherScaffoldState,
|
||||
defaultModifier: Modifier
|
||||
): Modifier {
|
||||
return defaultModifier
|
||||
.offset { state.currentOffset.toIntOffset() }
|
||||
.alpha(1f - state.currentProgress)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -791,17 +791,13 @@ internal class LauncherScaffoldState(
|
||||
}
|
||||
}
|
||||
|
||||
private val backInterpolation = PathInterpolator(0f, 0f, 0f, 1f)
|
||||
fun onPredictiveBack(progress: Float) {
|
||||
if (!isSettledOnSecondaryPage) return
|
||||
fun setProgress(progress: Float) {
|
||||
val gesture = currentGesture ?: return
|
||||
val anim = currentAnimation ?: return
|
||||
|
||||
val progress = backInterpolation.getInterpolation(progress)
|
||||
|
||||
when (gesture) {
|
||||
Gesture.TapSearchBar, Gesture.DoubleTap, Gesture.LongPress -> {
|
||||
currentZOffset = 1f - progress
|
||||
currentZOffset = progress
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -818,13 +814,13 @@ internal class LauncherScaffoldState(
|
||||
|
||||
currentOffset = if (anim == ScaffoldAnimation.Push) {
|
||||
Offset(
|
||||
x * size.width * (1f - progress * 0.1f),
|
||||
y * size.height * (1f - progress * 0.1f)
|
||||
x * size.width * progress,
|
||||
y * size.height * progress
|
||||
)
|
||||
} else {
|
||||
Offset(
|
||||
x * rubberbandThreshold * progress,
|
||||
y * rubberbandThreshold * progress
|
||||
x * rubberbandThreshold * (1f - progress),
|
||||
y * rubberbandThreshold * (1f - progress)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -832,6 +828,21 @@ internal class LauncherScaffoldState(
|
||||
}
|
||||
}
|
||||
|
||||
private val backInterpolation = PathInterpolator(0f, 0f, 0f, 1f)
|
||||
fun onPredictiveBack(progress: Float) {
|
||||
if (!isSettledOnSecondaryPage) return
|
||||
val anim = currentAnimation ?: return
|
||||
|
||||
val progress = backInterpolation.getInterpolation(progress)
|
||||
|
||||
val p = when(anim) {
|
||||
ScaffoldAnimation.Push -> 1f - progress * 0.1f
|
||||
else -> 1f - progress
|
||||
}
|
||||
|
||||
setProgress(p)
|
||||
}
|
||||
|
||||
suspend fun onPredictiveBackCancel() {
|
||||
val gesture = currentGesture ?: return
|
||||
val anim = currentAnimation ?: return
|
||||
|
||||
@@ -70,6 +70,8 @@ import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.feed.FeedIntegrationSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.feed.FeedIntegrationSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.filterbar.FilterBarSettingsRoute
|
||||
@@ -305,6 +307,9 @@ class SettingsActivity : BaseActivity() {
|
||||
entry<SmartspacerSettingsRoute> {
|
||||
SmartspacerSettingsScreen()
|
||||
}
|
||||
entry<FeedIntegrationSettingsRoute> {
|
||||
FeedIntegrationSettingsScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.mm20.launcher2.ui.settings.feed
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LargeMessage
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object FeedIntegrationSettingsRoute : NavKey
|
||||
|
||||
@Composable
|
||||
fun FeedIntegrationSettingsScreen() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel: FeedIntegrationSettingsScreenVM = viewModel()
|
||||
val selectedProvider by viewModel.providerPackage.collectAsState(null)
|
||||
|
||||
val providers = remember { viewModel.getFeedProviders(context) }
|
||||
PreferenceScreen(
|
||||
title = stringResource(R.string.preference_feed_integration),
|
||||
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/integrations/feed"
|
||||
) {
|
||||
if (providers.isEmpty()) {
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillParentMaxHeight()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
LargeMessage(
|
||||
icon = R.drawable.news_48px,
|
||||
text = stringResource(R.string.no_feed_providers),
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
for (prov in providers) {
|
||||
Preference(
|
||||
title = prov.label,
|
||||
icon = if (prov.packageName == selectedProvider) R.drawable.radio_button_checked_24px else R.drawable.radio_button_unchecked_24px,
|
||||
onClick = {
|
||||
viewModel.setProviderPackage(prov.packageName)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.mm20.launcher2.ui.settings.feed
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.feed.FeedProvider
|
||||
import de.mm20.launcher2.feed.FeedService
|
||||
import de.mm20.launcher2.preferences.feed.FeedSettings
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.text.Collator
|
||||
|
||||
class FeedIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val feedService: FeedService by inject()
|
||||
private val feedSettings: FeedSettings by inject()
|
||||
|
||||
val providerPackage = feedSettings.providerPackage
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(0), 1)
|
||||
|
||||
fun setProviderPackage(providerPackage: String?) {
|
||||
feedSettings.setProviderPackage(providerPackage)
|
||||
}
|
||||
|
||||
fun getFeedProviders(context: Context): List<FeedProvider> {
|
||||
return feedService.getAvailableFeedProviders()
|
||||
}
|
||||
}
|
||||
@@ -61,13 +61,14 @@ fun GestureSettingsScreen() {
|
||||
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
|
||||
}
|
||||
|
||||
val optionsWithFeed = options + (stringResource(R.string.gesture_action_feed) to GestureAction.Feed)
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
PreferenceScreen(title = stringResource(R.string.preference_screen_gestures)) {
|
||||
item {
|
||||
val appIconSize = 32.dp.toPixels()
|
||||
PreferenceCategory {
|
||||
|
||||
|
||||
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null)
|
||||
val swipeDownApp by viewModel.swipeDownApp.collectAsState(null)
|
||||
val swipeDownAppIcon by remember(swipeDownApp?.key) {
|
||||
@@ -128,7 +129,7 @@ fun GestureSettingsScreen() {
|
||||
icon = R.drawable.swipe_right_alt_24px,
|
||||
value = swipeRight,
|
||||
onValueChanged = { viewModel.setSwipeRight(it) },
|
||||
options = options,
|
||||
options = optionsWithFeed,
|
||||
app = swipeRightApp,
|
||||
appIcon = swipeRightAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeRightApp(it) }
|
||||
|
||||
@@ -11,6 +11,7 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.locals.LocalBackStack
|
||||
import de.mm20.launcher2.ui.settings.breezyweather.BreezyWeatherSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.feed.FeedIntegrationSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.media.MediaIntegrationSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.nextcloud.NextcloudSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.owncloud.OwncloudSettingsRoute
|
||||
@@ -45,6 +46,13 @@ fun IntegrationsSettingsScreen() {
|
||||
backStack.add(MediaIntegrationSettingsRoute)
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_feed_integration),
|
||||
icon = R.drawable.news_24px,
|
||||
onClick = {
|
||||
backStack.add(FeedIntegrationSettingsRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
||||
Reference in New Issue
Block a user