diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt index b20f9228c..817673928 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt @@ -38,6 +38,7 @@ import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.core.R.string +import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG import org.kiwix.kiwixmobile.language.composables.LANGUAGE_ITEM_CHECKBOX_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt index 9b60a862c..0471cd038 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/main/TopLevelDestinationTest.kt @@ -119,13 +119,13 @@ class TopLevelDestinationTest : BaseActivityTest() { } } clickBookmarksOnNavDrawer { - assertBookMarksDisplayed() - clickOnTrashIcon() + assertBookMarksDisplayed(composeTestRule) + clickOnTrashIcon(composeTestRule) assertDeleteBookmarksDialogDisplayed() } clickHistoryOnSideNav { - assertHistoryDisplayed() - clickOnTrashIcon() + assertHistoryDisplayed(composeTestRule) + clickOnTrashIcon(composeTestRule) assertDeleteHistoryDialogDisplayed() } clickHostBooksOnSideNav(ZimHostRobot::assertMenuWifiHotspotDiplayed) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt index d48a070b8..9ac594bd7 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt @@ -121,9 +121,8 @@ class NoteFragmentTest : BaseActivityTest() { it.navigate(R.id.notesFragment) } note { - assertToolbarExist() - assertNoteRecyclerViewExist() - assertSwitchWidgetExist() + assertToolbarExist(composeTestRule) + assertSwitchWidgetExist(composeTestRule) } LeakAssertions.assertNoLeaks() } @@ -140,9 +139,8 @@ class NoteFragmentTest : BaseActivityTest() { saveNote(composeTestRule) pressBack() openNoteFragment() - assertToolbarExist() - assertNoteRecyclerViewExist() - clickOnSavedNote() + assertToolbarExist(composeTestRule) + clickOnSavedNote(composeTestRule) clickOnOpenNote() assertNoteSaved(composeTestRule) // to close the note dialog. @@ -164,9 +162,8 @@ class NoteFragmentTest : BaseActivityTest() { note { openNoteFragment() - assertToolbarExist() - assertNoteRecyclerViewExist() - clickOnSavedNote() + assertToolbarExist(composeTestRule) + clickOnSavedNote(composeTestRule) clickOnOpenNote() assertNoteSaved(composeTestRule) pressBack() @@ -189,9 +186,8 @@ class NoteFragmentTest : BaseActivityTest() { saveNote(composeTestRule) pressBack() openNoteFragment() - assertToolbarExist() - assertNoteRecyclerViewExist() - clickOnSavedNote() + assertToolbarExist(composeTestRule) + clickOnSavedNote(composeTestRule) clickOnOpenNote() assertNoteSaved(composeTestRule) // to close the note dialog. @@ -213,14 +209,13 @@ class NoteFragmentTest : BaseActivityTest() { saveNote(composeTestRule) pressBack() openNoteFragment() - assertToolbarExist() - assertNoteRecyclerViewExist() - clickOnSavedNote() + assertToolbarExist(composeTestRule) + clickOnSavedNote(composeTestRule) clickOnOpenNote() assertNoteSaved(composeTestRule) clickOnDeleteIcon(composeTestRule) pressBack() - assertNoNotesTextDisplayed() + assertNoNotesTextDisplayed(composeTestRule) } } } @@ -252,12 +247,11 @@ class NoteFragmentTest : BaseActivityTest() { // delete the notes if any saved to properly run the test scenario note { openNoteFragment() - assertToolbarExist() - assertNoteRecyclerViewExist() - clickOnTrashIcon() + assertToolbarExist(composeTestRule) + clickOnTrashIcon(composeTestRule) assertDeleteNoteDialogDisplayed() clickOnDeleteButton() - assertNoNotesTextDisplayed() + assertNoNotesTextDisplayed(composeTestRule) pressBack() } } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt index 1017e588d..9cf4240c9 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt @@ -22,18 +22,15 @@ import android.content.Context import androidx.compose.ui.test.assertTextContains import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextReplacement -import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement @@ -42,11 +39,14 @@ import com.adevinta.android.barista.interaction.BaristaSleepInteractions import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.Findable.Text -import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.main.ADD_NOTE_TEXT_FILED_TESTING_TAG import org.kiwix.kiwixmobile.core.main.DELETE_MENU_BUTTON_TESTING_TAG import org.kiwix.kiwixmobile.core.main.SAVE_MENU_BUTTON_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.DELETE_MENU_ICON_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.NO_ITEMS_TEXT_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.PAGE_ITEM_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.SWITCH_TEXT_TESTING_TAG import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView @@ -57,16 +57,20 @@ fun note(func: NoteRobot.() -> Unit) = NoteRobot().apply(func) class NoteRobot : BaseRobot() { private val noteText = "Test Note" - fun assertToolbarExist() { - isVisible(ViewId(R.id.toolbar)) + fun assertToolbarExist(composeTestRule: ComposeContentTestRule) { + testFlakyView({ + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG) + .assertTextEquals(context.getString(R.string.pref_notes)) + }) } - fun assertNoteRecyclerViewExist() { - isVisible(ViewId(R.id.recycler_view)) - } - - fun assertSwitchWidgetExist() { - isVisible(ViewId(R.id.page_switch)) + fun assertSwitchWidgetExist(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(SWITCH_TEXT_TESTING_TAG) + .assertTextEquals(context.getString(R.string.notes_from_all_books)) + } } fun clickOnNoteMenuItem(context: Context) { @@ -117,15 +121,11 @@ class NoteRobot : BaseRobot() { testFlakyView({ onView(withText(R.string.pref_notes)).perform(click()) }) } - fun clickOnSavedNote() { - testFlakyView({ - onView(withId(R.id.recycler_view)).perform( - actionOnItemAtPosition( - 0, - click() - ) - ) - }) + fun clickOnSavedNote(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onAllNodesWithTag(PAGE_ITEM_TESTING_TAG)[0].performClick() + } } fun clickOnOpenNote() { @@ -158,8 +158,14 @@ class NoteRobot : BaseRobot() { }) } - fun clickOnTrashIcon() { - testFlakyView({ onView(withContentDescription(R.string.pref_clear_notes)).perform(click()) }) + fun clickOnTrashIcon(composeTestRule: ComposeContentTestRule) { + testFlakyView({ + composeTestRule.apply { + waitForIdle() + onNodeWithTag(DELETE_MENU_ICON_TESTING_TAG) + .performClick() + } + }) } fun assertDeleteNoteDialogDisplayed() { @@ -171,8 +177,12 @@ class NoteRobot : BaseRobot() { testFlakyView({ onView(ViewMatchers.withText("DELETE")).perform(click()) }) } - fun assertNoNotesTextDisplayed() { - testFlakyView({ isVisible(TextId(R.string.no_notes)) }) + fun assertNoNotesTextDisplayed(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(NO_ITEMS_TEXT_TESTING_TAG) + .assertTextEquals(context.getString(R.string.no_notes)) + } } fun assertHomePageIsLoadedOfTestZimFile() { diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/BookmarksRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/BookmarksRobot.kt index 28496938b..0277d27e9 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/BookmarksRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/BookmarksRobot.kt @@ -18,25 +18,29 @@ package org.kiwix.kiwixmobile.page.bookmarks -import androidx.recyclerview.widget.RecyclerView +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.longClick -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import applyWithViewHierarchyPrinting -import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed import com.adevinta.android.barista.interaction.BaristaSleepInteractions import org.kiwix.kiwixmobile.BaseRobot -import org.kiwix.kiwixmobile.Findable.StringId.ContentDesc import org.kiwix.kiwixmobile.Findable.StringId.TextId -import org.kiwix.kiwixmobile.Findable.Text import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.page.DELETE_MENU_ICON_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.NO_ITEMS_TEXT_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.PAGE_LIST_TEST_TAG +import org.kiwix.kiwixmobile.core.page.SWITCH_TEXT_TESTING_TAG import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView @@ -49,12 +53,20 @@ fun bookmarks(func: BookmarksRobot.() -> Unit) = class BookmarksRobot : BaseRobot() { private var retryCountForBookmarkAddedButton = 5 - fun assertBookMarksDisplayed() { - assertDisplayed(R.string.bookmarks_from_current_book) + fun assertBookMarksDisplayed(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(SWITCH_TEXT_TESTING_TAG) + .assertTextEquals(context.getString(R.string.bookmarks_from_current_book)) + } } - fun clickOnTrashIcon() { - clickOn(ContentDesc(R.string.pref_clear_all_bookmarks_title)) + fun clickOnTrashIcon(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(DELETE_MENU_ICON_TESTING_TAG) + .performClick() + } } fun assertDeleteBookmarksDialogDisplayed() { @@ -66,8 +78,12 @@ class BookmarksRobot : BaseRobot() { testFlakyView({ onView(withText("DELETE")).perform(click()) }) } - fun assertNoBookMarkTextDisplayed() { - testFlakyView({ isVisible(TextId(R.string.no_bookmarks)) }) + fun assertNoBookMarkTextDisplayed(composeTestRule: ComposeTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(NO_ITEMS_TEXT_TESTING_TAG) + .assertTextEquals(context.getString(R.string.no_bookmarks)) + } } fun clickOnSaveBookmarkImage() { @@ -97,14 +113,20 @@ class BookmarksRobot : BaseRobot() { } } - fun assertBookmarkSaved() { + fun assertBookmarkSaved(composeTestRule: ComposeContentTestRule) { pauseForBetterTestPerformance() - isVisible(Text("Test Zim")) + composeTestRule.apply { + waitForIdle() + composeTestRule.onNodeWithText("Test Zim").assertExists() + } } - fun assertBookmarkRemoved() { + fun assertBookmarkRemoved(composeTestRule: ComposeTestRule) { pauseForBetterTestPerformance() - onView(withText("Test Zim")).check(ViewAssertions.doesNotExist()) + composeTestRule.apply { + waitForIdle() + composeTestRule.onNodeWithText("Test Zim").assertDoesNotExist() + } } private fun pauseForBetterTestPerformance() { @@ -118,13 +140,19 @@ class BookmarksRobot : BaseRobot() { }) } - fun testAllBookmarkShowing(bookmarkList: ArrayList) { - bookmarkList.forEachIndexed { index, libkiwixBookmarkItem -> - testFlakyView({ - onView(withId(R.id.recycler_view)) - .perform(RecyclerViewActions.scrollToPosition(index)) - .check(matches(hasDescendant(withText(libkiwixBookmarkItem.title)))) - }) + fun testAllBookmarkShowing( + bookmarkList: ArrayList, + composeTestRule: ComposeTestRule + ) { + composeTestRule.apply { + waitForIdle() + bookmarkList.forEachIndexed { index, libkiwixBookmarkItem -> + testFlakyView({ + composeTestRule.onNodeWithTag(PAGE_LIST_TEST_TAG) + .performScrollToNode(hasText(libkiwixBookmarkItem.title)) + composeTestRule.onNodeWithText(libkiwixBookmarkItem.title).assertExists() + }) + } } } } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/LibkiwixBookmarkTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/LibkiwixBookmarkTest.kt index db2750340..ed40d750f 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/LibkiwixBookmarkTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/LibkiwixBookmarkTest.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.page.bookmarks +import androidx.compose.ui.test.junit4.createComposeRule import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.Lifecycle @@ -42,6 +43,7 @@ import org.kiwix.kiwixmobile.core.main.CoreReaderFragment import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.topLevel @@ -59,6 +61,9 @@ class LibkiwixBookmarkTest : BaseActivityTest() { @JvmField val retryRule = RetryRule() + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + private lateinit var kiwixMainActivity: KiwixMainActivity @Before @@ -119,20 +124,20 @@ class LibkiwixBookmarkTest : BaseActivityTest() { bookmarks { // delete any bookmark if already saved to properly perform this test case. longClickOnSaveBookmarkImage() - clickOnTrashIcon() + clickOnTrashIcon(composeTestRule) assertDeleteBookmarksDialogDisplayed() clickOnDeleteButton() - assertNoBookMarkTextDisplayed() + assertNoBookMarkTextDisplayed(composeTestRule) pressBack() // Test saving bookmark clickOnSaveBookmarkImage() clickOnOpenSavedBookmarkButton() - assertBookmarkSaved() + assertBookmarkSaved(composeTestRule) pressBack() // Test removing bookmark clickOnSaveBookmarkImage() longClickOnSaveBookmarkImage() - assertBookmarkRemoved() + assertBookmarkRemoved(composeTestRule) pressBack() // Save the bookmark to test whether it remains saved after the application restarts or not. clickOnSaveBookmarkImage() @@ -142,7 +147,7 @@ class LibkiwixBookmarkTest : BaseActivityTest() { @Test fun testBookmarkRemainsSavedOrNot() { topLevel { - clickBookmarksOnNavDrawer(BookmarksRobot::assertBookmarkSaved) + clickBookmarksOnNavDrawer { assertBookmarkSaved(composeTestRule) } } } @@ -160,10 +165,10 @@ class LibkiwixBookmarkTest : BaseActivityTest() { bookmarks { // delete any bookmark if already saved to properly perform this test case. longClickOnSaveBookmarkImage() - clickOnTrashIcon() + clickOnTrashIcon(composeTestRule) assertDeleteBookmarksDialogDisplayed() clickOnDeleteButton() - assertNoBookMarkTextDisplayed() + assertNoBookMarkTextDisplayed(composeTestRule) pressBack() } val navHostFragment: NavHostFragment = @@ -198,7 +203,7 @@ class LibkiwixBookmarkTest : BaseActivityTest() { bookmarks { // test all the saved bookmarks are showing on the bookmarks screen openBookmarkScreen() - testAllBookmarkShowing(bookmarkList) + testAllBookmarkShowing(bookmarkList, composeTestRule) } } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/HistoryRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/HistoryRobot.kt index 83957e6da..116f96caf 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/HistoryRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/HistoryRobot.kt @@ -18,23 +18,35 @@ package org.kiwix.kiwixmobile.page.history +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import applyWithViewHierarchyPrinting -import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed import org.kiwix.kiwixmobile.BaseRobot -import org.kiwix.kiwixmobile.Findable.StringId.ContentDesc import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.page.DELETE_MENU_ICON_TESTING_TAG +import org.kiwix.kiwixmobile.core.page.SWITCH_TEXT_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView fun history(func: HistoryRobot.() -> Unit) = HistoryRobot().applyWithViewHierarchyPrinting(func) class HistoryRobot : BaseRobot() { - fun assertHistoryDisplayed() { - assertDisplayed(R.string.history_from_current_book) + fun assertHistoryDisplayed(composeTestRule: ComposeTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(SWITCH_TEXT_TESTING_TAG) + .assertTextEquals(context.getString(R.string.history_from_current_book)) + } } - fun clickOnTrashIcon() { - clickOn(ContentDesc(R.string.pref_clear_all_history_title)) + fun clickOnTrashIcon(composeTestRule: ComposeTestRule) { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(DELETE_MENU_ICON_TESTING_TAG) + .performClick() + } } fun assertDeleteHistoryDialogDisplayed() { diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt index e50f27be0..983336766 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/shortcuts/GetContentShortcutTest.kt @@ -123,13 +123,13 @@ class GetContentShortcutTest { } } clickBookmarksOnNavDrawer { - assertBookMarksDisplayed() - clickOnTrashIcon() + assertBookMarksDisplayed(composeTestRule) + clickOnTrashIcon(composeTestRule) assertDeleteBookmarksDialogDisplayed() } clickHistoryOnSideNav { - assertHistoryDisplayed() - clickOnTrashIcon() + assertHistoryDisplayed(composeTestRule) + clickOnTrashIcon(composeTestRule) assertDeleteHistoryDialogDisplayed() } clickHostBooksOnSideNav(ZimHostRobot::assertMenuWifiHotspotDiplayed) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt index ff820a756..7e85e9132 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt @@ -38,6 +38,7 @@ import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.extensions.viewModel import org.kiwix.kiwixmobile.core.main.CoreMainActivity +import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.IconItem @@ -46,7 +47,6 @@ import org.kiwix.kiwixmobile.language.viewmodel.Action import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel import javax.inject.Inject -const val SEARCH_ICON_TESTING_TAG = "search" const val SAVE_ICON_TESTING_TAG = "saveLanguages" const val SEARCH_FIELD_TESTING_TAG = "searchField" diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryFragment.kt index d2813c15d..94feb8c22 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryFragment.kt @@ -133,7 +133,7 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal private val disposable = CompositeDisposable() private var permissionDeniedLayoutShowing = false private var zimFileUri: Uri? = null - val libraryScreenState = mutableStateOf( + private val libraryScreenState = mutableStateOf( LocalLibraryScreenState( fileSelectListState = FileSelectListState(emptyList()), snackBarHostState = SnackbarHostState(), @@ -156,13 +156,15 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal val isGranted = permissionResult.values.all { it } val isPermanentlyDenied = readStorageHasBeenPermanentlyDenied(isGranted) permissionDeniedLayoutShowing = isPermanentlyDenied - updateLibraryScreenState( - noFilesViewItem = Triple( - requireActivity().resources.getString(string.grant_read_storage_permission), - requireActivity().resources.getString(string.go_to_settings_label), - isPermanentlyDenied + if (permissionDeniedLayoutShowing) { + updateLibraryScreenState( + noFilesViewItem = Triple( + requireActivity().resources.getString(string.grant_read_storage_permission), + requireActivity().resources.getString(string.go_to_settings_label), + true + ) ) - ) + } } override fun inject(baseActivity: BaseActivity) { diff --git a/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt index 34e9106fd..bc5b90734 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt @@ -44,7 +44,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import org.kiwix.kiwixmobile.core.R -import org.kiwix.kiwixmobile.core.extensions.faviconToPainter +import org.kiwix.kiwixmobile.core.downloader.model.Base64String +import org.kiwix.kiwixmobile.core.downloader.model.toPainter import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.utils.ComposeDimens.BOOK_ICON_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP @@ -54,8 +55,8 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP import org.kiwix.kiwixmobile.core.zim_manager.KiloByte import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.ArticleCount -import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk +import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode const val BOOK_ITEM_CHECKBOX_TESTING_TAG = "bookItemCheckboxTestingTag" const val BOOK_ITEM_TESTING_TAG = "bookItemTestingTag" @@ -115,7 +116,7 @@ private fun BookContent( if (selectionMode == SelectionMode.MULTI) { BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index) } - BookIcon(bookOnDisk.book.faviconToPainter()) + BookIcon(Base64String(bookOnDisk.book.favicon).toPainter()) BookDetails(Modifier.weight(1f), bookOnDisk) } } diff --git a/app/src/main/res/navigation/kiwix_nav_graph.xml b/app/src/main/res/navigation/kiwix_nav_graph.xml index 8bf77c014..663efe176 100644 --- a/app/src/main/res/navigation/kiwix_nav_graph.xml +++ b/app/src/main/res/navigation/kiwix_nav_graph.xml @@ -94,13 +94,11 @@ + android:label="BookmarksFragment" /> + android:label="NotesFragment" /> + android:label="HistoryFragment" /> @@ -67,18 +64,44 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv @Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil private var actionMode: ActionMode? = null val compositeDisposable = CompositeDisposable() - abstract val screenTitle: String + abstract val screenTitle: Int abstract val noItemsString: String abstract val switchString: String abstract val searchQueryHint: String abstract val pageAdapter: PageAdapter abstract val switchIsChecked: Boolean - abstract val deleteIconTitle: String - private var fragmentPageBinding: FragmentPageBinding? = null - override val fragmentToolbar: Toolbar? by lazy { - fragmentPageBinding?.root?.findViewById(R.id.toolbar) - } - override val fragmentTitle: String? by lazy { screenTitle } + abstract val deleteIconTitle: Int + private val pageState: MutableState> = + mutableStateOf( + NotesState( + emptyList(), + true, + "" + ), + policy = referentialEqualityPolicy() + ) + + private val pageScreenState = mutableStateOf( + // Initial values are empty because this is an abstract class. + // Before the view is created, the abstract variables have no values. + // We update this state in `onViewCreated`, once the view is created and the + // abstract variables are initialized. + PageFragmentScreenState( + pageState = pageState.value, + isSearchActive = false, + searchQueryHint = "", + searchText = "", + searchValueChangedListener = {}, + screenTitle = ZERO, + noItemsString = "", + switchString = "", + switchIsChecked = true, + switchIsEnabled = true, + onSwitchCheckedChanged = {}, + deleteIconTitle = ZERO, + clearSearchButtonClickListener = {} + ) + ) private val actionModeCallback: ActionMode.Callback = object : ActionMode.Callback { @@ -104,77 +127,23 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv } } - private fun setupMenu() { - (requireActivity() as MenuHost).addMenuProvider( - object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.menu_page, menu) - val search = menu.findItem(R.id.menu_page_search).actionView as SearchView - search.apply { - setUpSearchView(requireActivity()) - queryHint = searchQueryHint - setOnQueryTextListener( - SimpleTextListener { query, _ -> - pageViewModel.actions.offer(Action.Filter(query)) - } - ) - } - menu.findItem(R.id.menu_pages_clear).title = deleteIconTitle // Bug fix #3825 - } - - @Suppress("ReturnCount") - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - when (menuItem.itemId) { - android.R.id.home -> { - pageViewModel.actions.offer(Action.Exit) - return true - } - - R.id.menu_pages_clear -> { - pageViewModel.actions.offer(Action.UserClickedDeleteButton) - return true - } - } - return false - } - }, - viewLifecycleOwner, - Lifecycle.State.RESUMED - ) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupMenu() - val activity = requireActivity() as CoreMainActivity - fragmentPageBinding?.recyclerView?.apply { - layoutManager = - LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - adapter = pageAdapter - fragmentTitle?.let(::setToolTipWithContentDescription) - } - fragmentPageBinding?.noPage?.text = noItemsString - - fragmentPageBinding?.pageSwitch?.apply { - text = switchString - isChecked = switchIsChecked - // hide switches for custom apps, see more info here https://github.com/kiwix/kiwix-android/issues/3523 - visibility = if (requireActivity().isCustomApp()) GONE else VISIBLE - } - compositeDisposable.add(pageViewModel.effects.subscribe { it.invokeWith(activity) }) - fragmentPageBinding?.pageSwitch?.setOnCheckedChangeListener { _, isChecked -> - pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked)) - } - pageViewModel.state.observe(viewLifecycleOwner, Observer(::render)) - - // hides keyboard when scrolled - fragmentPageBinding?.recyclerView?.addOnScrollListener( - SimpleRecyclerViewScrollListener { _, newState -> - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - fragmentPageBinding?.recyclerView?.closeKeyboard() - } - } + pageScreenState.value = pageScreenState.value.copy( + searchQueryHint = searchQueryHint, + searchText = "", + searchValueChangedListener = { onTextChanged(it) }, + clearSearchButtonClickListener = { onTextChanged("") }, + screenTitle = screenTitle, + noItemsString = noItemsString, + switchString = switchString, + switchIsChecked = switchIsChecked, + onSwitchCheckedChanged = { onSwitchCheckedChanged(it).invoke() }, + deleteIconTitle = deleteIconTitle ) + val activity = requireActivity() as CoreMainActivity + compositeDisposable.add(pageViewModel.effects.subscribe { it.invokeWith(activity) }) + pageViewModel.state.observe(viewLifecycleOwner, Observer(::render)) } override fun onCreateView( @@ -182,24 +151,121 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv container: ViewGroup?, savedInstanceState: Bundle? ): View? { - fragmentPageBinding = FragmentPageBinding.inflate(inflater, container, false) - return fragmentPageBinding?.root + return ComposeView(requireContext()).apply { + setContent { + PageScreen( + state = pageScreenState.value, + itemClickListener = this@PageFragment, + navigationIcon = { + NavigationIcon( + onClick = navigationIconClick() + ) + }, + actionMenuItems = actionMenuList( + isSearchActive = pageScreenState.value.isSearchActive, + onSearchClick = { + // Set the `isSearchActive` when the search button is clicked. + pageScreenState.value = pageScreenState.value.copy(isSearchActive = true) + }, + onDeleteClick = { pageViewModel.actions.offer(Action.UserClickedDeleteButton) } + ) + ) + } + } + } + + /** + * Handles changes to the search text input. + * - Updates the UI state with the latest search query. + * - Sends a filter action to the ViewModel to perform search/filtering logic. + * + * @param searchText The current text entered in the search bar. + */ + private fun onTextChanged(searchText: String) { + pageScreenState.value = pageScreenState.value.copy(searchText = searchText) + pageViewModel.actions.offer(Action.Filter(searchText)) + } + + /** + * Returns a lambda to handle switch toggle changes. + * - Updates the UI state to reflect the new checked status. + * - Sends an action to the ViewModel to handle the toggle event (e.g., show all items or filter). + * + * @param isChecked The new checked state of the switch. + */ + private fun onSwitchCheckedChanged(isChecked: Boolean): () -> Unit = { + pageScreenState.value = pageScreenState.value.copy(switchIsChecked = isChecked) + pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked)) + } + + /** + * Handles the click event for the navigation icon. + * - If search is active, it deactivates the search mode and clears the search text. + * - Otherwise, it triggers the default back navigation. + */ + private fun navigationIconClick(): () -> Unit = { + if (pageScreenState.value.isSearchActive) { + pageScreenState.value = pageScreenState.value.copy(isSearchActive = false) + onTextChanged("") + } else { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + + /** + * Builds the list of action menu items for the app bar. + * + * @param isSearchActive Whether the search mode is currently active. + * @param onSearchClick Callback to invoke when the search icon is clicked. + * @param onDeleteClick Callback to invoke when the delete icon is clicked. + * @return A list of [ActionMenuItem]s to be displayed in the app bar. + * + * - Shows the search icon only when search is not active. + * - Always includes the delete icon, with a content description for accessibility (#3825). + */ + private fun actionMenuList( + isSearchActive: Boolean, + onSearchClick: () -> Unit, + onDeleteClick: () -> Unit + ): List { + return listOfNotNull( + when { + !isSearchActive -> ActionMenuItem( + icon = IconItem.Drawable(R.drawable.action_search), + contentDescription = R.string.search_label, + onClick = onSearchClick, + testingTag = SEARCH_ICON_TESTING_TAG + ) + + else -> null + }, + ActionMenuItem( + icon = IconItem.Vector(Icons.Default.Delete), + // Adding content description for #3825. + contentDescription = deleteIconTitle, + onClick = onDeleteClick, + testingTag = DELETE_MENU_ICON_TESTING_TAG + ) + ) } override fun onDestroyView() { super.onDestroyView() compositeDisposable.clear() - fragmentPageBinding?.apply { - recyclerView.adapter = null - root.removeAllViews() - } - fragmentPageBinding = null } private fun render(state: PageState<*>) { - pageAdapter.items = state.visiblePageItems - fragmentPageBinding?.pageSwitch?.isEnabled = !state.isInSelectionState - fragmentPageBinding?.noPage?.visibility = if (state.pageItems.isEmpty()) VISIBLE else GONE + pageScreenState.value = pageScreenState.value.copy( + switchIsEnabled = !state.isInSelectionState, + // First, assign the existing state to force Compose to recognize a change. + // This helps when internal properties of items (like `isSelected`) change, + // but the list reference itself remains the same — Compose won't detect it otherwise. + pageState = pageState.value + ) + // Then, assign the actual updated state to trigger full recomposition. + pageScreenState.value = pageScreenState.value.copy( + pageState = state + ) if (state.isInSelectionState) { if (actionMode == null) { actionMode = diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragmentScreenState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragmentScreenState.kt new file mode 100644 index 000000000..3e26fedae --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragmentScreenState.kt @@ -0,0 +1,45 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.page + +import androidx.annotation.StringRes +import org.kiwix.kiwixmobile.core.page.viewmodel.PageState + +/** + * Represents the UI state for the PageFragment Screen. + * A Base screen for Bookmarks, History, and Notes screens. + * + * This data class encapsulates all UI-related states in a single object, + * reducing complexity in the Fragment. + */ +data class PageFragmentScreenState( + val pageState: PageState<*>, + val isSearchActive: Boolean, + val searchQueryHint: String, + val searchText: String, + val searchValueChangedListener: (String) -> Unit, + val clearSearchButtonClickListener: () -> Unit, + @StringRes val screenTitle: Int, + val noItemsString: String, + val switchString: String, + val switchIsChecked: Boolean, + val switchIsEnabled: Boolean = true, + val onSwitchCheckedChanged: (Boolean) -> Unit, + @StringRes val deleteIconTitle: Int +) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageListItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageListItem.kt new file mode 100644 index 000000000..80727191b --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageListItem.kt @@ -0,0 +1,94 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.page + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.style.TextOverflow +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.downloader.model.Base64String +import org.kiwix.kiwixmobile.core.downloader.model.toPainter +import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener +import org.kiwix.kiwixmobile.core.page.adapter.Page +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PAGE_LIST_ITEM_FAVICON_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP + +const val PAGE_ITEM_TESTING_TAG = "pageItemTestingTag" + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun PageListItem( + page: Page, + itemClickListener: OnItemClickListener +) { + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { itemClickListener.onItemClick(page) }, + onLongClick = { itemClickListener.onItemLongClick(page) } + ) + .background(MaterialTheme.colorScheme.surface) + .padding( + horizontal = SIXTEEN_DP, + vertical = EIGHT_DP + ) + .semantics { testTag = PAGE_ITEM_TESTING_TAG }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = if (page.isSelected) { + painterResource(id = R.drawable.ic_check_circle_blue_24dp) + } else { + Base64String(page.favicon).toPainter() + }, + contentDescription = stringResource(R.string.fav_icon), + modifier = Modifier + .size(PAGE_LIST_ITEM_FAVICON_SIZE) + ) + + Spacer(modifier = Modifier.width(SIXTEEN_DP)) + + Text( + text = page.title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageScreen.kt new file mode 100644 index 000000000..a2317018c --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageScreen.kt @@ -0,0 +1,247 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.page + +import android.app.Activity +import android.content.Context +import android.view.inputmethod.InputMethodManager +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +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.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.TextStyle +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp +import org.kiwix.kiwixmobile.core.main.CoreMainActivity +import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener +import org.kiwix.kiwixmobile.core.page.adapter.Page +import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.DateItem +import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar +import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView +import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem +import org.kiwix.kiwixmobile.core.ui.theme.AlabasterWhite +import org.kiwix.kiwixmobile.core.ui.theme.Black +import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme +import org.kiwix.kiwixmobile.core.ui.theme.White +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOURTEEN_SP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PAGE_SWITCH_LEFT_RIGHT_MARGIN +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PAGE_SWITCH_ROW_BOTTOM_MARGIN +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP +import org.threeten.bp.LocalDate +import org.threeten.bp.format.DateTimeFormatter +import org.threeten.bp.format.DateTimeParseException + +const val SWITCH_TEXT_TESTING_TAG = "switchTextTestingTag" +const val NO_ITEMS_TEXT_TESTING_TAG = "noItemsTextTestingTag" +const val PAGE_LIST_TEST_TAG = "pageListTestingTag" + +@Suppress("ComposableLambdaParameterNaming") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PageScreen( + state: PageFragmentScreenState, + itemClickListener: OnItemClickListener, + actionMenuItems: List, + navigationIcon: @Composable () -> Unit +) { + KiwixTheme { + Scaffold( + topBar = { + Column { + KiwixAppBar( + titleId = state.screenTitle, + navigationIcon = navigationIcon, + actionMenuItems = actionMenuItems, + searchBar = searchBarIfActive(state) + ) + PageSwitchRow(state) + } + } + ) { padding -> + val items = state.pageState.pageItems + Box( + modifier = Modifier + .padding( + top = padding.calculateTopPadding(), + start = padding.calculateStartPadding(LocalLayoutDirection.current), + end = padding.calculateEndPadding(LocalLayoutDirection.current) + ) + .fillMaxSize() + ) { + if (items.isEmpty()) { + Text( + text = state.noItemsString, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier + .align(Alignment.Center) + .semantics { testTag = NO_ITEMS_TEXT_TESTING_TAG } + ) + } else { + PageList( + state = state, + itemClickListener = itemClickListener + ) + } + } + } + } +} + +@Composable +private fun PageList( + state: PageFragmentScreenState, + itemClickListener: OnItemClickListener +) { + val listState = rememberLazyListState() + val context = LocalContext.current + + LaunchedEffect(listState) { + snapshotFlow { listState.isScrollInProgress } + .collect { isScrolling -> + if (isScrolling) { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow((context as? Activity)?.currentFocus?.windowToken, 0) + } + } + } + + LazyColumn(state = listState, modifier = Modifier.semantics { testTag = PAGE_LIST_TEST_TAG }) { + items(state.pageState.visiblePageItems) { item -> + when (item) { + is Page -> PageListItem(page = item, itemClickListener = itemClickListener) + is DateItem -> DateItemText(item) + } + } + } +} + +@Composable +private fun searchBarIfActive( + state: PageFragmentScreenState +): (@Composable () -> Unit)? = if (state.isSearchActive) { + { + KiwixSearchView( + placeholder = state.searchQueryHint, + value = state.searchText, + testTag = "", + onValueChange = { state.searchValueChangedListener(it) }, + onClearClick = { state.clearSearchButtonClickListener.invoke() } + ) + } +} else { + null +} + +@Composable +fun PageSwitchRow( + state: PageFragmentScreenState +) { + val context = LocalActivity.current as CoreMainActivity + // hide switches for custom apps, see more info here https://github.com/kiwix/kiwix-android/issues/3523 + if (!context.isCustomApp()) { + val switchTextColor = if (isSystemInDarkTheme()) { + AlabasterWhite + } else { + White + } + Row( + modifier = Modifier + .fillMaxWidth() + .background(Black) + .padding(bottom = PAGE_SWITCH_ROW_BOTTOM_MARGIN), + horizontalArrangement = Arrangement.Absolute.Right, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + state.switchString, + color = switchTextColor, + style = TextStyle(fontSize = FOURTEEN_SP), + modifier = Modifier.testTag(SWITCH_TEXT_TESTING_TAG) + ) + Switch( + checked = state.switchIsChecked, + onCheckedChange = { state.onSwitchCheckedChanged(it) }, + enabled = state.switchIsEnabled, + modifier = Modifier + .padding(horizontal = PAGE_SWITCH_LEFT_RIGHT_MARGIN), + colors = SwitchDefaults.colors( + uncheckedTrackColor = White + ) + ) + } + } +} + +@Composable +fun DateItemText(dateItem: DateItem) { + Text( + text = getFormattedDateLabel(dateItem.dateString), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(SIXTEEN_DP) + ) +} + +@Composable +private fun getFormattedDateLabel(dateString: String): String { + val today = LocalDate.now() + val yesterday = today.minusDays(1) + + val parsedDate = parseDateSafely(dateString) + return when (parsedDate) { + today -> stringResource(R.string.time_today) + yesterday -> stringResource(R.string.time_yesterday) + else -> dateString + } +} + +private fun parseDateSafely(dateString: String): LocalDate? { + return try { + LocalDate.parse(dateString, DateTimeFormatter.ofPattern("d MMM yyyy")) + } catch (_: DateTimeParseException) { + null + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/BookmarksFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/BookmarksFragment.kt index dd776ea11..cfda39f2a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/BookmarksFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/BookmarksFragment.kt @@ -16,11 +16,11 @@ class BookmarksFragment : PageFragment() { PageAdapter(PageItemDelegate(this)) } - override val screenTitle: String by lazy { getString(R.string.bookmarks) } + override val screenTitle: Int = R.string.bookmarks override val noItemsString: String by lazy { getString(R.string.no_bookmarks) } override val switchString: String by lazy { getString(R.string.bookmarks_from_current_book) } - override val deleteIconTitle: String by lazy { - getString(R.string.pref_clear_all_bookmarks_title) + override val deleteIconTitle: Int by lazy { + R.string.pref_clear_all_bookmarks_title } override val switchIsChecked: Boolean by lazy { sharedPreferenceUtil.showBookmarksAllBooks } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/HistoryFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/HistoryFragment.kt index f2545bfa5..8e01f27b5 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/HistoryFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/HistoryFragment.kt @@ -21,9 +21,9 @@ class HistoryFragment : PageFragment() { override val noItemsString: String by lazy { getString(R.string.no_history) } override val switchString: String by lazy { getString(R.string.history_from_current_book) } - override val screenTitle: String by lazy { getString(R.string.history) } - override val deleteIconTitle: String by lazy { - getString(R.string.pref_clear_all_history_title) + override val screenTitle: Int = R.string.history + override val deleteIconTitle: Int by lazy { + R.string.pref_clear_all_history_title } override val switchIsChecked: Boolean by lazy { sharedPreferenceUtil.showHistoryAllBooks } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/NotesFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/NotesFragment.kt index 68fd9827a..776dad427 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/NotesFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/NotesFragment.kt @@ -30,8 +30,7 @@ import org.kiwix.kiwixmobile.core.page.notes.viewmodel.NotesViewModel class NotesFragment : PageFragment() { override val pageViewModel by lazy { viewModel(viewModelFactory) } - override val screenTitle: String - get() = getString(R.string.pref_notes) + override val screenTitle: Int = R.string.pref_notes override val pageAdapter: PageAdapter by lazy { PageAdapter(PageDelegate.PageItemDelegate(this)) @@ -39,8 +38,8 @@ class NotesFragment : PageFragment() { override val noItemsString: String by lazy { getString(R.string.no_notes) } override val switchString: String by lazy { getString(R.string.notes_from_all_books) } - override val deleteIconTitle: String by lazy { - getString(R.string.pref_clear_notes) + override val deleteIconTitle: Int by lazy { + R.string.pref_clear_notes } override val switchIsChecked: Boolean by lazy { sharedPreferenceUtil.showNotesAllBooks } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt index b5140ec26..956165bdc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt @@ -39,8 +39,9 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens @Composable fun KiwixSearchView( - modifier: Modifier, + modifier: Modifier = Modifier, value: String, + placeholder: String = stringResource(R.string.search_label), testTag: String = "", onValueChange: (String) -> Unit, onClearClick: () -> Unit @@ -65,7 +66,7 @@ fun KiwixSearchView( value = value, placeholder = { Text( - text = stringResource(R.string.search_label), + text = placeholder, color = Color.LightGray, fontSize = ComposeDimens.EIGHTEEN_SP ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index 8016750a2..cbd077805 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -106,4 +106,9 @@ object ComposeDimens { val HELP_SCREEN_ITEM_TITLE_TEXT_SIZE = 20.sp val HELP_SCREEN_ITEM_TITLE_LETTER_SPACING = 0.0125.em val HELP_SCREEN_ARROW_ICON_SIZE = 35.dp + + // Page dimens + val PAGE_LIST_ITEM_FAVICON_SIZE = 40.dp + val PAGE_SWITCH_LEFT_RIGHT_MARGIN = 10.dp + val PAGE_SWITCH_ROW_BOTTOM_MARGIN = 8.dp } diff --git a/core/src/main/res/layout/fragment_page.xml b/core/src/main/res/layout/fragment_page.xml deleted file mode 100644 index 2fb96ca8a..000000000 --- a/core/src/main/res/layout/fragment_page.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - -