mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-09 23:40:26 -04:00
Merge pull request #4286 from kiwix/Fixes#4245
Migrated the `LocalFileTransferFragment` to Jetpack Compose.
This commit is contained in:
commit
8b499c6332
@ -19,23 +19,25 @@
|
|||||||
package org.kiwix.kiwixmobile.localFileTransfer
|
package org.kiwix.kiwixmobile.localFileTransfer
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.test.assertTextEquals
|
||||||
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
|
||||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
|
||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
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.StringId.TextId
|
|
||||||
import org.kiwix.kiwixmobile.Findable.ViewId
|
|
||||||
import org.kiwix.kiwixmobile.R
|
import org.kiwix.kiwixmobile.R
|
||||||
import org.kiwix.kiwixmobile.core.R.string
|
import org.kiwix.kiwixmobile.core.R.string
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.SHOWCASE_VIEW_MESSAGE_TESTING_TAG
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.SHOWCASE_VIEW_NEXT_BUTTON_TESTING_TAG
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG
|
||||||
|
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_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 uk.co.deanwild.materialshowcaseview.R.id
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authored by Ayush Shrivastava on 29/10/20
|
* Authored by Ayush Shrivastava on 29/10/20
|
||||||
@ -45,22 +47,32 @@ fun localFileTransfer(func: LocalFileTransferRobot.() -> Unit) =
|
|||||||
LocalFileTransferRobot().applyWithViewHierarchyPrinting(func)
|
LocalFileTransferRobot().applyWithViewHierarchyPrinting(func)
|
||||||
|
|
||||||
class LocalFileTransferRobot : BaseRobot() {
|
class LocalFileTransferRobot : BaseRobot() {
|
||||||
fun assertReceiveFileTitleVisible() {
|
fun assertReceiveFileTitleVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
isVisible(TextId(R.string.receive_files_title))
|
composeContentTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG)
|
||||||
|
.assertTextEquals(context.getString(R.string.receive_files_title))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertSearchDeviceMenuItemVisible() {
|
fun assertSearchDeviceMenuItemVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
isVisible(ViewId(R.id.menu_item_search_devices))
|
composeContentTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(SEARCH_ICON_TESTING_TAG).assertExists()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clickOnSearchDeviceMenuItem() {
|
fun clickOnSearchDeviceMenuItem(composeContentTestRule: ComposeContentTestRule) {
|
||||||
clickOn(ViewId(R.id.menu_item_search_devices))
|
composeContentTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(SEARCH_ICON_TESTING_TAG).performClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertLocalFileTransferScreenVisible() {
|
fun assertLocalFileTransferScreenVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_DOWNLOAD_TEST.toLong())
|
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_DOWNLOAD_TEST.toLong())
|
||||||
closeEnableWifiP2PDialogIfVisible()
|
closeEnableWifiP2PDialogIfVisible()
|
||||||
assertReceiveFileTitleVisible()
|
assertReceiveFileTitleVisible(composeContentTestRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun closeEnableWifiP2PDialogIfVisible() {
|
private fun closeEnableWifiP2PDialogIfVisible() {
|
||||||
@ -69,7 +81,7 @@ class LocalFileTransferRobot : BaseRobot() {
|
|||||||
onView(withText(string.request_enable_wifi)).check(matches(isDisplayed()))
|
onView(withText(string.request_enable_wifi)).check(matches(isDisplayed()))
|
||||||
pressBack()
|
pressBack()
|
||||||
})
|
})
|
||||||
} catch (ignore: Throwable) {
|
} catch (_: Throwable) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"LOCAL_FILE_TRANSFER_TEST",
|
"LOCAL_FILE_TRANSFER_TEST",
|
||||||
"Couldn't found WIFI P2P dialog, probably this is not exist"
|
"Couldn't found WIFI P2P dialog, probably this is not exist"
|
||||||
@ -77,53 +89,66 @@ class LocalFileTransferRobot : BaseRobot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertLocalLibraryVisible() {
|
fun assertLocalLibraryVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
isVisible(TextId(string.library))
|
composeContentTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG)
|
||||||
|
.assertTextEquals(context.getString(string.library))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertClickNearbyDeviceMessageVisible() {
|
fun assertClickNearbyDeviceMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
pauseForBetterTestPerformance()
|
pauseForBetterTestPerformance()
|
||||||
testFlakyView({
|
composeContentTestRule.apply {
|
||||||
onView(withId(id.tv_content))
|
waitForIdle()
|
||||||
.check(matches(withText(string.click_nearby_devices_message)))
|
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||||
})
|
.assertTextEquals(context.getString(string.click_nearby_devices_message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clickOnGotItButton() {
|
fun clickOnNextButton(composeContentTestRule: ComposeContentTestRule) {
|
||||||
pauseForBetterTestPerformance()
|
pauseForBetterTestPerformance()
|
||||||
testFlakyView({
|
composeContentTestRule.apply {
|
||||||
onView(withId(id.tv_dismiss))
|
waitForIdle()
|
||||||
.perform(click())
|
onNodeWithTag(SHOWCASE_VIEW_NEXT_BUTTON_TESTING_TAG)
|
||||||
})
|
.performClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertDeviceNameMessageVisible() {
|
fun assertDeviceNameMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
pauseForBetterTestPerformance()
|
pauseForBetterTestPerformance()
|
||||||
testFlakyView({
|
composeContentTestRule.apply {
|
||||||
onView(withId(id.tv_content))
|
waitForIdle()
|
||||||
.check(matches(withText(string.your_device_name_message)))
|
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||||
})
|
.assertTextEquals(context.getString(string.your_device_name_message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertNearbyDeviceListMessageVisible() {
|
fun assertNearbyDeviceListMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
pauseForBetterTestPerformance()
|
pauseForBetterTestPerformance()
|
||||||
testFlakyView({
|
composeContentTestRule.apply {
|
||||||
onView(withId(id.tv_content))
|
waitForIdle()
|
||||||
.check(matches(withText(string.nearby_devices_list_message)))
|
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||||
})
|
.assertTextEquals(context.getString(string.nearby_devices_list_message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertTransferZimFilesListMessageVisible() {
|
fun assertTransferZimFilesListMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
pauseForBetterTestPerformance()
|
pauseForBetterTestPerformance()
|
||||||
testFlakyView({
|
composeContentTestRule.apply {
|
||||||
onView(withId(id.tv_content))
|
waitForIdle()
|
||||||
.check(matches(withText(string.transfer_zim_files_list_message)))
|
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||||
})
|
.assertTextEquals(context.getString(string.transfer_zim_files_list_message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertClickNearbyDeviceMessageNotVisible() {
|
fun assertClickNearbyDeviceMessageNotVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||||
pauseForBetterTestPerformance()
|
pauseForBetterTestPerformance()
|
||||||
onView(withText(string.click_nearby_devices_message)).check(doesNotExist())
|
composeContentTestRule.apply {
|
||||||
|
waitForIdle()
|
||||||
|
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pauseForBetterTestPerformance() {
|
private fun pauseForBetterTestPerformance() {
|
||||||
|
@ -28,17 +28,10 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.accessibility.AccessibilityChecks
|
import androidx.test.espresso.accessibility.AccessibilityChecks
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import androidx.test.uiautomator.UiDevice
|
import androidx.test.uiautomator.UiDevice
|
||||||
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheck
|
|
||||||
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews
|
|
||||||
import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
|
|
||||||
import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck
|
|
||||||
import leakcanary.LeakAssertions
|
import leakcanary.LeakAssertions
|
||||||
import org.hamcrest.core.AllOf.allOf
|
|
||||||
import org.hamcrest.core.AnyOf.anyOf
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -89,22 +82,6 @@ class LocalFileTransferTest {
|
|||||||
init {
|
init {
|
||||||
AccessibilityChecks.enable().apply {
|
AccessibilityChecks.enable().apply {
|
||||||
setRunChecksFromRootView(true)
|
setRunChecksFromRootView(true)
|
||||||
setSuppressingResultMatcher(
|
|
||||||
anyOf(
|
|
||||||
allOf(
|
|
||||||
matchesCheck(SpeakableTextPresentCheck::class.java),
|
|
||||||
matchesViews(withId(uk.co.deanwild.materialshowcaseview.R.id.tv_skip))
|
|
||||||
),
|
|
||||||
allOf(
|
|
||||||
matchesCheck(TouchTargetSizeCheck::class.java),
|
|
||||||
matchesViews(withId(uk.co.deanwild.materialshowcaseview.R.id.tv_skip))
|
|
||||||
),
|
|
||||||
allOf(
|
|
||||||
matchesCheck(TouchTargetSizeCheck::class.java),
|
|
||||||
matchesViews(withId(R.id.text_view_device_name))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,12 +118,12 @@ class LocalFileTransferTest {
|
|||||||
library {
|
library {
|
||||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||||
clickFileTransferIcon(composeTestRule) {
|
clickFileTransferIcon(composeTestRule) {
|
||||||
assertReceiveFileTitleVisible()
|
assertReceiveFileTitleVisible(composeTestRule)
|
||||||
assertSearchDeviceMenuItemVisible()
|
assertSearchDeviceMenuItemVisible(composeTestRule)
|
||||||
clickOnSearchDeviceMenuItem()
|
clickOnSearchDeviceMenuItem(composeTestRule)
|
||||||
assertLocalFileTransferScreenVisible()
|
assertLocalFileTransferScreenVisible(composeTestRule)
|
||||||
pressBack()
|
pressBack()
|
||||||
assertLocalLibraryVisible()
|
assertLocalLibraryVisible(composeTestRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LeakAssertions.assertNoLeaks()
|
LeakAssertions.assertNoLeaks()
|
||||||
@ -155,7 +132,7 @@ class LocalFileTransferTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun showCaseFeature() {
|
fun showCaseFeature() {
|
||||||
shouldShowShowCaseFeatureToUser(true, isResetShowCaseId = true)
|
shouldShowShowCaseFeatureToUser(true)
|
||||||
activityScenario =
|
activityScenario =
|
||||||
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
||||||
moveToState(Lifecycle.State.RESUMED)
|
moveToState(Lifecycle.State.RESUMED)
|
||||||
@ -172,14 +149,14 @@ class LocalFileTransferTest {
|
|||||||
library {
|
library {
|
||||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||||
clickFileTransferIcon(composeTestRule) {
|
clickFileTransferIcon(composeTestRule) {
|
||||||
assertClickNearbyDeviceMessageVisible()
|
assertClickNearbyDeviceMessageVisible(composeTestRule)
|
||||||
clickOnGotItButton()
|
clickOnNextButton(composeTestRule)
|
||||||
assertDeviceNameMessageVisible()
|
assertDeviceNameMessageVisible(composeTestRule)
|
||||||
clickOnGotItButton()
|
clickOnNextButton(composeTestRule)
|
||||||
assertNearbyDeviceListMessageVisible()
|
assertNearbyDeviceListMessageVisible(composeTestRule)
|
||||||
clickOnGotItButton()
|
clickOnNextButton(composeTestRule)
|
||||||
assertTransferZimFilesListMessageVisible()
|
assertTransferZimFilesListMessageVisible(composeTestRule)
|
||||||
clickOnGotItButton()
|
clickOnNextButton(composeTestRule)
|
||||||
pressBack()
|
pressBack()
|
||||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||||
}
|
}
|
||||||
@ -189,7 +166,7 @@ class LocalFileTransferTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShowCaseFeatureShowOnce() {
|
fun testShowCaseFeatureShowOnce() {
|
||||||
shouldShowShowCaseFeatureToUser(true)
|
shouldShowShowCaseFeatureToUser(false)
|
||||||
activityScenario =
|
activityScenario =
|
||||||
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
||||||
moveToState(Lifecycle.State.RESUMED)
|
moveToState(Lifecycle.State.RESUMED)
|
||||||
@ -201,15 +178,12 @@ class LocalFileTransferTest {
|
|||||||
library {
|
library {
|
||||||
// test show case view show once.
|
// test show case view show once.
|
||||||
clickFileTransferIcon(composeTestRule) {
|
clickFileTransferIcon(composeTestRule) {
|
||||||
LocalFileTransferRobot::assertClickNearbyDeviceMessageNotVisible
|
assertClickNearbyDeviceMessageNotVisible(composeTestRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldShowShowCaseFeatureToUser(
|
private fun shouldShowShowCaseFeatureToUser(shouldShowShowCase: Boolean) {
|
||||||
shouldShowShowCase: Boolean,
|
|
||||||
isResetShowCaseId: Boolean = false
|
|
||||||
) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||||
putBoolean(SharedPreferenceUtil.PREF_SHOW_INTRO, false)
|
putBoolean(SharedPreferenceUtil.PREF_SHOW_INTRO, false)
|
||||||
putBoolean(SharedPreferenceUtil.PREF_WIFI_ONLY, false)
|
putBoolean(SharedPreferenceUtil.PREF_WIFI_ONLY, false)
|
||||||
@ -217,15 +191,5 @@ class LocalFileTransferTest {
|
|||||||
putBoolean(SharedPreferenceUtil.PREF_SHOW_SHOWCASE, shouldShowShowCase)
|
putBoolean(SharedPreferenceUtil.PREF_SHOW_SHOWCASE, shouldShowShowCase)
|
||||||
putString(SharedPreferenceUtil.PREF_LANG, "en")
|
putString(SharedPreferenceUtil.PREF_LANG, "en")
|
||||||
}
|
}
|
||||||
if (isResetShowCaseId) {
|
|
||||||
// To clear showCaseID to ensure the showcase view will show.
|
|
||||||
uk.co.deanwild.materialshowcaseview.PrefsManager.resetAll(context)
|
|
||||||
} else {
|
|
||||||
// set that Show Case is showed, because sometimes its change the
|
|
||||||
// order of test case on API level 33 and our test case fails.
|
|
||||||
val internal =
|
|
||||||
context.getSharedPreferences("material_showcaseview_prefs", Context.MODE_PRIVATE)
|
|
||||||
internal.edit().putInt("status_$SHOWCASE_ID", -1).apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@ 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.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.help.HelpRobot
|
import org.kiwix.kiwixmobile.help.HelpRobot
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferRobot
|
|
||||||
import org.kiwix.kiwixmobile.nav.destination.library.OnlineLibraryRobot
|
import org.kiwix.kiwixmobile.nav.destination.library.OnlineLibraryRobot
|
||||||
import org.kiwix.kiwixmobile.settings.SettingsRobot
|
import org.kiwix.kiwixmobile.settings.SettingsRobot
|
||||||
import org.kiwix.kiwixmobile.testutils.RetryRule
|
import org.kiwix.kiwixmobile.testutils.RetryRule
|
||||||
@ -115,7 +114,7 @@ class TopLevelDestinationTest : BaseActivityTest() {
|
|||||||
clickLibraryOnBottomNav {
|
clickLibraryOnBottomNav {
|
||||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||||
clickFileTransferIcon(composeTestRule) {
|
clickFileTransferIcon(composeTestRule) {
|
||||||
LocalFileTransferRobot::assertReceiveFileTitleVisible
|
assertReceiveFileTitleVisible(composeTestRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clickBookmarksOnNavDrawer {
|
clickBookmarksOnNavDrawer {
|
||||||
|
@ -39,7 +39,6 @@ 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.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.help.HelpRobot
|
import org.kiwix.kiwixmobile.help.HelpRobot
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferRobot
|
|
||||||
import org.kiwix.kiwixmobile.main.ACTION_GET_CONTENT
|
import org.kiwix.kiwixmobile.main.ACTION_GET_CONTENT
|
||||||
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
||||||
import org.kiwix.kiwixmobile.main.topLevel
|
import org.kiwix.kiwixmobile.main.topLevel
|
||||||
@ -119,7 +118,7 @@ class GetContentShortcutTest {
|
|||||||
clickLibraryOnBottomNav {
|
clickLibraryOnBottomNav {
|
||||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||||
clickFileTransferIcon(composeTestRule) {
|
clickFileTransferIcon(composeTestRule) {
|
||||||
LocalFileTransferRobot::assertReceiveFileTitleVisible
|
assertReceiveFileTitleVisible(composeTestRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clickBookmarksOnNavDrawer {
|
clickBookmarksOnNavDrawer {
|
||||||
|
@ -1,90 +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/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@file:Suppress("PackageNaming")
|
|
||||||
|
|
||||||
package org.kiwix.kiwixmobile.localFileTransfer
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.kiwix.kiwixmobile.R
|
|
||||||
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
|
|
||||||
import org.kiwix.kiwixmobile.databinding.ItemTransferListBinding
|
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.ERROR
|
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENDING
|
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENT
|
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.FileListAdapter.FileViewHolder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class, part of the local file sharing module.
|
|
||||||
*
|
|
||||||
* Defines the Adapter for the list of file-items displayed in {TransferProgressFragment}
|
|
||||||
*/
|
|
||||||
class FileListAdapter(private val fileItems: List<FileItem>) :
|
|
||||||
RecyclerView.Adapter<FileViewHolder>() {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder =
|
|
||||||
FileViewHolder(
|
|
||||||
ItemTransferListBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
this
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
|
|
||||||
holder.bind(fileItems[position])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = fileItems.size
|
|
||||||
|
|
||||||
inner class FileViewHolder(
|
|
||||||
private val itemTransferListBinding: ItemTransferListBinding,
|
|
||||||
val fileListAdapter: FileListAdapter
|
|
||||||
) :
|
|
||||||
BaseViewHolder<FileItem>(itemTransferListBinding.root) {
|
|
||||||
override fun bind(item: FileItem) {
|
|
||||||
itemTransferListBinding.textViewFileItemName.text = item.fileName
|
|
||||||
itemTransferListBinding.imageViewFileTransferred.isVisible = item.fileStatus != SENDING
|
|
||||||
itemTransferListBinding.progressBarTransferringFile.isVisible = item.fileStatus == SENDING
|
|
||||||
if (item.fileStatus != FileItem.FileStatus.TO_BE_SENT) {
|
|
||||||
// Icon for TO_BE_SENT is assigned by default in the item layout
|
|
||||||
itemTransferListBinding.progressBarTransferringFile.visibility = View.GONE
|
|
||||||
when (item.fileStatus) {
|
|
||||||
SENDING -> itemTransferListBinding.progressBarTransferringFile.visibility = View.VISIBLE
|
|
||||||
SENT -> {
|
|
||||||
itemTransferListBinding.imageViewFileTransferred.setImageResource(
|
|
||||||
R.drawable.ic_baseline_check_24px
|
|
||||||
)
|
|
||||||
itemTransferListBinding.progressBarTransferringFile.visibility = View.GONE
|
|
||||||
}
|
|
||||||
ERROR -> {
|
|
||||||
itemTransferListBinding.imageViewFileTransferred.setImageResource(
|
|
||||||
R.drawable.ic_baseline_error_24px
|
|
||||||
)
|
|
||||||
itemTransferListBinding.progressBarTransferringFile.visibility = View.GONE
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,52 +33,40 @@ import android.net.wifi.p2p.WifiP2pDevice
|
|||||||
import android.net.wifi.p2p.WifiP2pDeviceList
|
import android.net.wifi.p2p.WifiP2pDeviceList
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.MenuHost
|
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import org.kiwix.kiwixmobile.R
|
import org.kiwix.kiwixmobile.R
|
||||||
import org.kiwix.kiwixmobile.cachedComponent
|
import org.kiwix.kiwixmobile.cachedComponent
|
||||||
import org.kiwix.kiwixmobile.core.R.dimen
|
|
||||||
import org.kiwix.kiwixmobile.core.R.drawable
|
import org.kiwix.kiwixmobile.core.R.drawable
|
||||||
import org.kiwix.kiwixmobile.core.R.string
|
import org.kiwix.kiwixmobile.core.R.string
|
||||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||||
import org.kiwix.kiwixmobile.core.base.BaseFragment
|
import org.kiwix.kiwixmobile.core.base.BaseFragment
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isLandScapeMode
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isTablet
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.popNavigationBackstack
|
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.popNavigationBackstack
|
||||||
import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.toast
|
import org.kiwix.kiwixmobile.core.extensions.toast
|
||||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||||
import org.kiwix.kiwixmobile.core.navigateToAppSettings
|
import org.kiwix.kiwixmobile.core.navigateToAppSettings
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.models.IconItem.Vector
|
||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
|
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
|
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||||
import org.kiwix.kiwixmobile.databinding.FragmentLocalFileTransferBinding
|
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.WifiDirectManager.Companion.getDeviceStatus
|
import org.kiwix.kiwixmobile.localFileTransfer.WifiDirectManager.Companion.getDeviceStatus
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.adapter.WifiP2pDelegate
|
|
||||||
import org.kiwix.kiwixmobile.localFileTransfer.adapter.WifiPeerListAdapter
|
|
||||||
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence
|
|
||||||
import uk.co.deanwild.materialshowcaseview.ShowcaseConfig
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,7 +84,6 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const val URIS_KEY = "uris"
|
const val URIS_KEY = "uris"
|
||||||
const val SHOWCASE_ID = "MaterialShowcaseId"
|
|
||||||
|
|
||||||
@SuppressLint("GoogleAppIndexingApiWarning", "Registered")
|
@SuppressLint("GoogleAppIndexingApiWarning", "Registered")
|
||||||
class LocalFileTransferFragment :
|
class LocalFileTransferFragment :
|
||||||
@ -114,147 +101,64 @@ class LocalFileTransferFragment :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var sharedPreferenceUtil: SharedPreferenceUtil
|
lateinit var sharedPreferenceUtil: SharedPreferenceUtil
|
||||||
|
|
||||||
private var fileListAdapter: FileListAdapter? = null
|
private val deviceName = mutableStateOf("")
|
||||||
private var wifiPeerListAdapter: WifiPeerListAdapter? = null
|
private val isPeerSearching = mutableStateOf(false)
|
||||||
private var fragmentLocalFileTransferBinding: FragmentLocalFileTransferBinding? = null
|
private val peerDeviceList = mutableStateOf(emptyList<WifiP2pDevice>())
|
||||||
private var materialShowCaseSequence: MaterialShowcaseSequence? = null
|
private val transferFileList = mutableStateOf(emptyList<FileItem>())
|
||||||
private var searchIconView: View? = null
|
private var composeView: ComposeView? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? = ComposeView(requireContext()).also {
|
||||||
fragmentLocalFileTransferBinding =
|
composeView = it
|
||||||
FragmentLocalFileTransferBinding.inflate(inflater, container, false)
|
|
||||||
return fragmentLocalFileTransferBinding?.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupMenu()
|
|
||||||
val activity = requireActivity() as CoreMainActivity
|
val activity = requireActivity() as CoreMainActivity
|
||||||
val filesForTransfer = getFilesForTransfer()
|
val filesForTransfer = getFilesForTransfer()
|
||||||
val isReceiver = filesForTransfer.isEmpty()
|
val isReceiver = filesForTransfer.isEmpty()
|
||||||
setupToolbar(view, activity, isReceiver)
|
|
||||||
|
|
||||||
wifiPeerListAdapter = WifiPeerListAdapter(WifiP2pDelegate(wifiDirectManager::sendToDevice))
|
composeView?.setContent {
|
||||||
|
LocalFileTransferScreen(
|
||||||
setupPeerDevicesList(activity)
|
deviceName = deviceName.value,
|
||||||
|
toolbarTitle = if (isReceiver) {
|
||||||
|
R.string.receive_files_title
|
||||||
|
} else {
|
||||||
|
R.string.send_files_title
|
||||||
|
},
|
||||||
|
isPeerSearching = isPeerSearching.value,
|
||||||
|
peerDeviceList = peerDeviceList.value,
|
||||||
|
transferFileList = transferFileList.value,
|
||||||
|
actionMenuItems = actionMenuItem(),
|
||||||
|
onDeviceItemClick = { wifiDirectManager.sendToDevice(it) },
|
||||||
|
sharedPreferenceUtil = sharedPreferenceUtil,
|
||||||
|
navigationIcon = {
|
||||||
|
NavigationIcon(
|
||||||
|
iconItem = IconItem.Drawable(drawable.ic_close_white_24dp),
|
||||||
|
onClick = { activity.popNavigationBackstack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
displayFileTransferProgress(filesForTransfer)
|
displayFileTransferProgress(filesForTransfer)
|
||||||
|
|
||||||
wifiDirectManager.callbacks = this
|
wifiDirectManager.callbacks = this
|
||||||
wifiDirectManager.lifecycleCoroutineScope = lifecycleScope
|
wifiDirectManager.lifecycleCoroutineScope = lifecycleScope
|
||||||
wifiDirectManager.startWifiDirectManager(filesForTransfer)
|
wifiDirectManager.startWifiDirectManager(filesForTransfer)
|
||||||
fragmentLocalFileTransferBinding?.apply {
|
|
||||||
textViewDeviceName.setToolTipWithContentDescription(getString(string.your_device))
|
|
||||||
fileTransferShowCaseView.apply {
|
|
||||||
val fileTransferShowViewParams = layoutParams
|
|
||||||
fileTransferShowViewParams.width = getShowCaseViewWidth()
|
|
||||||
fileTransferShowViewParams.height = getShowCaseViewHeight()
|
|
||||||
layoutParams = fileTransferShowViewParams
|
|
||||||
}
|
|
||||||
nearbyDeviceShowCaseView.apply {
|
|
||||||
val nearbyDeviceShowCaseViewParams = layoutParams
|
|
||||||
nearbyDeviceShowCaseViewParams.width = getShowCaseViewWidth()
|
|
||||||
nearbyDeviceShowCaseViewParams.height = getShowCaseViewHeight()
|
|
||||||
layoutParams = nearbyDeviceShowCaseViewParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getShowCaseViewWidth(): Int {
|
private fun actionMenuItem() = listOf(
|
||||||
return when {
|
ActionMenuItem(
|
||||||
requireActivity().isTablet() -> {
|
Vector(Icons.Default.Search),
|
||||||
requireActivity().resources.getDimensionPixelSize(dimen.maximum_donation_popup_width)
|
string.search_label,
|
||||||
}
|
{ onSearchMenuClicked() },
|
||||||
|
testingTag = SEARCH_ICON_TESTING_TAG
|
||||||
requireActivity().isLandScapeMode() -> {
|
|
||||||
requireActivity().resources.getDimensionPixelSize(
|
|
||||||
dimen.showcase_view_maximum_width_in_landscape_mode
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
else -> FrameLayout.LayoutParams.MATCH_PARENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getShowCaseViewHeight(): Int =
|
|
||||||
requireActivity()
|
|
||||||
.resources
|
|
||||||
.getDimensionPixelSize(dimen.showcase_view_maximum_height)
|
|
||||||
|
|
||||||
private fun setupMenu() {
|
|
||||||
(requireActivity() as MenuHost).addMenuProvider(
|
|
||||||
object : MenuProvider {
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.wifi_file_share_items, menu)
|
|
||||||
if (sharedPreferenceUtil.prefShowShowCaseToUser) {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
searchIconView =
|
|
||||||
fragmentLocalFileTransferBinding?.root?.findViewById(R.id.menu_item_search_devices)
|
|
||||||
showCaseFeatureToUsers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
|
||||||
if (menuItem.itemId == R.id.menu_item_search_devices) {
|
|
||||||
// Permissions essential for this module
|
|
||||||
return onSearchMenuClicked()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewLifecycleOwner,
|
|
||||||
Lifecycle.State.RESUMED
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCaseFeatureToUsers() {
|
|
||||||
searchIconView?.let {
|
|
||||||
materialShowCaseSequence =
|
|
||||||
MaterialShowcaseSequence(activity, SHOWCASE_ID).apply {
|
|
||||||
val config =
|
|
||||||
ShowcaseConfig().apply {
|
|
||||||
// half second between each showcase view
|
|
||||||
delay = 500
|
|
||||||
}
|
|
||||||
setConfig(config)
|
|
||||||
addSequenceItem(
|
|
||||||
it,
|
|
||||||
getString(string.click_nearby_devices_message),
|
|
||||||
getString(string.got_it)
|
|
||||||
)
|
|
||||||
addSequenceItem(
|
|
||||||
fragmentLocalFileTransferBinding?.textViewDeviceName,
|
|
||||||
getString(string.your_device_name_message),
|
|
||||||
getString(string.got_it)
|
|
||||||
)
|
|
||||||
addSequenceItem(
|
|
||||||
fragmentLocalFileTransferBinding?.nearbyDeviceShowCaseView,
|
|
||||||
getString(string.nearby_devices_list_message),
|
|
||||||
getString(string.got_it)
|
|
||||||
)
|
|
||||||
addSequenceItem(
|
|
||||||
fragmentLocalFileTransferBinding?.fileTransferShowCaseView,
|
|
||||||
getString(string.transfer_zim_files_list_message),
|
|
||||||
getString(string.got_it)
|
|
||||||
)
|
|
||||||
setOnItemDismissedListener { showcaseView, _ ->
|
|
||||||
// To fix the memory leak by setting setTarget to null
|
|
||||||
// because the memory leak occurred inside the library.
|
|
||||||
// They had forgotten to detach the view after its successful use,
|
|
||||||
// so it holds the reference of these views in memory.
|
|
||||||
// By setting these views as null we remove the reference from
|
|
||||||
// the memory after they are successfully shown.
|
|
||||||
showcaseView.setTarget(null)
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSearchMenuClicked(): Boolean =
|
private fun onSearchMenuClicked(): Boolean =
|
||||||
when {
|
when {
|
||||||
@ -281,53 +185,24 @@ class LocalFileTransferFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPeerDevicesList(activity: CoreMainActivity) {
|
|
||||||
fragmentLocalFileTransferBinding?.listPeerDevices?.apply {
|
|
||||||
adapter = wifiPeerListAdapter
|
|
||||||
layoutManager = LinearLayoutManager(activity)
|
|
||||||
setHasFixedSize(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupToolbar(view: View, activity: CoreMainActivity, isReceiver: Boolean) {
|
|
||||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
|
||||||
toolbar.apply {
|
|
||||||
activity.setSupportActionBar(this)
|
|
||||||
title =
|
|
||||||
if (isReceiver) {
|
|
||||||
getString(R.string.receive_files_title)
|
|
||||||
} else {
|
|
||||||
getString(R.string.send_files_title)
|
|
||||||
}
|
|
||||||
setNavigationIcon(drawable.ic_close_white_24dp)
|
|
||||||
// set the contentDescription to navigation back button
|
|
||||||
getToolbarNavigationIcon()?.setToolTipWithContentDescription(
|
|
||||||
getString(string.toolbar_back_button_content_description)
|
|
||||||
)
|
|
||||||
setNavigationOnClickListener { activity.popNavigationBackstack() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFilesForTransfer() =
|
private fun getFilesForTransfer() =
|
||||||
LocalFileTransferFragmentArgs.fromBundle(requireArguments()).uris?.map(::FileItem).orEmpty()
|
LocalFileTransferFragmentArgs.fromBundle(requireArguments()).uris?.map(::FileItem).orEmpty()
|
||||||
|
|
||||||
private fun showPeerDiscoveryProgressBar() { // Setup UI for searching peers
|
private fun showPeerDiscoveryProgressBar() { // Setup UI for searching peers
|
||||||
fragmentLocalFileTransferBinding?.progressBarSearchingPeers?.visibility = View.VISIBLE
|
isPeerSearching.value = true
|
||||||
fragmentLocalFileTransferBinding?.listPeerDevices?.visibility = View.INVISIBLE
|
|
||||||
fragmentLocalFileTransferBinding?.textViewEmptyPeerList?.visibility = View.INVISIBLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// From WifiDirectManager.Callbacks interface
|
// From WifiDirectManager.Callbacks interface
|
||||||
override fun onUserDeviceDetailsAvailable(userDevice: WifiP2pDevice?) {
|
override fun onUserDeviceDetailsAvailable(userDevice: WifiP2pDevice?) {
|
||||||
// Update UI with user device's details
|
// Update UI with user device's details
|
||||||
if (userDevice != null) {
|
if (userDevice != null) {
|
||||||
fragmentLocalFileTransferBinding?.textViewDeviceName?.text = userDevice.deviceName
|
deviceName.value = userDevice.deviceName
|
||||||
Log.d(TAG, getDeviceStatus(userDevice.status))
|
Log.d(TAG, getDeviceStatus(userDevice.status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConnectionToPeersLost() {
|
override fun onConnectionToPeersLost() {
|
||||||
wifiPeerListAdapter?.items = emptyList()
|
peerDeviceList.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>) {
|
override fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>) {
|
||||||
@ -335,23 +210,20 @@ class LocalFileTransferFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun displayFileTransferProgress(filesToSend: List<FileItem>) {
|
private fun displayFileTransferProgress(filesToSend: List<FileItem>) {
|
||||||
fileListAdapter = FileListAdapter(filesToSend)
|
transferFileList.value = filesToSend
|
||||||
fragmentLocalFileTransferBinding?.recyclerViewTransferFiles?.apply {
|
|
||||||
adapter = fileListAdapter
|
|
||||||
layoutManager =
|
|
||||||
LinearLayoutManager(requireActivity())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFileStatusChanged(itemIndex: Int) {
|
override fun onFileStatusChanged(itemIndex: Int, fileStatus: FileItem.FileStatus) {
|
||||||
fileListAdapter?.notifyItemChanged(itemIndex)
|
val tempTransferList = transferFileList.value
|
||||||
|
tempTransferList[itemIndex].fileStatus = fileStatus
|
||||||
|
transferFileList.value = emptyList()
|
||||||
|
transferFileList.value = tempTransferList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateListOfAvailablePeers(peers: WifiP2pDeviceList) {
|
override fun updateListOfAvailablePeers(peers: WifiP2pDeviceList) {
|
||||||
val deviceList: List<WifiP2pDevice> = ArrayList<WifiP2pDevice>(peers.deviceList)
|
val deviceList: List<WifiP2pDevice> = ArrayList<WifiP2pDevice>(peers.deviceList)
|
||||||
fragmentLocalFileTransferBinding?.progressBarSearchingPeers?.visibility = View.GONE
|
isPeerSearching.value = false
|
||||||
fragmentLocalFileTransferBinding?.listPeerDevices?.visibility = View.VISIBLE
|
peerDeviceList.value = deviceList
|
||||||
wifiPeerListAdapter?.items = deviceList
|
|
||||||
if (deviceList.isEmpty()) {
|
if (deviceList.isEmpty()) {
|
||||||
Log.d(TAG, "No devices found")
|
Log.d(TAG, "No devices found")
|
||||||
}
|
}
|
||||||
@ -533,10 +405,6 @@ class LocalFileTransferFragment :
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
wifiDirectManager.stopWifiDirectManager()
|
wifiDirectManager.stopWifiDirectManager()
|
||||||
wifiDirectManager.callbacks = null
|
wifiDirectManager.callbacks = null
|
||||||
fragmentLocalFileTransferBinding?.root?.removeAllViews()
|
|
||||||
fragmentLocalFileTransferBinding = null
|
|
||||||
searchIconView = null
|
|
||||||
materialShowCaseSequence = null
|
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,371 @@
|
|||||||
|
/*
|
||||||
|
* Kiwix Android
|
||||||
|
* Copyright (c) 2025 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.localFileTransfer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.wifi.p2p.WifiP2pDevice
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import org.kiwix.kiwixmobile.R.drawable
|
||||||
|
import org.kiwix.kiwixmobile.core.R
|
||||||
|
import org.kiwix.kiwixmobile.core.R.string
|
||||||
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.KiwixShowCaseView
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.components.ShowcaseProperty
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.theme.DodgerBlue
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.DEFAULT_TEXT_ALPHA
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIFTEEN_DP
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_FOR_TRANSFER_SHOW_CASE_VIEW_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_FOR_TRANSFER_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_ITEM_ICON_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_ITEM_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICES_SHOW_CASE_VIEW_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICES_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICE_LIST_HEIGHT
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NO_DEVICE_FOUND_TEXT_PADDING
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONE_DP
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PEER_DEVICE_ITEM_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TEN_DP
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.YOUR_DEVICE_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||||
|
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
|
||||||
|
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.ERROR
|
||||||
|
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENDING
|
||||||
|
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENT
|
||||||
|
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.TO_BE_SENT
|
||||||
|
|
||||||
|
const val YOUR_DEVICE_SHOW_CASE_TAG = "yourDeviceShowCaseTag"
|
||||||
|
const val PEER_DEVICE_LIST_SHOW_CASE_TAG = "peerDeviceListShowCaseTag"
|
||||||
|
const val FILE_FOR_TRANSFER_SHOW_CASE_TAG = "fileForTransferShowCaseTag"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Suppress("ComposableLambdaParameterNaming", "LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun LocalFileTransferScreen(
|
||||||
|
deviceName: String,
|
||||||
|
@StringRes toolbarTitle: Int,
|
||||||
|
isPeerSearching: Boolean,
|
||||||
|
peerDeviceList: List<WifiP2pDevice>,
|
||||||
|
transferFileList: List<FileItem>,
|
||||||
|
actionMenuItems: List<ActionMenuItem>,
|
||||||
|
onDeviceItemClick: (WifiP2pDevice) -> Unit,
|
||||||
|
sharedPreferenceUtil: SharedPreferenceUtil,
|
||||||
|
navigationIcon: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val targets = remember { mutableStateMapOf<String, ShowcaseProperty>() }
|
||||||
|
val context = LocalContext.current
|
||||||
|
KiwixTheme {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
KiwixAppBar(
|
||||||
|
titleId = toolbarTitle,
|
||||||
|
actionMenuItems = actionMenuItems.map {
|
||||||
|
it.copy(
|
||||||
|
modifier =
|
||||||
|
Modifier.onGloballyPositioned { coordinates ->
|
||||||
|
targets[SEARCH_ICON_TESTING_TAG] = ShowcaseProperty(
|
||||||
|
index = ZERO,
|
||||||
|
coordinates = coordinates,
|
||||||
|
showCaseMessage = context.getString(string.click_nearby_devices_message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = navigationIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
YourDeviceHeader(deviceName, context, targets)
|
||||||
|
HorizontalDivider(
|
||||||
|
color = DodgerBlue,
|
||||||
|
thickness = ONE_DP,
|
||||||
|
modifier = Modifier.padding(horizontal = FIVE_DP)
|
||||||
|
)
|
||||||
|
NearbyDevicesSection(peerDeviceList, isPeerSearching, onDeviceItemClick, context, targets)
|
||||||
|
HorizontalDivider(
|
||||||
|
color = DodgerBlue,
|
||||||
|
thickness = ONE_DP,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = FIVE_DP)
|
||||||
|
)
|
||||||
|
TransferFilesSection(transferFileList, context, targets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShowShowCaseToUserIfNotShown(targets, sharedPreferenceUtil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShowShowCaseToUserIfNotShown(
|
||||||
|
targets: SnapshotStateMap<String, ShowcaseProperty>,
|
||||||
|
sharedPreferenceUtil: SharedPreferenceUtil
|
||||||
|
) {
|
||||||
|
if (sharedPreferenceUtil.prefShowShowCaseToUser) {
|
||||||
|
KiwixShowCaseView(targets = targets) {
|
||||||
|
sharedPreferenceUtil.showCaseViewForFileTransferShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NearbyDevicesSection(
|
||||||
|
peerDeviceList: List<WifiP2pDevice>,
|
||||||
|
isPeerSearching: Boolean,
|
||||||
|
onDeviceItemClick: (WifiP2pDevice) -> Unit,
|
||||||
|
context: Context,
|
||||||
|
targets: SnapshotStateMap<String, ShowcaseProperty>
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.defaultMinSize(minHeight = NEARBY_DEVICE_LIST_HEIGHT)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.nearby_devices),
|
||||||
|
fontSize = NEARBY_DEVICES_TEXT_SIZE,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = FIVE_DP)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
isPeerSearching -> ContentLoadingProgressBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(NO_DEVICE_FOUND_TEXT_PADDING)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
peerDeviceList.isEmpty() -> Text(
|
||||||
|
text = stringResource(R.string.no_devices_found),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(NO_DEVICE_FOUND_TEXT_PADDING)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
targets[PEER_DEVICE_LIST_SHOW_CASE_TAG] = ShowcaseProperty(
|
||||||
|
index = 2,
|
||||||
|
coordinates = coordinates,
|
||||||
|
showCaseMessage = context.getString(string.nearby_devices_list_message),
|
||||||
|
customSizeForShowcaseViewCircle = NEARBY_DEVICES_SHOW_CASE_VIEW_SIZE
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.defaultMinSize(minHeight = NEARBY_DEVICE_LIST_HEIGHT)
|
||||||
|
) {
|
||||||
|
items(peerDeviceList) { device ->
|
||||||
|
PeerDeviceItem(device, onDeviceItemClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TransferFilesSection(
|
||||||
|
transferFileList: List<FileItem>,
|
||||||
|
context: Context,
|
||||||
|
targets: SnapshotStateMap<String, ShowcaseProperty>
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.files_for_transfer),
|
||||||
|
fontSize = FILE_FOR_TRANSFER_TEXT_SIZE,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = TEN_DP)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
targets[FILE_FOR_TRANSFER_SHOW_CASE_TAG] = ShowcaseProperty(
|
||||||
|
index = 3,
|
||||||
|
coordinates = coordinates,
|
||||||
|
showCaseMessage = context.getString(string.transfer_zim_files_list_message),
|
||||||
|
customSizeForShowcaseViewCircle = FILE_FOR_TRANSFER_SHOW_CASE_VIEW_SIZE
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(transferFileList) { file ->
|
||||||
|
TransferFileItem(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun YourDeviceHeader(
|
||||||
|
deviceName: String,
|
||||||
|
context: Context,
|
||||||
|
targets: SnapshotStateMap<String, ShowcaseProperty>
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(horizontal = FIFTEEN_DP, vertical = FIVE_DP)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.your_device),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
fontSize = YOUR_DEVICE_TEXT_SIZE,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = FIVE_DP, bottom = ONE_DP)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
targets[YOUR_DEVICE_SHOW_CASE_TAG] = ShowcaseProperty(
|
||||||
|
index = 1,
|
||||||
|
coordinates = coordinates,
|
||||||
|
showCaseMessage = context.getString(string.your_device_name_message)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
val contentDescription = stringResource(R.string.device_name)
|
||||||
|
Text(
|
||||||
|
text = deviceName,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = PEER_DEVICE_ITEM_TEXT_SIZE,
|
||||||
|
modifier = Modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.semantics { this.contentDescription = contentDescription },
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TransferFileItem(
|
||||||
|
fileItem: FileItem
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(TEN_DP),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = fileItem.fileName,
|
||||||
|
fontSize = FILE_ITEM_TEXT_SIZE,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = FIVE_DP, vertical = ONE_DP),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
|
||||||
|
val modifier = Modifier
|
||||||
|
.size(FILE_ITEM_ICON_SIZE)
|
||||||
|
.padding(horizontal = FIVE_DP, vertical = ONE_DP)
|
||||||
|
when (fileItem.fileStatus) {
|
||||||
|
SENDING -> ContentLoadingProgressBar(modifier)
|
||||||
|
|
||||||
|
TO_BE_SENT,
|
||||||
|
SENT,
|
||||||
|
ERROR -> {
|
||||||
|
val iconRes = when (fileItem.fileStatus) {
|
||||||
|
FileItem.FileStatus.TO_BE_SENT -> drawable.ic_baseline_wait_24px
|
||||||
|
FileItem.FileStatus.SENT -> drawable.ic_baseline_check_24px
|
||||||
|
FileItem.FileStatus.ERROR -> drawable.ic_baseline_error_24px
|
||||||
|
else -> error("Unhandled status: ${fileItem.fileStatus}")
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(iconRes),
|
||||||
|
contentDescription = stringResource(R.string.status),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
@Composable
|
||||||
|
fun PeerDeviceItem(
|
||||||
|
wifiP2PDevice: WifiP2pDevice,
|
||||||
|
onDeviceItemClick: (WifiP2pDevice) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(TEN_DP)
|
||||||
|
.clickable(onClick = { onDeviceItemClick.invoke(wifiP2PDevice) }),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = wifiP2PDevice.deviceName,
|
||||||
|
fontSize = PEER_DEVICE_ITEM_TEXT_SIZE,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(3f)
|
||||||
|
.padding(horizontal = FIVE_DP, vertical = ONE_DP),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DEFAULT_TEXT_ALPHA)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -281,7 +281,7 @@ class WifiDirectManager @Inject constructor(
|
|||||||
|
|
||||||
fun changeStatus(itemIndex: Int, status: FileStatus) {
|
fun changeStatus(itemIndex: Int, status: FileStatus) {
|
||||||
filesForTransfer[itemIndex].fileStatus = status
|
filesForTransfer[itemIndex].fileStatus = status
|
||||||
callbacks?.onFileStatusChanged(itemIndex)
|
callbacks?.onFileStatusChanged(itemIndex, status)
|
||||||
if (status == FileStatus.ERROR) {
|
if (status == FileStatus.ERROR) {
|
||||||
context.toast(
|
context.toast(
|
||||||
context.getString(R.string.error_transferring, filesForTransfer[itemIndex].fileName)
|
context.getString(R.string.error_transferring, filesForTransfer[itemIndex].fileName)
|
||||||
@ -344,7 +344,7 @@ class WifiDirectManager @Inject constructor(
|
|||||||
fun onConnectionToPeersLost()
|
fun onConnectionToPeersLost()
|
||||||
fun updateListOfAvailablePeers(peers: WifiP2pDeviceList)
|
fun updateListOfAvailablePeers(peers: WifiP2pDeviceList)
|
||||||
fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>)
|
fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>)
|
||||||
fun onFileStatusChanged(itemIndex: Int)
|
fun onFileStatusChanged(itemIndex: Int, fileStatus: FileStatus)
|
||||||
fun onFileTransferComplete()
|
fun onFileTransferComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kiwix.kiwixmobile.localFileTransfer.adapter
|
|
||||||
|
|
||||||
import android.net.wifi.p2p.WifiP2pDevice
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
|
||||||
import org.kiwix.kiwixmobile.core.base.adapter.AdapterDelegate
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.viewBinding
|
|
||||||
import org.kiwix.kiwixmobile.databinding.RowPeerDeviceBinding
|
|
||||||
|
|
||||||
class WifiP2pDelegate(private val onItemClickAction: (WifiP2pDevice) -> Unit) :
|
|
||||||
AdapterDelegate<WifiP2pDevice> {
|
|
||||||
override fun createViewHolder(parent: ViewGroup): ViewHolder =
|
|
||||||
WifiP2pViewHolder(
|
|
||||||
parent.viewBinding(RowPeerDeviceBinding::inflate, false),
|
|
||||||
onItemClickAction
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun bind(viewHolder: ViewHolder, itemToBind: WifiP2pDevice) {
|
|
||||||
(viewHolder as WifiP2pViewHolder).bind(itemToBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isFor(item: WifiP2pDevice) = true
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kiwix.kiwixmobile.localFileTransfer.adapter
|
|
||||||
|
|
||||||
import android.net.wifi.p2p.WifiP2pDevice
|
|
||||||
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
|
|
||||||
import org.kiwix.kiwixmobile.databinding.RowPeerDeviceBinding
|
|
||||||
|
|
||||||
class WifiP2pViewHolder(
|
|
||||||
private val rowPeerDeviceBinding: RowPeerDeviceBinding,
|
|
||||||
private val onItemClickAction: (WifiP2pDevice) -> Unit
|
|
||||||
) : BaseViewHolder<WifiP2pDevice>(rowPeerDeviceBinding.root) {
|
|
||||||
override fun bind(item: WifiP2pDevice) {
|
|
||||||
rowPeerDeviceBinding.rowDeviceName.text = item.deviceName
|
|
||||||
containerView.setOnClickListener {
|
|
||||||
onItemClickAction.invoke(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package org.kiwix.kiwixmobile.localFileTransfer.adapter
|
|
||||||
|
|
||||||
import android.net.wifi.p2p.WifiP2pDevice
|
|
||||||
import org.kiwix.kiwixmobile.core.base.adapter.BaseDelegateAdapter
|
|
||||||
|
|
||||||
internal class WifiPeerListAdapter(wifiP2pDelegate: WifiP2pDelegate) :
|
|
||||||
BaseDelegateAdapter<WifiP2pDevice>(wifiP2pDelegate) {
|
|
||||||
override fun getIdFor(item: WifiP2pDevice) = item.deviceAddress.hashCode().toLong()
|
|
||||||
}
|
|
@ -184,7 +184,8 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
|||||||
): View? {
|
): View? {
|
||||||
fragmentDestinationDownloadBinding =
|
fragmentDestinationDownloadBinding =
|
||||||
FragmentDestinationDownloadBinding.inflate(inflater, container, false)
|
FragmentDestinationDownloadBinding.inflate(inflater, container, false)
|
||||||
val toolbar = fragmentDestinationDownloadBinding?.root?.findViewById<Toolbar>(R.id.toolbar)
|
val toolbar =
|
||||||
|
fragmentDestinationDownloadBinding?.root?.findViewById<Toolbar>(org.kiwix.kiwixmobile.core.R.id.toolbar)
|
||||||
val activity = activity as CoreMainActivity
|
val activity = activity as CoreMainActivity
|
||||||
activity.setSupportActionBar(toolbar)
|
activity.setSupportActionBar(toolbar)
|
||||||
activity.supportActionBar?.apply {
|
activity.supportActionBar?.apply {
|
||||||
|
@ -1,172 +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="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferFragment">
|
|
||||||
|
|
||||||
|
|
||||||
<include layout="@layout/layout_toolbar" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_your_device"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="15dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingTop="5dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:text="@string/your_device"
|
|
||||||
android:textSize="13sp"
|
|
||||||
android:textStyle="italic"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_device_name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="15dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/device_name"
|
|
||||||
android:gravity="start|center"
|
|
||||||
android:minHeight="@dimen/material_minimum_height_and_width"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="5dp"
|
|
||||||
android:textIsSelectable="true"
|
|
||||||
android:textSize="17sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_your_device"
|
|
||||||
tools:hint="@string/device_name" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/view_device_list_boundary"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginStart="5dp"
|
|
||||||
android:layout_marginEnd="5dp"
|
|
||||||
android:background="@color/dodger_blue"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_device_name" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_available_device"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingTop="5dp"
|
|
||||||
android:text="@string/nearby_devices"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:fontFamily="monospace"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/view_device_list_boundary" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/list_peer_devices"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_available_device"
|
|
||||||
app:layout_constraintVertical_bias="0.0" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_empty_peer_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="50dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="@string/no_devices_found"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_available_device" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/nearby_device_show_case_view"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:layout_margin="50dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_available_device" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progress_bar_searching_peers"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="50dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_available_device" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/view_file_list_boundary"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginStart="5dp"
|
|
||||||
android:layout_marginTop="201dp"
|
|
||||||
android:layout_marginEnd="5dp"
|
|
||||||
android:background="@color/dodger_blue"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/text_view_files_for_transfer"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_available_device" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_files_for_transfer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:text="@string/files_for_transfer"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:fontFamily="monospace"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/recycler_view_transfer_files"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/view_file_list_boundary" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/file_transfer_show_case_view"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:layout_margin="50dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_files_for_transfer" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler_view_transfer_files"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:contentDescription="@string/files_for_transfer"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_files_for_transfer"
|
|
||||||
tools:listitem="@layout/item_transfer_list" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,66 +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="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minHeight="64dp">
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
android:id="@+id/item_language_checkbox"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/activity_horizontal_margin"
|
|
||||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/item_language_name"
|
|
||||||
style="@style/list_item_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/activity_horizontal_margin"
|
|
||||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
|
||||||
android:alpha="0.90"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/item_language_localized_name"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/item_language_checkbox"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
|
||||||
tools:text="English" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/item_language_localized_name"
|
|
||||||
style="@style/list_item_body"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:alpha="0.63"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/item_language_name"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/item_language_name"
|
|
||||||
tools:text="English" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/item_language_books_count"
|
|
||||||
style="@style/list_item_body"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/activity_horizontal_margin"
|
|
||||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
|
||||||
android:alpha="0.63"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="9 books" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/item_language_clickable_area"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="?selectableItemBackground"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,61 +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="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="10dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_file_item_name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:textIsSelectable="true"
|
|
||||||
android:textSize="14sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:hint="File name" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progress_bar_transferring_file"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="1.0"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/text_view_file_item_name"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_view_file_transferred"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/status"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:src="@drawable/ic_baseline_wait_24px"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="1.0"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/text_view_file_item_name"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="10dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/row_device_name"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="3"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
android:textSize="17sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
tools:hint="Device Name" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -337,12 +337,6 @@ object Libs {
|
|||||||
*/
|
*/
|
||||||
const val junit: String = "androidx.test.ext:junit:" + Versions.junit
|
const val junit: String = "androidx.test.ext:junit:" + Versions.junit
|
||||||
|
|
||||||
/**
|
|
||||||
* https://github.com/deano2390/MaterialShowcaseView
|
|
||||||
*/
|
|
||||||
const val material_show_case_view: String =
|
|
||||||
"com.github.deano2390:MaterialShowcaseView:" + Versions.material_show_case_view
|
|
||||||
|
|
||||||
const val roomKtx = "androidx.room:room-ktx:" + Versions.roomVersion
|
const val roomKtx = "androidx.room:room-ktx:" + Versions.roomVersion
|
||||||
|
|
||||||
const val roomCompiler = "androidx.room:room-compiler:" + Versions.roomVersion
|
const val roomCompiler = "androidx.room:room-compiler:" + Versions.roomVersion
|
||||||
|
@ -106,8 +106,6 @@ object Versions {
|
|||||||
|
|
||||||
const val junit: String = "1.1.5"
|
const val junit: String = "1.1.5"
|
||||||
|
|
||||||
const val material_show_case_view: String = "1.3.7"
|
|
||||||
|
|
||||||
const val roomVersion = "2.5.2"
|
const val roomVersion = "2.5.2"
|
||||||
|
|
||||||
const val zxing = "3.5.3"
|
const val zxing = "3.5.3"
|
||||||
|
@ -231,7 +231,6 @@ class AllProjectConfigurer {
|
|||||||
implementation(Libs.rxandroid)
|
implementation(Libs.rxandroid)
|
||||||
implementation(Libs.rxjava)
|
implementation(Libs.rxjava)
|
||||||
implementation(Libs.preference_ktx)
|
implementation(Libs.preference_ktx)
|
||||||
implementation(Libs.material_show_case_view)
|
|
||||||
implementation(Libs.roomKtx)
|
implementation(Libs.roomKtx)
|
||||||
annotationProcessor(Libs.roomCompiler)
|
annotationProcessor(Libs.roomCompiler)
|
||||||
implementation(Libs.roomRuntime)
|
implementation(Libs.roomRuntime)
|
||||||
|
@ -53,7 +53,7 @@ import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
|||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val SEARCH_ICON_TESTING_TAG = "search"
|
const val SEARCH_ICON_TESTING_TAG = "searchIconTestingTag"
|
||||||
const val DELETE_MENU_ICON_TESTING_TAG = "deleteMenuIconTestingTag"
|
const val DELETE_MENU_ICON_TESTING_TAG = "deleteMenuIconTestingTag"
|
||||||
|
|
||||||
abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActivityExtensions {
|
abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActivityExtensions {
|
||||||
|
@ -138,7 +138,7 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
|
|||||||
IconButton(
|
IconButton(
|
||||||
enabled = menuItem.isEnabled,
|
enabled = menuItem.isEnabled,
|
||||||
onClick = menuItem.onClick,
|
onClick = menuItem.onClick,
|
||||||
modifier = Modifier.testTag(menuItem.testingTag)
|
modifier = menuItem.modifier.testTag(menuItem.testingTag)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = menuItem.icon.toPainter(),
|
painter = menuItem.icon.toPainter(),
|
||||||
|
@ -0,0 +1,296 @@
|
|||||||
|
/*
|
||||||
|
* Kiwix Android
|
||||||
|
* Copyright (c) 2025 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.ui.components
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.testTag
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import org.kiwix.kiwixmobile.core.R
|
||||||
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.theme.DodgerBlue
|
||||||
|
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_ALPHA
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_ANIMATION_END
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_ANIMATION_START
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_RADIUS_EXTRA
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_VIEW_MESSAGE_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
const val SHOWCASE_VIEW_ROUND_ANIMATION_DURATION = 2000
|
||||||
|
const val ONE = 1
|
||||||
|
const val TWO = 1
|
||||||
|
const val SIXTEEN = 16
|
||||||
|
|
||||||
|
const val SHOWCASE_VIEW_NEXT_BUTTON_TESTING_TAG = "showcaseViewNextButtonTestingTag"
|
||||||
|
const val SHOWCASE_VIEW_MESSAGE_TESTING_TAG = "showCaseViewMessageTestingTag"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun KiwixShowCaseView(
|
||||||
|
targets: SnapshotStateMap<String, ShowcaseProperty>,
|
||||||
|
onShowCaseCompleted: () -> Unit
|
||||||
|
) {
|
||||||
|
val orderedTargets = targets.values.sortedBy { it.index }
|
||||||
|
var currentIndex by remember { mutableStateOf(ZERO) }
|
||||||
|
val currentTarget = orderedTargets.getOrNull(currentIndex)
|
||||||
|
|
||||||
|
currentTarget?.let {
|
||||||
|
AnimatedShowCase(target = it) {
|
||||||
|
currentIndex++
|
||||||
|
if (currentIndex >= orderedTargets.size) onShowCaseCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AnimatedShowCase(
|
||||||
|
target: ShowcaseProperty,
|
||||||
|
onShowCaseCompleted: () -> Unit
|
||||||
|
) {
|
||||||
|
val targetRect = target.coordinates.boundsInRoot()
|
||||||
|
val innerAnimation = remember { Animatable(PULSE_ANIMATION_START) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val (width, height) = with(density) {
|
||||||
|
val size = target.customSizeForShowcaseViewCircle?.toPx()
|
||||||
|
Pair(size ?: targetRect.width, size ?: targetRect.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
val radiusBase = max(width, height) / TWO.toFloat()
|
||||||
|
val pulseRadius by innerAnimation.asState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
innerAnimation.animateTo(
|
||||||
|
targetValue = PULSE_ANIMATION_END,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(SHOWCASE_VIEW_ROUND_ANIMATION_DURATION, easing = FastOutLinearInEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInput(target) {
|
||||||
|
detectTapGestures {
|
||||||
|
if (targetRect.contains(it)) onShowCaseCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphicsLayer(alpha = PULSE_ALPHA)
|
||||||
|
) {
|
||||||
|
drawOverlay(targetRect, radiusBase, pulseRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowCaseMessage(target, targetRect, radiusBase)
|
||||||
|
NextButton(onShowCaseCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the overlay and animated spotlight.
|
||||||
|
*/
|
||||||
|
private fun DrawScope.drawOverlay(
|
||||||
|
targetRect: Rect,
|
||||||
|
baseRadius: Float,
|
||||||
|
animatedFraction: Float
|
||||||
|
) {
|
||||||
|
drawRect(color = DodgerBlue.copy(alpha = SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA), size = size)
|
||||||
|
drawCircle(
|
||||||
|
color = Color.White,
|
||||||
|
radius = baseRadius * (ONE + animatedFraction),
|
||||||
|
center = targetRect.center,
|
||||||
|
alpha = ONE - animatedFraction
|
||||||
|
)
|
||||||
|
drawCircle(
|
||||||
|
color = Color.White,
|
||||||
|
radius = baseRadius + PULSE_RADIUS_EXTRA,
|
||||||
|
center = targetRect.center,
|
||||||
|
blendMode = BlendMode.Clear
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
|
@Composable
|
||||||
|
private fun ShowCaseMessage(
|
||||||
|
target: ShowcaseProperty,
|
||||||
|
targetRect: Rect,
|
||||||
|
targetRadius: Float
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var offset by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
var calculated by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||||
|
val screenWidth = with(density) { maxWidth.toPx() }
|
||||||
|
val screenHeight = with(density) { maxHeight.toPx() }
|
||||||
|
|
||||||
|
if (calculated) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = target.showCaseMessage,
|
||||||
|
color = target.showCaseMessageColor,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = SHOWCASE_VIEW_MESSAGE_TEXT_SIZE,
|
||||||
|
shadow = Shadow(
|
||||||
|
Color.Black.copy(alpha = SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA),
|
||||||
|
defaultBlurOffsetForMessageAndNextButton(),
|
||||||
|
blurRadius = SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS
|
||||||
|
)
|
||||||
|
),
|
||||||
|
modifier = Modifier.semantics { testTag = SHOWCASE_VIEW_MESSAGE_TESTING_TAG }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = target.showCaseMessage,
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(PULSE_ANIMATION_START)
|
||||||
|
.onGloballyPositioned {
|
||||||
|
val size = it.size
|
||||||
|
val width = size.width.toFloat()
|
||||||
|
val height = size.height.toFloat()
|
||||||
|
val center = targetRect.center
|
||||||
|
|
||||||
|
val posY = when {
|
||||||
|
screenHeight - (center.y + targetRadius) > height + SIXTEEN -> center.y + targetRadius + SIXTEEN
|
||||||
|
center.y - targetRadius > height + SIXTEEN -> center.y - targetRadius - height - SIXTEEN
|
||||||
|
else -> screenHeight / TWO - height / TWO
|
||||||
|
}
|
||||||
|
|
||||||
|
val posX = when {
|
||||||
|
screenWidth - targetRect.right > width + SIXTEEN -> targetRect.right + SIXTEEN
|
||||||
|
targetRect.left > width + SIXTEEN -> targetRect.left - width - SIXTEEN
|
||||||
|
else -> screenWidth / TWO - width / TWO
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = Offset(posX, posY)
|
||||||
|
calculated = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultBlurOffsetForMessageAndNextButton() =
|
||||||
|
Offset(SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA, SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for the "Next" button in the showcase.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun NextButton(onClick: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(SIXTEEN_DP),
|
||||||
|
verticalArrangement = Arrangement.Bottom,
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.semantics { testTag = SHOWCASE_VIEW_NEXT_BUTTON_TESTING_TAG }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = context.getString(R.string.next),
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
fontSize = SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = White,
|
||||||
|
shadow = Shadow(
|
||||||
|
Color.Black.copy(alpha = SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA),
|
||||||
|
defaultBlurOffsetForMessageAndNextButton(),
|
||||||
|
blurRadius = SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single item in the showcase view sequence.
|
||||||
|
*
|
||||||
|
* @param index The order in which this target should be shown in the showcase flow.
|
||||||
|
* @param coordinates Layout coordinates used to determine position and size of the target view on screen.
|
||||||
|
* @param showCaseMessage Message to be displayed near the highlighted target.
|
||||||
|
* @param showCaseMessageColor Optional color for the message text (default is white).
|
||||||
|
* @param blurOpacity Controls the opacity of the background overlay behind the highlight (default is 0.8).
|
||||||
|
* @param customSizeForShowcaseViewCircle Optional custom size for the radius of the highlight circle.
|
||||||
|
* If null, it uses the size of the target's bounds.
|
||||||
|
*/
|
||||||
|
data class ShowcaseProperty(
|
||||||
|
val index: Int,
|
||||||
|
val coordinates: LayoutCoordinates,
|
||||||
|
val showCaseMessage: String,
|
||||||
|
val showCaseMessageColor: Color = Color.White,
|
||||||
|
val blurOpacity: Float = SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA,
|
||||||
|
val customSizeForShowcaseViewCircle: Dp? = null,
|
||||||
|
)
|
@ -19,6 +19,7 @@
|
|||||||
package org.kiwix.kiwixmobile.core.ui.models
|
package org.kiwix.kiwixmobile.core.ui.models
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||||
|
|
||||||
@ -28,5 +29,6 @@ data class ActionMenuItem(
|
|||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
val iconTint: Color = White,
|
val iconTint: Color = White,
|
||||||
val isEnabled: Boolean = true,
|
val isEnabled: Boolean = true,
|
||||||
val testingTag: String
|
val testingTag: String,
|
||||||
|
val modifier: Modifier = Modifier
|
||||||
)
|
)
|
||||||
|
@ -44,6 +44,7 @@ object ComposeDimens {
|
|||||||
val TWENTY_DP = 20.dp
|
val TWENTY_DP = 20.dp
|
||||||
val SEVENTEEN_DP = 17.dp
|
val SEVENTEEN_DP = 17.dp
|
||||||
val SIXTEEN_DP = 16.dp
|
val SIXTEEN_DP = 16.dp
|
||||||
|
val FIFTEEN_DP = 15.dp
|
||||||
val TWELVE_DP = 12.dp
|
val TWELVE_DP = 12.dp
|
||||||
val TEN_DP = 10.dp
|
val TEN_DP = 10.dp
|
||||||
val EIGHT_DP = 8.dp
|
val EIGHT_DP = 8.dp
|
||||||
@ -51,6 +52,7 @@ object ComposeDimens {
|
|||||||
val FIVE_DP = 5.dp
|
val FIVE_DP = 5.dp
|
||||||
val FOUR_DP = 4.dp
|
val FOUR_DP = 4.dp
|
||||||
val TWO_DP = 2.dp
|
val TWO_DP = 2.dp
|
||||||
|
val ONE_DP = 1.dp
|
||||||
val SEVENTY_DP = 70.dp
|
val SEVENTY_DP = 70.dp
|
||||||
val SIXTY_FOUR_DP = 64.dp
|
val SIXTY_FOUR_DP = 64.dp
|
||||||
|
|
||||||
@ -62,6 +64,9 @@ object ComposeDimens {
|
|||||||
// Default letter spacing in text according to theme
|
// Default letter spacing in text according to theme
|
||||||
val DEFAULT_LETTER_SPACING = 0.0333.em
|
val DEFAULT_LETTER_SPACING = 0.0333.em
|
||||||
|
|
||||||
|
// Default Text alpha.
|
||||||
|
const val DEFAULT_TEXT_ALPHA = 0.67f
|
||||||
|
|
||||||
// Shape configuration sizes. See Shape.kt
|
// Shape configuration sizes. See Shape.kt
|
||||||
val EXTRA_SMALL_ROUND_SHAPE_SIZE = 4.dp
|
val EXTRA_SMALL_ROUND_SHAPE_SIZE = 4.dp
|
||||||
val SMALL_ROUND_SHAPE_SIZE = 8.dp
|
val SMALL_ROUND_SHAPE_SIZE = 8.dp
|
||||||
@ -111,4 +116,27 @@ object ComposeDimens {
|
|||||||
val PAGE_LIST_ITEM_FAVICON_SIZE = 40.dp
|
val PAGE_LIST_ITEM_FAVICON_SIZE = 40.dp
|
||||||
val PAGE_SWITCH_LEFT_RIGHT_MARGIN = 10.dp
|
val PAGE_SWITCH_LEFT_RIGHT_MARGIN = 10.dp
|
||||||
val PAGE_SWITCH_ROW_BOTTOM_MARGIN = 8.dp
|
val PAGE_SWITCH_ROW_BOTTOM_MARGIN = 8.dp
|
||||||
|
|
||||||
|
// LocalFileTransferFragment dimens
|
||||||
|
val PEER_DEVICE_ITEM_TEXT_SIZE = 17.sp
|
||||||
|
val FILE_ITEM_TEXT_SIZE = 14.sp
|
||||||
|
val FILE_ITEM_ICON_SIZE = 24.dp
|
||||||
|
val NEARBY_DEVICE_LIST_HEIGHT = 160.dp
|
||||||
|
val NO_DEVICE_FOUND_TEXT_PADDING = 50.dp
|
||||||
|
val YOUR_DEVICE_TEXT_SIZE = 13.sp
|
||||||
|
val FILE_FOR_TRANSFER_TEXT_SIZE = 16.sp
|
||||||
|
val NEARBY_DEVICES_TEXT_SIZE = 16.sp
|
||||||
|
|
||||||
|
// KiwixShowCase view dimens
|
||||||
|
val SHOWCASE_VIEW_MESSAGE_TEXT_SIZE = 17.sp
|
||||||
|
val SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE = 20.sp
|
||||||
|
val FILE_FOR_TRANSFER_SHOW_CASE_VIEW_SIZE = 100.dp
|
||||||
|
val NEARBY_DEVICES_SHOW_CASE_VIEW_SIZE = 100.dp
|
||||||
|
const val SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS = 3f
|
||||||
|
const val SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA = 0.5f
|
||||||
|
const val SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA = 0.8f
|
||||||
|
const val PULSE_ANIMATION_START = 0f
|
||||||
|
const val PULSE_ANIMATION_END = 1f
|
||||||
|
const val PULSE_ALPHA = 0.99f
|
||||||
|
const val PULSE_RADIUS_EXTRA = 20f
|
||||||
}
|
}
|
||||||
|
@ -137,6 +137,10 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
|
|||||||
ContextWrapper(context).externalMediaDirs[0]?.path
|
ContextWrapper(context).externalMediaDirs[0]?.path
|
||||||
?: context.filesDir.path // a workaround for emulators
|
?: context.filesDir.path // a workaround for emulators
|
||||||
|
|
||||||
|
fun showCaseViewForFileTransferShown() {
|
||||||
|
sharedPreferences.edit { putBoolean(PREF_SHOW_SHOWCASE, false) }
|
||||||
|
}
|
||||||
|
|
||||||
fun putPrefBookMarkMigrated(isMigrated: Boolean) =
|
fun putPrefBookMarkMigrated(isMigrated: Boolean) =
|
||||||
sharedPreferences.edit { putBoolean(PREF_BOOKMARKS_MIGRATED, isMigrated) }
|
sharedPreferences.edit { putBoolean(PREF_BOOKMARKS_MIGRATED, isMigrated) }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user