Fixed: An extra space appeared at the bottom when navigating to a ZIM page with greater height and then returning to the previous page.

* Removed unnecessary code and files from the project.
* Refactored several UI test cases to align with the Compose UI.
* Fixed: Some lint issues.
This commit is contained in:
MohitMaliFtechiz 2025-06-26 00:52:06 +05:30
parent db22e245b7
commit a51615e249
27 changed files with 166 additions and 1154 deletions

View File

@ -109,7 +109,7 @@ class TopLevelDestinationTest : BaseActivityTest() {
fun testTopLevelDestination() { fun testTopLevelDestination() {
topLevel { topLevel {
clickReaderOnBottomNav { clickReaderOnBottomNav {
assertReaderScreenDisplayed() assertReaderScreenDisplayed(composeTestRule)
} }
clickDownloadOnBottomNav { clickDownloadOnBottomNav {
onlineLibrary { onlineLibrary {

View File

@ -42,6 +42,7 @@ import org.kiwix.kiwixmobile.nav.destination.library.local.NO_FILE_TEXT_TESTING_
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.testutils.TestUtils.refresh import org.kiwix.kiwixmobile.testutils.TestUtils.refresh
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout
import org.kiwix.kiwixmobile.ui.BOOK_ITEM_TESTING_TAG import org.kiwix.kiwixmobile.ui.BOOK_ITEM_TESTING_TAG
fun library(func: LibraryRobot.() -> Unit) = LibraryRobot().applyWithViewHierarchyPrinting(func) fun library(func: LibraryRobot.() -> Unit) = LibraryRobot().applyWithViewHierarchyPrinting(func)
@ -102,8 +103,8 @@ class LibraryRobot : BaseRobot() {
} }
fun waitUntilZimFilesRefreshing(composeTestRule: ComposeContentTestRule) { fun waitUntilZimFilesRefreshing(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance()
testFlakyView({ testFlakyView({
composeTestRule.waitUntilTimeout()
composeTestRule.onNodeWithTag(CONTENT_LOADING_PROGRESSBAR_TESTING_TAG) composeTestRule.onNodeWithTag(CONTENT_LOADING_PROGRESSBAR_TESTING_TAG)
.assertIsNotDisplayed() .assertIsNotDisplayed()
}) })

View File

@ -18,15 +18,20 @@
package org.kiwix.kiwixmobile.nav.destination.reader package org.kiwix.kiwixmobile.nav.destination.reader
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithTag
import applyWithViewHierarchyPrinting import applyWithViewHierarchyPrinting
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.core.main.reader.READER_SCREEN_TESTING_TAG
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout
fun reader(func: ReaderRobot.() -> Unit) = ReaderRobot().applyWithViewHierarchyPrinting(func) fun reader(func: ReaderRobot.() -> Unit) = ReaderRobot().applyWithViewHierarchyPrinting(func)
class ReaderRobot : BaseRobot() { class ReaderRobot : BaseRobot() {
fun assertReaderScreenDisplayed() { fun assertReaderScreenDisplayed(composeTestRule: ComposeContentTestRule) {
isVisible(ViewId(R.id.activity_main_root)) composeTestRule.apply {
waitUntilTimeout()
onNodeWithTag(READER_SCREEN_TESTING_TAG).assertExists()
}
} }
} }

View File

@ -20,11 +20,13 @@ package org.kiwix.kiwixmobile.page.history
import android.util.Log import android.util.Log
import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
@ -36,11 +38,13 @@ import junit.framework.AssertionFailedError
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.main.reader.TAB_SWITCHER_VIEW_TESTING_TAG
import org.kiwix.kiwixmobile.core.page.DELETE_MENU_ICON_TESTING_TAG import org.kiwix.kiwixmobile.core.page.DELETE_MENU_ICON_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG
import org.kiwix.kiwixmobile.core.utils.dialog.ALERT_DIALOG_TITLE_TEXT_TESTING_TAG import org.kiwix.kiwixmobile.core.utils.dialog.ALERT_DIALOG_TITLE_TEXT_TESTING_TAG
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout
fun navigationHistory(func: NavigationHistoryRobot.() -> Unit) = fun navigationHistory(func: NavigationHistoryRobot.() -> Unit) =
NavigationHistoryRobot().applyWithViewHierarchyPrinting(func) NavigationHistoryRobot().applyWithViewHierarchyPrinting(func)
@ -55,12 +59,14 @@ class NavigationHistoryRobot : BaseRobot() {
isVisible(ViewId(readerFragment)) isVisible(ViewId(readerFragment))
} }
fun closeTabSwitcherIfVisible() { fun closeTabSwitcherIfVisible(composeTestRule: ComposeContentTestRule) {
try { try {
pauseForBetterTestPerformance() composeTestRule.apply {
isVisible(ViewId(R.id.tab_switcher_close_all_tabs)) waitUntilTimeout()
pressBack() onNodeWithTag(TAB_SWITCHER_VIEW_TESTING_TAG).assertExists()
} catch (_: Exception) { pressBack()
}
} catch (_: AssertionError) {
Log.i( Log.i(
"NAVIGATION_HISTORY_TEST", "NAVIGATION_HISTORY_TEST",
"Couldn't found tab switcher, probably it is not visible" "Couldn't found tab switcher, probably it is not visible"
@ -91,14 +97,24 @@ class NavigationHistoryRobot : BaseRobot() {
) )
} }
fun longClickOnBackwardButton() { fun longClickOnBackwardButton(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() composeTestRule.apply {
testFlakyView({ onView(withId(R.id.bottom_toolbar_arrow_back)).perform(longClick()) }) waitUntilTimeout()
testFlakyView({
onNodeWithContentDescription(context.getString(R.string.go_to_previous_page))
.performTouchInput { longClick() }
})
}
} }
fun longClickOnForwardButton() { fun longClickOnForwardButton(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() composeTestRule.apply {
longClickOn(ViewId(R.id.bottom_toolbar_arrow_forward)) waitUntilTimeout()
testFlakyView({
onNodeWithContentDescription(context.getString(R.string.go_to_next_page))
.performTouchInput { longClick() }
})
}
} }
fun assertBackwardNavigationHistoryDialogDisplayed(composeTestRule: ComposeContentTestRule) { fun assertBackwardNavigationHistoryDialogDisplayed(composeTestRule: ComposeContentTestRule) {
@ -117,9 +133,12 @@ class NavigationHistoryRobot : BaseRobot() {
} }
} }
fun clickOnBackwardButton() { fun clickOnBackwardButton(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() composeTestRule.apply {
clickOn(ViewId(R.id.bottom_toolbar_arrow_back)) waitUntilTimeout()
onNodeWithContentDescription(context.getString(R.string.go_to_previous_page))
.performClick()
}
} }
fun assertForwardNavigationHistoryDialogDisplayed(composeTestRule: ComposeContentTestRule) { fun assertForwardNavigationHistoryDialogDisplayed(composeTestRule: ComposeContentTestRule) {

View File

@ -147,14 +147,14 @@ class NavigationHistoryTest : BaseActivityTest() {
} }
StandardActions.closeDrawer() // close the drawer if open before running the test cases. StandardActions.closeDrawer() // close the drawer if open before running the test cases.
navigationHistory { navigationHistory {
closeTabSwitcherIfVisible() closeTabSwitcherIfVisible(composeTestRule)
checkZimFileLoadedSuccessful(R.id.readerFragment) checkZimFileLoadedSuccessful(R.id.readerFragment)
clickOnAndroidArticle() clickOnAndroidArticle()
longClickOnBackwardButton() longClickOnBackwardButton(composeTestRule)
assertBackwardNavigationHistoryDialogDisplayed(composeTestRule) assertBackwardNavigationHistoryDialogDisplayed(composeTestRule)
pressBack() pressBack()
clickOnBackwardButton() clickOnBackwardButton(composeTestRule)
longClickOnForwardButton() longClickOnForwardButton(composeTestRule)
assertForwardNavigationHistoryDialogDisplayed(composeTestRule) assertForwardNavigationHistoryDialogDisplayed(composeTestRule)
clickOnDeleteHistory(composeTestRule) clickOnDeleteHistory(composeTestRule)
assertDeleteDialogDisplayed(composeTestRule) assertDeleteDialogDisplayed(composeTestRule)

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.reader package org.kiwix.kiwixmobile.reader
import android.os.Build import android.os.Build
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -46,6 +47,7 @@ import org.kiwix.kiwixmobile.BaseActivityTest
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil 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.core.utils.TestingUtils.RETRY_RULE_ORDER
import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections
@ -64,6 +66,9 @@ class KiwixReaderFragmentTest : BaseActivityTest() {
@JvmField @JvmField
val retryRule = RetryRule() val retryRule = RetryRule()
@get:Rule(order = COMPOSE_TEST_RULE_ORDER)
val composeTestRule = createComposeRule()
private lateinit var kiwixMainActivity: KiwixMainActivity private lateinit var kiwixMainActivity: KiwixMainActivity
@Before @Before
@ -136,10 +141,10 @@ class KiwixReaderFragmentTest : BaseActivityTest() {
openKiwixReaderFragmentWithFile(zimFile) openKiwixReaderFragmentWithFile(zimFile)
reader { reader {
checkZimFileLoadedSuccessful(R.id.readerFragment) checkZimFileLoadedSuccessful(R.id.readerFragment)
clickOnTabIcon() clickOnTabIcon(composeTestRule)
clickOnClosedAllTabsButton() clickOnClosedAllTabsButton(composeTestRule)
clickOnUndoButton() clickOnUndoButton(composeTestRule)
assertTabRestored() assertTabRestored(composeTestRule)
pressBack() pressBack()
checkZimFileLoadedSuccessful(R.id.readerFragment) checkZimFileLoadedSuccessful(R.id.readerFragment)
} }
@ -171,6 +176,8 @@ class KiwixReaderFragmentTest : BaseActivityTest() {
openKiwixReaderFragmentWithFile(downloadingZimFile) openKiwixReaderFragmentWithFile(downloadingZimFile)
reader { reader {
checkZimFileLoadedSuccessful(R.id.readerFragment) checkZimFileLoadedSuccessful(R.id.readerFragment)
clickOnTabIcon(composeTestRule)
clickOnTabIcon(composeTestRule)
// test the whole welcome page is loaded or not // test the whole welcome page is loaded or not
assertArticleLoaded("Hydrogène") assertArticleLoaded("Hydrogène")
assertArticleLoaded("Automobile") assertArticleLoaded("Automobile")

View File

@ -18,10 +18,12 @@
package org.kiwix.kiwixmobile.reader package org.kiwix.kiwixmobile.reader
import androidx.test.espresso.Espresso.onView import androidx.compose.ui.test.assertTextEquals
import androidx.test.espresso.action.ViewActions.click import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.compose.ui.test.onAllNodesWithTag
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.webClick import androidx.test.espresso.web.webdriver.DriverAtoms.webClick
@ -29,11 +31,13 @@ import androidx.test.espresso.web.webdriver.Locator
import applyWithViewHierarchyPrinting import applyWithViewHierarchyPrinting
import com.adevinta.android.barista.interaction.BaristaSleepInteractions import com.adevinta.android.barista.interaction.BaristaSleepInteractions
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.Text
import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.main.reader.CLOSE_ALL_TABS_BUTTON_TESTING_TAG
import org.kiwix.kiwixmobile.core.main.reader.TAB_MENU_ITEM_TESTING_TAG
import org.kiwix.kiwixmobile.core.main.reader.TAB_TITLE_TESTING_TAG
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout
fun reader(func: ReaderRobot.() -> Unit) = ReaderRobot().applyWithViewHierarchyPrinting(func) fun reader(func: ReaderRobot.() -> Unit) = ReaderRobot().applyWithViewHierarchyPrinting(func)
@ -45,30 +49,43 @@ class ReaderRobot : BaseRobot() {
isVisible(ViewId(readerFragment)) isVisible(ViewId(readerFragment))
} }
fun clickOnTabIcon() { fun clickOnTabIcon(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() composeTestRule.apply {
testFlakyView({ onView(withId(R.id.ic_tab_switcher_text)).perform(click()) }) waitUntilTimeout()
testFlakyView({
onNodeWithTag(TAB_MENU_ITEM_TESTING_TAG).performClick()
})
}
} }
fun clickOnClosedAllTabsButton() { fun clickOnClosedAllTabsButton(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() composeTestRule.apply {
clickOn(ViewId(R.id.tab_switcher_close_all_tabs)) waitUntilTimeout()
testFlakyView({
onNodeWithTag(CLOSE_ALL_TABS_BUTTON_TESTING_TAG).performClick()
})
}
} }
fun clickOnUndoButton() { fun clickOnUndoButton(composeTestRule: ComposeContentTestRule) {
try { try {
onView(withText("UNDO")).perform(click()) composeTestRule.apply {
} catch (runtimeException: RuntimeException) { onNodeWithText("UNDO", useUnmergedTree = true)
.performClick()
}
} catch (_: AssertionError) {
if (retryCountForClickOnUndoButton > 0) { if (retryCountForClickOnUndoButton > 0) {
retryCountForClickOnUndoButton-- retryCountForClickOnUndoButton--
clickOnUndoButton() clickOnUndoButton(composeTestRule)
} }
} }
} }
fun assertTabRestored() { fun assertTabRestored(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() composeTestRule.apply {
isVisible(Text("Test Zim")) waitUntilTimeout()
onAllNodesWithTag(TAB_TITLE_TESTING_TAG)[0].assertTextEquals("Test Zim")
}
} }
private fun pauseForBetterTestPerformance() { private fun pauseForBetterTestPerformance() {

View File

@ -27,9 +27,6 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.Locator import androidx.test.espresso.web.webdriver.Locator
@ -38,7 +35,7 @@ import applyWithViewHierarchyPrinting
import com.adevinta.android.barista.interaction.BaristaSleepInteractions import com.adevinta.android.barista.interaction.BaristaSleepInteractions
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG
import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.components.NAVIGATION_ICON_TESTING_TAG import org.kiwix.kiwixmobile.core.ui.components.NAVIGATION_ICON_TESTING_TAG
@ -122,12 +119,16 @@ class SearchRobot : BaseRobot() {
composeTestRule.onNodeWithTag(NAVIGATION_ICON_TESTING_TAG).performClick() composeTestRule.onNodeWithTag(NAVIGATION_ICON_TESTING_TAG).performClick()
} }
private fun openSearchScreen() { private fun openSearchScreen(composeTestRule: ComposeContentTestRule) {
testFlakyView({ onView(withId(R.id.menu_search)).perform(click()) }) testFlakyView(
{
composeTestRule.onNodeWithTag(SEARCH_ICON_TESTING_TAG).performClick()
}
)
} }
fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) { fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) {
openSearchScreen() openSearchScreen(composeTestRule)
searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule) searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule)
clickOnSearchItemInSearchList(composeTestRule) clickOnSearchItemInSearchList(composeTestRule)
checkZimFileSearchSuccessful(org.kiwix.kiwixmobile.R.id.readerFragment) checkZimFileSearchSuccessful(org.kiwix.kiwixmobile.R.id.readerFragment)

View File

@ -128,7 +128,7 @@ class GetContentShortcutTest {
onlineLibrary { assertOnlineLibraryFragmentDisplayed(composeTestRule) } onlineLibrary { assertOnlineLibraryFragmentDisplayed(composeTestRule) }
topLevel { topLevel {
clickReaderOnBottomNav { clickReaderOnBottomNav {
assertReaderScreenDisplayed() assertReaderScreenDisplayed(composeTestRule)
} }
clickDownloadOnBottomNav { clickDownloadOnBottomNav {
onlineLibrary { assertOnlineLibraryFragmentDisplayed(composeTestRule) } onlineLibrary { assertOnlineLibraryFragmentDisplayed(composeTestRule) }

View File

@ -48,6 +48,7 @@ import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.extensions.update import org.kiwix.kiwixmobile.core.extensions.update
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.reader.HIDE_TAB_SWITCHER_DELAY
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromSearchScreen import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromSearchScreen
@ -62,8 +63,6 @@ import org.kiwix.kiwixmobile.core.utils.files.FileUtils
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import java.io.File import java.io.File
private const val HIDE_TAB_SWITCHER_DELAY: Long = 300
class KiwixReaderFragment : CoreReaderFragment() { class KiwixReaderFragment : CoreReaderFragment() {
private var isFullScreenVideo: Boolean = false private var isFullScreenVideo: Boolean = false

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_language_search"
android:icon="@drawable/action_search"
android:title="@string/search_label"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconifiedByDefault="true"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/menu_language_save"
android:icon="@drawable/ic_check_white_24dp"
android:title="@string/save_languages"
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/action_search"
android:title="@string/search_label"
android:visible="true"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconifiedByDefault="true"
app:showAsAction="always|collapseActionView" />
<item
android:id="@+id/select_language"
android:icon="@drawable/ic_language_white_24dp"
android:title="@string/pref_language_chooser"
android:visible="true"
app:showAsAction="ifRoom" />
<item
android:id="@+id/get_zim_nearby_device"
android:icon="@drawable/ic_baseline_mobile_screen_share_24px"
android:title="@string/get_content_from_nearby_device"
app:showAsAction="always" />
</menu>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferFragment">
<item
android:id="@+id/menu_item_search_devices"
android:title="@string/search_for_peers"
app:showAsAction="always"
android:icon="@drawable/action_search" />
</menu>

View File

@ -17,19 +17,12 @@
*/ */
package org.kiwix.kiwixmobile.core.di.modules package org.kiwix.kiwixmobile.core.di.modules
import android.app.Activity
import android.view.Menu
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.di.ActivityScope import org.kiwix.kiwixmobile.core.di.ActivityScope
import org.kiwix.kiwixmobile.core.main.KiwixWebView
import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.MainMenu.Factory
import org.kiwix.kiwixmobile.core.main.MainMenu.MenuClickListener
import org.kiwix.kiwixmobile.core.main.MainRepositoryActions import org.kiwix.kiwixmobile.core.main.MainRepositoryActions
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
@ -44,33 +37,5 @@ abstract class ActivityModule {
@ActivityScope @ActivityScope
fun providesMainPresenter(dataSource: DataSource): MainRepositoryActions = fun providesMainPresenter(dataSource: DataSource): MainRepositoryActions =
MainRepositoryActions(dataSource) MainRepositoryActions(dataSource)
@Provides
@ActivityScope
fun providesMainMenuFactory(
activity: Activity,
zimReaderContainer: ZimReaderContainer
): MainMenu.Factory = object : Factory {
override fun create(
menu: Menu,
webViews: MutableList<KiwixWebView>,
urlIsValid: Boolean,
menuClickListener: MenuClickListener,
disableReadAloud: Boolean,
disableTabs: Boolean,
disableSearch: Boolean
): MainMenu =
MainMenu(
activity,
zimReaderContainer.zimFileReader,
menu,
webViews,
urlIsValid,
disableReadAloud,
disableTabs,
disableSearch,
menuClickListener
)
}
} }
} }

View File

@ -1,194 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main
import android.app.Activity
import android.content.res.Configuration
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import androidx.core.view.isVisible
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
const val REQUEST_FILE_SEARCH = 1236
@Suppress("LongParameterList")
class MainMenu(
private val activity: Activity,
zimFileReader: ZimFileReader?,
menu: Menu,
webViews: MutableList<KiwixWebView>,
urlIsValid: Boolean,
disableReadAloud: Boolean = false,
disableTabs: Boolean = false,
private val disableSearch: Boolean = false,
private val menuClickListener: MenuClickListener
) {
interface Factory {
fun create(
menu: Menu,
webViews: MutableList<KiwixWebView>,
urlIsValid: Boolean,
menuClickListener: MenuClickListener,
disableReadAloud: Boolean,
disableTabs: Boolean,
disableSearch: Boolean = false
): MainMenu
}
interface MenuClickListener {
fun onTabMenuClicked()
fun onHomeMenuClicked()
fun onAddNoteMenuClicked()
fun onRandomArticleMenuClicked()
fun onReadAloudMenuClicked()
fun onFullscreenMenuClicked()
fun onSearchMenuClickedMenuClicked()
}
init {
activity.menuInflater.inflate(R.menu.menu_main, menu)
}
private val search = menu.findItem(R.id.menu_search)
private var tabSwitcher: MenuItem? = menu.findItem(R.id.menu_tab_switcher)
private var tabSwitcherTextView: TextView? =
tabSwitcher?.actionView?.findViewById(R.id.ic_tab_switcher_text)
private val addNote = menu.findItem(R.id.menu_add_note)
private val randomArticle = menu.findItem(R.id.menu_random_article)
private val fullscreen = menu.findItem(R.id.menu_fullscreen)
private var readAloud: MenuItem? = menu.findItem(R.id.menu_read_aloud)
private var isInTabSwitcher: Boolean = false
init {
if (disableReadAloud) {
readAloud?.isVisible = false
readAloud = null
}
if (disableTabs) {
tabSwitcher?.isVisible = false
tabSwitcherTextView?.isVisible = false
tabSwitcher = null
tabSwitcherTextView = null
}
if (disableSearch) {
search?.isVisible = false
}
randomArticle.setShowAsAction(
if (activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
MenuItem.SHOW_AS_ACTION_IF_ROOM
} else {
MenuItem.SHOW_AS_ACTION_NEVER
}
)
tabSwitcher?.actionView?.apply {
setOnClickListener { menuClickListener.onTabMenuClicked() }
setToolTipWithContentDescription(resources.getString(R.string.switch_tabs))
}
addNote.menuItemClickListener { menuClickListener.onAddNoteMenuClicked() }
randomArticle.menuItemClickListener { menuClickListener.onRandomArticleMenuClicked() }
readAloud.menuItemClickListener { menuClickListener.onReadAloudMenuClicked() }
fullscreen.menuItemClickListener { menuClickListener.onFullscreenMenuClicked() }
showWebViewOptions(urlIsValid)
zimFileReader?.let {
onFileOpened(urlIsValid)
}
updateTabIcon(webViews.size)
}
fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
android.R.id.home -> {
menuClickListener.onHomeMenuClicked()
true
}
else -> false
}
fun onFileOpened(urlIsValid: Boolean) {
setVisibility(urlIsValid, randomArticle, search, readAloud, addNote, fullscreen, tabSwitcher)
search.setOnMenuItemClickListener { navigateToSearch() }
}
fun hideBookSpecificMenuItems() {
setVisibility(false, search, tabSwitcher, randomArticle, addNote, readAloud)
}
fun showBookSpecificMenuItems() {
setVisibility(true, search, tabSwitcher, randomArticle, addNote, readAloud)
}
fun showTabSwitcherOptions() {
isInTabSwitcher = true
setVisibility(false, randomArticle, readAloud, addNote, fullscreen)
}
fun showWebViewOptions(urlIsValid: Boolean) {
isInTabSwitcher = false
fullscreen.isVisible = true
setVisibility(urlIsValid, randomArticle, search, readAloud, addNote, tabSwitcher)
}
fun updateTabIcon(tabs: Int) {
tabSwitcherTextView?.text = if (tabs > 99) ":D" else "$tabs"
}
private fun navigateToSearch(): Boolean {
menuClickListener.onSearchMenuClickedMenuClicked()
return true
}
fun onTextToSpeechStartedTalking() {
readAloud?.setTitle(R.string.menu_read_aloud_stop)
}
fun onTextToSpeechStoppedTalking() {
readAloud?.setTitle(R.string.menu_read_aloud)
}
private fun setVisibility(visibility: Boolean, vararg menuItems: MenuItem?) {
menuItems.forEach {
if (it == search && disableSearch) {
it?.isVisible = false
} else {
it?.isVisible = visibility
}
}
}
fun tryExpandSearch(zimFileReader: ZimFileReader?) {
if (search.isVisible && zimFileReader != null) {
navigateToSearch()
}
}
fun isInTabSwitcher(): Boolean = isInTabSwitcher
}
private fun MenuItem?.menuItemClickListener(function: (MenuItem) -> Unit) {
this?.setOnMenuItemClickListener {
function.invoke(it)
true
}
}

View File

@ -1,200 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main
import android.annotation.SuppressLint
import android.text.TextUtils
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import org.kiwix.kiwixmobile.core.R
import com.google.android.material.R.attr
import org.kiwix.kiwixmobile.core.extensions.getAttribute
import org.kiwix.kiwixmobile.core.extensions.setImageDrawableCompat
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.core.extensions.tint
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getWindowHeight
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getWindowWidth
import org.kiwix.kiwixmobile.core.utils.StyleUtils.fromHtml
class TabsAdapter internal constructor(
private val activity: AppCompatActivity,
private val webViews: List<KiwixWebView>,
private val painter: DarkModeViewPainter
) : RecyclerView.Adapter<TabsAdapter.ViewHolder>() {
init {
setHasStableIds(true)
}
private var listener: TabClickListener? = null
var selected = 0
@SuppressLint("ResourceType") override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val context = parent.context
val margin16 = context.resources.getDimensionPixelSize(R.dimen.activity_horizontal_margin)
val closeImageWidthAndHeight =
context.resources.getDimensionPixelSize(R.dimen.close_tab_button_size)
val close =
ImageView(context)
.apply {
id = R.id.tabsAdapterCloseImageView
setImageDrawableCompat(R.drawable.ic_clear_white_24dp)
setToolTipWithContentDescription(resources.getString(R.string.close_tab))
val outValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.actionBarItemBackground, outValue, true)
setBackgroundResource(outValue.resourceId)
tint(context.getAttribute(attr.colorOnSurface))
}
val cardView =
MaterialCardView(context)
.apply {
id = R.id.tabsAdapterCardView
useCompatPadding = true
}
val textView =
TextView(context)
.apply {
id = R.id.tabsAdapterTextView
maxLines = 1
ellipsize = TextUtils.TruncateAt.END
}
val constraintLayout =
ConstraintLayout(context)
.apply {
isFocusableInTouchMode = true
addView(
cardView,
ConstraintLayout.LayoutParams(
activity.getWindowWidth() / 2,
-activity.getToolbarHeight() / 2 + activity.getWindowHeight() / 2
)
)
addView(
close,
ConstraintLayout.LayoutParams(closeImageWidthAndHeight, closeImageWidthAndHeight)
)
layoutParams =
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
addView(
textView,
ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT)
)
}
ConstraintSet()
.apply {
clone(constraintLayout)
connect(cardView.id, TOP, PARENT_ID, TOP)
connect(cardView.id, BOTTOM, PARENT_ID, BOTTOM)
connect(cardView.id, START, PARENT_ID, START, margin16)
connect(cardView.id, END, PARENT_ID, END, margin16)
connect(close.id, END, cardView.id, END)
connect(close.id, BOTTOM, cardView.id, TOP)
connect(textView.id, BOTTOM, cardView.id, TOP)
connect(textView.id, START, cardView.id, START, margin16 / 8)
connect(textView.id, END, close.id, START)
applyTo(constraintLayout)
}
return ViewHolder(constraintLayout, textView, close, cardView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val webView = webViews[position]
webView.parent?.let { (it as ViewGroup).removeView(webView) }
val webViewTitle = webView.title.fromHtml().toString()
holder.apply {
title.text = webViewTitle
close.setOnClickListener { v: View -> listener?.onCloseTab(v, adapterPosition) }
materialCardView.apply {
removeAllViews()
// Create a new FrameLayout to hold the web view and custom view
val frameLayout = FrameLayout(context)
// Add the web view to the frame layout
frameLayout.addView(webView)
// Create a custom view that covers the entire
// webView(which prevent to clicks inside the webView) and handles tab selection
val view =
View(context).apply {
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
setOnClickListener { v: View ->
selected = adapterPosition
listener?.onSelectTab(v, selected)
notifyDataSetChanged()
}
}
// Add the custom view to the frame layout
frameLayout.addView(view)
// Add the frame layout to the material card view
addView(
frameLayout,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
}
}
if (webViewTitle != activity.getString(R.string.menu_home)) {
painter.update(webView) // if the webView is not opened yet
}
}
override fun getItemCount(): Int = webViews.size
override fun getItemId(position: Int): Long = webViews[position].hashCode().toLong()
fun setTabClickListener(listener: TabClickListener) {
this.listener = listener
}
interface TabClickListener {
fun onSelectTab(view: View, position: Int)
fun onCloseTab(view: View, position: Int)
}
class ViewHolder(
view: View,
val title: TextView,
val close: ImageView,
val materialCardView: MaterialCardView
) :
RecyclerView.ViewHolder(view)
}

View File

@ -27,7 +27,6 @@ import android.content.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Canvas
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener import android.media.AudioManager.OnAudioFocusChangeListener
import android.net.Uri import android.net.Uri
@ -44,24 +43,18 @@ import android.view.Gravity.BOTTOM
import android.view.Gravity.CENTER_HORIZONTAL import android.view.Gravity.CENTER_HORIZONTAL
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnimationUtils
import android.webkit.WebBackForwardList import android.webkit.WebBackForwardList
import android.webkit.WebView import android.webkit.WebView
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.AnimRes
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
@ -83,10 +76,8 @@ import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@ -114,14 +105,12 @@ import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.databinding.FragmentReaderBinding
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.consumeObservable import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.consumeObservable
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.hasNotificationPermission import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.hasNotificationPermission
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isLandScapeMode import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isLandScapeMode
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.observeNavigationResult import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.observeNavigationResult
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.requestNotificationPermission import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.requestNotificationPermission
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.findFirstTextView
import org.kiwix.kiwixmobile.core.extensions.closeFullScreenMode import org.kiwix.kiwixmobile.core.extensions.closeFullScreenMode
import org.kiwix.kiwixmobile.core.extensions.showFullScreenMode import org.kiwix.kiwixmobile.core.extensions.showFullScreenMode
import org.kiwix.kiwixmobile.core.extensions.snack import org.kiwix.kiwixmobile.core.extensions.snack
@ -140,14 +129,11 @@ import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech
import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnInitSucceedListener import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnInitSucceedListener
import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnSpeakingListener import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnSpeakingListener
import org.kiwix.kiwixmobile.core.main.KiwixWebView import org.kiwix.kiwixmobile.core.main.KiwixWebView
import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.MainRepositoryActions import org.kiwix.kiwixmobile.core.main.MainRepositoryActions
import org.kiwix.kiwixmobile.core.main.OnSwipeTouchListener
import org.kiwix.kiwixmobile.core.main.ServiceWorkerUninitialiser import org.kiwix.kiwixmobile.core.main.ServiceWorkerUninitialiser
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener
import org.kiwix.kiwixmobile.core.main.TabsAdapter
import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS
import org.kiwix.kiwixmobile.core.main.WebViewCallback import org.kiwix.kiwixmobile.core.main.WebViewCallback
import org.kiwix.kiwixmobile.core.main.WebViewProvider import org.kiwix.kiwixmobile.core.main.WebViewProvider
@ -202,10 +188,10 @@ import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
const val SEARCH_ITEM_TITLE_KEY = "searchItemTitle" const val SEARCH_ITEM_TITLE_KEY = "searchItemTitle"
const val HIDE_TAB_SWITCHER_DELAY: Long = 300
@Suppress("LargeClass") @Suppress("LargeClass")
abstract class CoreReaderFragment : abstract class CoreReaderFragment :
@ -219,15 +205,9 @@ abstract class CoreReaderFragment :
ShowDonationDialogCallback { ShowDonationDialogCallback {
protected val webViewList = mutableStateListOf<KiwixWebView>() protected val webViewList = mutableStateListOf<KiwixWebView>()
private val webUrlsFlow = MutableStateFlow("") private val webUrlsFlow = MutableStateFlow("")
private var fragmentReaderBinding: FragmentReaderBinding? = null
var toolbar: Toolbar? = null
var drawerLayout: DrawerLayout? = null var drawerLayout: DrawerLayout? = null
protected var tableDrawerRightContainer: NavigationView? = null protected var tableDrawerRightContainer: NavigationView? = null
var tabSwitcherRoot: View? = null
var activityMainRoot: View? = null
@JvmField @JvmField
@Inject @Inject
@ -262,10 +242,6 @@ abstract class CoreReaderFragment :
var painter: DarkModeViewPainter? = null var painter: DarkModeViewPainter? = null
protected var currentWebViewIndex by mutableStateOf(0) protected var currentWebViewIndex by mutableStateOf(0)
private var currentTtsWebViewIndex = 0 private var currentTtsWebViewIndex = 0
protected var actionBar: ActionBar? = null
protected var mainMenu: MainMenu? = null
private var tabRecyclerView: RecyclerView? = null
private var isFirstTimeMainPageLoaded = true private var isFirstTimeMainPageLoaded = true
private var isFromManageExternalLaunch = false private var isFromManageExternalLaunch = false
private val savingTabsMutex = Mutex() private val savingTabsMutex = Mutex()
@ -295,7 +271,6 @@ abstract class CoreReaderFragment :
private var documentParser: DocumentParser? = null private var documentParser: DocumentParser? = null
private var tts: KiwixTextToSpeech? = null private var tts: KiwixTextToSpeech? = null
private var compatCallback: CompatFindActionModeCallback? = null private var compatCallback: CompatFindActionModeCallback? = null
private var tabsAdapter: TabsAdapter? = null
private var zimReaderSource: ZimReaderSource? = null private var zimReaderSource: ZimReaderSource? = null
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var tempWebViewForUndo: KiwixWebView? = null private var tempWebViewForUndo: KiwixWebView? = null
@ -304,7 +279,6 @@ abstract class CoreReaderFragment :
private var isFirstRun = false private var isFirstRun = false
private var tableDrawerAdapter: TableDrawerAdapter? = null private var tableDrawerAdapter: TableDrawerAdapter? = null
private var tableDrawerRight: RecyclerView? = null private var tableDrawerRight: RecyclerView? = null
private var tabCallback: ItemTouchHelper.Callback? = null
private var donationLayout: FrameLayout? = null private var donationLayout: FrameLayout? = null
private var bookmarkingJob: Job? = null private var bookmarkingJob: Job? = null
private var isBookmarked = false private var isBookmarked = false
@ -517,53 +491,13 @@ abstract class CoreReaderFragment :
activity?.let { activity?.let {
WebView(it).destroy() // Workaround for buggy webViews see #710 WebView(it).destroy() // Workaround for buggy webViews see #710
} }
prepareViews()
handleLocaleCheck() handleLocaleCheck()
activity?.setSupportActionBar(toolbar)
actionBar = activity?.supportActionBar
initHideBackToTopTimer() initHideBackToTopTimer()
initTabCallback()
toolbar?.setOnTouchListener(
object : OnSwipeTouchListener(requireActivity()) {
@SuppressLint("SyntheticAccessor")
override fun onSwipeBottom() {
showTabSwitcher()
}
override fun onSwipeLeft() {
if (currentWebViewIndex < webViewList.size - 1) {
val current: View? = getCurrentWebView()
startAnimation(current, R.anim.transition_left)
selectTab(currentWebViewIndex + 1)
}
}
override fun onSwipeRight() {
if (currentWebViewIndex > 0) {
val current: View? = getCurrentWebView()
startAnimation(current, R.anim.transition_right)
selectTab(currentWebViewIndex - 1)
}
}
override fun onTap(e: MotionEvent?) {
e?.let {
val titleTextView = toolbar?.findFirstTextView() ?: return@onTap
titleTextView.let {
// only initiate search if it is on the reader screen
mainMenu?.tryExpandSearch(zimReaderContainer?.zimFileReader)
}
}
}
}
)
loadDrawerViews() loadDrawerViews()
tableDrawerRight = tableDrawerRight =
tableDrawerRightContainer?.getHeaderView(0)?.findViewById(R.id.right_drawer_list) tableDrawerRightContainer?.getHeaderView(0)?.findViewById(R.id.right_drawer_list)
addFileReader() addFileReader()
setupTabsAdapter()
setTableDrawerInfo() setTableDrawerInfo()
setTabListener()
activity?.let { activity?.let {
compatCallback = CompatFindActionModeCallback(it) compatCallback = CompatFindActionModeCallback(it)
} }
@ -572,13 +506,6 @@ abstract class CoreReaderFragment :
loadPrefs() loadPrefs()
updateTitle() updateTitle()
handleIntentExtras(requireActivity().intent) handleIntentExtras(requireActivity().intent)
tabRecyclerView?.let {
it.adapter = tabsAdapter
tabCallback?.let { callBack ->
ItemTouchHelper(callBack).attachToRecyclerView(it)
}
}
// Only check intent on first start of activity. Otherwise the intents will enter infinite loops // Only check intent on first start of activity. Otherwise the intents will enter infinite loops
// when "Don't keep activities" is on. // when "Don't keep activities" is on.
if (savedInstanceState == null) { if (savedInstanceState == null) {
@ -700,50 +627,6 @@ abstract class CoreReaderFragment :
unsupportedMimeTypeHandler?.setAlertDialogShower(alertDialogShower as AlertDialogShower) unsupportedMimeTypeHandler?.setAlertDialogShower(alertDialogShower as AlertDialogShower)
} }
private fun prepareViews() {
fragmentReaderBinding?.let { readerBinding ->
with(readerBinding.root) {
activityMainRoot = findViewById(R.id.activity_main_root)
toolbar = findViewById(R.id.toolbar)
tabSwitcherRoot = findViewById(R.id.activity_main_tab_switcher)
tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view)
donationLayout = findViewById(R.id.donation_layout)
}
}
}
private fun initTabCallback() {
tabCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int = makeMovementFlags(0, ItemTouchHelper.UP or ItemTouchHelper.DOWN)
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
viewHolder.itemView.alpha = 1 - abs(dY) / viewHolder.itemView.measuredHeight
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
closeTab(viewHolder.adapterPosition)
}
}
}
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun initHideBackToTopTimer() { private fun initHideBackToTopTimer() {
hideBackToTopTimer = object : CountDownTimer(1200, 1200) { hideBackToTopTimer = object : CountDownTimer(1200, 1200) {
@ -811,22 +694,6 @@ abstract class CoreReaderFragment :
}) })
} }
private fun setTabListener() {
tabsAdapter?.setTabClickListener(object : TabsAdapter.TabClickListener {
override fun onSelectTab(view: View, position: Int) {
hideTabSwitcher()
selectTab(position)
// Bug Fix #592
updateBottomToolbarArrowsAlpha()
}
override fun onCloseTab(view: View, position: Int) {
closeTab(position)
}
})
}
private fun setTableDrawerInfo() { private fun setTableDrawerInfo() {
tableDrawerRight?.apply { tableDrawerRight?.apply {
layoutManager = LinearLayoutManager(requireActivity()) layoutManager = LinearLayoutManager(requireActivity())
@ -836,22 +703,6 @@ abstract class CoreReaderFragment :
} }
} }
private fun setupTabsAdapter() {
tabsAdapter = painter?.let {
TabsAdapter(
requireActivity() as AppCompatActivity,
webViewList,
it
).apply {
registerAdapterDataObserver(object : AdapterDataObserver() {
override fun onChanged() {
readerMenuState?.updateTabIcon(itemCount)
}
})
}
}
}
private fun addFileReader() { private fun addFileReader() {
documentParserJs = requireActivity().readFile("js/documentParser.js") documentParserJs = requireActivity().readFile("js/documentParser.js")
documentSections?.clear() documentSections?.clear()
@ -901,7 +752,6 @@ abstract class CoreReaderFragment :
) )
} }
showSearchPlaceHolderInToolbar(true) showSearchPlaceHolderInToolbar(true)
startAnimation(tabSwitcherRoot, R.anim.slide_down)
readerMenuState?.showTabSwitcherOptions() readerMenuState?.showTabSwitcherOptions()
} }
@ -926,13 +776,6 @@ abstract class CoreReaderFragment :
} }
} }
protected fun startAnimation(
view: View?,
@AnimRes anim: Int
) {
view?.startAnimation(AnimationUtils.loadAnimation(view.context, anim))
}
/** /**
* @param shouldCloseZimBook A flag to indicate whether the ZIM book should be closed. * @param shouldCloseZimBook A flag to indicate whether the ZIM book should be closed.
* - Default is `true`, which ensures normal behavior for most scenarios. * - Default is `true`, which ensures normal behavior for most scenarios.
@ -1074,7 +917,7 @@ abstract class CoreReaderFragment :
@Suppress("ReturnCount", "NestedBlockDepth") @Suppress("ReturnCount", "NestedBlockDepth")
override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super { override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super {
when { when {
tabSwitcherRoot?.visibility == VISIBLE -> { readerScreenState.value.showTabSwitcher -> {
selectTab( selectTab(
if (currentWebViewIndex < webViewList.size) { if (currentWebViewIndex < webViewList.size) {
currentWebViewIndex currentWebViewIndex
@ -1300,16 +1143,11 @@ abstract class CoreReaderFragment :
} }
safelyCancelBookmarkJob() safelyCancelBookmarkJob()
unBindViewsAndBinding() unBindViewsAndBinding()
tabCallback = null
hideBackToTopTimer?.cancel() hideBackToTopTimer?.cancel()
hideBackToTopTimer = null hideBackToTopTimer = null
stopOngoingLoadingAndClearWebViewList() stopOngoingLoadingAndClearWebViewList()
actionBar = null
mainMenu = null
tabRecyclerView?.adapter = null
tableDrawerRight?.adapter = null tableDrawerRight?.adapter = null
tableDrawerAdapter = null tableDrawerAdapter = null
tabsAdapter = null
tempWebViewListForUndo.clear() tempWebViewListForUndo.clear()
// create a base Activity class that class this. // create a base Activity class that class this.
deleteCachedFiles(requireActivity()) deleteCachedFiles(requireActivity())
@ -1336,17 +1174,10 @@ abstract class CoreReaderFragment :
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun unBindViewsAndBinding() { private fun unBindViewsAndBinding() {
activityMainRoot = null
tabRecyclerView = null
tabSwitcherRoot = null
compatCallback?.finish() compatCallback?.finish()
compatCallback = null compatCallback = null
toolbar?.setOnTouchListener(null)
toolbar = null
drawerLayout = null drawerLayout = null
tableDrawerRightContainer = null tableDrawerRightContainer = null
fragmentReaderBinding?.root?.removeAllViews()
fragmentReaderBinding = null
donationLayout?.removeAllViews() donationLayout?.removeAllViews()
donationLayout = null donationLayout = null
} }
@ -1522,18 +1353,7 @@ abstract class CoreReaderFragment :
reopenBook() reopenBook()
} }
tempWebViewForUndo?.let { tempWebViewForUndo?.let {
if (tabSwitcherRoot?.visibility == GONE) {
// Remove the top margin from the webView when the tabSwitcher is not visible.
// We have added this margin in `TabsAdapter` to not show the top margin in tabs.
// `tempWebViewForUndo` saved with that margin so before showing it to the `contentFrame`
// We need to set full width and height for properly showing the content of webView.
it.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
}
webViewList.add(index, it) webViewList.add(index, it)
tabsAdapter?.notifyDataSetChanged()
readerScreenState.value.snackBarHostState.snack( readerScreenState.value.snackBarHostState.snack(
context?.getString(R.string.tab_restored).orEmpty(), context?.getString(R.string.tab_restored).orEmpty(),
lifecycleScope = lifecycleScope lifecycleScope = lifecycleScope
@ -1555,7 +1375,6 @@ abstract class CoreReaderFragment :
currentWebViewIndex = position currentWebViewIndex = position
val webView = safelyGetWebView(position) ?: return val webView = safelyGetWebView(position) ?: return
safelyAddWebView(webView) safelyAddWebView(webView)
tabsAdapter?.selected = currentWebViewIndex
updateBottomToolbarVisibility() updateBottomToolbarVisibility()
loadPrefs() loadPrefs()
updateUrlFlow() updateUrlFlow()
@ -1999,7 +1818,6 @@ abstract class CoreReaderFragment :
private fun restoreDeletedTabs() { private fun restoreDeletedTabs() {
if (tempWebViewListForUndo.isNotEmpty()) { if (tempWebViewListForUndo.isNotEmpty()) {
webViewList.addAll(tempWebViewListForUndo) webViewList.addAll(tempWebViewListForUndo)
tabsAdapter?.notifyDataSetChanged()
readerScreenState.value.snackBarHostState.snack( readerScreenState.value.snackBarHostState.snack(
context?.getString(R.string.tabs_restored).orEmpty(), context?.getString(R.string.tabs_restored).orEmpty(),
lifecycleScope = lifecycleScope lifecycleScope = lifecycleScope
@ -2397,10 +2215,8 @@ abstract class CoreReaderFragment :
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
// Forcing redraw of RecyclerView children so that the tabs are properly oriented on rotation
tabRecyclerView?.adapter = tabsAdapter
// force redraw of donation layout if it is showing. // force redraw of donation layout if it is showing.
if (donationLayout?.isVisible == true) { if (readerScreenState.value.shouldShowDonationPopup) {
showDonationLayout() showDonationLayout()
} }
} }
@ -2647,7 +2463,6 @@ abstract class CoreReaderFragment :
return return
} }
updateTableOfContents() updateTableOfContents()
tabsAdapter?.notifyDataSetChanged()
updateBottomToolbarArrowsAlpha() updateBottomToolbarArrowsAlpha()
val zimFileReader = zimReaderContainer?.zimFileReader val zimFileReader = zimReaderContainer?.zimFileReader
if (hasValidFileAndUrl(getCurrentWebView()?.url, zimFileReader)) { if (hasValidFileAndUrl(getCurrentWebView()?.url, zimFileReader)) {
@ -2681,13 +2496,19 @@ abstract class CoreReaderFragment :
private fun hasValidFileAndUrl(url: String?, zimFileReader: ZimFileReader?): Boolean = private fun hasValidFileAndUrl(url: String?, zimFileReader: ZimFileReader?): Boolean =
url != null && zimFileReader != null url != null && zimFileReader != null
override fun webViewFailedLoading(url: String) { override fun webViewFailedLoading(failingUrl: String) {
if (isAdded) { if (isAdded) {
// If a URL fails to load, update the bookmark toggle. // If a URL fails to load, update the bookmark toggle.
// This fixes the scenario where the previous page is bookmarked and the next // This fixes the scenario where the previous page is bookmarked and the next
// page fails to load, ensuring the bookmark toggle is unset correctly. // page fails to load, ensuring the bookmark toggle is unset correctly.
updateUrlFlow() updateUrlFlow()
Log.d(TAG_KIWIX, String.format(getString(R.string.error_article_url_not_found), url)) Log.d(
TAG_KIWIX,
String.format(
getString(R.string.error_article_url_not_found),
failingUrl
)
)
} }
} }
@ -2707,7 +2528,6 @@ abstract class CoreReaderFragment :
override fun webViewTitleUpdated(title: String) { override fun webViewTitleUpdated(title: String) {
updateTabIcon(webViewList.size) updateTabIcon(webViewList.size)
tabsAdapter?.notifyDataSetChanged()
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")

View File

@ -89,7 +89,6 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
@ -97,6 +96,8 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -146,6 +147,10 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
import org.kiwix.kiwixmobile.core.utils.StyleUtils.fromHtml import org.kiwix.kiwixmobile.core.utils.StyleUtils.fromHtml
const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTestingTag" const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTestingTag"
const val TAB_SWITCHER_VIEW_TESTING_TAG = "tabSwitcherViewTestingTag"
const val READER_SCREEN_TESTING_TAG = "readerScreenTestingTag"
const val CLOSE_ALL_TABS_BUTTON_TESTING_TAG = "closeAllTabsButtonTestingTag"
const val TAB_TITLE_TESTING_TAG = "tabTitleTestingTag"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming") @Suppress("ComposableLambdaParameterNaming")
@ -157,10 +162,10 @@ fun ReaderScreen(
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() } val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
LaunchedEffect(bottomAppBarScrollBehavior.state.heightOffset) { LaunchedEffect(bottomAppBarScrollBehavior.state.heightOffset) {
onBottomScrollOffsetChanged(scrollBehavior.state.heightOffset) onBottomScrollOffsetChanged(bottomAppBarScrollBehavior.state.heightOffset)
} }
KiwixDialogTheme { KiwixDialogTheme {
Scaffold( Scaffold(
@ -169,21 +174,21 @@ fun ReaderScreen(
ReaderTopBar( ReaderTopBar(
state, state,
actionMenuItems, actionMenuItems,
scrollBehavior, topAppBarScrollBehavior,
navigationIcon navigationIcon
) )
}, },
floatingActionButton = { BackToTopFab(state) }, floatingActionButton = { BackToTopFab(state) },
modifier = Modifier modifier = Modifier
.systemBarsPadding() .systemBarsPadding()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection)
.padding(bottom = bottomNavHeightInDp) .padding(bottom = bottomNavHeightInDp)
.semantics { testTag = READER_SCREEN_TESTING_TAG }
) { paddingValues -> ) { paddingValues ->
ReaderContentLayout( ReaderContentLayout(
state, state,
Modifier.padding(paddingValues), Modifier.padding(paddingValues),
bottomAppBarScrollBehavior bottomAppBarScrollBehavior,
topAppBarScrollBehavior
) )
} }
} }
@ -195,7 +200,7 @@ fun ReaderScreen(
private fun ReaderTopBar( private fun ReaderTopBar(
state: ReaderScreenState, state: ReaderScreenState,
actionMenuItems: List<ActionMenuItem>, actionMenuItems: List<ActionMenuItem>,
scrollBehavior: TopAppBarScrollBehavior, topAppBarScrollBehavior: TopAppBarScrollBehavior,
navigationIcon: @Composable () -> Unit, navigationIcon: @Composable () -> Unit,
) { ) {
if (!state.shouldShowFullScreenMode && !state.fullScreenItem.first) { if (!state.shouldShowFullScreenMode && !state.fullScreenItem.first) {
@ -203,7 +208,7 @@ private fun ReaderTopBar(
title = if (state.showTabSwitcher) "" else state.readerScreenTitle, title = if (state.showTabSwitcher) "" else state.readerScreenTitle,
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems, actionMenuItems = actionMenuItems,
topAppBarScrollBehavior = scrollBehavior, topAppBarScrollBehavior = topAppBarScrollBehavior,
searchBar = searchBar =
searchPlaceHolderIfActive(state.searchPlaceHolderItemForCustomApps) searchPlaceHolderIfActive(state.searchPlaceHolderItemForCustomApps)
) )
@ -215,7 +220,8 @@ private fun ReaderTopBar(
private fun ReaderContentLayout( private fun ReaderContentLayout(
state: ReaderScreenState, state: ReaderScreenState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
bottomAppBarScrollBehavior: BottomAppBarScrollBehavior bottomAppBarScrollBehavior: BottomAppBarScrollBehavior,
topAppBarScrollBehavior: TopAppBarScrollBehavior
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
TabSwitcherAnimated(state) TabSwitcherAnimated(state)
@ -225,7 +231,7 @@ private fun ReaderContentLayout(
state.fullScreenItem.first -> ShowFullScreenView(state) state.fullScreenItem.first -> ShowFullScreenView(state)
else -> { else -> {
ShowZIMFileContent(state) ShowZIMFileContent(state, bottomAppBarScrollBehavior, topAppBarScrollBehavior)
ShowProgressBarIfZIMFilePageIsLoading(state) ShowProgressBarIfZIMFilePageIsLoading(state)
Column(Modifier.align(Alignment.BottomCenter)) { Column(Modifier.align(Alignment.BottomCenter)) {
TtsControls(state) TtsControls(state)
@ -255,11 +261,11 @@ private fun TabSwitcherAnimated(state: ReaderScreenState) {
val transitionSpec = remember { val transitionSpec = remember {
slideInVertically( slideInVertically(
initialOffsetY = { -it }, initialOffsetY = { -it },
animationSpec = tween(durationMillis = 300) animationSpec = tween(durationMillis = HIDE_TAB_SWITCHER_DELAY.toInt())
) + fadeIn() togetherWith ) + fadeIn() togetherWith
slideOutVertically( slideOutVertically(
targetOffsetY = { -it }, targetOffsetY = { -it },
animationSpec = tween(durationMillis = 300) animationSpec = tween(durationMillis = HIDE_TAB_SWITCHER_DELAY.toInt())
) + fadeOut() ) + fadeOut()
} }
@ -267,7 +273,9 @@ private fun TabSwitcherAnimated(state: ReaderScreenState) {
visible = state.showTabSwitcher, visible = state.showTabSwitcher,
enter = transitionSpec.targetContentEnter, enter = transitionSpec.targetContentEnter,
exit = transitionSpec.initialContentExit, exit = transitionSpec.initialContentExit,
modifier = Modifier.zIndex(1f), modifier = Modifier
.zIndex(1f)
.semantics { testTag = TAB_SWITCHER_VIEW_TESTING_TAG },
) { ) {
TabSwitcherView( TabSwitcherView(
state.kiwixWebViewList, state.kiwixWebViewList,
@ -349,8 +357,13 @@ private fun BoxScope.CloseFullScreenImageButton(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ShowZIMFileContent(state: ReaderScreenState) { private fun ShowZIMFileContent(
state: ReaderScreenState,
bottomAppBarScrollBehavior: BottomAppBarScrollBehavior,
topAppBarScrollBehavior: TopAppBarScrollBehavior
) {
state.selectedWebView?.let { selectedWebView -> state.selectedWebView?.let { selectedWebView ->
key(selectedWebView) { key(selectedWebView) {
AndroidView( AndroidView(
@ -359,14 +372,23 @@ private fun ShowZIMFileContent(state: ReaderScreenState) {
FrameLayout(context).apply { FrameLayout(context).apply {
// Ensure the WebView has no parent before adding // Ensure the WebView has no parent before adding
(selectedWebView.parent as? ViewGroup)?.removeView(selectedWebView) (selectedWebView.parent as? ViewGroup)?.removeView(selectedWebView)
selectedWebView.setOnScrollChangeListener(null)
selectedWebView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val deltaY = (scrollY - oldScrollY).toFloat()
if (deltaY == 0f) return@setOnScrollChangeListener
// SAFELY drive top and bottom app bars
topAppBarScrollBehavior.state.heightOffset -= deltaY
bottomAppBarScrollBehavior.state.heightOffset -= deltaY
}
selectedWebView.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
addView(selectedWebView) addView(selectedWebView)
} }
}, },
modifier = Modifier modifier = Modifier.fillMaxSize()
.fillMaxSize()
.verticalScroll(rememberScrollState())
) )
// TODO handle the unnecessary vertical scroll when webView page chnaged.
} }
} }
} }
@ -657,6 +679,7 @@ private fun BoxScope.CloseAllTabButton(onCloseAllTabs: () -> Unit) {
.graphicsLayer { .graphicsLayer {
rotationZ = rotation rotationZ = rotation
} }
.semantics { testTag = CLOSE_ALL_TABS_BUTTON_TESTING_TAG }
.clickable( .clickable(
enabled = !isAnimating, enabled = !isAnimating,
onClick = { onClick = {
@ -732,7 +755,8 @@ private fun TabItemHeader(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.padding(end = FOUR_DP) .padding(end = FOUR_DP)
.weight(1f), .weight(1f)
.semantics { testTag = TAB_TITLE_TESTING_TAG },
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
IconButton( IconButton(

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Kiwix Android
~ Copyright (c) 2020 Kiwix <android.kiwix.org>
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/navigation_fragment_main_drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/reader_fragment_content" />
<TextView
android:id="@+id/no_open_book_text"
style="@style/no_content"
android:text="@string/no_open_book"
android:layout_marginTop="@dimen/material_design_appbar_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45" />
<Button
android:id="@+id/go_to_library_button_no_open_book"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/open_library"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/no_open_book_text"
app:layout_constraintStart_toStartOf="@+id/no_open_book_text"
app:layout_constraintTop_toBottomOf="@id/no_open_book_text"/>
<RelativeLayout
android:id="@+id/fullscreen_video_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:visibility="invisible" />
<FrameLayout
android:id="@+id/donation_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/actionBarItemBackground"
android:minHeight="@dimen/material_minimum_height_and_width"
android:minWidth="@dimen/material_minimum_height_and_width"
android:paddingStart="12dp"
android:paddingEnd="12dp"
tools:ignore="Overdraw">
<TextView
android:id="@+id/ic_tab_switcher_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/border_tab_switcher"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="12sp"
android:textStyle="bold"
android:minHeight="20dp"
android:minWidth="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="12"
tools:textColor="@android:color/black" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Kiwix Android
~ Copyright (c) 2020 Kiwix <android.kiwix.org>
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/KiwixTheme"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
</merge>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<include layout="@layout/layout_toolbar" />
</com.google.android.material.appbar.AppBarLayout>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:popupTheme="@style/KiwixTheme"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbarWithSearchPlaceholder"
android:layout_width="match_parent"
android:visibility="gone"
android:background="@drawable/search_placeholder_background"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_label"
android:padding="5dip"
android:layout_marginStart="5dip"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:drawableRightCompat="@drawable/action_search"
android:drawablePadding="10dip" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
</merge>

View File

@ -1,145 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Kiwix Android
~ Copyright (c) 2020 Kiwix <android.kiwix.org>
~ 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 <http://www.gnu.org/licenses/>.
~
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/activity_main_content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include
android:id="@+id/activity_main_tab_switcher"
layout="@layout/tab_switcher"
android:visibility="gone" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/fragment_main_app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/layout_toolbar" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/main_fragment_progress_view"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="@dimen/progress_view_height"
android:indeterminate="false"
android:max="100"
android:theme="@style/ThemeOverlay.KiwixTheme.ProgressBar"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
tools:progress="70" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:layout_anchor="@id/bottom_toolbar">
<Button
android:id="@+id/activity_main_button_pause_tts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:alpha="0.6"
android:text="@string/tts_pause"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@+id/activity_main_button_stop_tts"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/activity_main_button_stop_tts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:alpha="0.6"
android:text="@string/stop"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/activity_main_button_pause_tts"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/activity_main_tts_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="activity_main_button_pause_tts,activity_main_button_stop_tts"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/bottom_toolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/activity_main_back_to_top_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/action_find_previous"
android:visibility="gone"
app:fabSize="mini"
android:contentDescription="@string/pref_back_to_top"
app:layout_anchor="@id/bottom_toolbar"
app:layout_dodgeInsetEdges="bottom"
tools:visibility="visible" />
</org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout>
<ImageButton
android:id="@+id/activity_main_fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fullscreen_control_button_margin"
android:background="?colorOnSurface"
android:contentDescription="@string/menu_exit_full_screen"
android:src="@drawable/fullscreen_exit"
app:tint="?colorSurface"
android:visibility="gone"
android:minWidth="@dimen/material_minimum_height_and_width"
android:minHeight="@dimen/material_minimum_height_and_width"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tab_switcher_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/tab_switcher_close_all_tabs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:contentDescription="@string/close_all_tabs"
android:src="@drawable/ic_close_black_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,50 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/menu_search"
android:icon="@drawable/action_search"
android:title="@string/search_label"
android:visible="false"
app:showAsAction="always"
tools:visible="true"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/menu_tab_switcher"
android:title="@string/switch_tabs"
app:actionLayout="@layout/ic_tab_switcher"
app:showAsAction="always" />
<item
android:id="@+id/menu_add_note"
android:icon="@drawable/ic_add_note"
android:title="@string/take_notes"
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/menu_random_article"
android:icon="@drawable/action_randomarticle"
android:title="@string/menu_random_article"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
<item
android:id="@+id/menu_fullscreen"
android:title="@string/menu_full_screen"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
<item
android:id="@+id/menu_read_aloud"
android:title="@string/menu_read_aloud"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
</menu>

View File

@ -13,14 +13,16 @@
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/find_prev" android:id="@+id/find_prev"
android:icon="@drawable/action_find_previous" android:icon="@drawable/action_find_previous"
android:title="@string/previous" android:title="@string/previous"
app:showAsAction="always" /> app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item <item
android:id="@+id/find_next" android:id="@+id/find_next"
android:icon="@drawable/action_find_next" android:icon="@drawable/action_find_next"