From 6b07e43d9ed7f3701f6059ee2d41b3ed20d84b6e Mon Sep 17 00:00:00 2001 From: OutOfCode <0ut0fCode@gmail.com> Date: Tue, 23 May 2023 10:04:59 +0330 Subject: [PATCH] update latest version (1.0.7) --- android/.gitignore | 21 +++ android/app/build.gradle | 6 +- .../cfscanner/model/ScanButtonState.kt | 1 + .../filternet/cfscanner/model/ScanSettings.kt | 2 + .../filternet/cfscanner/scanner/CFScanner.kt | 89 +++++++++--- .../cfscanner/service/CloudScannerService.kt | 26 +++- .../cfscanner/ui/common/HeaderPage.kt | 4 +- .../main/history/component/ScanCardItem.kt | 10 +- .../ui/page/main/scan/ScanContract.kt | 3 + .../cfscanner/ui/page/main/scan/ScanScreen.kt | 38 +++-- .../ui/page/main/scan/ScanScreenVM.kt | 10 +- .../ui/page/main/scan/component/LogBox.kt | 7 +- .../ui/page/main/scan/component/OptionSide.kt | 85 ++++++++++++ .../ui/page/main/scan/component/ScanButton.kt | 77 +++++++++-- .../page/main/settings/ScanSettingsScreen.kt | 130 +++++++++++++++++- .../cfscanner/ui/page/root/MainScreen.kt | 9 +- .../components/PlayfulBottomNavigation.kt | 6 +- .../sub/scan_details/ScanDetailsScreen.kt | 37 ++--- .../sub/scan_details/ScanDetailsScreenVM.kt | 2 +- .../scan_details/component/ConnectionCell.kt | 9 +- .../ir/filternet/cfscanner/utils/Utils.kt | 14 ++ .../filternet/cfscanner/utils/_ExtCompose.kt | 2 + .../app/src/main/res/drawable/side_button.xml | 24 ++++ android/app/src/main/res/raw/connecting.json | 1 + .../app/src/main/res/values-fa/strings.xml | 13 ++ android/app/src/main/res/values/strings.xml | 13 ++ android/settings.gradle | 1 + 27 files changed, 561 insertions(+), 79 deletions(-) create mode 100644 android/.gitignore create mode 100644 android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/OptionSide.kt create mode 100644 android/app/src/main/res/drawable/side_button.xml create mode 100644 android/app/src/main/res/raw/connecting.json diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..654e7936 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,21 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +app/debug/ + +.idea/ + +app/google-services.json diff --git a/android/app/build.gradle b/android/app/build.gradle index f78a1f5b..7b010ec4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId "ir.filternet.cfscanner" minSdk 24 targetSdk 33 - versionCode 6 - versionName "1.0.6" + versionCode 7 + versionName "1.0.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true @@ -223,6 +223,8 @@ dependencies { implementation "dev.chrisbanes.snapper:snapper:0.2.1" implementation "com.github.skydoves:whatif:1.1.2" implementation 'com.coolerfall:android-http-download-manager:2.0.0' + + implementation "com.airbnb.android:lottie-compose:6.0.0" } appmetrica { diff --git a/android/app/src/main/java/ir/filternet/cfscanner/model/ScanButtonState.kt b/android/app/src/main/java/ir/filternet/cfscanner/model/ScanButtonState.kt index 50d01569..7ef0c2b3 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/model/ScanButtonState.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/model/ScanButtonState.kt @@ -4,5 +4,6 @@ sealed class ScanButtonState { object Ready : ScanButtonState() data class Scanning(val scan:Scan ,val progress: ScanProgress) : ScanButtonState() data class Disabled(val message:String = "") : ScanButtonState() + object WaitingForNetwork : ScanButtonState() data class Paused(val message:String = "") : ScanButtonState() } \ No newline at end of file diff --git a/android/app/src/main/java/ir/filternet/cfscanner/model/ScanSettings.kt b/android/app/src/main/java/ir/filternet/cfscanner/model/ScanSettings.kt index 988c17b9..25e6d417 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/model/ScanSettings.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/model/ScanSettings.kt @@ -6,6 +6,8 @@ data class ScanSettings( val worker: Float = 4.99f, val speedTestSize: Float = 300f, val fronting: String = BuildConfig.FrontingAddress, + val pingFilter: Float = 2900f, + val autoSkipPortion:Float = 0f, val autoFetch: Boolean = true, val shuffle: Boolean = false, val customRange: Boolean = false, diff --git a/android/app/src/main/java/ir/filternet/cfscanner/scanner/CFScanner.kt b/android/app/src/main/java/ir/filternet/cfscanner/scanner/CFScanner.kt index d6817232..b85a3ae2 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/scanner/CFScanner.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/scanner/CFScanner.kt @@ -26,6 +26,7 @@ import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import kotlin.random.Random @Singleton class CFScanner @Inject constructor( @@ -48,6 +49,8 @@ class CFScanner @Inject constructor( private val cidrs: ArrayList = arrayListOf() private var scan: Scan? = null + private var skipCurrentRange: Boolean = false + fun startScan(scan: Scan, options: ScanOptions = ScanOptions()) { discoveryJob?.cancel() discoveryJob = launch(Dispatchers.IO) { @@ -88,20 +91,28 @@ class CFScanner @Inject constructor( return discoveryJob?.isActive ?: false } + + fun skipCurrentRange() { + if (!skipCurrentRange) { + skipCurrentRange = true + } + } + private suspend fun CoroutineScope.startScan(scan: Scan, options: ScanOptions) { val scope = this val config = scan.config val isp = scan.isp - val allCIDR = getScanCidrs(scan,options) - this@CFScanner.scan = scan.copy(scanCidrOrder = allCIDR.joinToString(" "){ it.uid.toString() }) + val allCIDR = getScanCidrs(scan, options) + this@CFScanner.scan = scan.copy(scanCidrOrder = allCIDR.joinToString(" ") { it.uid.toString() }) delay(1000) /* set options */ val parallel = options.parallel frontDomain = options.frontingDomain + val autoSkip = options.autoSkipPortion /* load previous scan history*/ val (lastCidr, lastConnection) = getLastDetails(scan) @@ -111,7 +122,7 @@ class CFScanner @Inject constructor( /* ================= */ /* Start Process Log */ - logger.add(Log("worker", context.getString(R.string.worker_initialized,parallel), STATUS.SUCCESS)) + logger.add(Log("worker", context.getString(R.string.worker_initialized, parallel), STATUS.SUCCESS)) Timber.d("CFScanner: Start Scan by $parallel worker") /* ================= */ @@ -123,14 +134,14 @@ class CFScanner @Inject constructor( yield() } val ipCount = allCIDR.map { calculateUsableHostCountBySubnetMask(it.subnetMask) }.reduce { a, i -> a + i } - logger.add(Log("ips", context.getString(R.string.ip_found_to_check,ipCount), STATUS.SUCCESS)) + logger.add(Log("ips", context.getString(R.string.ip_found_to_check, ipCount), STATUS.SUCCESS)) delay(1000) runBlocking { // worker controller val semaphore = Semaphore(parallel) - + var skipped = 0 allCIDR.let { // delete scanned cidr @@ -153,10 +164,12 @@ class CFScanner @Inject constructor( } ?: -1 /* Start Scan Log */ - logger.add(Log(cidr.address, context.getString(R.string.start_scan_for,cidr.address,cidr.subnetMask), STATUS.INPROGRESS)) + logger.add(Log(cidr.address, context.getString(R.string.start_scan_for, cidr.address, cidr.subnetMask), STATUS.INPROGRESS)) Timber.d("CFScanner: Start Scan for ${cidr.address}/${cidr.subnetMask} by $count ips") /* ============== */ + var failed = 0 + skipped = 0 repeat(count) { index -> val ip = getIpAddressByIndex(cidr.address, cidr.subnetMask, index) @@ -166,6 +179,22 @@ class CFScanner @Inject constructor( return@repeat } + // auto skip + if (autoSkip >= 1 && !skipCurrentRange) { + val shouldSkip = (((failed*1f) / count)*100f) >= autoSkip + if (shouldSkip){ + logger.add(Log(ip+ Random(100).toString(), context.getString(R.string.auto_skip_triggered_for,cidr.address), STATUS.SUCCESS)) + skipCurrentRange() + } + } + + // skip range check + if (skipCurrentRange) { + skipped++ + return@repeat + } + + semaphore.acquire() scope.launch { ip?.let { @@ -174,13 +203,26 @@ class CFScanner @Inject constructor( logger.add(Log(it, it, STATUS.INPROGRESS)) Timber.d("CFScanner: check status for $it") yield() - val delay = checkIpDelay(config, it) + var delay = checkIpDelay(config, it) yield() if (delay > 0) { - founded++ - logger.add(Log(it, "$it ($delay ms)", STATUS.SUCCESS)) - Timber.d("CFScanner: result status for $it ==> Success (${delay}ms)") + + // apply ping limit filter on result + if (delay > options.pingFilter) { + logger.add(Log(it, context.getString(R.string.ping_error, it), STATUS.FAILED)) + failed++ + delay = -1 + } else { + // reset failed count to avoid auto skip + failed = 0 + + founded++ + logger.add(Log(it, "$it ($delay ms)", STATUS.SUCCESS)) + Timber.d("CFScanner: result status for $it ==> Success (${delay}ms)") + } + } else { + failed++ Timber.d("CFScanner: result status for $it ==> Failed!") } @@ -199,7 +241,14 @@ class CFScanner @Inject constructor( semaphore.release() } } - logger.add(Log(cidr.address, context.getString(R.string.end_scan_for,cidr.address,cidr.subnetMask), STATUS.SUCCESS)) + + skipCurrentRange = false + + if (skipped > 0) { + logger.add(Log(cidr.address + Random(100).toString(), context.getString(R.string.skipped_range, skipped, cidr.address), STATUS.SUCCESS)) + } else { + logger.add(Log(cidr.address, context.getString(R.string.end_scan_for, cidr.address, cidr.subnetMask), STATUS.SUCCESS)) + } } semaphore.tryAcquire(parallel, 10, TimeUnit.SECONDS) @@ -218,8 +267,8 @@ class CFScanner @Inject constructor( // 2. convert cidrs order to cidr list val cidrList = cidrRepository.getAllCIDR(options.autoFetch) - // filter custom range - .let { if(options.customRange) it.filter { it.custom } else it } + // filter custom range + .let { if (options.customRange) it.filter { it.custom } else it } return if (!cidrIdList.isNullOrEmpty()) { cidrList.sortedBy { cidrIdList.indexOf(it.uid) } @@ -238,14 +287,14 @@ class CFScanner @Inject constructor( private suspend fun checkIpDelay(config: Config, address: String, port: Int = 443): Long { // 1. check port opening if (!isPortOpen(address, port, 2000)) { - logger.add(Log(address, context.getString(R.string.port_close,address), STATUS.FAILED)) + logger.add(Log(address, context.getString(R.string.port_close, address), STATUS.FAILED)) Timber.d("CFScanner: check status for $address ==> Port $port is not Open!") return -1 } // 2. check domain fronting if (!checkDomainFronting(address)) { - logger.add(Log(address, context.getString(R.string.fronting_error,address), STATUS.FAILED)) + logger.add(Log(address, context.getString(R.string.fronting_error, address), STATUS.FAILED)) Timber.d("CFScanner: check status for $address ==> Fronting not Ok!") return -1 } @@ -317,7 +366,7 @@ class CFScanner @Inject constructor( val conf = v2rarUtils.createServerConfig(config)?.fullConfig?.getByCustomVnextOutbound(port, ip)?.getByCustomInbound(ports)?.toPrettyPrinting()!! val client = V2RayClient(context, conf) client.connect("$ip:$port") - logger.add(Log(ip, context.getString(R.string.v2ray_cooldown,ip), STATUS.INPROGRESS)) + logger.add(Log(ip, context.getString(R.string.v2ray_cooldown, ip), STATUS.INPROGRESS)) delay(2000) val delay = client.measureDelay() @@ -328,14 +377,14 @@ class CFScanner @Inject constructor( client.disconnect() } - }catch (e:Exception){ + } catch (e: Exception) { e.printStackTrace() - logger.add(Log(ip, context.getString(R.string.v2ray_failed,ip), STATUS.FAILED)) + logger.add(Log(ip, context.getString(R.string.v2ray_failed, ip), STATUS.FAILED)) } if (delay < 0) { - logger.add(Log(ip, context.getString(R.string.v2ray_failed,ip), STATUS.FAILED)) + logger.add(Log(ip, context.getString(R.string.v2ray_failed, ip), STATUS.FAILED)) } return delay } @@ -382,6 +431,8 @@ class CFScanner @Inject constructor( data class ScanOptions( val parallel: Int = 4, val frontingDomain: String = "", + val pingFilter: Float = 2900f, + val autoSkipPortion: Float = 0f, val autoFetch: Boolean = true, val shuffle: Boolean = false, val customRange: Boolean = false, diff --git a/android/app/src/main/java/ir/filternet/cfscanner/service/CloudScannerService.kt b/android/app/src/main/java/ir/filternet/cfscanner/service/CloudScannerService.kt index cbeb129d..16b079a7 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/service/CloudScannerService.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/service/CloudScannerService.kt @@ -61,7 +61,7 @@ class CloudScannerService : Service(), private var isp: ISP? = null private var scan: Scan? = null - private var serviceStatus: ServiceStatus = ServiceStatus.Idle() + private var serviceStatus: ServiceStatus = ServiceStatus.WaitingForNetwork(scan) /* connection flow */ @@ -184,6 +184,10 @@ class CloudScannerService : Service(), stopScan() } + fun skipCurrentRange() { + _skipCurrentRange() + } + fun getLastScan(): Scan? = scan fun getServiceStatus(): ServiceStatus = serviceStatus @@ -220,7 +224,9 @@ class CloudScannerService : Service(), frontingDomain = scanSettings.fronting, autoFetch = scanSettings.autoFetch, shuffle = scanSettings.shuffle, - customRange = scanSettings.customRange + customRange = scanSettings.customRange, + pingFilter = scanSettings.pingFilter, + autoSkipPortion = scanSettings.autoSkipPortion, ) ) } @@ -228,11 +234,19 @@ class CloudScannerService : Service(), private fun stopScan() { Timber.d("CloudScannerService stopScan") - cfScanner?.apply { + cfScanner.apply { stopScan(true) } } + + private fun _skipCurrentRange() { + Timber.d("CloudScannerService skipCurrentRange") + cfScanner.apply { + skipCurrentRange() + } + } + /** * save success connection besides of last connection for each scan * @param connections List @@ -242,6 +256,7 @@ class CloudScannerService : Service(), Timber.d("CloudScannerService: add connections to db: ${connections.size}") val healthyConnections = connections.filter { it.delay > 0 } val lastConnection = connections.lastOrNull() + if (healthyConnections.isNotEmpty()) { connectionRepository.insertAll(healthyConnections) } @@ -303,7 +318,7 @@ class CloudScannerService : Service(), this.scan = scan if (networkManager.getNetworkState() == NetworkManager.NetworkState.DISCONNECTED) { - setServiceStatus(ServiceStatus.Disabled(reason)) + setServiceStatus(ServiceStatus.WaitingForNetwork(scan)) } else { setServiceStatus(ServiceStatus.Paused(scan, ScanProgress(), reason)) } @@ -334,7 +349,7 @@ class CloudScannerService : Service(), if (cfScanner.isRunning()) { cfScanner.stopScan(byUser = false, reason = "No internet connection") } - setServiceStatus(ServiceStatus.Disabled("No internet connection")) + setServiceStatus(ServiceStatus.WaitingForNetwork(scan)) } NetworkManager.NetworkState.DISCONNECTED -> { @@ -368,6 +383,7 @@ class CloudScannerService : Service(), data class Idle(val isp: ISP? = null) : ServiceStatus() data class Scanning(val scan: Scan) : ServiceStatus() data class Paused(val scan: Scan, val progress: ScanProgress, val message: String) : ServiceStatus() + data class WaitingForNetwork(val scan: Scan? = null) : ServiceStatus() data class Disabled(val message: String) : ServiceStatus() } } \ No newline at end of file diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/common/HeaderPage.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/common/HeaderPage.kt index 5114a1be..bbe9f178 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/common/HeaderPage.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/common/HeaderPage.kt @@ -19,9 +19,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import ir.filternet.cfscanner.R import ir.filternet.cfscanner.utils.mirror @Composable @@ -36,7 +38,7 @@ fun HeaderPage(title: String,modifier:Modifier = Modifier, onBackPress: () -> Un Icons.Rounded.ArrowBackIos, contentDescription = null, Modifier .clip(RoundedCornerShape(50)) - .clickable { + .clickable(onClickLabel = stringResource(id = R.string.back)) { onBackPress() } .background(MaterialTheme.colors.primary.copy(alpha = 0.05f)) diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/history/component/ScanCardItem.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/history/component/ScanCardItem.kt index 2fd7b4d3..c9493a7b 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/history/component/ScanCardItem.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/history/component/ScanCardItem.kt @@ -20,6 +20,9 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -33,7 +36,10 @@ import ir.filternet.cfscanner.R @Composable fun ScanCardItem(scan: Scan, click: (scan: Scan) -> Unit = {}) { + val context = LocalContext.current val founded = scan.progress.successConnectionCount + val configName = scan.config.name + val ispName = scan.isp.parseToCommonName(context) Card( Modifier .fillMaxWidth(0.9f) @@ -46,7 +52,7 @@ fun ScanCardItem(scan: Scan, click: (scan: Scan) -> Unit = {}) { Row( Modifier .fillMaxSize() - .clickable { click(scan) }, + .clickable(onClickLabel = "Visit scan result for $configName Config by $ispName Internet Service Provider") { click(scan) }, verticalAlignment = Alignment.CenterVertically ) { @@ -67,7 +73,7 @@ fun ScanCardItem(scan: Scan, click: (scan: Scan) -> Unit = {}) { .weight(1f) .padding(start = 8.dp) ) { - Text(text = "${scan.config.name} | ${scan.isp.parseToCommonName(context)}") + Text(text = "$configName | $ispName") Spacer(modifier = Modifier.height(5.dp)) Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanContract.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanContract.kt index 4c302896..e46606e5 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanContract.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanContract.kt @@ -11,6 +11,8 @@ class ScanContract { sealed class Event : ViewEvent { object StartScan : Event() object StopScan : Event() + object DisableNotificationDialog : Event() + object SkipCurrentRange : Event() data class DeleteConfig(val config: Config) : Event() data class UpdateConfig(val config: Config) : Event() data class AddConfig(val config: String) : Event() @@ -24,6 +26,7 @@ class ScanContract { val logs: List = emptyList(), val buttonState: ScanButtonState = ScanButtonState.Disabled(), val configEdit: Config? = null, + val dismissNotificationDialog:Boolean = false, ) : ViewState sealed class Effect : ViewSideEffect { diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreen.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreen.kt index c753f088..acd4855a 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreen.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreen.kt @@ -1,24 +1,42 @@ package ir.filternet.cfscanner.ui.page.main.scan -import android.content.Intent import android.widget.Toast -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Language import androidx.compose.material.icons.rounded.Webhook -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import ir.filternet.cfscanner.contracts.SIDE_EFFECTS_KEY import ir.filternet.cfscanner.model.ScanButtonState import ir.filternet.cfscanner.service.CloudScannerService import ir.filternet.cfscanner.ui.common.ScanningDetailsView -import ir.filternet.cfscanner.ui.page.main.scan.component.* +import ir.filternet.cfscanner.ui.page.main.scan.component.CloudLogo +import ir.filternet.cfscanner.ui.page.main.scan.component.ConfigEditDialog +import ir.filternet.cfscanner.ui.page.main.scan.component.ConfigSelectionBox +import ir.filternet.cfscanner.ui.page.main.scan.component.LoadingView +import ir.filternet.cfscanner.ui.page.main.scan.component.LogBox +import ir.filternet.cfscanner.ui.page.main.scan.component.NotificationPermissionRequest +import ir.filternet.cfscanner.ui.page.main.scan.component.ScanButton import ir.filternet.cfscanner.utils.isNotificationEnabled import ir.filternet.cfscanner.utils.parseToCommonName +import ir.filternet.cfscanner.utils.tryStartForegroundService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -60,6 +78,7 @@ fun ScanScreen( val configs = state.configs val loading = state.loading val buttonState = state.buttonState + val dismissNotification = state.dismissNotificationDialog val scanning = state.buttonState is ScanButtonState.Scanning Box(modifier = Modifier.fillMaxSize()) { @@ -94,7 +113,9 @@ fun ScanScreen( Spacer(modifier = Modifier.height(15.dp)) - LogBox(Modifier.fillMaxSize(), log) + LogBox(Modifier.fillMaxSize(), log){ + onEventSent.invoke(ScanContract.Event.SkipCurrentRange) + } } if (buttonState !is ScanButtonState.Scanning) @@ -117,7 +138,7 @@ fun ScanScreen( onEventSent(ScanContract.Event.StopScan) } is ScanButtonState.Ready, is ScanButtonState.Paused -> { - ContextCompat.startForegroundService(context, Intent(context, CloudScannerService::class.java)) + context.tryStartForegroundService(CloudScannerService::class.java) onEventSent(ScanContract.Event.StartScan) } else -> {} @@ -140,10 +161,11 @@ fun ScanScreen( if (showPermissionDialog) NotificationPermissionRequest { showPermissionDialog = false + onEventSent.invoke(ScanContract.Event.DisableNotificationDialog) } LaunchedEffect(Unit) { - if (!context.isNotificationEnabled()) { + if (!context.isNotificationEnabled() && !dismissNotification) { showPermissionDialog = true } } diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreenVM.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreenVM.kt index 249f351b..cf2589e3 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreenVM.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/ScanScreenVM.kt @@ -68,7 +68,12 @@ class ScanScreenVM @Inject constructor( is ScanContract.Event.StopScan -> { stopScan() } - + is ScanContract.Event.DisableNotificationDialog -> { + setState { copy(dismissNotificationDialog = true) } + } + is ScanContract.Event.SkipCurrentRange -> { + binder?.skipCurrentRange() + } } } @@ -201,6 +206,9 @@ class ScanScreenVM @Inject constructor( is CloudScannerService.ServiceStatus.Disabled -> { setState { copy(buttonState = ScanButtonState.Disabled(status.message)) } } + is CloudScannerService.ServiceStatus.WaitingForNetwork -> { + setState { copy(buttonState = ScanButtonState.WaitingForNetwork) } + } else -> {} } diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/LogBox.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/LogBox.kt index 06a3ed08..394ed68e 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/LogBox.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/LogBox.kt @@ -34,7 +34,7 @@ import ir.filternet.cfscanner.ui.theme.Orange import ir.filternet.cfscanner.ui.theme.Red @Composable -fun LogBox(modifier: Modifier = Modifier, log: List? = emptyList()) { +fun LogBox(modifier: Modifier = Modifier, log: List? = emptyList(),onSkipClick:()->Unit = {}) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val state = rememberLazyListState() @@ -69,6 +69,11 @@ fun LogBox(modifier: Modifier = Modifier, log: List? = emptyList()) { .background(Brush.verticalGradient(0f to Color.Transparent, 1f to MaterialTheme.colors.background)) ) + + OptionSide{ + onSkipClick() + } + LaunchedEffect(log) { val index = if (log?.lastIndex == null || log.lastIndex < 0) 0 else log.lastIndex state.scrollToItem(index, 0) diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/OptionSide.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/OptionSide.kt new file mode 100644 index 00000000..108b54a1 --- /dev/null +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/OptionSide.kt @@ -0,0 +1,85 @@ +package ir.filternet.cfscanner.ui.page.main.scan.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInHorizontally +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import ir.filternet.cfscanner.R +import ir.filternet.cfscanner.utils.clickableWithNoRipple +import ir.filternet.cfscanner.utils.isRTL +import kotlinx.coroutines.delay + +@Composable +fun BoxScope.OptionSide(onClick:()->Unit = {}) { + + var show by remember{ mutableStateOf(false) } + val isRTL = isRTL() + + AnimatedVisibility( + visible = show, + enter = slideInHorizontally(tween(500)){ ((if(isRTL) -1 else 1 )*it)/2}+ fadeIn(), + modifier = Modifier + .align(Alignment.CenterEnd) + .height(175.dp) + ) { + Box( + Modifier + .align(Alignment.CenterEnd) + .height(175.dp) + ) { + Image( + painter = rememberAsyncImagePainter(R.drawable.side_button), + contentDescription = stringResource(R.string.skip_this_range), + modifier = Modifier + .align(Alignment.CenterEnd) + .clickableWithNoRipple { onClick() } + .height(175.dp) + ) + + Text( + text = stringResource(R.string.skip_this_range), + Modifier + .align(Alignment.CenterEnd) + .padding(end = 10.dp) + .vertical() + .rotate(-90f) + ) + } + } + + LaunchedEffect(Unit){ + delay(700) + show = true + } + +} + +fun Modifier.vertical() = + layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.height, placeable.width) { + placeable.place( + x = -(placeable.width / 2 - placeable.height / 2), + y = -(placeable.height / 2 - placeable.width / 2) + ) + } + } diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/ScanButton.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/ScanButton.kt index 91a22ccb..ec9fd5c4 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/ScanButton.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/scan/component/ScanButton.kt @@ -1,8 +1,13 @@ package ir.filternet.cfscanner.ui.page.main.scan.component import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Canvas import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -13,11 +18,17 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import ir.filternet.cfscanner.R import ir.filternet.cfscanner.model.ScanButtonState +import ir.filternet.cfscanner.ui.theme.Gray @Composable fun BoxScope.ScanButton(state: ScanButtonState, click: () -> Unit = {}) { @@ -27,31 +38,73 @@ fun BoxScope.ScanButton(state: ScanButtonState, click: () -> Unit = {}) { AnimatedVisibility( visible = visible, - enter = slideInVertically { 2*it }, - exit = slideOutVertically { 2*it }, + enter = slideInVertically { 2 * it }, + exit = slideOutVertically { 2 * it }, modifier = Modifier .padding(bottom = 75.dp) .fillMaxWidth(0.5f) .align(Alignment.BottomCenter) ) { - val buttonText = when(state){ - is ScanButtonState.Ready,is ScanButtonState.Paused -> stringResource(id = R.string.start_scan) + val buttonText = when (state) { + is ScanButtonState.Ready, is ScanButtonState.Paused -> stringResource(id = R.string.start_scan) is ScanButtonState.Scanning -> stringResource(id = R.string.pause_scan) + is ScanButtonState.WaitingForNetwork -> stringResource(id = R.string.waiting_for_network) is ScanButtonState.Disabled -> "" } - Card( - Modifier.fillMaxWidth(0.2f).height(45.dp), - shape = RoundedCornerShape(50), - backgroundColor = MaterialTheme.colors.primary, - elevation = 7.dp, + + val isDeactivated = state is ScanButtonState.WaitingForNetwork || state is ScanButtonState.Disabled + val buttonColorAnimated by animateColorAsState(targetValue = if (isDeactivated) Gray else MaterialTheme.colors.primary, tween(400)) + val buttonTextColorAnimated by animateColorAsState(targetValue = if (isDeactivated) Color.White else MaterialTheme.colors.background , tween(250)) + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.connecting)) + + Box( + Modifier + .fillMaxWidth(0.2f) + .height((45 + 22.5).dp) ) { - Box(modifier = Modifier.fillMaxSize().clickable { click() }, contentAlignment = Alignment.Center){ - Text(text = buttonText, style = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold)) + + Card( + Modifier + .height(45.dp) + .align(Alignment.BottomCenter), + shape = RoundedCornerShape(50), + backgroundColor = buttonColorAnimated, + elevation = 7.dp, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .clickable(!isDeactivated) { click() }, contentAlignment = Alignment.Center + ) { + + Text(text = buttonText, style = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold), color =buttonTextColorAnimated) + } } - } + AnimatedVisibility( + visible = state is ScanButtonState.WaitingForNetwork, + modifier =Modifier + .size(45.dp) + .align(Alignment.TopCenter), + enter = slideInVertically{ it/2} + fadeIn(), + exit = slideOutVertically{it/2}+ fadeOut(tween(150)) + ) { + Box() { + Canvas(modifier = Modifier.fillMaxSize(0.85f).align(Alignment.Center)) { + drawArc(buttonColorAnimated, 0f, -180f, true) + } + + Column(Modifier.align(Alignment.TopCenter)) { + LottieAnimation(composition, restartOnPlay = true, iterations = Int.MAX_VALUE) + Spacer(modifier = Modifier.height(90.dp)) + } + + } + } + + } } } \ No newline at end of file diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/settings/ScanSettingsScreen.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/settings/ScanSettingsScreen.kt index 6b3942f6..b61c9c2d 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/settings/ScanSettingsScreen.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/main/settings/ScanSettingsScreen.kt @@ -1,5 +1,6 @@ package ir.filternet.cfscanner.ui.page.main.settings +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -28,12 +29,12 @@ import dev.vivvvek.seeker.Segment import dev.vivvvek.seeker.rememberSeekerState import ir.filternet.cfscanner.BuildConfig import ir.filternet.cfscanner.R -import ir.filternet.cfscanner.ui.page.main.history.HistoryContract +import ir.filternet.cfscanner.ui.theme.Gray +import ir.filternet.cfscanner.ui.theme.Green import ir.filternet.cfscanner.utils.AppConfig import ir.filternet.cfscanner.utils.mirror import ir.filternet.cfscanner.utils.openBrowser import kotlinx.coroutines.flow.Flow -import timber.log.Timber @Composable fun ScanSettingScreen( @@ -68,6 +69,14 @@ fun ScanSettingScreen( onEventSent.invoke(ScanSettingsContract.Event.UpdateSettings(scan.copy(speedTestSize = it))) } Spacer(modifier = Modifier.height(15.dp)) + PingFilterCell(scan.pingFilter) { + onEventSent.invoke(ScanSettingsContract.Event.UpdateSettings(scan.copy(pingFilter = it))) + } + Spacer(modifier = Modifier.height(15.dp)) + AutoSkipCell(scan.autoSkipPortion) { + onEventSent.invoke(ScanSettingsContract.Event.UpdateSettings(scan.copy(autoSkipPortion = it))) + } + Spacer(modifier = Modifier.height(15.dp)) FrontingCell(scan.fronting) { onEventSent.invoke(ScanSettingsContract.Event.UpdateSettings(scan.copy(fronting = it))) } @@ -216,6 +225,123 @@ fun SpeedCell(num: Float, onChange: (Float) -> Unit = {}) { } + +@Composable +fun PingFilterCell(num: Float, onChange: (Float) -> Unit = {}) { + + var value by remember(num) { mutableStateOf(num) } + val interactionSource = remember { MutableInteractionSource() } + val isDragging by interactionSource.collectIsDraggedAsState() + val state = rememberSeekerState() + val thumbRadius by animateDpAsState(if (isDragging) 10.dp else 5.dp) + + + LaunchedEffect(isDragging) { + if (!isDragging && num != value) + onChange(value) + } + + + Column( + Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.onSurface) + .padding(10.dp) + ) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.ping_filter), Modifier.weight(1f)) + Text(text = "${value.toInt()}ms", fontSize = 14.sp) + } + + Spacer(modifier = Modifier.height(5.dp)) + + Seeker( + value = value, + modifier = Modifier.height(20.dp), + range = 150f..8000f, + state = state, + interactionSource = interactionSource, + dimensions = SeekerDefaults.seekerDimensions(thumbRadius = thumbRadius), + onValueChange = { value = it } + ) + + Text(text = stringResource(R.string.ping_filter_desc), modifier = Modifier.padding(8.dp), fontSize = 13.sp, fontWeight = FontWeight.Light) + } + +} + + +@Composable +fun AutoSkipCell(num: Float, onChange: (Float) -> Unit = {}) { + + var value by remember(num) { mutableStateOf(num) } + val interactionSource = remember { MutableInteractionSource() } + val isDragging by interactionSource.collectIsDraggedAsState() + val state = rememberSeekerState() + val thumbRadius by animateDpAsState(if (isDragging) 10.dp else 5.dp) + + + LaunchedEffect(isDragging) { + if (!isDragging && num != value) + onChange(value) + } + + + Column( + Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.onSurface) + .padding(10.dp) + ) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.auto_skip), Modifier.weight(1f)) + + Switch( + checked = value.toInt() > 0, + onCheckedChange = { + value = if (it) 30f else 0f + onChange(value) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = Green, + uncheckedThumbColor = Gray, + checkedTrackColor = Green.copy(0.4f), + uncheckedTrackColor = Gray.copy(0.4f) + ) + ) + } + + + AnimatedVisibility(visible = value.toInt() > 0) { + Spacer(modifier = Modifier.height(5.dp)) + + Seeker( + value = value, + modifier = Modifier.height(20.dp), + range = 1f..90f, + state = state, + interactionSource = interactionSource, + dimensions = SeekerDefaults.seekerDimensions(thumbRadius = thumbRadius), + onValueChange = { value = it } + ) + } + + val percent = if (value.toInt() > 0) value.toInt().toString() + "%" else stringResource(R.string.specific_proportion) + Text(text = stringResource(R.string.auto_skip_desc, percent), modifier = Modifier.padding(8.dp), fontSize = 13.sp, fontWeight = FontWeight.Light) + } + +} + @Composable fun FrontingCell(address: String? = null, onChange: (String) -> Unit = {}) { diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/MainScreen.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/MainScreen.kt index ce0d1d90..a9322b1a 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/MainScreen.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/MainScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import ir.filternet.cfscanner.R import ir.filternet.cfscanner.contracts.SIDE_EFFECTS_KEY @@ -27,6 +28,7 @@ import ir.filternet.cfscanner.ui.navigation.CFScannerSubNavigation import ir.filternet.cfscanner.ui.page.root.components.PlayfulBottomNavigation import ir.filternet.cfscanner.ui.page.root.components.UpdateHeader import ir.filternet.cfscanner.utils.installFile +import ir.filternet.cfscanner.utils.tryStartForegroundService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -71,7 +73,7 @@ fun MainScreen( Column { UpdateHeader(update, onDownload = { - ContextCompat.startForegroundService(context, Intent(context, CloudUpdateService::class.java)) + context.tryStartForegroundService(CloudUpdateService::class.java) onEventSent.invoke(MainContract.Event.StartDownloadUpdate) }, onCancel = { @@ -103,7 +105,10 @@ fun MainScreen( .fillMaxWidth() .align(Alignment.BottomCenter), index = selectedIndex, - icons = arrayOf(R.drawable.ic_cloud_done, R.drawable.ic_network_check, R.drawable.ic_settings), + icons = arrayOf( + R.drawable.ic_cloud_done to stringResource(R.string.scan_history), + R.drawable.ic_network_check to stringResource(R.string.scanner_page), + R.drawable.ic_settings to stringResource(R.string.settings_page)), unselectedColor = MaterialTheme.colors.onPrimary.copy(0.8f), selectedColor = MaterialTheme.colors.onPrimary, indicatorColor = MaterialTheme.colors.onPrimary, diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/components/PlayfulBottomNavigation.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/components/PlayfulBottomNavigation.kt index 1d2a9661..be964922 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/components/PlayfulBottomNavigation.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/root/components/PlayfulBottomNavigation.kt @@ -29,7 +29,7 @@ import kotlin.math.roundToInt @Composable fun PlayfulBottomNavigation( modifier: Modifier = Modifier, - @IntegerRes icons: Array, + icons: Array>, index: Int = 0, duration: Int = 1200, unselectedColor: Color = Color.White, @@ -57,8 +57,8 @@ fun PlayfulBottomNavigation( content = { repeat(icons.size) { Icon( - painter = painterResource(id = icons[it]), - contentDescription = null, + painter = painterResource(id = icons[it].first), + contentDescription = icons[it].second, modifier = Modifier .size(40.dp) // .background(iconColor.copy(0.2f), RoundedCornerShape(50)) diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreen.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreen.kt index 71d22aef..6aa4b340 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreen.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreen.kt @@ -33,6 +33,7 @@ import ir.filternet.cfscanner.ui.common.HeaderPage import ir.filternet.cfscanner.ui.page.main.scan.component.LoadingView import ir.filternet.cfscanner.ui.page.sub.scan_details.component.* import ir.filternet.cfscanner.utils.clickableWithNoRipple +import ir.filternet.cfscanner.utils.tryStartForegroundService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -98,7 +99,7 @@ fun ScanDetailsScreen( if (scan != null) item { ActionCell(isScanning, !deleteable, { - ContextCompat.startForegroundService(context, Intent(context, CloudScannerService::class.java)) + context.tryStartForegroundService(CloudScannerService::class.java) onEventSent.invoke(ScanDetailsContract.Event.ResumeScan) }, stop = { onEventSent.invoke(ScanDetailsContract.Event.StopScan) @@ -143,7 +144,9 @@ fun ScanDetailsScreen( Text(text = stringResource(R.string.sort_by), color = Color.Black, modifier = Modifier.padding(start = 15.dp)) Text( "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", - modifier = Modifier.weight(1f).padding(start = 15.dp), + modifier = Modifier + .weight(1f) + .padding(start = 15.dp), textAlign = TextAlign.Center, maxLines = 1, color = MaterialTheme.colors.background @@ -172,20 +175,20 @@ fun ScanDetailsScreen( } - AnimatedVisibility(sort.value in 1..2,Modifier.width(IntrinsicSize.Min)) { - Column { - SpeedCheckButton(speedStatus, { - ContextCompat.startForegroundService(context, Intent(context, CloudSpeedService::class.java)) - if(sort.value==1){ - onEventSent(ScanDetailsContract.Event.StartSortAllByPing) - }else{ - onEventSent(ScanDetailsContract.Event.StartSortAllBySpeed) - } - - }) { - onEventSent(ScanDetailsContract.Event.StopSortAll) - } - } + AnimatedVisibility(sort.value in 1..2, Modifier.width(IntrinsicSize.Min)) { + Column { + SpeedCheckButton(speedStatus, { + context.tryStartForegroundService(CloudSpeedService::class.java) + if (sort.value == 1) { + onEventSent(ScanDetailsContract.Event.StartSortAllByPing) + } else { + onEventSent(ScanDetailsContract.Event.StartSortAllBySpeed) + } + + }) { + onEventSent(ScanDetailsContract.Event.StopSortAll) + } + } } } @@ -195,7 +198,7 @@ fun ScanDetailsScreen( items(connections, { it.ip }) { - ConnectionCell(Modifier.animateItemPlacement(), it, speedChecking,configs) { + ConnectionCell(Modifier.animateItemPlacement(), it, speedChecking, configs) { onEventSent.invoke(ScanDetailsContract.Event.UpdateSpeed(it)) } } diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreenVM.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreenVM.kt index f21e4068..4c47945d 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreenVM.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/ScanDetailsScreenVM.kt @@ -125,7 +125,7 @@ class ScanDetailsScreenVM @Inject constructor( Timber.e("DetailsScanScreenVM ==> Scan get: $scan") setState { copy(loading = true, scan = scan) } - val connections = connectionRepository.getAllConnectionByScanID(scan) + val connections = connectionRepository.getAllConnectionByScanID(scan).distinctBy { it.ip } Timber.e("DetailsScanScreenVM ==> Scan connection count: ${connections.size}") setState { copy(loading = false) } setConnectionBySort(connections) diff --git a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/component/ConnectionCell.kt b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/component/ConnectionCell.kt index 06f92519..6bd3383d 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/component/ConnectionCell.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/ui/page/sub/scan_details/component/ConnectionCell.kt @@ -36,7 +36,9 @@ import ir.filternet.cfscanner.R import ir.filternet.cfscanner.model.Config import ir.filternet.cfscanner.model.Connection import ir.filternet.cfscanner.utils.applyNewAddress +import ir.filternet.cfscanner.utils.applyNewAddressByISPname import ir.filternet.cfscanner.utils.convertToServerConfig +import ir.filternet.cfscanner.utils.parseToCommonName import ir.filternet.cfscanner.utils.share2Clipboard @OptIn(ExperimentalFoundationApi::class) @@ -123,10 +125,11 @@ fun ConnectionCell( .clip(RoundedCornerShape(50)) .combinedClickable(onClick = { val configID = connection.scan?.config?.uid + val scanIsp = connection.scan?.isp val config = configs .find { it.uid == configID } ?.convertToServerConfig() - ?.applyNewAddress(connection.ip) + ?.applyNewAddressByISPname(connection.ip,scanIsp?.parseToCommonName(context)) if (config != null) { Toast .makeText(context, context.getString(R.string.config_copied_to_clipboard), Toast.LENGTH_SHORT) @@ -149,8 +152,8 @@ fun ConnectionCell( ConfigDropDown( expanded, configs, onSelect = { - val configID = connection.scan?.config?.uid - val config = it.convertToServerConfig().applyNewAddress(connection.ip)?.let { + val ispName = connection.scan?.isp + val config = it.convertToServerConfig().applyNewAddressByISPname(connection.ip,ispName?.parseToCommonName(context))?.let { share2Clipboard(context, it) Toast.makeText(context, context.getString(R.string.config_copied_to_clipboard), Toast.LENGTH_SHORT).show() } diff --git a/android/app/src/main/java/ir/filternet/cfscanner/utils/Utils.kt b/android/app/src/main/java/ir/filternet/cfscanner/utils/Utils.kt index 76956b9e..bfb7afba 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/utils/Utils.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/utils/Utils.kt @@ -3,10 +3,12 @@ package ir.filternet.cfscanner.utils import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.text.TextUtils import android.util.Base64 import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.WriterException @@ -14,6 +16,7 @@ import com.google.zxing.qrcode.QRCodeWriter import ir.filternet.cfscanner.model.Config import ir.filternet.cfscanner.scanner.v2ray.EConfigType import ir.filternet.cfscanner.scanner.v2ray.ServerConfig +import ir.filternet.cfscanner.service.CloudScannerService import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -25,9 +28,20 @@ import java.net.URLEncoder import java.util.* import kotlin.math.pow +fun Context.tryStartForegroundService(serviceClass:Class<*>){ + if(isNotificationEnabled()){ + ContextCompat.startForegroundService(this, Intent(this, serviceClass)) + }else{ + startService(Intent(this, serviceClass)) + } +} fun percep(min: Float, max: Float, now: Float): Float = (now.coerceIn(min, max) - min) / (max - min) +fun ServerConfig.applyNewAddressByISPname(address: String,ispName:String?): ServerConfig? { + val newOutbeans = this.fullConfig?.getByCustomVnextOutbound(address)?.outbounds?.firstOrNull() ?: return null + return this.copy(remarks = remarks.let { "$it|${ispName?:address}" },outboundBean = newOutbeans) +} fun ServerConfig.applyNewAddress(address: String): ServerConfig? { val newOutbeans = this.fullConfig?.getByCustomVnextOutbound(address)?.outbounds?.firstOrNull() ?: return null diff --git a/android/app/src/main/java/ir/filternet/cfscanner/utils/_ExtCompose.kt b/android/app/src/main/java/ir/filternet/cfscanner/utils/_ExtCompose.kt index 4b8ce843..38210d35 100644 --- a/android/app/src/main/java/ir/filternet/cfscanner/utils/_ExtCompose.kt +++ b/android/app/src/main/java/ir/filternet/cfscanner/utils/_ExtCompose.kt @@ -24,6 +24,8 @@ import androidx.lifecycle.LifecycleEventObserver import ir.filternet.cfscanner.service.CloudSpeedService import timber.log.Timber +@Composable +fun isRTL()= LocalLayoutDirection.current == LayoutDirection.Rtl @Composable fun Dp.toPx(): Float = with(LocalDensity.current) { this@toPx.toPx() } diff --git a/android/app/src/main/res/drawable/side_button.xml b/android/app/src/main/res/drawable/side_button.xml new file mode 100644 index 00000000..ff600998 --- /dev/null +++ b/android/app/src/main/res/drawable/side_button.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/android/app/src/main/res/raw/connecting.json b/android/app/src/main/res/raw/connecting.json new file mode 100644 index 00000000..e74f26c4 --- /dev/null +++ b/android/app/src/main/res/raw/connecting.json @@ -0,0 +1 @@ +{"nm":"01 - Wifi","ddd":0,"h":500,"w":500,"meta":{"g":"@lottiefiles/toolkit-js 0.26.1"},"layers":[{"ty":4,"nm":"Line3 Outlines 5","sr":1,"st":195,"op":384,"ip":220,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[186.596,88.467,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,205.612,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":220},{"s":[100],"t":230}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-48.732,0],[-31.716,-31.448]],"o":[[31.8,-32.072],[48.193,0],[0,0]],"v":[[-124.096,25.967],[0.53,-25.967],[124.096,24.906]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[186.596,88.467],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":220},{"s":[100],"t":260}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":0,"ix":1},"m":1}],"ind":1},{"ty":4,"nm":"Line2 Outlines 5","sr":1,"st":195,"op":384,"ip":210,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.595,81.219,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,249.783,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":210},{"s":[100],"t":220}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-35.064,0],[-22.759,-22.491]],"o":[[22.842,-23.115],[34.525,0],[0,0]],"v":[[-89.095,18.719],[0.531,-18.719],[89.095,17.657]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[151.595,81.219],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":210},{"s":[100],"t":250}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":0,"ix":1},"m":1}],"ind":2},{"ty":4,"nm":"Line1 Outlines 5","sr":1,"st":195,"op":384,"ip":200,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[113.409,73.312,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250.999,293.955,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":200},{"s":[100],"t":210}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-20.152,0],[-12.984,-12.719]],"o":[[13.069,-13.343],[19.612,0],[0,0]],"v":[[-50.908,10.811],[0.531,-10.811],[50.908,9.75]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[113.408,73.312],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":200},{"s":[100],"t":240}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":0,"ix":1},"m":1}],"ind":3},{"ty":4,"nm":"Line3 Outlines 3","sr":1,"st":145,"op":241,"ip":170,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[186.596,88.467,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,205.612,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":170},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":180},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":230},{"s":[0],"t":240}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-48.732,0],[-31.716,-31.448]],"o":[[31.8,-32.072],[48.193,0],[0,0]],"v":[[-124.096,25.967],[0.53,-25.967],[124.096,24.906]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[186.596,88.467],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":170},{"s":[100],"t":210}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":200},{"s":[100],"t":240}],"ix":1},"m":1}],"ind":4},{"ty":4,"nm":"Line2 Outlines 3","sr":1,"st":145,"op":231,"ip":160,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.595,81.219,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,249.783,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":160},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":170},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":220},{"s":[0],"t":230}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-35.064,0],[-22.759,-22.491]],"o":[[22.842,-23.115],[34.525,0],[0,0]],"v":[[-89.095,18.719],[0.531,-18.719],[89.095,17.657]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[151.595,81.219],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":160},{"s":[100],"t":200}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":190},{"s":[100],"t":230}],"ix":1},"m":1}],"ind":5},{"ty":4,"nm":"Line1 Outlines 3","sr":1,"st":145,"op":222,"ip":150,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[113.409,73.312,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250.999,293.955,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":150},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":160},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":211},{"s":[0],"t":221}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-20.152,0],[-12.984,-12.719]],"o":[[13.069,-13.343],[19.612,0],[0,0]],"v":[[-50.908,10.811],[0.531,-10.811],[50.908,9.75]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[113.408,73.312],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":150},{"s":[100],"t":190}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":180},{"s":[100],"t":220}],"ix":1},"m":1}],"ind":6},{"ty":4,"nm":"Line3 Outlines 2","sr":1,"st":95,"op":191,"ip":120,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[186.596,88.467,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,205.612,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":120},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":130},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":180},{"s":[0],"t":190}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-48.732,0],[-31.716,-31.448]],"o":[[31.8,-32.072],[48.193,0],[0,0]],"v":[[-124.096,25.967],[0.53,-25.967],[124.096,24.906]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[186.596,88.467],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":120},{"s":[100],"t":160}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":150},{"s":[100],"t":190}],"ix":1},"m":1}],"ind":7},{"ty":4,"nm":"Line2 Outlines 2","sr":1,"st":95,"op":181,"ip":110,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.595,81.219,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,249.783,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":110},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":120},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":170},{"s":[0],"t":180}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-35.064,0],[-22.759,-22.491]],"o":[[22.842,-23.115],[34.525,0],[0,0]],"v":[[-89.095,18.719],[0.531,-18.719],[89.095,17.657]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[151.595,81.219],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":110},{"s":[100],"t":150}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":140},{"s":[100],"t":180}],"ix":1},"m":1}],"ind":8},{"ty":4,"nm":"Line1 Outlines 2","sr":1,"st":95,"op":172,"ip":100,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[113.409,73.312,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250.999,293.955,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":100},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":110},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":161},{"s":[0],"t":171}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-20.152,0],[-12.984,-12.719]],"o":[[13.069,-13.343],[19.612,0],[0,0]],"v":[[-50.908,10.811],[0.531,-10.811],[50.908,9.75]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[113.408,73.312],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":100},{"s":[100],"t":140}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":130},{"s":[100],"t":170}],"ix":1},"m":1}],"ind":9},{"ty":4,"nm":"Line3 Outlines","sr":1,"st":45,"op":141,"ip":70,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[186.596,88.467,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,205.612,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":70},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":80},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":130},{"s":[0],"t":140}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-48.732,0],[-31.716,-31.448]],"o":[[31.8,-32.072],[48.193,0],[0,0]],"v":[[-124.096,25.967],[0.53,-25.967],[124.096,24.906]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[186.596,88.467],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":70},{"s":[100],"t":110}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":100},{"s":[100],"t":140}],"ix":1},"m":1}],"ind":10},{"ty":4,"nm":"Line2 Outlines","sr":1,"st":45,"op":131,"ip":60,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.595,81.219,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,249.783,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":60},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":70},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":120},{"s":[0],"t":130}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-35.064,0],[-22.759,-22.491]],"o":[[22.842,-23.115],[34.525,0],[0,0]],"v":[[-89.095,18.719],[0.531,-18.719],[89.095,17.657]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[151.595,81.219],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":60},{"s":[100],"t":100}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":90},{"s":[100],"t":130}],"ix":1},"m":1}],"ind":11},{"ty":4,"nm":"Line1 Outlines","sr":1,"st":45,"op":122,"ip":50,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[113.409,73.312,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250.999,293.955,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":50},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":60},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":111},{"s":[0],"t":121}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-20.152,0],[-12.984,-12.719]],"o":[[13.069,-13.343],[19.612,0],[0,0]],"v":[[-50.908,10.811],[0.531,-10.811],[50.908,9.75]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[113.408,73.312],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":50},{"s":[100],"t":90}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":80},{"s":[100],"t":120}],"ix":1},"m":1}],"ind":12},{"ty":4,"nm":"Dot Outlines","sr":1,"st":2,"op":384,"ip":50,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[18.75,18.75,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0,"y":1},"s":[0,0,100],"t":50},{"s":[100,100,100],"t":70}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,337,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,-10.217],[10.217,0],[0,10.217],[-10.217,0]],"o":[[0,10.217],[-10.217,0],[0,-10.217],[10.217,0]],"v":[[18.5,0],[0,18.5],[-18.5,0],[0,-18.5]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[18.75,18.75],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":13},{"ty":4,"nm":"Line3 Outlines 6","sr":1,"st":-5,"op":91,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[186.596,88.467,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,205.612,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":80},{"s":[0],"t":90}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-48.732,0],[-31.716,-31.448]],"o":[[31.8,-32.072],[48.193,0],[0,0]],"v":[[-124.096,25.967],[0.53,-25.967],[124.096,24.906]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[0.8902,0.8902,0.8902],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[186.596,88.467],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":50},{"s":[100],"t":90}],"ix":1},"m":1}],"ind":14},{"ty":4,"nm":"Line2 Outlines 6","sr":1,"st":-5,"op":81,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.595,81.219,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,249.783,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":70},{"s":[0],"t":80}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-35.064,0],[-22.759,-22.491]],"o":[[22.842,-23.115],[34.525,0],[0,0]],"v":[[-89.095,18.719],[0.531,-18.719],[89.095,17.657]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[0.8902,0.8902,0.8902],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[151.595,81.219],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":40},{"s":[100],"t":80}],"ix":1},"m":1}],"ind":15},{"ty":4,"nm":"Line1 Outlines 6","sr":1,"st":-5,"op":72,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[113.409,73.312,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250.999,293.955,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":61},{"s":[0],"t":71}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-20.152,0],[-12.984,-12.719]],"o":[[13.069,-13.343],[19.612,0],[0,0]],"v":[[-50.908,10.811],[0.531,-10.811],[50.908,9.75]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":10,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"c":{"a":0,"k":[0.8902,0.8902,0.8902],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[113.408,73.312],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.75,"y":0},"i":{"x":0.25,"y":1},"s":[0],"t":30},{"s":[100],"t":70}],"ix":1},"m":1}],"ind":16},{"ty":4,"nm":"Dot 2 Outlines","sr":1,"st":-48,"op":384,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[18.75,18.75,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[250,337,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,-10.217],[10.217,0],[0,10.217],[-10.217,0]],"o":[[0,10.217],[-10.217,0],[0,-10.217],[10.217,0]],"v":[[18.5,0],[0,18.5],[-18.5,0],[0,-18.5]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.8902,0.8902,0.8902],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[18.75,18.75],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":17}],"v":"5.4.2","fr":48,"op":384,"ip":0,"assets":[]} \ No newline at end of file diff --git a/android/app/src/main/res/values-fa/strings.xml b/android/app/src/main/res/values-fa/strings.xml index 1a7f9573..4ca7f8ba 100644 --- a/android/app/src/main/res/values-fa/strings.xml +++ b/android/app/src/main/res/values-fa/strings.xml @@ -123,6 +123,7 @@ پایان اسکن برای %s/%d اسکن با موفقیت تمام شد. %s (پورت بسته) + %s (محدودیت پینگ) %s (ادرس نامعتبر) %s (اتصال V2ray) %s (خطا اتصال V2ray) @@ -156,4 +157,16 @@ فرایند دانلود آپدیت برنامه به پایان رسید. دریافت آپدیت %s\%% آپدیت آماده نصب است + در انتظار اینترنت + %d مورد رد شد برای %s + پَرش از رنج فعلی + فیلتر پینگ + با تعیین حداکثر پینگ قابل قبول نتایج جستجو را محدود کنید. + پَرش خودکار + برای پرش از رنج های کثیف در صورت کثیف بودن %s از IP ها استفاده می شود. + نسبت مشخصی + اعمال پَرش خودکار برای %s + صفحه اسکنر + صفحه تنظیمات + بازگشت \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2df1b6b6..95fc1258 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -125,6 +125,7 @@ Scan Finished Successfully. %s (Port Not Open) %s (Fronting Error) + %s (Ping Limit) %s (V2ray Cooldown) %s (V2ray Failed!) getting CIDRs from network ... @@ -157,4 +158,16 @@ Update Downloading process finished. Update Downloading %s\%% Update is Read to Install + Waiting for Network + %d IPs skipped for %s + Skip this range + Ping Filter + filter scan result by setting maximum acceptable ping value. + Auto Skip + use to skip dirty CIDR after checking %s of IPs. + specific proportion + Auto skip triggered for %s + Scanner Page + Settings page + Back \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 40432a3e..7e4d4f85 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -12,6 +12,7 @@ dependencyResolutionManagement { mavenCentral() maven { url 'https://maven.google.com' } maven { url 'https://jitpack.io' } + maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } jcenter() // Warning: this repository is going to shut down soo } }