mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -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
|
||||
|
||||
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.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import applyWithViewHierarchyPrinting
|
||||
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
||||
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.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.testFlakyView
|
||||
import uk.co.deanwild.materialshowcaseview.R.id
|
||||
|
||||
/**
|
||||
* Authored by Ayush Shrivastava on 29/10/20
|
||||
@ -45,22 +47,32 @@ fun localFileTransfer(func: LocalFileTransferRobot.() -> Unit) =
|
||||
LocalFileTransferRobot().applyWithViewHierarchyPrinting(func)
|
||||
|
||||
class LocalFileTransferRobot : BaseRobot() {
|
||||
fun assertReceiveFileTitleVisible() {
|
||||
isVisible(TextId(R.string.receive_files_title))
|
||||
fun assertReceiveFileTitleVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG)
|
||||
.assertTextEquals(context.getString(R.string.receive_files_title))
|
||||
}
|
||||
}
|
||||
|
||||
fun assertSearchDeviceMenuItemVisible() {
|
||||
isVisible(ViewId(R.id.menu_item_search_devices))
|
||||
fun assertSearchDeviceMenuItemVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SEARCH_ICON_TESTING_TAG).assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
fun clickOnSearchDeviceMenuItem() {
|
||||
clickOn(ViewId(R.id.menu_item_search_devices))
|
||||
fun clickOnSearchDeviceMenuItem(composeContentTestRule: ComposeContentTestRule) {
|
||||
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())
|
||||
closeEnableWifiP2PDialogIfVisible()
|
||||
assertReceiveFileTitleVisible()
|
||||
assertReceiveFileTitleVisible(composeContentTestRule)
|
||||
}
|
||||
|
||||
private fun closeEnableWifiP2PDialogIfVisible() {
|
||||
@ -69,7 +81,7 @@ class LocalFileTransferRobot : BaseRobot() {
|
||||
onView(withText(string.request_enable_wifi)).check(matches(isDisplayed()))
|
||||
pressBack()
|
||||
})
|
||||
} catch (ignore: Throwable) {
|
||||
} catch (_: Throwable) {
|
||||
Log.i(
|
||||
"LOCAL_FILE_TRANSFER_TEST",
|
||||
"Couldn't found WIFI P2P dialog, probably this is not exist"
|
||||
@ -77,53 +89,66 @@ class LocalFileTransferRobot : BaseRobot() {
|
||||
}
|
||||
}
|
||||
|
||||
fun assertLocalLibraryVisible() {
|
||||
isVisible(TextId(string.library))
|
||||
fun assertLocalLibraryVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG)
|
||||
.assertTextEquals(context.getString(string.library))
|
||||
}
|
||||
}
|
||||
|
||||
fun assertClickNearbyDeviceMessageVisible() {
|
||||
fun assertClickNearbyDeviceMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
pauseForBetterTestPerformance()
|
||||
testFlakyView({
|
||||
onView(withId(id.tv_content))
|
||||
.check(matches(withText(string.click_nearby_devices_message)))
|
||||
})
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||
.assertTextEquals(context.getString(string.click_nearby_devices_message))
|
||||
}
|
||||
}
|
||||
|
||||
fun clickOnGotItButton() {
|
||||
fun clickOnNextButton(composeContentTestRule: ComposeContentTestRule) {
|
||||
pauseForBetterTestPerformance()
|
||||
testFlakyView({
|
||||
onView(withId(id.tv_dismiss))
|
||||
.perform(click())
|
||||
})
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SHOWCASE_VIEW_NEXT_BUTTON_TESTING_TAG)
|
||||
.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun assertDeviceNameMessageVisible() {
|
||||
fun assertDeviceNameMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
pauseForBetterTestPerformance()
|
||||
testFlakyView({
|
||||
onView(withId(id.tv_content))
|
||||
.check(matches(withText(string.your_device_name_message)))
|
||||
})
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||
.assertTextEquals(context.getString(string.your_device_name_message))
|
||||
}
|
||||
}
|
||||
|
||||
fun assertNearbyDeviceListMessageVisible() {
|
||||
fun assertNearbyDeviceListMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
pauseForBetterTestPerformance()
|
||||
testFlakyView({
|
||||
onView(withId(id.tv_content))
|
||||
.check(matches(withText(string.nearby_devices_list_message)))
|
||||
})
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||
.assertTextEquals(context.getString(string.nearby_devices_list_message))
|
||||
}
|
||||
}
|
||||
|
||||
fun assertTransferZimFilesListMessageVisible() {
|
||||
fun assertTransferZimFilesListMessageVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
pauseForBetterTestPerformance()
|
||||
testFlakyView({
|
||||
onView(withId(id.tv_content))
|
||||
.check(matches(withText(string.transfer_zim_files_list_message)))
|
||||
})
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||
.assertTextEquals(context.getString(string.transfer_zim_files_list_message))
|
||||
}
|
||||
}
|
||||
|
||||
fun assertClickNearbyDeviceMessageNotVisible() {
|
||||
fun assertClickNearbyDeviceMessageNotVisible(composeContentTestRule: ComposeContentTestRule) {
|
||||
pauseForBetterTestPerformance()
|
||||
onView(withText(string.click_nearby_devices_message)).check(doesNotExist())
|
||||
composeContentTestRule.apply {
|
||||
waitForIdle()
|
||||
onNodeWithTag(SHOWCASE_VIEW_MESSAGE_TESTING_TAG)
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
private fun pauseForBetterTestPerformance() {
|
||||
|
@ -28,17 +28,10 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.accessibility.AccessibilityChecks
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
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 org.hamcrest.core.AllOf.allOf
|
||||
import org.hamcrest.core.AnyOf.anyOf
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -89,22 +82,6 @@ class LocalFileTransferTest {
|
||||
init {
|
||||
AccessibilityChecks.enable().apply {
|
||||
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 {
|
||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||
clickFileTransferIcon(composeTestRule) {
|
||||
assertReceiveFileTitleVisible()
|
||||
assertSearchDeviceMenuItemVisible()
|
||||
clickOnSearchDeviceMenuItem()
|
||||
assertLocalFileTransferScreenVisible()
|
||||
assertReceiveFileTitleVisible(composeTestRule)
|
||||
assertSearchDeviceMenuItemVisible(composeTestRule)
|
||||
clickOnSearchDeviceMenuItem(composeTestRule)
|
||||
assertLocalFileTransferScreenVisible(composeTestRule)
|
||||
pressBack()
|
||||
assertLocalLibraryVisible()
|
||||
assertLocalLibraryVisible(composeTestRule)
|
||||
}
|
||||
}
|
||||
LeakAssertions.assertNoLeaks()
|
||||
@ -155,7 +132,7 @@ class LocalFileTransferTest {
|
||||
|
||||
@Test
|
||||
fun showCaseFeature() {
|
||||
shouldShowShowCaseFeatureToUser(true, isResetShowCaseId = true)
|
||||
shouldShowShowCaseFeatureToUser(true)
|
||||
activityScenario =
|
||||
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
||||
moveToState(Lifecycle.State.RESUMED)
|
||||
@ -172,14 +149,14 @@ class LocalFileTransferTest {
|
||||
library {
|
||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||
clickFileTransferIcon(composeTestRule) {
|
||||
assertClickNearbyDeviceMessageVisible()
|
||||
clickOnGotItButton()
|
||||
assertDeviceNameMessageVisible()
|
||||
clickOnGotItButton()
|
||||
assertNearbyDeviceListMessageVisible()
|
||||
clickOnGotItButton()
|
||||
assertTransferZimFilesListMessageVisible()
|
||||
clickOnGotItButton()
|
||||
assertClickNearbyDeviceMessageVisible(composeTestRule)
|
||||
clickOnNextButton(composeTestRule)
|
||||
assertDeviceNameMessageVisible(composeTestRule)
|
||||
clickOnNextButton(composeTestRule)
|
||||
assertNearbyDeviceListMessageVisible(composeTestRule)
|
||||
clickOnNextButton(composeTestRule)
|
||||
assertTransferZimFilesListMessageVisible(composeTestRule)
|
||||
clickOnNextButton(composeTestRule)
|
||||
pressBack()
|
||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||
}
|
||||
@ -189,7 +166,7 @@ class LocalFileTransferTest {
|
||||
|
||||
@Test
|
||||
fun testShowCaseFeatureShowOnce() {
|
||||
shouldShowShowCaseFeatureToUser(true)
|
||||
shouldShowShowCaseFeatureToUser(false)
|
||||
activityScenario =
|
||||
ActivityScenario.launch(KiwixMainActivity::class.java).apply {
|
||||
moveToState(Lifecycle.State.RESUMED)
|
||||
@ -201,15 +178,12 @@ class LocalFileTransferTest {
|
||||
library {
|
||||
// test show case view show once.
|
||||
clickFileTransferIcon(composeTestRule) {
|
||||
LocalFileTransferRobot::assertClickNearbyDeviceMessageNotVisible
|
||||
assertClickNearbyDeviceMessageNotVisible(composeTestRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowShowCaseFeatureToUser(
|
||||
shouldShowShowCase: Boolean,
|
||||
isResetShowCaseId: Boolean = false
|
||||
) {
|
||||
private fun shouldShowShowCaseFeatureToUser(shouldShowShowCase: Boolean) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(SharedPreferenceUtil.PREF_SHOW_INTRO, false)
|
||||
putBoolean(SharedPreferenceUtil.PREF_WIFI_ONLY, false)
|
||||
@ -217,15 +191,5 @@ class LocalFileTransferTest {
|
||||
putBoolean(SharedPreferenceUtil.PREF_SHOW_SHOWCASE, shouldShowShowCase)
|
||||
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.RETRY_RULE_ORDER
|
||||
import org.kiwix.kiwixmobile.help.HelpRobot
|
||||
import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferRobot
|
||||
import org.kiwix.kiwixmobile.nav.destination.library.OnlineLibraryRobot
|
||||
import org.kiwix.kiwixmobile.settings.SettingsRobot
|
||||
import org.kiwix.kiwixmobile.testutils.RetryRule
|
||||
@ -115,7 +114,7 @@ class TopLevelDestinationTest : BaseActivityTest() {
|
||||
clickLibraryOnBottomNav {
|
||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||
clickFileTransferIcon(composeTestRule) {
|
||||
LocalFileTransferRobot::assertReceiveFileTitleVisible
|
||||
assertReceiveFileTitleVisible(composeTestRule)
|
||||
}
|
||||
}
|
||||
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.RETRY_RULE_ORDER
|
||||
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.KiwixMainActivity
|
||||
import org.kiwix.kiwixmobile.main.topLevel
|
||||
@ -119,7 +118,7 @@ class GetContentShortcutTest {
|
||||
clickLibraryOnBottomNav {
|
||||
assertGetZimNearbyDeviceDisplayed(composeTestRule)
|
||||
clickFileTransferIcon(composeTestRule) {
|
||||
LocalFileTransferRobot::assertReceiveFileTitleVisible
|
||||
assertReceiveFileTitleVisible(composeTestRule)
|
||||
}
|
||||
}
|
||||
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.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.content.ContextCompat
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.kiwix.kiwixmobile.R
|
||||
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.string
|
||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||
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.getToolbarNavigationIcon
|
||||
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
|
||||
import org.kiwix.kiwixmobile.core.extensions.toast
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
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.dialog.AlertDialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
|
||||
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.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
|
||||
|
||||
/**
|
||||
@ -96,7 +84,6 @@ import javax.inject.Inject
|
||||
*/
|
||||
|
||||
const val URIS_KEY = "uris"
|
||||
const val SHOWCASE_ID = "MaterialShowcaseId"
|
||||
|
||||
@SuppressLint("GoogleAppIndexingApiWarning", "Registered")
|
||||
class LocalFileTransferFragment :
|
||||
@ -114,147 +101,64 @@ class LocalFileTransferFragment :
|
||||
@Inject
|
||||
lateinit var sharedPreferenceUtil: SharedPreferenceUtil
|
||||
|
||||
private var fileListAdapter: FileListAdapter? = null
|
||||
private var wifiPeerListAdapter: WifiPeerListAdapter? = null
|
||||
private var fragmentLocalFileTransferBinding: FragmentLocalFileTransferBinding? = null
|
||||
private var materialShowCaseSequence: MaterialShowcaseSequence? = null
|
||||
private var searchIconView: View? = null
|
||||
private val deviceName = mutableStateOf("")
|
||||
private val isPeerSearching = mutableStateOf(false)
|
||||
private val peerDeviceList = mutableStateOf(emptyList<WifiP2pDevice>())
|
||||
private val transferFileList = mutableStateOf(emptyList<FileItem>())
|
||||
private var composeView: ComposeView? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentLocalFileTransferBinding =
|
||||
FragmentLocalFileTransferBinding.inflate(inflater, container, false)
|
||||
return fragmentLocalFileTransferBinding?.root
|
||||
): View? = ComposeView(requireContext()).also {
|
||||
composeView = it
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupMenu()
|
||||
val activity = requireActivity() as CoreMainActivity
|
||||
val filesForTransfer = getFilesForTransfer()
|
||||
val isReceiver = filesForTransfer.isEmpty()
|
||||
setupToolbar(view, activity, isReceiver)
|
||||
|
||||
wifiPeerListAdapter = WifiPeerListAdapter(WifiP2pDelegate(wifiDirectManager::sendToDevice))
|
||||
|
||||
setupPeerDevicesList(activity)
|
||||
composeView?.setContent {
|
||||
LocalFileTransferScreen(
|
||||
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)
|
||||
|
||||
wifiDirectManager.callbacks = this
|
||||
wifiDirectManager.lifecycleCoroutineScope = lifecycleScope
|
||||
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 {
|
||||
return when {
|
||||
requireActivity().isTablet() -> {
|
||||
requireActivity().resources.getDimensionPixelSize(dimen.maximum_donation_popup_width)
|
||||
}
|
||||
|
||||
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 actionMenuItem() = listOf(
|
||||
ActionMenuItem(
|
||||
Vector(Icons.Default.Search),
|
||||
string.search_label,
|
||||
{ onSearchMenuClicked() },
|
||||
testingTag = SEARCH_ICON_TESTING_TAG
|
||||
)
|
||||
}
|
||||
|
||||
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 =
|
||||
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() =
|
||||
LocalFileTransferFragmentArgs.fromBundle(requireArguments()).uris?.map(::FileItem).orEmpty()
|
||||
|
||||
private fun showPeerDiscoveryProgressBar() { // Setup UI for searching peers
|
||||
fragmentLocalFileTransferBinding?.progressBarSearchingPeers?.visibility = View.VISIBLE
|
||||
fragmentLocalFileTransferBinding?.listPeerDevices?.visibility = View.INVISIBLE
|
||||
fragmentLocalFileTransferBinding?.textViewEmptyPeerList?.visibility = View.INVISIBLE
|
||||
isPeerSearching.value = true
|
||||
}
|
||||
|
||||
// From WifiDirectManager.Callbacks interface
|
||||
override fun onUserDeviceDetailsAvailable(userDevice: WifiP2pDevice?) {
|
||||
// Update UI with user device's details
|
||||
if (userDevice != null) {
|
||||
fragmentLocalFileTransferBinding?.textViewDeviceName?.text = userDevice.deviceName
|
||||
deviceName.value = userDevice.deviceName
|
||||
Log.d(TAG, getDeviceStatus(userDevice.status))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionToPeersLost() {
|
||||
wifiPeerListAdapter?.items = emptyList()
|
||||
peerDeviceList.value = emptyList()
|
||||
}
|
||||
|
||||
override fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>) {
|
||||
@ -335,23 +210,20 @@ class LocalFileTransferFragment :
|
||||
}
|
||||
|
||||
private fun displayFileTransferProgress(filesToSend: List<FileItem>) {
|
||||
fileListAdapter = FileListAdapter(filesToSend)
|
||||
fragmentLocalFileTransferBinding?.recyclerViewTransferFiles?.apply {
|
||||
adapter = fileListAdapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(requireActivity())
|
||||
}
|
||||
transferFileList.value = filesToSend
|
||||
}
|
||||
|
||||
override fun onFileStatusChanged(itemIndex: Int) {
|
||||
fileListAdapter?.notifyItemChanged(itemIndex)
|
||||
override fun onFileStatusChanged(itemIndex: Int, fileStatus: FileItem.FileStatus) {
|
||||
val tempTransferList = transferFileList.value
|
||||
tempTransferList[itemIndex].fileStatus = fileStatus
|
||||
transferFileList.value = emptyList()
|
||||
transferFileList.value = tempTransferList
|
||||
}
|
||||
|
||||
override fun updateListOfAvailablePeers(peers: WifiP2pDeviceList) {
|
||||
val deviceList: List<WifiP2pDevice> = ArrayList<WifiP2pDevice>(peers.deviceList)
|
||||
fragmentLocalFileTransferBinding?.progressBarSearchingPeers?.visibility = View.GONE
|
||||
fragmentLocalFileTransferBinding?.listPeerDevices?.visibility = View.VISIBLE
|
||||
wifiPeerListAdapter?.items = deviceList
|
||||
isPeerSearching.value = false
|
||||
peerDeviceList.value = deviceList
|
||||
if (deviceList.isEmpty()) {
|
||||
Log.d(TAG, "No devices found")
|
||||
}
|
||||
@ -533,10 +405,6 @@ class LocalFileTransferFragment :
|
||||
override fun onDestroyView() {
|
||||
wifiDirectManager.stopWifiDirectManager()
|
||||
wifiDirectManager.callbacks = null
|
||||
fragmentLocalFileTransferBinding?.root?.removeAllViews()
|
||||
fragmentLocalFileTransferBinding = null
|
||||
searchIconView = null
|
||||
materialShowCaseSequence = null
|
||||
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) {
|
||||
filesForTransfer[itemIndex].fileStatus = status
|
||||
callbacks?.onFileStatusChanged(itemIndex)
|
||||
callbacks?.onFileStatusChanged(itemIndex, status)
|
||||
if (status == FileStatus.ERROR) {
|
||||
context.toast(
|
||||
context.getString(R.string.error_transferring, filesForTransfer[itemIndex].fileName)
|
||||
@ -344,7 +344,7 @@ class WifiDirectManager @Inject constructor(
|
||||
fun onConnectionToPeersLost()
|
||||
fun updateListOfAvailablePeers(peers: WifiP2pDeviceList)
|
||||
fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>)
|
||||
fun onFileStatusChanged(itemIndex: Int)
|
||||
fun onFileStatusChanged(itemIndex: Int, fileStatus: FileStatus)
|
||||
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? {
|
||||
fragmentDestinationDownloadBinding =
|
||||
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
|
||||
activity.setSupportActionBar(toolbar)
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 roomCompiler = "androidx.room:room-compiler:" + Versions.roomVersion
|
||||
|
@ -106,8 +106,6 @@ object Versions {
|
||||
|
||||
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 zxing = "3.5.3"
|
||||
|
@ -231,7 +231,6 @@ class AllProjectConfigurer {
|
||||
implementation(Libs.rxandroid)
|
||||
implementation(Libs.rxjava)
|
||||
implementation(Libs.preference_ktx)
|
||||
implementation(Libs.material_show_case_view)
|
||||
implementation(Libs.roomKtx)
|
||||
annotationProcessor(Libs.roomCompiler)
|
||||
implementation(Libs.roomRuntime)
|
||||
|
@ -53,7 +53,7 @@ import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
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"
|
||||
|
||||
abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActivityExtensions {
|
||||
|
@ -138,7 +138,7 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
|
||||
IconButton(
|
||||
enabled = menuItem.isEnabled,
|
||||
onClick = menuItem.onClick,
|
||||
modifier = Modifier.testTag(menuItem.testingTag)
|
||||
modifier = menuItem.modifier.testTag(menuItem.testingTag)
|
||||
) {
|
||||
Icon(
|
||||
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
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||
|
||||
@ -28,5 +29,6 @@ data class ActionMenuItem(
|
||||
val onClick: () -> Unit,
|
||||
val iconTint: Color = White,
|
||||
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 SEVENTEEN_DP = 17.dp
|
||||
val SIXTEEN_DP = 16.dp
|
||||
val FIFTEEN_DP = 15.dp
|
||||
val TWELVE_DP = 12.dp
|
||||
val TEN_DP = 10.dp
|
||||
val EIGHT_DP = 8.dp
|
||||
@ -51,6 +52,7 @@ object ComposeDimens {
|
||||
val FIVE_DP = 5.dp
|
||||
val FOUR_DP = 4.dp
|
||||
val TWO_DP = 2.dp
|
||||
val ONE_DP = 1.dp
|
||||
val SEVENTY_DP = 70.dp
|
||||
val SIXTY_FOUR_DP = 64.dp
|
||||
|
||||
@ -62,6 +64,9 @@ object ComposeDimens {
|
||||
// Default letter spacing in text according to theme
|
||||
val DEFAULT_LETTER_SPACING = 0.0333.em
|
||||
|
||||
// Default Text alpha.
|
||||
const val DEFAULT_TEXT_ALPHA = 0.67f
|
||||
|
||||
// Shape configuration sizes. See Shape.kt
|
||||
val EXTRA_SMALL_ROUND_SHAPE_SIZE = 4.dp
|
||||
val SMALL_ROUND_SHAPE_SIZE = 8.dp
|
||||
@ -111,4 +116,27 @@ object ComposeDimens {
|
||||
val PAGE_LIST_ITEM_FAVICON_SIZE = 40.dp
|
||||
val PAGE_SWITCH_LEFT_RIGHT_MARGIN = 10.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
|
||||
?: context.filesDir.path // a workaround for emulators
|
||||
|
||||
fun showCaseViewForFileTransferShown() {
|
||||
sharedPreferences.edit { putBoolean(PREF_SHOW_SHOWCASE, false) }
|
||||
}
|
||||
|
||||
fun putPrefBookMarkMigrated(isMigrated: Boolean) =
|
||||
sharedPreferences.edit { putBoolean(PREF_BOOKMARKS_MIGRATED, isMigrated) }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user