diff --git a/app/src/main/java/org/wikipedia/categories/CategoryActivity.kt b/app/src/main/java/org/wikipedia/categories/CategoryActivity.kt index f67237a4374..29f7c64be5d 100644 --- a/app/src/main/java/org/wikipedia/categories/CategoryActivity.kt +++ b/app/src/main/java/org/wikipedia/categories/CategoryActivity.kt @@ -3,246 +3,448 @@ package org.wikipedia.categories import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.TextView +import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.annotation.StringRes -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.clip +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.paging.LoadState -import androidx.paging.LoadStateAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.tabs.TabLayout -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch +import androidx.paging.compose.collectAsLazyPagingItems +import coil3.compose.AsyncImage import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.activity.BaseActivity -import org.wikipedia.adapter.PagingDataAdapterPatched -import org.wikipedia.databinding.ActivityCategoryBinding +import org.wikipedia.analytics.eventplatform.BreadCrumbLogEvent +import org.wikipedia.compose.components.WikiTopAppBar +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.dataclient.WikiSite import org.wikipedia.history.HistoryEntry import org.wikipedia.page.ExclusiveBottomSheetPresenter import org.wikipedia.page.Namespace import org.wikipedia.page.PageTitle import org.wikipedia.page.linkpreview.LinkPreviewDialog -import org.wikipedia.readinglist.database.ReadingList -import org.wikipedia.util.ResourceUtil +import org.wikipedia.theme.Theme import org.wikipedia.util.StringUtil -import org.wikipedia.views.DrawableItemDecoration -import org.wikipedia.views.PageItemView -import org.wikipedia.views.WikiErrorView +import org.wikipedia.views.imageservice.ImageService class CategoryActivity : BaseActivity() { - private lateinit var binding: ActivityCategoryBinding - - private val categoryMembersAdapter = CategoryMembersAdapter() - private val categoryMembersLoadHeader = LoadingItemAdapter { categoryMembersAdapter.retry(); } - private val categoryMembersLoadFooter = LoadingItemAdapter { categoryMembersAdapter.retry(); } - private val categoryMembersConcatAdapter = categoryMembersAdapter.withLoadStateHeaderAndFooter(categoryMembersLoadHeader, categoryMembersLoadFooter) - private val subcategoriesAdapter = CategoryMembersAdapter() - private val subcategoriesLoadHeader = LoadingItemAdapter { subcategoriesAdapter.retry() } - private val subcategoriesLoadFooter = LoadingItemAdapter { subcategoriesAdapter.retry() } - private val subcategoriesConcatAdapter = subcategoriesAdapter.withLoadStateHeaderAndFooter(subcategoriesLoadHeader, subcategoriesLoadFooter) - - private val itemCallback = ItemCallback() private val viewModel: CategoryActivityViewModel by viewModels() public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityCategoryBinding.inflate(layoutInflater) - setContentView(binding.root) - - setStatusBarColor(ResourceUtil.getThemedColor(this, android.R.attr.windowBackground)) - setSupportActionBar(binding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowTitleEnabled(false) - binding.toolbarTitle.text = StringUtil.removeHTMLTags(viewModel.pageTitle.displayText) - - binding.categoryRecycler.layoutManager = LinearLayoutManager(this) - binding.categoryRecycler.addItemDecoration(DrawableItemDecoration(this, R.attr.list_divider, drawStart = false, drawEnd = false)) - binding.categoryRecycler.adapter = categoryMembersConcatAdapter - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.categoryMembersFlow.collectLatest { - categoryMembersAdapter.submitData(lifecycleScope, it) + setContent { + BaseTheme { + CategoryScreen( + viewModel = viewModel, + onNavigateBack = { finish() }, + onShowCategoryDialog = { + ExclusiveBottomSheetPresenter.show( + supportFragmentManager, + CategoryDialog.newInstance(viewModel.pageTitle) + ) + }, + onNavigateToCategory = { title -> + startActivity(newIntent(this, title)) + }, + onShowArticlePreview = { title -> + val entry = HistoryEntry(title, HistoryEntry.SOURCE_CATEGORY) + ExclusiveBottomSheetPresenter.show( + supportFragmentManager, + LinkPreviewDialog.newInstance(entry) + ) } - } - launch { - viewModel.subcategoriesFlow.collectLatest { - subcategoriesAdapter.submitData(lifecycleScope, it) - } - } - launch { - categoryMembersAdapter.loadStateFlow.collectLatest { - categoryMembersLoadHeader.loadState = it.refresh - categoryMembersLoadFooter.loadState = it.append - val showEmpty = (it.append is LoadState.NotLoading && it.append.endOfPaginationReached && categoryMembersAdapter.itemCount == 0) - if (showEmpty) { - categoryMembersConcatAdapter.addAdapter(EmptyItemAdapter(R.string.category_empty)) - } - } - } - launch { - subcategoriesAdapter.loadStateFlow.collectLatest { - subcategoriesLoadHeader.loadState = it.refresh - subcategoriesLoadFooter.loadState = it.append - val showEmpty = (it.append is LoadState.NotLoading && it.append.endOfPaginationReached && subcategoriesAdapter.itemCount == 0) - if (showEmpty) { - subcategoriesConcatAdapter.addAdapter(EmptyItemAdapter(R.string.subcategory_empty)) - } - } - } + ) } } - - binding.categoryTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - viewModel.showSubcategories = tab.position == 1 - if (viewModel.showSubcategories) { - binding.categoryRecycler.adapter = subcategoriesConcatAdapter - } else { - binding.categoryRecycler.adapter = categoryMembersConcatAdapter - } - } - - override fun onTabUnselected(tab: TabLayout.Tab) {} - override fun onTabReselected(tab: TabLayout.Tab) {} - }) - binding.categoryTabLayout.selectTab(binding.categoryTabLayout.getTabAt(if (viewModel.showSubcategories) 1 else 0)) } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_category, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.menu_categories -> { - ExclusiveBottomSheetPresenter.show(supportFragmentManager, CategoryDialog.newInstance(viewModel.pageTitle)) - true - } - else -> super.onOptionsItemSelected(item) + companion object { + fun newIntent(context: Context, categoryTitle: PageTitle): Intent { + return Intent(context, CategoryActivity::class.java) + .putExtra(Constants.ARG_TITLE, categoryTitle) } } +} - private fun loadPage(title: PageTitle) { - if (viewModel.showSubcategories) { - startActivity(newIntent(this, title)) - } else { - val entry = HistoryEntry(title, HistoryEntry.SOURCE_CATEGORY) - ExclusiveBottomSheetPresenter.show(supportFragmentManager, LinkPreviewDialog.newInstance(entry)) - } - } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CategoryScreen( + viewModel: CategoryActivityViewModel, + onNavigateBack: () -> Unit, + onShowCategoryDialog: () -> Unit, + onNavigateToCategory: (PageTitle) -> Unit, + onShowArticlePreview: (PageTitle) -> Unit +) { + val context = LocalContext.current + var selectedTabIndex by remember { mutableIntStateOf(if (viewModel.showSubcategories) 1 else 0) } + var showMenu by remember { mutableStateOf(false) } - private inner class LoadingItemAdapter(private val retry: () -> Unit) : LoadStateAdapter() { - override fun onBindViewHolder(holder: LoadingViewHolder, loadState: LoadState) { - holder.bindItem(loadState, retry) - } + Scaffold( + containerColor = WikipediaTheme.colors.paperColor, + topBar = { + Column { + WikiTopAppBar( + title = StringUtil.removeHTMLTags(viewModel.pageTitle.displayText), + titleStyle = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + lineHeight = 24.sp + ), + onNavigationClick = { + BreadCrumbLogEvent.logClick(context, "navigationButton") + onNavigateBack() + }, + actions = { + Box { + IconButton(onClick = { + showMenu = true + }) { + Icon( + painter = painterResource(R.drawable.ic_more_vert_white_24dp), + tint = WikipediaTheme.colors.primaryColor, + contentDescription = stringResource(R.string.menu_feed_overflow_label) + ) + } - override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadingViewHolder { - return LoadingViewHolder(layoutInflater.inflate(R.layout.item_list_progress, parent, false)) - } - } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + containerColor = WikipediaTheme.colors.paperColor, + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.action_item_categories), + color = WikipediaTheme.colors.primaryColor + ) + }, + onClick = { + showMenu = false + BreadCrumbLogEvent.logClick(context, "categoryButton") + onShowCategoryDialog() + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_category_black_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null + ) + } + ) + } + } + } + ) - private inner class EmptyItemAdapter(@StringRes private val text: Int) : RecyclerView.Adapter() { - override fun onBindViewHolder(holder: EmptyViewHolder, position: Int) { - holder.bindItem(text) + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, + containerColor = WikipediaTheme.colors.paperColor, + contentColor = WikipediaTheme.colors.progressiveColor, + indicator = { + TabRowDefaults.PrimaryIndicator( + color = WikipediaTheme.colors.progressiveColor, + modifier = Modifier.tabIndicatorOffset(selectedTabIndex), + width = Dp.Unspecified + ) + }, + divider = { + HorizontalDivider(color = WikipediaTheme.colors.borderColor) + } + ) { + Tab( + selected = selectedTabIndex == 0, + onClick = { + selectedTabIndex = 0 + viewModel.showSubcategories = false + }, + text = { + Text( + text = stringResource(R.string.category_tab_articles), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold + ), + color = if (selectedTabIndex == 0) { + WikipediaTheme.colors.progressiveColor + } else { + WikipediaTheme.colors.placeholderColor + } + ) + } + ) + Tab( + selected = selectedTabIndex == 1, + onClick = { + selectedTabIndex = 1 + viewModel.showSubcategories = true + }, + text = { + Text( + text = stringResource(R.string.category_tab_subcategories), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold + ), + color = if (selectedTabIndex == 1) { + WikipediaTheme.colors.progressiveColor + } else { + WikipediaTheme.colors.placeholderColor + } + ) + } + ) + } + } } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmptyViewHolder { - return EmptyViewHolder(layoutInflater.inflate(R.layout.item_list_progress, parent, false)) + ) { paddingValues -> + if (selectedTabIndex == 0) { + CategoryMembersList( + viewModel = viewModel, + isSubcategories = false, + onItemClick = onShowArticlePreview, + modifier = Modifier.padding(paddingValues) + ) + } else { + CategoryMembersList( + viewModel = viewModel, + isSubcategories = true, + onItemClick = onNavigateToCategory, + modifier = Modifier.padding(paddingValues) + ) } + } +} - override fun getItemCount(): Int { return 1 } +@Composable +fun CategoryMembersList( + viewModel: CategoryActivityViewModel, + isSubcategories: Boolean, + onItemClick: (PageTitle) -> Unit, + modifier: Modifier = Modifier +) { + val pagingItems = if (isSubcategories) { + viewModel.subcategoriesFlow.collectAsLazyPagingItems() + } else { + viewModel.categoryMembersFlow.collectAsLazyPagingItems() } - private inner class CategoryMemberDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: PageTitle, newItem: PageTitle): Boolean { - return oldItem.prefixedText == newItem.prefixedText && oldItem.namespace == newItem.namespace - } + val loadState = pagingItems.loadState - override fun areContentsTheSame(oldItem: PageTitle, newItem: PageTitle): Boolean { - return areItemsTheSame(oldItem, newItem) - } - } + Box(modifier = modifier.fillMaxSize()) { + when { + loadState.refresh is LoadState.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(24.dp), + color = WikipediaTheme.colors.progressiveColor + ) + } - private inner class CategoryMembersAdapter : PagingDataAdapterPatched(CategoryMemberDiffCallback()) { - override fun onCreateViewHolder(parent: ViewGroup, pos: Int): CategoryItemHolder { - val view = PageItemView(this@CategoryActivity) - view.callback = itemCallback - return CategoryItemHolder(view) - } + loadState.refresh is LoadState.Error -> { + val error = (loadState.refresh as LoadState.Error).error + WikiErrorView( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center) + .padding(16.dp), + caught = error, + pageTitle = viewModel.pageTitle, + errorClickEvents = WikiErrorClickEvents( + retryClickListener = { pagingItems.retry() } + ), + retryForGenericError = true + ) + } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - getItem(position)?.let { - (holder as CategoryItemHolder).bindItem(it) + loadState.append is LoadState.NotLoading && loadState.append.endOfPaginationReached && pagingItems.itemCount == 0 -> { + Text( + text = stringResource( + if (isSubcategories) R.string.subcategory_empty else R.string.category_empty + ), + modifier = Modifier + .align(Alignment.Center) + .padding(16.dp), + color = WikipediaTheme.colors.secondaryColor, + style = MaterialTheme.typography.bodyLarge + ) } - } - } - private inner class LoadingViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun bindItem(loadState: LoadState, retry: () -> Unit) { - val errorView = itemView.findViewById(R.id.errorView) - val progressBar = itemView.findViewById(R.id.progressBar) - progressBar.isVisible = loadState is LoadState.Loading - errorView.isVisible = loadState is LoadState.Error - errorView.retryClickListener = View.OnClickListener { retry() } - if (loadState is LoadState.Error) { - errorView.setError(loadState.error, viewModel.pageTitle) + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(count = pagingItems.itemCount) { index -> + pagingItems[index]?.let { pageTitle -> + CategoryItem( + pageTitle = pageTitle, + onClick = { onItemClick(pageTitle) } + ) + if (index < pagingItems.itemCount - 1) { + HorizontalDivider( + color = WikipediaTheme.colors.borderColor, + thickness = 1.dp + ) + } + } + } + + if (loadState.append is LoadState.Loading) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = WikipediaTheme.colors.progressiveColor + ) + } + } + } + + if (loadState.append is LoadState.Error) { + item { + val error = (loadState.append as LoadState.Error).error + WikiErrorView( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + caught = error, + pageTitle = viewModel.pageTitle, + errorClickEvents = WikiErrorClickEvents( + retryClickListener = { pagingItems.retry() } + ), + retryForGenericError = true + ) + } + } + } } } } +} - private inner class EmptyViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun bindItem(@StringRes text: Int) { - val errorView = itemView.findViewById(R.id.errorView) - val progressBar = itemView.findViewById(R.id.progressBar) - val emptyMessage = itemView.findViewById(R.id.emptyMessage) - progressBar.isVisible = false - errorView.isVisible = false - emptyMessage.text = getString(text) - emptyMessage.isVisible = true - } - } +@Composable +fun CategoryItem( + pageTitle: PageTitle, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current - private inner class CategoryItemHolder constructor(val view: PageItemView) : RecyclerView.ViewHolder(view) { - fun bindItem(title: PageTitle) { - view.item = title - view.setTitle(if (title.namespace() !== Namespace.CATEGORY) title.displayText else StringUtil.removeUnderscores(title.text)) - view.setImageUrl(title.thumbUrl) - view.setImageVisible(!title.thumbUrl.isNullOrEmpty()) - view.setDescription(title.description) - } - } + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .background(WikipediaTheme.colors.paperColor) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = if (pageTitle.namespace() !== Namespace.CATEGORY) { + pageTitle.displayText + } else { + StringUtil.removeUnderscores(pageTitle.text) + }, + color = WikipediaTheme.colors.primaryColor, + style = MaterialTheme.typography.bodyLarge, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) - private inner class ItemCallback : PageItemView.Callback { - override fun onClick(item: PageTitle?) { - item?.let { loadPage(it) } + if (!pageTitle.description.isNullOrEmpty()) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = pageTitle.description.orEmpty(), + color = WikipediaTheme.colors.secondaryColor, + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 14.sp), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } } - override fun onLongClick(item: PageTitle?): Boolean { - return false + if (!pageTitle.thumbUrl.isNullOrEmpty()) { + val request = ImageService.getRequest(context, url = pageTitle.thumbUrl) + AsyncImage( + model = request, + placeholder = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + error = BrushPainter(SolidColor(WikipediaTheme.colors.borderColor)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + ) } - - override fun onActionClick(item: PageTitle?, view: View) {} - - override fun onListChipClick(readingList: ReadingList) {} } +} - companion object { - fun newIntent(context: Context, categoryTitle: PageTitle): Intent { - return Intent(context, CategoryActivity::class.java) - .putExtra(Constants.ARG_TITLE, categoryTitle) - } +@Preview(showBackground = true) +@Composable +private fun CategoryItemPreview() { + BaseTheme(currentTheme = Theme.LIGHT) { + CategoryItem( + pageTitle = PageTitle( + "Example Article", + WikiSite("https://en.wikipedia.org/".toUri(), "en") + ).apply { + description = "This is an example article description" + }, + onClick = {} + ) } } diff --git a/app/src/main/java/org/wikipedia/categories/CategoryDialog.kt b/app/src/main/java/org/wikipedia/categories/CategoryDialog.kt index e0d1bae80b1..81134466dc4 100644 --- a/app/src/main/java/org/wikipedia/categories/CategoryDialog.kt +++ b/app/src/main/java/org/wikipedia/categories/CategoryDialog.kt @@ -1,128 +1,292 @@ package org.wikipedia.categories -import android.graphics.Typeface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.core.os.bundleOf -import androidx.core.view.isVisible import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle import org.wikipedia.Constants import org.wikipedia.R -import org.wikipedia.databinding.DialogCategoriesBinding -import org.wikipedia.extensions.setLayoutDirectionByLang +import org.wikipedia.compose.components.HtmlText +import org.wikipedia.compose.components.error.WikiErrorClickEvents +import org.wikipedia.compose.components.error.WikiErrorView +import org.wikipedia.compose.theme.BaseTheme +import org.wikipedia.compose.theme.WikipediaTheme +import org.wikipedia.dataclient.WikiSite import org.wikipedia.page.ExtendedBottomSheetDialogFragment import org.wikipedia.page.PageTitle -import org.wikipedia.readinglist.database.ReadingList +import org.wikipedia.theme.Theme import org.wikipedia.util.Resource import org.wikipedia.util.StringUtil -import org.wikipedia.util.log.L -import org.wikipedia.views.DrawableItemDecoration -import org.wikipedia.views.PageItemView class CategoryDialog : ExtendedBottomSheetDialogFragment() { - private var _binding: DialogCategoriesBinding? = null - private val binding get() = _binding!! - - private val itemCallback = ItemCallback() private val viewModel: CategoryDialogViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = DialogCategoriesBinding.inflate(inflater, container, false) - binding.categoriesRecycler.layoutManager = LinearLayoutManager(requireActivity()) - binding.categoriesRecycler.addItemDecoration(DrawableItemDecoration(requireContext(), R.attr.list_divider, drawStart = false, drawEnd = false)) - binding.categoriesDialogPageTitle.text = StringUtil.fromHtml(viewModel.pageTitle.displayText) - binding.root.setLayoutDirectionByLang(viewModel.pageTitle.wikiSite.languageCode) - - binding.categoriesError.isVisible = false - binding.categoriesNoneFound.isVisible = false - binding.categoriesRecycler.isVisible = false - binding.dialogCategoriesProgress.isVisible = true - binding.categoriesError.backClickListener = View.OnClickListener { dismiss() } - - viewModel.categoriesData.observe(this) { - binding.dialogCategoriesProgress.isVisible = false - if (it is Resource.Success) { - layOutCategories(it.data) - } else if (it is Resource.Error) { - binding.categoriesRecycler.isVisible = false - binding.categoriesError.setError(it.throwable) - binding.categoriesError.isVisible = true - L.e(it.throwable) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + var categoriesData by remember { mutableStateOf>>(Resource.Loading()) } + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(viewModel) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.categoriesData.observe(lifecycleOwner) { data -> + categoriesData = data + } + } + } + + BaseTheme { + CategoryDialogContent( + pageTitle = viewModel.pageTitle, + categoriesData = categoriesData, + onCategoryClick = { title -> + startActivity(CategoryActivity.newIntent(requireActivity(), title)) + }, + onDismiss = { dismiss() } + ) + } } } - - return binding.root } - override fun onDestroy() { - _binding = null - super.onDestroy() + companion object { + fun newInstance(title: PageTitle): CategoryDialog { + return CategoryDialog().apply { arguments = bundleOf(Constants.ARG_TITLE to title) } + } } +} - private fun layOutCategories(categoryList: List) { - binding.categoriesRecycler.isVisible = categoryList.isNotEmpty() - binding.categoriesNoneFound.isVisible = categoryList.isEmpty() - binding.categoriesError.visibility = View.GONE - binding.categoriesRecycler.adapter = CategoryAdapter(categoryList) - } +@Composable +fun CategoryDialogContent( + pageTitle: PageTitle, + categoriesData: Resource>, + onCategoryClick: (PageTitle) -> Unit, + onDismiss: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(WikipediaTheme.colors.paperColor) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_category_black_24dp), + tint = WikipediaTheme.colors.secondaryColor, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) - private inner class CategoryItemHolder constructor(val view: PageItemView) : RecyclerView.ViewHolder(view) { - fun bindItem(title: PageTitle) { - view.item = title - view.setTitle(StringUtil.removeNamespace(title.displayText)) - } - } + Column( + modifier = Modifier + .weight(1f) + .padding(start = 20.dp) + ) { + Text( + text = stringResource(R.string.action_item_categories), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ), + color = WikipediaTheme.colors.primaryColor, + maxLines = 1 + ) - private inner class CategoryAdapter(val categoryList: List) : RecyclerView.Adapter() { - override fun getItemCount(): Int { - return categoryList.size + HtmlText( + text = pageTitle.displayText, + style = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp), + color = WikipediaTheme.colors.primaryColor, + maxLines = 1, + modifier = Modifier.padding(top = 4.dp) + ) + } } - override fun onCreateViewHolder(parent: ViewGroup, pos: Int): CategoryItemHolder { - val view = PageItemView(requireContext()) - view.setImageVisible(false) - view.setTitleTypeface(Typeface.NORMAL) - return CategoryItemHolder(view) - } + HorizontalDivider( + color = WikipediaTheme.colors.borderColor, + thickness = 0.5.dp + ) - override fun onBindViewHolder(holder: CategoryItemHolder, pos: Int) { - holder.bindItem(categoryList[pos]) - } + when (categoriesData) { + is Resource.Success -> { + val categories = categoriesData.data + if (categories.isEmpty()) { + Text( + text = stringResource(R.string.page_no_categories), + color = WikipediaTheme.colors.primaryColor, + modifier = Modifier.padding(16.dp) + ) + } else { + LazyColumn { + items(categories) { category -> + CategoryDialogItem( + pageTitle = category, + onClick = { onCategoryClick(category) } + ) + HorizontalDivider( + color = WikipediaTheme.colors.borderColor, + thickness = 1.dp + ) + } + } + } + } - override fun onViewAttachedToWindow(holder: CategoryItemHolder) { - super.onViewAttachedToWindow(holder) - holder.view.callback = itemCallback - } + is Resource.Error -> { + WikiErrorView( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + caught = categoriesData.throwable, + errorClickEvents = WikiErrorClickEvents( + backClickListener = { onDismiss() } + ) + ) + } - override fun onViewDetachedFromWindow(holder: CategoryItemHolder) { - holder.view.callback = null - super.onViewDetachedFromWindow(holder) + else -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = WikipediaTheme.colors.progressiveColor + ) + } + } } } +} - private inner class ItemCallback : PageItemView.Callback { - override fun onClick(item: PageTitle?) { - if (item != null) { - startActivity(CategoryActivity.newIntent(requireActivity(), item)) - } - } +@Composable +fun CategoryDialogItem( + pageTitle: PageTitle, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .background(WikipediaTheme.colors.paperColor) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = StringUtil.removeNamespace(pageTitle.displayText), + color = WikipediaTheme.colors.primaryColor, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + } +} - override fun onLongClick(item: PageTitle?): Boolean { - return false - } +@Preview +@Composable +private fun CategoryDialogContentLoadingPreview() { + val wikiSite = WikiSite("https://en.wikipedia.org/".toUri(), "en") + BaseTheme(currentTheme = Theme.LIGHT) { + CategoryDialogContent( + pageTitle = PageTitle("Albert Einstein", wikiSite), + categoriesData = Resource.Loading(), + onCategoryClick = {}, + onDismiss = {} + ) + } +} - override fun onActionClick(item: PageTitle?, view: View) {} +@Preview +@Composable +private fun CategoryDialogContentSuccessPreview() { + val wikiSite = WikiSite("https://en.wikipedia.org/".toUri(), "en") + BaseTheme(currentTheme = Theme.LIGHT) { + CategoryDialogContent( + pageTitle = PageTitle("Albert Einstein", wikiSite), + categoriesData = Resource.Success( + listOf( + PageTitle("Category:Physics", wikiSite), + PageTitle("Category:Chemistry", wikiSite), + PageTitle("Category:Biology", wikiSite) + ) + ), + onCategoryClick = {}, + onDismiss = {} + ) + } +} - override fun onListChipClick(readingList: ReadingList) {} +@Preview +@Composable +private fun CategoryDialogContentEmptyPreview() { + val wikiSite = WikiSite("https://en.wikipedia.org/".toUri(), "en") + BaseTheme(currentTheme = Theme.LIGHT) { + CategoryDialogContent( + pageTitle = PageTitle("Albert Einstein", wikiSite), + categoriesData = Resource.Success(emptyList()), + onCategoryClick = {}, + onDismiss = {} + ) } +} - companion object { - fun newInstance(title: PageTitle): CategoryDialog { - return CategoryDialog().apply { arguments = bundleOf(Constants.ARG_TITLE to title) } - } +@Preview +@Composable +private fun CategoryDialogContentDarkPreview() { + val wikiSite = WikiSite("https://en.wikipedia.org/".toUri(), "en") + BaseTheme(currentTheme = Theme.DARK) { + CategoryDialogContent( + pageTitle = PageTitle("Albert Einstein", wikiSite), + categoriesData = Resource.Success( + listOf( + PageTitle("Category:Physics", wikiSite), + PageTitle("Category:Chemistry", wikiSite) + ) + ), + onCategoryClick = {}, + onDismiss = {} + ) } } diff --git a/app/src/main/res/layout/activity_category.xml b/app/src/main/res/layout/activity_category.xml deleted file mode 100644 index 2f33a0842bc..00000000000 --- a/app/src/main/res/layout/activity_category.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_categories.xml b/app/src/main/res/layout/dialog_categories.xml deleted file mode 100644 index e78da1b4887..00000000000 --- a/app/src/main/res/layout/dialog_categories.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_category.xml b/app/src/main/res/menu/menu_category.xml deleted file mode 100644 index d50ee4dcb5e..00000000000 --- a/app/src/main/res/menu/menu_category.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - \ No newline at end of file