From e2309af37b6c0a8036868afb1df0d45f5c080558 Mon Sep 17 00:00:00 2001 From: Antonio Corrales Date: Wed, 29 May 2024 17:23:44 +0200 Subject: [PATCH] - Create readable content feature --- .../desarrollodroide/data/mapper/Mapper.kt | 9 ++ .../data/repository/BookmarksRepository.kt | 7 + .../repository/BookmarksRepositoryImpl.kt | 20 +++ .../GetBookmarkReadableContentUseCase.kt | 24 +++ gradle.properties | 4 +- gradle/libs.versions.toml | 2 +- .../desarrollodroide/model/ReadableContent.kt | 6 + .../desarrollodroide/model/ReadableMessage.kt | 6 + .../model/ReadableContentResponseDTO.kt | 7 + .../network/model/ReadableMessageDto.kt | 6 + .../network/retrofit/RetrofitNetwork.kt | 7 + .../pagekeeper/MainActivity.kt | 3 + .../pagekeeper/di/AppModule.kt | 7 + .../pagekeeper/di/PresenterModule.kt | 8 + .../pagekeeper/navigation/NavItem.kt | 9 ++ .../pagekeeper/navigation/Navigation.kt | 3 + .../pagekeeper/ui/feed/FeedScreen.kt | 4 +- .../pagekeeper/ui/feed/FeedViewModel.kt | 13 +- .../pagekeeper/ui/home/HomeScreen.kt | 45 +++++- .../ui/readablecontent/ErrorView.kt | 64 ++++++++ .../readablecontent/ReadableContentScreen.kt | 142 ++++++++++++++++++ .../ReadableContentViewModel.kt | 73 +++++++++ .../ui/readablecontent/TopSection.kt | 75 +++++++++ 23 files changed, 527 insertions(+), 17 deletions(-) create mode 100644 domain/src/main/java/com/desarrollodroide/domain/usecase/GetBookmarkReadableContentUseCase.kt create mode 100644 model/src/main/java/com/desarrollodroide/model/ReadableContent.kt create mode 100644 model/src/main/java/com/desarrollodroide/model/ReadableMessage.kt create mode 100644 network/src/main/java/com/desarrollodroide/network/model/ReadableContentResponseDTO.kt create mode 100644 network/src/main/java/com/desarrollodroide/network/model/ReadableMessageDto.kt create mode 100644 presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ErrorView.kt create mode 100644 presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentScreen.kt create mode 100644 presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentViewModel.kt create mode 100644 presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/TopSection.kt diff --git a/data/src/main/java/com/desarrollodroide/data/mapper/Mapper.kt b/data/src/main/java/com/desarrollodroide/data/mapper/Mapper.kt index b8a6b22..a8f350c 100644 --- a/data/src/main/java/com/desarrollodroide/data/mapper/Mapper.kt +++ b/data/src/main/java/com/desarrollodroide/data/mapper/Mapper.kt @@ -155,5 +155,14 @@ fun LoginResponseMessageDTO.toDomainModel() = LoginResponseMessage( token = token?:"" ) +fun ReadableContentResponseDTO.toDomainModel() = ReadableContent( + ok = ok?:false, + message = message?.toDomainModel() ?: ReadableMessage("", "") +) + +fun ReadableMessageDto.toDomainModel() = ReadableMessage( + content = content?:"", + html = html?:"" +) diff --git a/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepository.kt b/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepository.kt index 775d7b7..db5e5d8 100644 --- a/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepository.kt +++ b/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepository.kt @@ -4,6 +4,7 @@ import androidx.paging.PagingData import kotlinx.coroutines.flow.Flow import com.desarrollodroide.model.Bookmark import com.desarrollodroide.common.result.Result +import com.desarrollodroide.model.ReadableContent import com.desarrollodroide.model.Tag import com.desarrollodroide.model.UpdateCachePayload @@ -53,4 +54,10 @@ interface BookmarksRepository { serverUrl: String, updateCachePayload: UpdateCachePayload ): Flow> + + fun getBookmarkReadableContent( + token: String, + serverUrl: String, + bookmarkId: Int + ): Flow> } \ No newline at end of file diff --git a/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepositoryImpl.kt b/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepositoryImpl.kt index 5064cd9..ff60890 100644 --- a/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepositoryImpl.kt +++ b/data/src/main/java/com/desarrollodroide/data/repository/BookmarksRepositoryImpl.kt @@ -11,11 +11,13 @@ import com.desarrollodroide.data.local.room.dao.BookmarksDao import com.desarrollodroide.data.mapper.* import com.desarrollodroide.data.repository.paging.BookmarkPagingSource import com.desarrollodroide.model.Bookmark +import com.desarrollodroide.model.ReadableContent import com.desarrollodroide.model.Tag import com.desarrollodroide.model.UpdateCachePayload import com.desarrollodroide.network.model.BookmarkDTO import com.desarrollodroide.network.model.BookmarkResponseDTO import com.desarrollodroide.network.model.BookmarksDTO +import com.desarrollodroide.network.model.ReadableContentResponseDTO import com.desarrollodroide.network.retrofit.NetworkBoundResource import com.desarrollodroide.network.retrofit.NetworkNoCacheResource import com.desarrollodroide.network.retrofit.RetrofitNetwork @@ -179,5 +181,23 @@ class BookmarksRepositoryImpl( }.asFlow().flowOn(Dispatchers.IO) override suspend fun deleteAllLocalBookmarks() { bookmarksDao.deleteAll() } + + override fun getBookmarkReadableContent( + token: String, + serverUrl: String, + bookmarkId: Int + ) = object : + NetworkNoCacheResource(errorHandler = errorHandler) { + override suspend fun fetchFromRemote(): Response = apiService.getBookmarkReadableContent( + url = "${serverUrl.removeTrailingSlash()}/api/v1/bookmarks/${bookmarkId}/readable", + authorization = "Bearer $token", + ) + + override fun fetchResult(data: ReadableContentResponseDTO): Flow { + return flow { + emit(data.toDomainModel()) + } + } + }.asFlow().flowOn(Dispatchers.IO) } diff --git a/domain/src/main/java/com/desarrollodroide/domain/usecase/GetBookmarkReadableContentUseCase.kt b/domain/src/main/java/com/desarrollodroide/domain/usecase/GetBookmarkReadableContentUseCase.kt new file mode 100644 index 0000000..6ffc703 --- /dev/null +++ b/domain/src/main/java/com/desarrollodroide/domain/usecase/GetBookmarkReadableContentUseCase.kt @@ -0,0 +1,24 @@ +package com.desarrollodroide.domain.usecase + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import com.desarrollodroide.common.result.Result +import com.desarrollodroide.data.repository.BookmarksRepository +import com.desarrollodroide.model.ReadableContent + +class GetBookmarkReadableContentUseCase( + private val bookmarksRepository: BookmarksRepository +) { + operator fun invoke( + serverUrl: String, + token: String, + bookmarkId: Int + ): Flow> { + return bookmarksRepository.getBookmarkReadableContent( + token = token, + serverUrl = serverUrl, + bookmarkId = bookmarkId + ).flowOn(Dispatchers.IO) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ae9900b..cc9172b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,8 +25,8 @@ android.nonTransitiveRClass=true compileSdkVersion=34 minSdkVersion=21 targetSdkVersion=34 -versionCode=40 -versionName=1.31.01 +versionCode=41 +versionName=1.32 android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd6b720..383b840 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ junitPlatformSuiteApi = "1.8.1" koinAndroidxCompose = "3.4.2" mockitoCore = "3.9.0" mockitoKotlin = "3.2.0" -compose = "1.6.4" +compose = "1.7.0-beta01" composeMaterial3 = "1.2.1" # gradlePlugin and lint need to be updated together gradlePlugin = "7.3.1" diff --git a/model/src/main/java/com/desarrollodroide/model/ReadableContent.kt b/model/src/main/java/com/desarrollodroide/model/ReadableContent.kt new file mode 100644 index 0000000..a52a869 --- /dev/null +++ b/model/src/main/java/com/desarrollodroide/model/ReadableContent.kt @@ -0,0 +1,6 @@ +package com.desarrollodroide.model + +data class ReadableContent( + val ok: Boolean, + val message: ReadableMessage, +) \ No newline at end of file diff --git a/model/src/main/java/com/desarrollodroide/model/ReadableMessage.kt b/model/src/main/java/com/desarrollodroide/model/ReadableMessage.kt new file mode 100644 index 0000000..7c88a35 --- /dev/null +++ b/model/src/main/java/com/desarrollodroide/model/ReadableMessage.kt @@ -0,0 +1,6 @@ +package com.desarrollodroide.model + +data class ReadableMessage( + val content: String, + val html: String +) \ No newline at end of file diff --git a/network/src/main/java/com/desarrollodroide/network/model/ReadableContentResponseDTO.kt b/network/src/main/java/com/desarrollodroide/network/model/ReadableContentResponseDTO.kt new file mode 100644 index 0000000..eb5084b --- /dev/null +++ b/network/src/main/java/com/desarrollodroide/network/model/ReadableContentResponseDTO.kt @@ -0,0 +1,7 @@ +package com.desarrollodroide.network.model + +data class ReadableContentResponseDTO ( + val ok: Boolean?, + val message: ReadableMessageDto?, +) + diff --git a/network/src/main/java/com/desarrollodroide/network/model/ReadableMessageDto.kt b/network/src/main/java/com/desarrollodroide/network/model/ReadableMessageDto.kt new file mode 100644 index 0000000..63b7c7b --- /dev/null +++ b/network/src/main/java/com/desarrollodroide/network/model/ReadableMessageDto.kt @@ -0,0 +1,6 @@ +package com.desarrollodroide.network.model + +data class ReadableMessageDto( + val content: String?, + val html: String? +) \ No newline at end of file diff --git a/network/src/main/java/com/desarrollodroide/network/retrofit/RetrofitNetwork.kt b/network/src/main/java/com/desarrollodroide/network/retrofit/RetrofitNetwork.kt index 89fd23f..6d0c4b6 100644 --- a/network/src/main/java/com/desarrollodroide/network/retrofit/RetrofitNetwork.kt +++ b/network/src/main/java/com/desarrollodroide/network/retrofit/RetrofitNetwork.kt @@ -6,6 +6,7 @@ import com.desarrollodroide.network.model.BookmarkResponseDTO import com.desarrollodroide.network.model.BookmarksDTO import com.desarrollodroide.network.model.LivenessResponseDTO import com.desarrollodroide.network.model.LoginResponseDTO +import com.desarrollodroide.network.model.ReadableContentResponseDTO import com.desarrollodroide.network.model.SessionDTO import com.desarrollodroide.network.model.TagDTO import com.desarrollodroide.network.model.TagsDTO @@ -127,4 +128,10 @@ interface RetrofitNetwork { @Url url: String ): Response + @GET() + suspend fun getBookmarkReadableContent( + @Url url: String, + @Header("Authorization") authorization: String, + ): Response + } diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/MainActivity.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/MainActivity.kt index 70a06a4..adea8ee 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/MainActivity.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/MainActivity.kt @@ -1,9 +1,11 @@ package com.desarrollodroide.pagekeeper import android.content.Context +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.annotation.RequiresApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface @@ -19,6 +21,7 @@ class MainActivity : ComponentActivity() { private val themeManager: ThemeManager by inject() + @RequiresApi(Build.VERSION_CODES.N) @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/AppModule.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/AppModule.kt index 3fbe078..33062d2 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/AppModule.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/AppModule.kt @@ -8,6 +8,7 @@ import com.desarrollodroide.domain.usecase.AddBookmarkUseCase import com.desarrollodroide.domain.usecase.DeleteBookmarkUseCase import com.desarrollodroide.domain.usecase.DownloadFileUseCase import com.desarrollodroide.domain.usecase.EditBookmarkUseCase +import com.desarrollodroide.domain.usecase.GetBookmarkReadableContentUseCase import com.desarrollodroide.domain.usecase.GetBookmarksUseCase import com.desarrollodroide.domain.usecase.GetPagingBookmarksUseCase import com.desarrollodroide.domain.usecase.GetTagsUseCase @@ -94,6 +95,12 @@ fun appModule() = module { ) } + single { + GetBookmarkReadableContentUseCase( + bookmarksRepository = get() + ) + } + single { ThemeManagerImpl(get()) as ThemeManager } diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/PresenterModule.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/PresenterModule.kt index d2422f8..4a46543 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/PresenterModule.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/di/PresenterModule.kt @@ -4,6 +4,7 @@ import com.desarrollodroide.pagekeeper.ui.feed.FeedViewModel import com.desarrollodroide.pagekeeper.ui.login.LoginViewModel import com.desarrollodroide.pagekeeper.ui.bookmarkeditor.BookmarkViewModel import com.desarrollodroide.pagekeeper.ui.feed.SearchViewModel +import com.desarrollodroide.pagekeeper.ui.readablecontent.ReadableContentViewModel import com.desarrollodroide.pagekeeper.ui.settings.SettingsViewModel import org.koin.dsl.module import org.koin.androidx.viewmodel.dsl.viewModel @@ -57,4 +58,11 @@ fun presenterModule() = module { ) } + viewModel { + ReadableContentViewModel( + getBookmarkReadableContentUseCase = get(), + settingsPreferenceDataSource = get(), + ) + } + } \ No newline at end of file diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/NavItem.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/NavItem.kt index 285cdb7..4aed56a 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/NavItem.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/NavItem.kt @@ -1,5 +1,6 @@ package com.desarrollodroide.pagekeeper.navigation +import android.net.Uri import androidx.navigation.NavType import androidx.navigation.navArgument @@ -13,6 +14,14 @@ sealed class NavItem( object SettingsNavItem : NavItem("settings") object TermsOfUseNavItem : NavItem("termsOfUse") object PrivacyPolicyNavItem : NavItem("privacyPolicy") + object ReadableContentNavItem : NavItem("readable_content/{bookmarkId}/{bookmarkUrl}/{bookmarkDate}/{bookmarkTitle}") { + fun createRoute(bookmarkId: Int, bookmarkUrl: String, bookmarkDate: String, bookmarkTitle: String): String { + val encodedUrl = Uri.encode(bookmarkUrl) + val encodedDate = Uri.encode(bookmarkDate) + val encodedTitle = Uri.encode(bookmarkTitle) + return "readable_content/$bookmarkId/$encodedUrl/$encodedDate/$encodedTitle" + } + } object FeedNavItem : NavItem("feed") diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/Navigation.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/Navigation.kt index 2d971be..e44441c 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/Navigation.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/navigation/Navigation.kt @@ -1,5 +1,7 @@ package com.desarrollodroide.pagekeeper.navigation +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry @@ -14,6 +16,7 @@ import com.desarrollodroide.pagekeeper.ui.login.LoginViewModel import org.koin.androidx.compose.get import java.io.File +@RequiresApi(Build.VERSION_CODES.N) @ExperimentalFoundationApi @Composable fun Navigation( diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedScreen.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedScreen.kt index cf566f4..0234b41 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedScreen.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedScreen.kt @@ -51,6 +51,7 @@ import java.io.File fun FeedScreen( feedViewModel: FeedViewModel, goToLogin: () -> Unit, + goToReadableContent:(Bookmark) -> Unit, openUrlInBrowser: (String) -> Unit, shareEpubFile: (File) -> Unit, isCategoriesVisible: MutableState, @@ -85,8 +86,7 @@ fun FeedScreen( goToLogin() }, onBookmarkSelect = { bookmark -> - Log.v("FeedContent", feedViewModel.getUrl(bookmark)) - openUrlInBrowser(feedViewModel.getUrl(bookmark)) + goToReadableContent(bookmark) }, onRefreshFeed = { feedViewModel.refreshFeed() diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedViewModel.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedViewModel.kt index 0c5c1de..d59f4ab 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedViewModel.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/feed/FeedViewModel.kt @@ -60,7 +60,6 @@ class FeedViewModel( MutableStateFlow(value = PagingData.empty()) val bookmarksState: MutableStateFlow> get() = _bookmarksState - private val _tagsState = MutableStateFlow(UiState>(idle = true)) val tagsState = _tagsState.asStateFlow() @@ -135,13 +134,10 @@ class FeedViewModel( is Result.Error -> { Log.v("FeedViewModel", "Error getting tags: ${result.error?.message}") } - is Result.Loading -> { Log.v("FeedViewModel", "Loading, updating tags from cache...") _tagsState.success(result.data) - } - is Result.Success -> { Log.v("FeedViewModel", "Tags loaded successfully.") _tagsState.success(result.data) @@ -202,6 +198,8 @@ class FeedViewModel( _bookmarksUiState.isUpdating(false) refreshFeed() } + + else -> {} } } } @@ -238,15 +236,11 @@ class FeedViewModel( .collect { result -> when (result) { is Result.Error -> { - Log.v( - "FeedViewModel", - "Error deleting bookmark: ${result.error?.message}" - ) + Log.v("FeedViewModel","Error deleting bookmark: ${result.error?.message}") _bookmarksUiState.error( errorMessage = result.error?.message ?: "Unknown error" ) } - is Result.Loading -> { Log.v("FeedViewModel", "Deleting bookmark...") _bookmarksUiState.isLoading(true) @@ -257,6 +251,7 @@ class FeedViewModel( _bookmarksUiState.isLoading(false) refreshFeed() } + else -> {} } } } diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/home/HomeScreen.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/home/HomeScreen.kt index 16869ca..ebf342f 100644 --- a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/home/HomeScreen.kt +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/home/HomeScreen.kt @@ -1,14 +1,15 @@ package com.desarrollodroide.pagekeeper.ui.home +import android.net.Uri +import android.os.Build import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.Modifier import org.koin.androidx.compose.get import androidx.compose.runtime.* @@ -26,15 +27,16 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import androidx.paging.compose.collectAsLazyPagingItems import com.desarrollodroide.data.helpers.SHIORI_ANDROID_CLIENT_GITHUB_URL import com.desarrollodroide.pagekeeper.navigation.NavItem @@ -45,7 +47,9 @@ import com.desarrollodroide.pagekeeper.ui.settings.SettingsScreen import com.desarrollodroide.pagekeeper.ui.settings.TermsOfUseScreen import java.io.File import com.desarrollodroide.pagekeeper.R +import com.desarrollodroide.pagekeeper.ui.readablecontent.ReadableContentScreen +@RequiresApi(Build.VERSION_CODES.N) @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( @@ -98,6 +102,14 @@ fun HomeScreen( shareEpubFile = shareEpubFile, isSearchBarVisible = isSearchBarVisible, setShowTopBar = setShowTopBar, + goToReadableContent = { bookmark-> + navController.navigate(NavItem.ReadableContentNavItem.createRoute( + bookmarkId = bookmark.id, + bookmarkUrl = bookmark.url, + bookmarkDate = bookmark.modified, + bookmarkTitle = bookmark.title + )) + }, ) } @@ -135,6 +147,33 @@ fun HomeScreen( } ) } + composable( + route = NavItem.ReadableContentNavItem.route, + arguments = listOf( + navArgument("bookmarkId") { type = NavType.IntType }, + navArgument("bookmarkUrl") { type = NavType.StringType }, + navArgument("bookmarkDate") { type = NavType.StringType }, + navArgument("bookmarkTitle") { type = NavType.StringType } + ) + ) { backStackEntry -> + val bookmarkId = backStackEntry.arguments?.getInt("bookmarkId") ?: 0 + val bookmarkUrl = backStackEntry.arguments?.getString("bookmarkUrl")?.let { Uri.decode(it) } ?: "" + val bookmarkDate = backStackEntry.arguments?.getString("bookmarkDate")?.let { Uri.decode(it) } ?: "" + val bookmarkTitle = backStackEntry.arguments?.getString("bookmarkTitle")?.let { Uri.decode(it) } ?: "" + + ReadableContentScreen( + readableContentViewModel = get(), + bookmarkUrl = bookmarkUrl, + bookmarkId = bookmarkId, + bookmarkDate = bookmarkDate, + onBack = { + navController.navigateUp() + }, + openUrlInBrowser = openUrlInBrowser, + bookmarkTitle = bookmarkTitle + ) + } + } } diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ErrorView.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ErrorView.kt new file mode 100644 index 0000000..6cf4b38 --- /dev/null +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ErrorView.kt @@ -0,0 +1,64 @@ +package com.desarrollodroide.pagekeeper.ui.readablecontent + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun ErrorView(errorMessage: String) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + modifier = Modifier.size(64.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Error", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = errorMessage, + fontSize = 16.sp, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ErrorViewPreview() { + MaterialTheme { + ErrorView(errorMessage = "Something went wrong. Please try again later.") + } +} diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentScreen.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentScreen.kt new file mode 100644 index 0000000..27142b8 --- /dev/null +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentScreen.kt @@ -0,0 +1,142 @@ +package com.desarrollodroide.pagekeeper.ui.readablecontent + +import android.content.Intent +import android.os.Build +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.viewinterop.AndroidView +import com.desarrollodroide.pagekeeper.ui.components.InfiniteProgressDialog + +@RequiresApi(Build.VERSION_CODES.N) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReadableContentScreen( + readableContentViewModel: ReadableContentViewModel, + onBack: () -> Unit, + bookmarkUrl: String, + bookmarkId: Int, + openUrlInBrowser: (String) -> Unit, + bookmarkDate: String, + bookmarkTitle: String +) { + BackHandler { + onBack() + } + LaunchedEffect(Unit) { + readableContentViewModel.loadInitialData() + readableContentViewModel.getBookmarkReadableContent(bookmarkId) + } + val readableContentState = readableContentViewModel.readableContentState.collectAsState() + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { Text("Content", style = MaterialTheme.typography.titleLarge) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) + }, + containerColor = MaterialTheme.colorScheme.background + ) { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + ) { + if (readableContentState.value.isLoading) { + InfiniteProgressDialog(onDismissRequest = {}) + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + item { + TopSection( + title = bookmarkTitle, + date = bookmarkDate, + onClick = { openUrlInBrowser.invoke(bookmarkUrl) } + ) + } + item { + if (readableContentState.value.error != null) { + ErrorView(errorMessage = readableContentState.value.error ?: "Error getting readable content") + } else { + readableContentState.value.data?.let { readableMessage -> + AndroidView(factory = { context -> + WebView(context).apply { + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + val css = """ + (function() { + var style = document.createElement('style'); + style.innerHTML = ` + img { + max-width: 100%; + height: auto; + } + `; + document.head.appendChild(style); + })(); + """.trimIndent() + view?.evaluateJavascript(css, null) + } + + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + request?.url?.let { url -> + val intent = Intent(Intent.ACTION_VIEW, url) + context.startActivity(intent) + return true + } + return false + } + } + settings.javaScriptEnabled = true + loadDataWithBaseURL( + null, + readableMessage.html, + "text/html", + "UTF-8", + null + ) + } + }) + } + } + } + } + } + } + } +} + + diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentViewModel.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentViewModel.kt new file mode 100644 index 0000000..d0c4b6f --- /dev/null +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/ReadableContentViewModel.kt @@ -0,0 +1,73 @@ +package com.desarrollodroide.pagekeeper.ui.readablecontent + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.desarrollodroide.common.result.Result +import com.desarrollodroide.data.local.preferences.SettingsPreferenceDataSource +import com.desarrollodroide.domain.usecase.GetBookmarkReadableContentUseCase +import com.desarrollodroide.model.ReadableMessage +import com.desarrollodroide.pagekeeper.ui.components.UiState +import com.desarrollodroide.pagekeeper.ui.components.isLoading +import com.desarrollodroide.pagekeeper.ui.components.success +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +class ReadableContentViewModel( + private val settingsPreferenceDataSource: SettingsPreferenceDataSource, + private val getBookmarkReadableContentUseCase: GetBookmarkReadableContentUseCase +) : ViewModel() { + + private var serverUrl = "" + private var token = "" + + private val _readableContentState = MutableStateFlow(UiState(idle = true)) + val readableContentState = _readableContentState.asStateFlow() + + fun loadInitialData() { + viewModelScope.launch { + serverUrl = settingsPreferenceDataSource.getUrl() + token = settingsPreferenceDataSource.getToken() + } + } + + fun getBookmarkReadableContent( + bookmarkId: Int + ) { + viewModelScope.launch { + getBookmarkReadableContentUseCase.invoke( + serverUrl = serverUrl, + token = token, + bookmarkId = bookmarkId + ) + .distinctUntilChanged() + .collect() { result -> + when (result) { + is Result.Error -> { + Log.v( + "ReadableContent", + "Error getting bookmark readable content: ${result.error?.message}" + ) + } + is Result.Loading -> { + Log.v( + "ReadableContent", + "Loading, getting bookmark readable content..." + ) + _readableContentState.isLoading(true) + } + + is Result.Success -> { + Log.v("ReadableContent", "Get bookmark readable content successfully.") + result.data?.let { + _readableContentState.success(it.message) + } + } + else -> {} + } + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/TopSection.kt b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/TopSection.kt new file mode 100644 index 0000000..43c6232 --- /dev/null +++ b/presentation/src/main/java/com/desarrollodroide/pagekeeper/ui/readablecontent/TopSection.kt @@ -0,0 +1,75 @@ +package com.desarrollodroide.pagekeeper.ui.readablecontent + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun TopSection( + title: String, + date: String, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text( + text = date, + fontSize = 14.sp, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = title, + fontSize = 24.sp, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Button( + onClick = onClick, + ) { + Text("View Original", color = Color.White) + } + } + HorizontalDivider(modifier = Modifier.padding(vertical = 10.dp)) + } +} + +@RequiresApi(Build.VERSION_CODES.N) +@Preview(showBackground = true) +@Composable +fun TopSectionPreview() { + MaterialTheme { + TopSection( + title = "A Developer’s Roadmap to Predictive Back (Views)", + date = "Added 27 May 2024, 16:41:09", + onClick = {} + ) + } +} +