From da6c39e14a2528c7918dcad918722b90123aa5e6 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Thu, 5 Dec 2024 16:17:18 +0900 Subject: [PATCH] update to use datastore instead of room --- .../login/bluesky/BlueskyLoginScreen.kt | 256 ------------------ .../login/mastodon/MastodonCallbackScreen.kt | 130 --------- .../login/mastodon/MastodonLoginScreen.kt | 178 ------------ .../login/misskey/MisskeyCallbackScreen.kt | 130 --------- .../login/misskey/MisskeyLoginScreen.kt | 182 ------------- .../serviceselect/ServiceSelectScreen.kt | 4 +- .../ui/screen/settings/GuestSettingScreen.kt | 21 +- shared/build.gradle.kts | 3 + .../flare/di/PlatformModule.android.kt | 9 + .../dev/dimension/flare/di/KoinHelper.kt | 6 - .../flare/di/PlatformModule.apple.kt | 24 ++ .../flare/data/database/app/AppDatabase.kt | 13 +- .../data/database/app/dao/GuestDataDao.kt | 17 -- .../data/database/app/model/DbGuestData.kt | 13 - .../GuestDiscoverStatusPagingSource.kt | 2 +- .../{ => mastodon}/GuestMastodonDataSource.kt | 10 +- .../GuestSearchStatusPagingSource.kt | 2 +- .../GuestStatusDetailPagingSource.kt | 2 +- .../GuestTimelinePagingSource.kt | 2 +- .../GuestUserTimelinePagingSource.kt | 2 +- .../flare/data/datastore/ProvideDataStore.kt | 27 ++ .../flare/data/datastore/model/GuestData.kt | 36 +++ .../data/repository/AccountRepository.kt | 31 +-- .../data/repository/ApplicationRepository.kt | 2 +- .../login/MastodonCallbackPresenter.kt | 2 +- .../login/MisskeyCallbackPresenter.kt | 2 +- .../settings/GuestConfigPresenter.kt | 30 +- .../flare/di/PlatformModule.linux.kt | 8 + 28 files changed, 166 insertions(+), 978 deletions(-) delete mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/login/bluesky/BlueskyLoginScreen.kt delete mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonCallbackScreen.kt delete mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonLoginScreen.kt delete mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyCallbackScreen.kt delete mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyLoginScreen.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/dao/GuestDataDao.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/model/DbGuestData.kt rename shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/{ => mastodon}/GuestDiscoverStatusPagingSource.kt (94%) rename shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/{ => mastodon}/GuestMastodonDataSource.kt (97%) rename shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/{ => mastodon}/GuestSearchStatusPagingSource.kt (96%) rename shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/{ => mastodon}/GuestStatusDetailPagingSource.kt (96%) rename shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/{ => mastodon}/GuestTimelinePagingSource.kt (94%) rename shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/{ => mastodon}/GuestUserTimelinePagingSource.kt (96%) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/ProvideDataStore.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/login/bluesky/BlueskyLoginScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/login/bluesky/BlueskyLoginScreen.kt deleted file mode 100644 index 9929eae1c..000000000 --- a/app/src/main/java/dev/dimension/flare/ui/screen/login/bluesky/BlueskyLoginScreen.kt +++ /dev/null @@ -1,256 +0,0 @@ -package dev.dimension.flare.ui.screen.login.bluesky - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.delete -import androidx.compose.foundation.text.input.insert -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -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.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import compose.icons.FontAwesomeIcons -import compose.icons.fontawesomeicons.Solid -import compose.icons.fontawesomeicons.solid.CaretDown -import dev.dimension.flare.R -import dev.dimension.flare.ui.common.plus -import dev.dimension.flare.ui.component.BackButton -import dev.dimension.flare.ui.component.FAIcon -import dev.dimension.flare.ui.component.FlareScaffold -import dev.dimension.flare.ui.component.OutlinedSecureTextField2 -import dev.dimension.flare.ui.component.OutlinedTextField2 -import dev.dimension.flare.ui.presenter.invoke -import dev.dimension.flare.ui.presenter.login.BlueskyLoginPresenter -import dev.dimension.flare.ui.theme.screenHorizontalPadding -import moe.tlaster.precompose.molecule.producePresenter - -// @Composable -// @Destination( -// wrappers = [ThemeWrapper::class], -// ) -// fun BlueskyLoginRoute(navigator: DestinationsNavigator) { -// BlueskyLoginScreen( -// onBack = navigator::navigateUp, -// toHome = { -// navigator.navigate(HomeRouteDestination) { -// popUpTo(BlueskyLoginRouteDestination) { -// inclusive = true -// } -// } -// }, -// ) -// } - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@Composable -private fun BlueskyLoginScreen( - onBack: () -> Unit = {}, - toHome: () -> Unit = {}, -) { - val state by producePresenter { - loginPresenter(toHome) - } - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - FlareScaffold( - topBar = { - TopAppBar( - title = { - }, - navigationIcon = { - BackButton(onBack = onBack) - }, - ) - }, - ) { - Column( - modifier = - Modifier - .fillMaxSize() - .padding(it + PaddingValues(horizontal = screenHorizontalPadding)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(0.8f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - Text( - text = stringResource(id = R.string.bluesky_login_title), - style = MaterialTheme.typography.headlineMedium, - ) - Text( - text = stringResource(id = R.string.bluesky_login_message), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - ) - } - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(2f) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - OutlinedTextField2( - state = state.baseUrl, - label = { - Text(text = stringResource(id = R.string.bluesky_login_base_url_hint)) - }, - enabled = !state.state.loading, - modifier = - Modifier - .fillMaxWidth() - .focusRequester( - focusRequester = focusRequester, - ), - lineLimits = TextFieldLineLimits.SingleLine, - trailingIcon = { - IconButton( - onClick = { - state.setDropdown(!state.showDropdown) - }, - ) { - FAIcon( - imageVector = FontAwesomeIcons.Solid.CaretDown, - contentDescription = stringResource(id = R.string.navigate_back), - ) - DropdownMenu( - expanded = state.showDropdown, - onDismissRequest = { state.setDropdown(false) }, - ) { - KnownInstance.entries.forEach { - DropdownMenuItem( - text = { Text(text = it.url) }, - onClick = { state.selectBaseUrl(it.url) }, - ) - } - } - } - }, - ) - OutlinedTextField2( - state = state.username, - label = { - Text(text = stringResource(id = R.string.bluesky_login_username_hint)) - }, - enabled = !state.state.loading, - modifier = - Modifier - .fillMaxWidth() - .focusRequester( - focusRequester = focusRequester, - ), - lineLimits = TextFieldLineLimits.SingleLine, - ) - OutlinedSecureTextField2( - state = state.password, - label = { - Text(text = stringResource(id = R.string.bluesky_login_password_hint)) - }, - enabled = !state.state.loading, - modifier = - Modifier - .fillMaxWidth(), - lineLimits = TextFieldLineLimits.SingleLine, - onKeyboardAction = { - state.state.login( - state.baseUrl.text.toString(), - state.username.text.toString(), - state.password.text.toString(), - ) - }, - ) - Button( - onClick = { - state.state.login( - state.baseUrl.text.toString(), - state.username.text.toString(), - state.password.text.toString(), - ) - }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = R.string.login_button)) - } - state.state.error?.let { error -> - Text(text = error.toString()) - } - } - } - } -} - -private enum class KnownInstance( - val url: String, -) { - Main("https://bsky.social"), - Staging("https://staging.bsky.dev"), - Local("http://localhost:2583"), -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun loginPresenter(toHome: () -> Unit = {}) = - run { - val baseUrl by remember { mutableStateOf(TextFieldState("https://bsky.social")) } - val username by remember { mutableStateOf(TextFieldState("")) } - val password by remember { mutableStateOf(TextFieldState("")) } - var showDropdown by remember { mutableStateOf(false) } - - val state = - remember(toHome) { - BlueskyLoginPresenter(toHome) - }.invoke() - - object { - val baseUrl = baseUrl - val username = username - val password = password - var showDropdown = showDropdown - val state = state - - fun setDropdown(value: Boolean) { - showDropdown = value - } - - fun selectBaseUrl(value: String) { - baseUrl.edit { - this.delete(0, baseUrl.text.length) - this.insert(0, value) - } - showDropdown = false - } - } - } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonCallbackScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonCallbackScreen.kt deleted file mode 100644 index db3e605b9..000000000 --- a/app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonCallbackScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package dev.dimension.flare.ui.screen.login.mastodon - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import dev.dimension.flare.R -import dev.dimension.flare.ui.common.plus -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.presenter.invoke -import dev.dimension.flare.ui.presenter.login.MastodonCallbackPresenter -import dev.dimension.flare.ui.theme.screenHorizontalPadding -import moe.tlaster.precompose.molecule.producePresenter - -// @Destination( -// deepLinks = [ -// DeepLink( -// uriPattern = "${AppDeepLink.Callback.MASTODON}?code={code}", -// ), -// ], -// wrappers = [ThemeWrapper::class], -// ) -// @Composable -// fun MastodonCallbackRoute( -// code: String?, -// navigator: DestinationsNavigator, -// ) { -// MastodonCallbackScreen( -// code = code, -// toHome = { -// navigator.navigate(HomeRouteDestination) { -// popUpTo(MastodonCallbackRouteDestination) { -// inclusive = true -// } -// } -// }, -// ) -// } - -@Composable -internal fun MastodonCallbackScreen( - code: String?, - toHome: () -> Unit, -) { - val state by producePresenter { - mastodonCallbackPresenter( - code = code, - toHome = toHome, - ) - } - Scaffold { - Column( - modifier = - Modifier - .fillMaxSize() - .padding(it + PaddingValues(horizontal = screenHorizontalPadding)), - horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(1f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - Text( - text = stringResource(id = R.string.mastodon_login_title), - style = MaterialTheme.typography.headlineMedium, - ) - Text( - text = stringResource(id = R.string.mastodon_login_verify_message), - style = MaterialTheme.typography.bodyMedium, - textAlign = androidx.compose.ui.text.style.TextAlign.Center, - ) - } - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(2f) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - when (val data = state) { - is UiState.Error -> { - Text(text = data.throwable.message ?: "Unknown error") - } - - is UiState.Loading -> { - CircularProgressIndicator() - } - - is UiState.Success -> Unit - } - } - } - } -} - -@Composable -private fun mastodonCallbackPresenter( - code: String?, - toHome: () -> Unit, -): UiState { - if (code == null) { - return UiState.Error(Throwable("No code")) - } - return remember(code, toHome) { - MastodonCallbackPresenter( - code = code, - toHome = toHome, - ) - }.invoke() -} diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonLoginScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonLoginScreen.kt deleted file mode 100644 index 6ffeaf7d2..000000000 --- a/app/src/main/java/dev/dimension/flare/ui/screen/login/mastodon/MastodonLoginScreen.kt +++ /dev/null @@ -1,178 +0,0 @@ -package dev.dimension.flare.ui.screen.login.mastodon - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import dev.dimension.flare.R -import dev.dimension.flare.data.repository.ApplicationRepository -import dev.dimension.flare.ui.common.plus -import dev.dimension.flare.ui.component.BackButton -import dev.dimension.flare.ui.component.FlareScaffold -import dev.dimension.flare.ui.component.OutlinedTextField2 -import dev.dimension.flare.ui.component.ThemeWrapper -import dev.dimension.flare.ui.presenter.login.mastodonLoginUseCase -import dev.dimension.flare.ui.theme.screenHorizontalPadding -import kotlinx.coroutines.launch -import moe.tlaster.precompose.molecule.producePresenter -import org.koin.compose.koinInject - -@Destination( - wrappers = [ThemeWrapper::class], -) -@Composable -fun MastodonLoginRoute(navigator: DestinationsNavigator) { - MastodonLoginScreen( - onBack = navigator::navigateUp, - ) -} - -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) -@Composable -internal fun MastodonLoginScreen(onBack: () -> Unit = {}) { - val uriHandler = LocalUriHandler.current - val state by producePresenter { - loginPresenter( - launchUrl = { - uriHandler.openUri(it) - }, - ) - } - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - FlareScaffold( - topBar = { - TopAppBar( - title = { - }, - navigationIcon = { - BackButton(onBack = onBack) - }, - ) - }, - ) { - Column( - modifier = - Modifier - .fillMaxSize() - .padding(it + PaddingValues(horizontal = screenHorizontalPadding)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(0.8f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - Text( - text = stringResource(id = R.string.mastodon_login_title), - style = MaterialTheme.typography.headlineMedium, - ) - Text( - text = stringResource(id = R.string.mastodon_login_message), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - ) - } - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(2f) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - OutlinedTextField2( - state = state.hostTextState, - label = { - Text(text = stringResource(id = R.string.mastodon_login_hint)) - }, - enabled = !state.loading, - modifier = - Modifier - .fillMaxWidth() - .focusRequester( - focusRequester = focusRequester, - ), - lineLimits = TextFieldLineLimits.SingleLine, - ) - Button( - onClick = state::login, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = R.string.login_button)) - } - state.error?.let { error -> - Text(text = error) - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun loginPresenter(launchUrl: (String) -> Unit) = - run { - val applicationRepository: ApplicationRepository = koinInject() - val hostTextState by remember { - mutableStateOf(TextFieldState("")) - } - var loading by remember { mutableStateOf(false) } - var error by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() - object { - val hostTextState = hostTextState - val loading = loading - val error = error - - fun login() { - scope.launch { - loading = true - error = null - mastodonLoginUseCase( - domain = hostTextState.text.toString(), - applicationRepository = applicationRepository, - launchOAuth = launchUrl, - ).onFailure { - error = it.message - } - loading = false - } - } - } - } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyCallbackScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyCallbackScreen.kt deleted file mode 100644 index a59324e8f..000000000 --- a/app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyCallbackScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package dev.dimension.flare.ui.screen.login.misskey - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import dev.dimension.flare.R -import dev.dimension.flare.ui.common.plus -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.presenter.invoke -import dev.dimension.flare.ui.presenter.login.MisskeyCallbackPresenter -import dev.dimension.flare.ui.theme.screenHorizontalPadding -import moe.tlaster.precompose.molecule.producePresenter - -// @Destination( -// deepLinks = [ -// DeepLink( -// uriPattern = "${AppDeepLink.Callback.MISSKEY}?session={session}", -// ), -// ], -// wrappers = [ThemeWrapper::class], -// ) -// @Composable -// fun MisskeyCallbackRoute( -// session: String?, -// navigator: DestinationsNavigator, -// ) { -// MisskeyCallbackScreen( -// session = session, -// toHome = { -// navigator.navigate(HomeRouteDestination) { -// popUpTo(MisskeyCallbackRouteDestination) { -// inclusive = true -// } -// } -// }, -// ) -// } - -@Composable -internal fun MisskeyCallbackScreen( - session: String?, - toHome: () -> Unit, -) { - val state by producePresenter { - misskeyCallbackPresenter( - session = session, - toHome = toHome, - ) - } - Scaffold { - Column( - modifier = - Modifier - .fillMaxSize() - .padding(it + PaddingValues(horizontal = screenHorizontalPadding)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(1f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - Text( - text = stringResource(id = R.string.misskey_login_title), - style = MaterialTheme.typography.headlineMedium, - ) - Text( - text = stringResource(id = R.string.mastodon_login_verify_message), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - ) - } - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(2f) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - when (val data = state) { - is UiState.Error -> { - Text(text = data.throwable.message ?: "Unknown error") - } - - is UiState.Loading -> { - CircularProgressIndicator() - } - - is UiState.Success -> Unit - } - } - } - } -} - -@Composable -private fun misskeyCallbackPresenter( - session: String?, - toHome: () -> Unit, -): UiState = - remember( - session, - toHome, - ) { - MisskeyCallbackPresenter( - session = session, - toHome = toHome, - ) - }.invoke() diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyLoginScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyLoginScreen.kt deleted file mode 100644 index b3751a008..000000000 --- a/app/src/main/java/dev/dimension/flare/ui/screen/login/misskey/MisskeyLoginScreen.kt +++ /dev/null @@ -1,182 +0,0 @@ -package dev.dimension.flare.ui.screen.login.misskey - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import dev.dimension.flare.R -import dev.dimension.flare.data.repository.ApplicationRepository -import dev.dimension.flare.ui.common.plus -import dev.dimension.flare.ui.component.BackButton -import dev.dimension.flare.ui.component.FlareScaffold -import dev.dimension.flare.ui.component.OutlinedTextField2 -import dev.dimension.flare.ui.component.ThemeWrapper -import dev.dimension.flare.ui.presenter.login.misskeyLoginUseCase -import dev.dimension.flare.ui.theme.screenHorizontalPadding -import kotlinx.coroutines.launch -import moe.tlaster.precompose.molecule.producePresenter -import org.koin.compose.koinInject - -@Destination( - wrappers = [ThemeWrapper::class], -) -@Composable -fun MisskeyLoginRoute(navigator: DestinationsNavigator) { - MisskeyLoginScreen( - onBack = navigator::navigateUp, - ) -} - -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) -@Composable -fun MisskeyLoginScreen( - modifier: Modifier = Modifier, - onBack: () -> Unit = {}, -) { - val uriHandler = LocalUriHandler.current - val state by producePresenter { - loginPresenter( - launchUrl = { - uriHandler.openUri(it) - }, - ) - } - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - FlareScaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { - }, - navigationIcon = { - BackButton(onBack = onBack) - }, - ) - }, - ) { - Column( - modifier = - Modifier - .fillMaxSize() - .padding(it + PaddingValues(horizontal = screenHorizontalPadding)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(0.8f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - Text( - text = stringResource(id = R.string.misskey_login_title), - style = MaterialTheme.typography.headlineMedium, - ) - Text( - text = stringResource(id = R.string.misskey_login_message), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - ) - } - Column( - modifier = - Modifier - .fillMaxWidth() - .weight(2f) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - OutlinedTextField2( - state = state.hostTextState, - label = { - Text(text = stringResource(id = R.string.misskey_login_hint)) - }, - enabled = !state.loading, - modifier = - Modifier - .fillMaxWidth() - .focusRequester( - focusRequester = focusRequester, - ), - lineLimits = TextFieldLineLimits.SingleLine, - ) - Button( - onClick = state::login, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = R.string.login_button)) - } - state.error?.let { error -> - Text(text = error) - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun loginPresenter(launchUrl: (String) -> Unit) = - run { - val applicationRepository: ApplicationRepository = koinInject() - val hostTextState by remember { - mutableStateOf(TextFieldState("")) - } - var loading by remember { mutableStateOf(false) } - var error by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() - object { - val hostTextState = hostTextState - val loading = loading - val error = error - - fun login() { - scope.launch { - loading = true - error = null - misskeyLoginUseCase( - host = hostTextState.text.toString(), - applicationRepository = applicationRepository, - launchOAuth = launchUrl, - ).onFailure { - error = it.message - } - loading = false - } - } - } - } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/serviceselect/ServiceSelectScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/serviceselect/ServiceSelectScreen.kt index 8b87a37f8..1b1c6065e 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/serviceselect/ServiceSelectScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/serviceselect/ServiceSelectScreen.kt @@ -51,8 +51,8 @@ import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid +import compose.icons.fontawesomeicons.solid.CircleQuestion import compose.icons.fontawesomeicons.solid.MagnifyingGlass -import compose.icons.fontawesomeicons.solid.Question import compose.icons.fontawesomeicons.solid.Xmark import dev.dimension.flare.R import dev.dimension.flare.common.onEmpty @@ -206,7 +206,7 @@ fun ServiceSelectScreen( ) }.onError { FAIcon( - imageVector = FontAwesomeIcons.Solid.Question, + imageVector = FontAwesomeIcons.Solid.CircleQuestion, contentDescription = null, ) }.onLoading { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt index 09955465c..0af8d1899 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt @@ -23,7 +23,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.spec.DestinationStyle import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid -import compose.icons.fontawesomeicons.solid.Question +import compose.icons.fontawesomeicons.solid.CircleQuestion import dev.dimension.flare.R import dev.dimension.flare.model.logoUrl import dev.dimension.flare.ui.component.FAIcon @@ -95,14 +95,21 @@ private fun GuestSettingScreen(onBack: () -> Unit) { leadingIcon = { state.platformType .onSuccess { - NetworkImage( - it.logoUrl, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) + if (it in state.supportedPlatforms) { + NetworkImage( + it.logoUrl, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + FAIcon( + imageVector = FontAwesomeIcons.Solid.CircleQuestion, + contentDescription = null, + ) + } }.onError { FAIcon( - imageVector = FontAwesomeIcons.Solid.Question, + imageVector = FontAwesomeIcons.Solid.CircleQuestion, contentDescription = null, ) }.onLoading { diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 701dfb454..90deb2dbd 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -69,6 +69,8 @@ kotlin { implementation(libs.room.runtime) implementation(libs.room.paging) implementation(libs.sqlite.bundled) + implementation(libs.datastore) + implementation(libs.kotlinx.serialization.protobuf) } } val commonTest by getting { @@ -82,6 +84,7 @@ kotlin { implementation(libs.compose.foundation) implementation(libs.core.ktx) implementation(libs.ktor.client.okhttp) + implementation(libs.koin.android) } } val appleMain by getting { diff --git a/shared/src/androidMain/kotlin/dev/dimension/flare/di/PlatformModule.android.kt b/shared/src/androidMain/kotlin/dev/dimension/flare/di/PlatformModule.android.kt index efdecd033..019c70d8c 100644 --- a/shared/src/androidMain/kotlin/dev/dimension/flare/di/PlatformModule.android.kt +++ b/shared/src/androidMain/kotlin/dev/dimension/flare/di/PlatformModule.android.kt @@ -1,11 +1,20 @@ package dev.dimension.flare.di +import androidx.datastore.dataStoreFile import dev.dimension.flare.data.database.DriverFactory +import dev.dimension.flare.data.datastore.AppDataStore +import org.koin.android.ext.koin.androidContext import org.koin.core.module.Module import org.koin.core.module.dsl.singleOf import org.koin.dsl.module actual val platformModule: Module = module { + single { + val context = androidContext() + AppDataStore { + context.dataStoreFile(it).absolutePath + } + } singleOf(::DriverFactory) } diff --git a/shared/src/appleMain/kotlin/dev/dimension/flare/di/KoinHelper.kt b/shared/src/appleMain/kotlin/dev/dimension/flare/di/KoinHelper.kt index 4e5d24e66..ebe9db353 100644 --- a/shared/src/appleMain/kotlin/dev/dimension/flare/di/KoinHelper.kt +++ b/shared/src/appleMain/kotlin/dev/dimension/flare/di/KoinHelper.kt @@ -1,10 +1,7 @@ package dev.dimension.flare.di import dev.dimension.flare.common.InAppNotification -import dev.dimension.flare.data.repository.AccountRepository -import dev.dimension.flare.data.repository.ApplicationRepository import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.koin.core.context.startKoin import org.koin.dsl.binds import org.koin.dsl.module @@ -22,7 +19,4 @@ object KoinHelper : KoinComponent { ) } } - - val accountRepository: AccountRepository by inject() - val applicationRepository: ApplicationRepository by inject() } diff --git a/shared/src/appleMain/kotlin/dev/dimension/flare/di/PlatformModule.apple.kt b/shared/src/appleMain/kotlin/dev/dimension/flare/di/PlatformModule.apple.kt index efdecd033..bea028259 100644 --- a/shared/src/appleMain/kotlin/dev/dimension/flare/di/PlatformModule.apple.kt +++ b/shared/src/appleMain/kotlin/dev/dimension/flare/di/PlatformModule.apple.kt @@ -1,11 +1,35 @@ package dev.dimension.flare.di import dev.dimension.flare.data.database.DriverFactory +import dev.dimension.flare.data.datastore.AppDataStore +import kotlinx.cinterop.ExperimentalForeignApi import org.koin.core.module.Module import org.koin.core.module.dsl.singleOf import org.koin.dsl.module +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSURL +import platform.Foundation.NSUserDomainMask actual val platformModule: Module = module { + single { + AppDataStore { fileName -> + "${fileDirectory()}/$fileName" + } + } singleOf(::DriverFactory) } + +@OptIn(ExperimentalForeignApi::class) +private fun fileDirectory(): String { + val documentDirectory: NSURL? = + NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + ) + return requireNotNull(documentDirectory).path!! +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/AppDatabase.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/AppDatabase.kt index 63070721d..e10d86201 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/AppDatabase.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/AppDatabase.kt @@ -1,6 +1,5 @@ package dev.dimension.flare.data.database.app -import androidx.room.AutoMigration import androidx.room.ConstructedBy import androidx.room.Database import androidx.room.RoomDatabase @@ -8,7 +7,6 @@ import androidx.room.RoomDatabaseConstructor import androidx.room.TypeConverters import dev.dimension.flare.data.database.app.dao.AccountDao import dev.dimension.flare.data.database.app.dao.ApplicationDao -import dev.dimension.flare.data.database.app.dao.GuestDataDao import dev.dimension.flare.data.database.app.dao.KeywordFilterDao import dev.dimension.flare.data.database.app.dao.SearchHistoryDao @@ -18,15 +16,8 @@ import dev.dimension.flare.data.database.app.dao.SearchHistoryDao dev.dimension.flare.data.database.app.model.DbApplication::class, dev.dimension.flare.data.database.app.model.DbKeywordFilter::class, dev.dimension.flare.data.database.app.model.DbSearchHistory::class, - dev.dimension.flare.data.database.app.model.DbGuestData::class, - ], - version = 4, - autoMigrations = [ - AutoMigration( - from = 3, - to = 4, - ), ], + version = 3, ) @TypeConverters( dev.dimension.flare.data.database.adapter.MicroBlogKeyConverter::class, @@ -41,8 +32,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun keywordFilterDao(): KeywordFilterDao abstract fun searchHistoryDao(): SearchHistoryDao - - abstract fun guestDataDao(): GuestDataDao } // The Room compiler generates the `actual` implementations. diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/dao/GuestDataDao.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/dao/GuestDataDao.kt deleted file mode 100644 index 935bc9033..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/dao/GuestDataDao.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.dimension.flare.data.database.app.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import dev.dimension.flare.data.database.app.model.DbGuestData -import kotlinx.coroutines.flow.Flow - -@Dao -interface GuestDataDao { - @Query("SELECT * FROM DbGuestData LIMIT 1") - fun get(): Flow - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(account: DbGuestData) -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/model/DbGuestData.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/model/DbGuestData.kt deleted file mode 100644 index 14760ea91..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/app/model/DbGuestData.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.dimension.flare.data.database.app.model - -import androidx.room.Entity -import androidx.room.PrimaryKey -import dev.dimension.flare.model.PlatformType - -@Entity -data class DbGuestData( - @PrimaryKey - val id: Int = 0, - val host: String, - val platformType: PlatformType, -) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDiscoverStatusPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestDiscoverStatusPagingSource.kt similarity index 94% rename from shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDiscoverStatusPagingSource.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestDiscoverStatusPagingSource.kt index b3fbe141e..048daa2e5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDiscoverStatusPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestDiscoverStatusPagingSource.kt @@ -1,4 +1,4 @@ -package dev.dimension.flare.data.datasource.guest +package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.PagingSource import androidx.paging.PagingState diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestMastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt similarity index 97% rename from shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestMastodonDataSource.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt index aa114c099..2fd14a677 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestMastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestMastodonDataSource.kt @@ -1,4 +1,4 @@ -package dev.dimension.flare.data.datasource.guest +package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.Pager import androidx.paging.PagingConfig @@ -190,10 +190,10 @@ class GuestMastodonDataSource( pageSize: Int, scope: CoroutineScope, pagingKey: String, - ): Flow> = - Pager(PagingConfig(pageSize = pageSize)) { - GuestDiscoverStatusPagingSource(host = host, service = service) - }.flow.cachedIn(scope) + ): Flow> = TODO() +// Pager(PagingConfig(pageSize = pageSize)) { +// GuestDiscoverStatusPagingSource(host = host, service = service) +// }.flow.cachedIn(scope) override fun discoverHashtags(pageSize: Int): Flow> = Pager( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestSearchStatusPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestSearchStatusPagingSource.kt similarity index 96% rename from shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestSearchStatusPagingSource.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestSearchStatusPagingSource.kt index 67235afff..060c6cde3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestSearchStatusPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestSearchStatusPagingSource.kt @@ -1,4 +1,4 @@ -package dev.dimension.flare.data.datasource.guest +package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.PagingSource import androidx.paging.PagingState diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestStatusDetailPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestStatusDetailPagingSource.kt similarity index 96% rename from shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestStatusDetailPagingSource.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestStatusDetailPagingSource.kt index e578daf37..c42d5cf9f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestStatusDetailPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestStatusDetailPagingSource.kt @@ -1,4 +1,4 @@ -package dev.dimension.flare.data.datasource.guest +package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.PagingSource import androidx.paging.PagingState diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestTimelinePagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestTimelinePagingSource.kt similarity index 94% rename from shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestTimelinePagingSource.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestTimelinePagingSource.kt index 6c800e42d..b963083c3 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestTimelinePagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestTimelinePagingSource.kt @@ -1,4 +1,4 @@ -package dev.dimension.flare.data.datasource.guest +package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.PagingSource import androidx.paging.PagingState diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestUserTimelinePagingSource.kt similarity index 96% rename from shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt rename to shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestUserTimelinePagingSource.kt index a2e46d5f2..50b5bb89c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/mastodon/GuestUserTimelinePagingSource.kt @@ -1,4 +1,4 @@ -package dev.dimension.flare.data.datasource.guest +package dev.dimension.flare.data.datasource.guest.mastodon import androidx.paging.PagingSource import androidx.paging.PagingState diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/ProvideDataStore.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/ProvideDataStore.kt new file mode 100644 index 000000000..378363ef8 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/ProvideDataStore.kt @@ -0,0 +1,27 @@ +package dev.dimension.flare.data.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.okio.OkioStorage +import dev.dimension.flare.data.datastore.model.GuestData +import dev.dimension.flare.data.datastore.model.GuestDataSerializer +import okio.FileSystem +import okio.Path.Companion.toPath +import okio.SYSTEM + +internal class AppDataStore( + private val producePath: (fileName: String) -> String, +) { + val guestDataStore: DataStore by lazy { + DataStoreFactory.create( + storage = + OkioStorage( + fileSystem = FileSystem.SYSTEM, + serializer = GuestDataSerializer, + producePath = { + producePath.invoke("guest_data.pb").toPath() + }, + ), + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt new file mode 100644 index 000000000..8f1675a48 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt @@ -0,0 +1,36 @@ +package dev.dimension.flare.data.datastore.model + +import androidx.datastore.core.okio.OkioSerializer +import dev.dimension.flare.model.PlatformType +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.protobuf.ProtoBuf +import okio.BufferedSink +import okio.BufferedSource + +private const val DEFAULT_GUEST_HOST = "mastodon.social" +private val DEFAULT_GUEST_PLATFORM_TYPE = PlatformType.Mastodon +internal val supportedGuestPlatforms = listOf(PlatformType.Mastodon) + +@Serializable +internal data class GuestData( + val host: String, + val platformType: PlatformType, +) + +@OptIn(ExperimentalSerializationApi::class) +internal data object GuestDataSerializer : OkioSerializer { + override val defaultValue: GuestData + get() = GuestData(DEFAULT_GUEST_HOST, DEFAULT_GUEST_PLATFORM_TYPE) + + override suspend fun readFrom(source: BufferedSource): GuestData = ProtoBuf.decodeFromByteArray(source.readByteArray()) + + override suspend fun writeTo( + t: GuestData, + sink: BufferedSink, + ) { + sink.write(ProtoBuf.encodeToByteArray(t)) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt index b54228902..2c4ccd190 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt @@ -10,9 +10,9 @@ import dev.dimension.flare.common.Locale import dev.dimension.flare.common.encodeJson import dev.dimension.flare.data.database.app.AppDatabase import dev.dimension.flare.data.database.app.model.DbAccount -import dev.dimension.flare.data.database.app.model.DbGuestData -import dev.dimension.flare.data.datasource.guest.GuestMastodonDataSource +import dev.dimension.flare.data.datasource.guest.mastodon.GuestMastodonDataSource import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datastore.AppDataStore import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.model.PlatformType @@ -31,12 +31,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.datetime.Clock -private const val DEFAULT_GUEST_HOST = "mastodon.social" -private val DEFAULT_GUEST_PLATFORM_TYPE = PlatformType.Mastodon - -class AccountRepository( +internal class AccountRepository( private val appDatabase: AppDatabase, private val coroutineScope: CoroutineScope, + internal val appDataStore: AppDataStore, ) { val activeAccount: Flow by lazy { appDatabase.accountDao().activeAccount().map { @@ -49,24 +47,6 @@ class AccountRepository( } } - val guestData by lazy { - appDatabase.guestDataDao().get().map { - it ?: DbGuestData(host = DEFAULT_GUEST_HOST, platformType = DEFAULT_GUEST_PLATFORM_TYPE) - } - } - - fun setGuestData( - host: String, - platformType: PlatformType, - ) = coroutineScope.launch { - appDatabase.guestDataDao().insert( - DbGuestData( - host = host, - platformType = platformType, - ), - ) - } - fun addAccount(account: UiAccount) = coroutineScope.launch { appDatabase.accountDao().insert( @@ -146,7 +126,8 @@ internal fun accountServiceProvider( repository: AccountRepository, ): UiState { if (accountType is AccountType.Guest) { - val guestData by repository.guestData.collectAsUiState() + val guestData by repository.appDataStore.guestDataStore.data + .collectAsUiState() return guestData.map { remember(it) { when (it.platformType) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/ApplicationRepository.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/ApplicationRepository.kt index 5626c3232..7d1e2013b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/ApplicationRepository.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/ApplicationRepository.kt @@ -7,7 +7,7 @@ import dev.dimension.flare.ui.model.UiApplication import dev.dimension.flare.ui.model.UiApplication.Companion.toUi import kotlinx.coroutines.flow.firstOrNull -class ApplicationRepository( +internal class ApplicationRepository( private val database: AppDatabase, ) { suspend fun findByHost(host: String): UiApplication? = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MastodonCallbackPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MastodonCallbackPresenter.kt index 938057167..2c26c4b4b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MastodonCallbackPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MastodonCallbackPresenter.kt @@ -93,7 +93,7 @@ class MastodonCallbackPresenter( } } -suspend fun mastodonLoginUseCase( +internal suspend fun mastodonLoginUseCase( domain: String, applicationRepository: ApplicationRepository, launchOAuth: (String) -> Unit, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MisskeyCallbackPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MisskeyCallbackPresenter.kt index 1332eee09..c5f6b6e94 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MisskeyCallbackPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/MisskeyCallbackPresenter.kt @@ -93,7 +93,7 @@ class MisskeyCallbackPresenter( } } -suspend fun misskeyLoginUseCase( +internal suspend fun misskeyLoginUseCase( host: String, applicationRepository: ApplicationRepository, launchOAuth: (String) -> Unit, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt index 79f6888ba..859ea34e2 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt @@ -9,6 +9,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import dev.dimension.flare.data.datastore.AppDataStore +import dev.dimension.flare.data.datastore.model.supportedGuestPlatforms import dev.dimension.flare.data.network.nodeinfo.NodeInfoService import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.model.PlatformType @@ -18,11 +20,16 @@ import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.model.takeSuccess import dev.dimension.flare.ui.presenter.PresenterBase +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -30,11 +37,14 @@ class GuestConfigPresenter : PresenterBase(), KoinComponent { private val accountRepository by inject() + private val appDataStore by inject() + private val coroutineScope by inject() @Immutable interface State { val data: UiState val platformType: UiState + val supportedPlatforms: ImmutableList fun setHost(value: String) @@ -49,7 +59,7 @@ class GuestConfigPresenter : @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @Composable override fun body(): State { - val guestData by accountRepository.guestData.collectAsUiState() + val guestData by appDataStore.guestDataStore.data.collectAsUiState() var host by remember { mutableStateOf("") } val hostFlow = remember { @@ -61,7 +71,7 @@ class GuestConfigPresenter : host = it.host } } - val detectedPlatformType by remember(hostFlow) { + val detectedPlatformType by remember>>(hostFlow) { hostFlow.flatMapLatest { flow { runCatching { @@ -78,10 +88,12 @@ class GuestConfigPresenter : val canSave = detectedPlatformType is UiState.Success && host.isNotBlank() && - detectedPlatformType.takeSuccess() in listOf(PlatformType.Mastodon) + detectedPlatformType.takeSuccess() in supportedGuestPlatforms return object : State { override val data = guestData.map { it.host } override val platformType = detectedPlatformType + override val supportedPlatforms: ImmutableList + get() = supportedGuestPlatforms.toImmutableList() override fun setHost(value: String) { host = value @@ -91,10 +103,14 @@ class GuestConfigPresenter : host: String, platformType: PlatformType, ) { - accountRepository.setGuestData( - host = host, - platformType = platformType, - ) + coroutineScope.launch { + appDataStore.guestDataStore.updateData { + it.copy( + host = host, + platformType = platformType, + ) + } + } } override val canSave = canSave diff --git a/shared/src/linuxMain/kotlin/dev/dimension/flare/di/PlatformModule.linux.kt b/shared/src/linuxMain/kotlin/dev/dimension/flare/di/PlatformModule.linux.kt index efdecd033..fb1a5e648 100644 --- a/shared/src/linuxMain/kotlin/dev/dimension/flare/di/PlatformModule.linux.kt +++ b/shared/src/linuxMain/kotlin/dev/dimension/flare/di/PlatformModule.linux.kt @@ -1,11 +1,19 @@ package dev.dimension.flare.di import dev.dimension.flare.data.database.DriverFactory +import dev.dimension.flare.data.datastore.AppDataStore import org.koin.core.module.Module import org.koin.core.module.dsl.singleOf import org.koin.dsl.module actual val platformModule: Module = module { + single { + AppDataStore( + producePath = { fileName -> + "~/.config/Flare/$fileName" + } + ) + } singleOf(::DriverFactory) }