WIP: Migrate UI to Jetpack Compose
This commit is contained in:
parent
3f98551b9d
commit
65838d6905
48 changed files with 1023 additions and 1861 deletions
8
.idea/inspectionProfiles/Project_Default.xml
Normal file
8
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
|
@ -5,6 +5,48 @@
|
||||||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||||
</configurations>
|
</configurations>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="DesignSurface">
|
||||||
|
<option name="filePathToZoomLevelMap">
|
||||||
|
<map>
|
||||||
|
<entry key="../../../../layout/compose-model-1622823449309.xml" value="0.1523829431438127" />
|
||||||
|
<entry key="../../../../layout/compose-model-1622824814438.xml" value="0.33" />
|
||||||
|
<entry key="../../../../layout/compose-model-1622917622395.xml" value="0.2170608108108108" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623014109717.xml" value="0.337037037037037" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623014799493.xml" value="0.10015558148580318" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623015496605.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623015889966.xml" value="0.1072324414715719" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623016793844.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623332506723.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623332556375.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623353007224.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623353013891.xml" value="2.0" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623358629334.xml" value="0.10618436406067679" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623358815178.xml" value="1.6830769230769231" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623359670724.xml" value="0.11413043478260869" />
|
||||||
|
<entry key="../../../../layout/compose-model-1623360495184.xml" value="0.20880752102919348" />
|
||||||
|
<entry key="app/src/main/res/drawable-v26/ic_shortcut_enable.xml" value="0.27447916666666666" />
|
||||||
|
<entry key="app/src/main/res/drawable-v26/ic_shortcut_pause.xml" value="0.27447916666666666" />
|
||||||
|
<entry key="app/src/main/res/drawable/background_splash.xml" value="0.409375" />
|
||||||
|
<entry key="app/src/main/res/drawable/horizontal_rule.xml" value="0.1" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.27447916666666666" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_pause.xml" value="0.409375" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_play.xml" value="0.27447916666666666" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_play_arrow.xml" value="0.27447916666666666" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_settings.xml" value="0.409375" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_shortcut_background.xml" value="0.27447916666666666" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_shortcut_enable.xml" value="0.409375" />
|
||||||
|
<entry key="app/src/main/res/drawable/ic_shortcut_pause.xml" value="0.409375" />
|
||||||
|
<entry key="app/src/main/res/layout/activity_main.xml" value="0.16111111111111112" />
|
||||||
|
<entry key="app/src/main/res/layout/dialog_disable_custom_time.xml" value="0.22708333333333333" />
|
||||||
|
<entry key="app/src/main/res/layout/fragment_add_pi_hole.xml" value="0.3493975903614458" />
|
||||||
|
<entry key="app/src/main/res/layout/fragment_info.xml" value="0.24010416666666667" />
|
||||||
|
<entry key="app/src/main/res/layout/fragment_main.xml" value="0.14175724637681159" />
|
||||||
|
<entry key="app/src/main/res/layout/fragment_retrieve_api_key.xml" value="0.24010416666666667" />
|
||||||
|
<entry key="app/src/main/res/layout/or_divider.xml" value="0.17147922998986828" />
|
||||||
|
<entry key="app/src/main/res/menu/main.xml" value="0.3932291666666667" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -59,14 +59,16 @@ android {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
compose = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":piholeclient"))
|
implementation(project(":piholeclient"))
|
||||||
implementation(libs.bundles.coroutines)
|
implementation(libs.bundles.coroutines)
|
||||||
|
implementation(libs.bundles.compose)
|
||||||
implementation(libs.hilt.android.core)
|
implementation(libs.hilt.android.core)
|
||||||
|
implementation(libs.hilt.navigation.compose)
|
||||||
kapt(libs.hilt.android.kapt)
|
kapt(libs.hilt.android.kapt)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
@ -78,7 +80,5 @@ dependencies {
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.test.ext)
|
androidTestImplementation(libs.test.ext)
|
||||||
androidTestImplementation(libs.espresso)
|
androidTestImplementation(libs.espresso)
|
||||||
implementation(libs.bundles.navigation)
|
|
||||||
implementation(libs.bundles.lifecycle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ package com.wbrawner.pihelper
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.wbrawner.piholeclient.PiHoleApiService
|
import com.wbrawner.piholeclient.PiHoleApiService
|
||||||
import com.wbrawner.piholeclient.VersionResponse
|
import com.wbrawner.piholeclient.VersionResponse
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.ConnectException
|
import java.net.ConnectException
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
@ -48,11 +48,10 @@ class AddPiHelperViewModel @Inject constructor(
|
||||||
apiService.apiKey = this.apiKey
|
apiService.apiKey = this.apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
val piHoleIpAddress = MutableLiveData<String?>()
|
val loadingMessage = MutableStateFlow<String?>(null)
|
||||||
val scanningIp = MutableLiveData<String?>()
|
val errorMessage = MutableStateFlow<String?>(null)
|
||||||
val authenticated = MutableLiveData<Boolean>()
|
|
||||||
|
|
||||||
suspend fun beginScanning(deviceIpAddress: String) {
|
suspend fun beginScanning(deviceIpAddress: String, onSuccess: () -> Unit, onFailure: () -> Unit) {
|
||||||
val addressParts = deviceIpAddress.split(".").toMutableList()
|
val addressParts = deviceIpAddress.split(".").toMutableList()
|
||||||
var chunks = 1
|
var chunks = 1
|
||||||
// If the Pi-hole is correctly set up, then there should be a special host for it as
|
// If the Pi-hole is correctly set up, then there should be a special host for it as
|
||||||
|
@ -71,20 +70,25 @@ class AddPiHelperViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
chunks *= 2
|
chunks *= 2
|
||||||
}
|
}
|
||||||
scan(ipAddresses)
|
if (scan(ipAddresses)) {
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun scan(ipAddresses: MutableList<String>) {
|
private suspend fun scan(ipAddresses: MutableList<String>): Boolean {
|
||||||
if (ipAddresses.isEmpty()) {
|
if (ipAddresses.isEmpty()) {
|
||||||
scanningIp.postValue(null)
|
loadingMessage.value = null
|
||||||
piHoleIpAddress.postValue(null)
|
return false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val ipAddress = ipAddresses.removeAt(0)
|
val ipAddress = ipAddresses.removeAt(0)
|
||||||
scanningIp.postValue(ipAddress)
|
loadingMessage.value = "Scanning $ipAddress..."
|
||||||
if (!connectToIpAddress(ipAddress)) {
|
return if (!connectToIpAddress(ipAddress)) {
|
||||||
scan(ipAddresses)
|
scan(ipAddresses)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,33 +106,32 @@ class AddPiHelperViewModel @Inject constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (version == null) {
|
if (version != null) {
|
||||||
false
|
|
||||||
} else {
|
|
||||||
piHoleIpAddress.postValue(ipAddress)
|
|
||||||
baseUrl = ipAddress
|
baseUrl = ipAddress
|
||||||
true
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun authenticateWithPassword(password: String) {
|
suspend fun authenticateWithPassword(password: String): Boolean {
|
||||||
// The Pi-hole API key is just the web password hashed twice with SHA-256
|
// The Pi-hole API key is just the web password hashed twice with SHA-256
|
||||||
authenticateWithApiKey(password.hash().hash())
|
return authenticateWithApiKey(password.hash().hash())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun authenticateWithApiKey(apiKey: String) {
|
suspend fun authenticateWithApiKey(apiKey: String): Boolean {
|
||||||
// This uses the topItems endpoint to test that the API key is working since it requires
|
// This uses the topItems endpoint to test that the API key is working since it requires
|
||||||
// authentication and is fairly simple to determine whether or not the request was
|
// authentication and is fairly simple to determine whether or not the request was
|
||||||
// successful
|
// successful
|
||||||
|
errorMessage.value = null
|
||||||
apiService.apiKey = apiKey
|
apiService.apiKey = apiKey
|
||||||
try {
|
return try {
|
||||||
apiService.getTopItems()
|
apiService.getTopItems()
|
||||||
this.apiKey = apiKey
|
this.apiKey = apiKey
|
||||||
authenticated.postValue(true)
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Pi-helper", "Unable to authenticate with API key", e)
|
Log.e("Pi-helper", "Unable to authenticate with API key", e)
|
||||||
authenticated.postValue(false)
|
errorMessage.value = "Authentication failed"
|
||||||
throw e
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
package com.wbrawner.pihelper
|
|
||||||
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.wbrawner.pihelper.databinding.FragmentAddPiHoleBinding
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AddPiHoleFragment : Fragment() {
|
|
||||||
|
|
||||||
private val viewModel: AddPiHelperViewModel by activityViewModels()
|
|
||||||
private var _binding: FragmentAddPiHoleBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentAddPiHoleBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
val navController = findNavController()
|
|
||||||
binding.scanNetworkButton.setOnClickListener {
|
|
||||||
navController.navigate(
|
|
||||||
R.id.action_addPiHoleFragment_to_scanNetworkFragment,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
FragmentNavigatorExtras(binding.piHelperLogo to "piHelperLogo")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
binding.ipAddress.setOnEditorActionListener { _, _, _ ->
|
|
||||||
binding.connectButton.performClick()
|
|
||||||
}
|
|
||||||
binding.connectButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
if (viewModel.connectToIpAddress(binding.ipAddress.text.toString())) {
|
|
||||||
navController.navigate(
|
|
||||||
R.id.action_addPiHoleFragment_to_retrieveApiKeyFragment,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
FragmentNavigatorExtras(binding.piHelperLogo to "piHelperLogo")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AlertDialog.Builder(view.context)
|
|
||||||
.setTitle(R.string.connection_failed_title)
|
|
||||||
.setMessage(R.string.connection_failed)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
showProgress(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showProgress(show: Boolean) {
|
|
||||||
if (show) {
|
|
||||||
binding.piHelperLogo.startAnimation(
|
|
||||||
RotateAnimation(
|
|
||||||
0f,
|
|
||||||
360f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f
|
|
||||||
).apply {
|
|
||||||
duration =
|
|
||||||
resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
|
||||||
repeatMode = Animation.RESTART
|
|
||||||
repeatCount = Animation.INFINITE
|
|
||||||
interpolator = LinearInterpolator()
|
|
||||||
fillAfter = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
binding.piHelperLogo.clearAnimation()
|
|
||||||
}
|
|
||||||
binding.connectionForm.visibility = if (show) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
135
app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
Normal file
135
app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddScreen(navController: NavController, addPiHelperViewModel: AddPiHelperViewModel) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val loadingMessage by addPiHelperViewModel.loadingMessage.collectAsState()
|
||||||
|
AddPiholeForm(
|
||||||
|
scanNetwork = { navController.navigate(Screens.SCAN.route) },
|
||||||
|
connectToPihole = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (addPiHelperViewModel.connectToIpAddress(it)) {
|
||||||
|
Log.d("AddScreen", "Connected, going to auth")
|
||||||
|
navController.navigate(Screens.AUTH.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadingMessage = loadingMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddPiholeForm(
|
||||||
|
scanNetwork: () -> Unit,
|
||||||
|
connectToPihole: (String) -> Unit,
|
||||||
|
loadingMessage: String? = null
|
||||||
|
) {
|
||||||
|
val (host: String, setHost: (String) -> Unit) = remember { mutableStateOf("pi.hole") }
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
|
) {
|
||||||
|
LoadingSpinner(loadingMessage != null)
|
||||||
|
Text(
|
||||||
|
text = "If you're not sure what the IP address for your Pi-hole is, Pi-helper can " +
|
||||||
|
"attempt to find it for you by scanning your network.",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
PrimaryButton(text = "Scan Network", onClick = scanNetwork)
|
||||||
|
OrDivider()
|
||||||
|
Text(
|
||||||
|
text = "If you already know the IP address or host of your Pi-hole, you can also " +
|
||||||
|
"enter it below:",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = host,
|
||||||
|
onValueChange = setHost,
|
||||||
|
label = { Text("Pi-hole Host") }
|
||||||
|
)
|
||||||
|
PrimaryButton(text = "Connect to Pi-hole", onClick = { connectToPihole(host) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OrDivider() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(2.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.clip(RectangleShape)
|
||||||
|
.background(MaterialTheme.colors.onSurface),
|
||||||
|
)
|
||||||
|
Text("OR")
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(2.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.clip(RectangleShape)
|
||||||
|
.background(MaterialTheme.colors.onSurface),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun AddPiholeForm_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
AddPiholeForm(scanNetwork = {}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun AddPiholeForm_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
AddPiholeForm(scanNetwork = {}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun OrDivider_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
OrDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun OrDivider_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
OrDivider()
|
||||||
|
}
|
||||||
|
}
|
78
app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
Normal file
78
app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AuthScreen(navController: NavController, addPiHelperViewModel: AddPiHelperViewModel) {
|
||||||
|
val (password: String, setPassword: (String) -> Unit) = remember { mutableStateOf("") }
|
||||||
|
val (apiKey: String, setApiKey: (String) -> Unit) = remember { mutableStateOf("") }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_app_logo),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Pi-helper has successfully connected to your Pi-Hole!",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "You'll need to authenticate in order to enable and disable the Pi-hole.",
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = password,
|
||||||
|
onValueChange = setPassword,
|
||||||
|
label = { Text("Pi-hole Password") },
|
||||||
|
visualTransformation = PasswordVisualTransformation()
|
||||||
|
)
|
||||||
|
PrimaryButton(text = "Authenticate with Password") {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (addPiHelperViewModel.authenticateWithPassword(password)) {
|
||||||
|
navController.navigate(Screens.MAIN.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OrDivider()
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = apiKey,
|
||||||
|
onValueChange = setApiKey,
|
||||||
|
label = { Text("Pi-hole API Key") },
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
)
|
||||||
|
PrimaryButton(text = "Authenticate with API Key") {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (addPiHelperViewModel.authenticateWithApiKey(password)) {
|
||||||
|
navController.navigate(Screens.MAIN.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,87 +0,0 @@
|
||||||
package com.wbrawner.pihelper
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Html
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE
|
|
||||||
import com.wbrawner.pihelper.databinding.FragmentInfoBinding
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class InfoFragment : Fragment() {
|
|
||||||
private val viewModel: AddPiHelperViewModel by activityViewModels()
|
|
||||||
private var _binding: FragmentInfoBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentInfoBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
|
||||||
(activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.action_settings)
|
|
||||||
val html = getString(R.string.content_info)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
binding.infoContent.text = if (Build.VERSION.SDK_INT < 24)
|
|
||||||
Html.fromHtml(html)
|
|
||||||
else
|
|
||||||
Html.fromHtml(html, 0)
|
|
||||||
binding.infoContent.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
binding.forgetPiHoleButton.setOnClickListener {
|
|
||||||
AlertDialog.Builder(view.context)
|
|
||||||
.setTitle(R.string.confirm_forget_pihole)
|
|
||||||
.setMessage(R.string.warning_cannot_be_undone)
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
.setPositiveButton(R.string.action_forget_pihole) { _, _ ->
|
|
||||||
viewModel.forgetPihole()
|
|
||||||
val refreshIntent = Intent(
|
|
||||||
view.context.applicationContext,
|
|
||||||
MainActivity::class.java
|
|
||||||
).apply {
|
|
||||||
action = ACTION_FORGET_PIHOLE
|
|
||||||
addFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
and Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
activity?.startActivity(refreshIntent)
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
if (item.itemId == android.R.id.home) {
|
|
||||||
findNavController().navigateUp()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
88
app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
Normal file
88
app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoScreen(navController: NavController, addPiHelperViewModel: AddPiHelperViewModel) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
|
) {
|
||||||
|
LoadingSpinner()
|
||||||
|
val message = buildAnnotatedString {
|
||||||
|
val text = "Pi-helper was made with ❤ by William Brawner. You can find the source " +
|
||||||
|
"code or report issues on the GitHub page for the project."
|
||||||
|
val name = text.indexOf("William")
|
||||||
|
val github = text.indexOf("GitHub")
|
||||||
|
append(text)
|
||||||
|
addStringAnnotation(
|
||||||
|
"me",
|
||||||
|
annotation = "https://wbrawner.com",
|
||||||
|
start = name,
|
||||||
|
end = name + 15
|
||||||
|
)
|
||||||
|
addStyle(
|
||||||
|
style = SpanStyle(
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
),
|
||||||
|
start = name,
|
||||||
|
end = name + 15
|
||||||
|
)
|
||||||
|
addStringAnnotation(
|
||||||
|
"github",
|
||||||
|
annotation = "https://github.com/wbrawner/pihelper-android",
|
||||||
|
start = github,
|
||||||
|
end = github + 11,
|
||||||
|
)
|
||||||
|
addStyle(
|
||||||
|
style = SpanStyle(
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
),
|
||||||
|
start = github,
|
||||||
|
end = github + 11,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
ClickableText(
|
||||||
|
text = message,
|
||||||
|
style = TextStyle.Default.copy(textAlign = TextAlign.Center)
|
||||||
|
) {
|
||||||
|
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
|
||||||
|
uriHandler.openUri(annotation.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = {
|
||||||
|
addPiHelperViewModel.forgetPihole()
|
||||||
|
navController.navigate(Screens.ADD.route)
|
||||||
|
navController.backQueue.clear()
|
||||||
|
}) {
|
||||||
|
Text(text = "Forget Pi-hole")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,99 +1,109 @@
|
||||||
package com.wbrawner.pihelper
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.navigation.NavController
|
import androidx.compose.animation.core.*
|
||||||
import androidx.navigation.findNavController
|
import androidx.compose.foundation.Image
|
||||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
|
import androidx.compose.material.MaterialTheme
|
||||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
|
import androidx.compose.material.Surface
|
||||||
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
|
import androidx.compose.runtime.Composable
|
||||||
import com.wbrawner.pihelper.databinding.ActivityMainBinding
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val addPiHoleViewModel: AddPiHelperViewModel by viewModels()
|
|
||||||
private val navController: NavController by lazy {
|
|
||||||
findNavController(R.id.content_main)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
setContent {
|
||||||
setContentView(binding.root)
|
val navController = rememberNavController()
|
||||||
window.setBackgroundDrawable(
|
val addPiHoleViewModel: AddPiHelperViewModel = viewModel()
|
||||||
ColorDrawable(
|
val startDestination = when {
|
||||||
ContextCompat.getColor(
|
addPiHoleViewModel.baseUrl == null -> {
|
||||||
this,
|
Screens.ADD
|
||||||
R.color.colorSurface
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val args = when (intent.action) {
|
|
||||||
ACTION_ENABLE -> {
|
|
||||||
if (addPiHoleViewModel.apiKey == null) {
|
|
||||||
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
Bundle().apply { putBoolean(ACTION_ENABLE, true) }
|
|
||||||
}
|
}
|
||||||
}
|
addPiHoleViewModel.apiKey == null -> {
|
||||||
ACTION_DISABLE -> {
|
Screens.AUTH
|
||||||
if (addPiHoleViewModel.apiKey == null) {
|
|
||||||
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
Bundle().apply {
|
|
||||||
putBoolean(ACTION_DISABLE, true)
|
|
||||||
putLong(EXTRA_DURATION, intent.getIntExtra(EXTRA_DURATION, 10).toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ACTION_FORGET_PIHOLE -> {
|
|
||||||
if (intent.component?.packageName == packageName) {
|
|
||||||
while (navController.popBackStack()) {
|
|
||||||
// Do nothing, just pop all the items off the back stack
|
|
||||||
}
|
|
||||||
// Just return an empty bundle so that the navigation branch below will load
|
|
||||||
// the correct screen
|
|
||||||
Bundle()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
when {
|
|
||||||
navController.currentDestination?.id != R.id.placeholder && args == null -> {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addPiHoleViewModel.baseUrl.isNullOrBlank() -> {
|
|
||||||
navController.navigate(R.id.addPiHoleFragment, args)
|
|
||||||
}
|
|
||||||
addPiHoleViewModel.apiKey.isNullOrBlank() -> {
|
|
||||||
navController.navigate(R.id.addPiHoleFragment)
|
|
||||||
navController.navigate(R.id.retrieveApiKeyFragment, args)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
navController.navigate(R.id.mainFragment, args)
|
Screens.MAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PihelperTheme {
|
||||||
|
NavHost(navController, startDestination = startDestination.route) {
|
||||||
|
composable(Screens.ADD.route) {
|
||||||
|
AddScreen(navController, addPiHoleViewModel)
|
||||||
|
}
|
||||||
|
composable(Screens.SCAN.route) {
|
||||||
|
ScanScreen(navController, addPiHoleViewModel)
|
||||||
|
}
|
||||||
|
composable(Screens.AUTH.route) {
|
||||||
|
AuthScreen(
|
||||||
|
navController = navController,
|
||||||
|
addPiHelperViewModel = addPiHoleViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(Screens.MAIN.route) {
|
||||||
|
MainScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable(Screens.INFO.route) {
|
||||||
|
InfoScreen(
|
||||||
|
navController = navController,
|
||||||
|
addPiHelperViewModel = addPiHoleViewModel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (!navController.navigateUp()) {
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
if (navController.currentDestination?.id == R.id.placeholder) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ACTION_FORGET_PIHOLE = "com.wbrawner.pihelper.ACTION_FORGET_PIHOLE"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Screens(val route: String) {
|
||||||
|
ADD("add"),
|
||||||
|
SCAN("scan"),
|
||||||
|
AUTH("auth"),
|
||||||
|
MAIN("main"),
|
||||||
|
INFO("info"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingSpinner(animate: Boolean = false) {
|
||||||
|
val animation = rememberInfiniteTransition()
|
||||||
|
val rotation by animation.animateValue(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 360f,
|
||||||
|
typeConverter = Float.VectorConverter,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.rotate(if (animate) rotation else 0f),
|
||||||
|
painter = painterResource(id = R.drawable.ic_app_logo),
|
||||||
|
contentDescription = "Loading",
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colors.onBackground)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun LoadingSpinner_Preview() {
|
||||||
|
LoadingSpinner()
|
||||||
|
}
|
|
@ -1,216 +0,0 @@
|
||||||
package com.wbrawner.pihelper
|
|
||||||
|
|
||||||
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat.getColor
|
|
||||||
import androidx.core.text.set
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.wbrawner.pihelper.databinding.DialogDisableCustomTimeBinding
|
|
||||||
import com.wbrawner.pihelper.databinding.FragmentMainBinding
|
|
||||||
import com.wbrawner.piholeclient.Status
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MainFragment : Fragment() {
|
|
||||||
private val viewModel: PiHelperViewModel by activityViewModels()
|
|
||||||
private var _binding: FragmentMainBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (arguments?.getBoolean(ACTION_ENABLE) == true) {
|
|
||||||
viewModel.enablePiHole()
|
|
||||||
} else if (arguments?.getBoolean(ACTION_DISABLE) == true) {
|
|
||||||
viewModel.disablePiHole(arguments?.getLong(EXTRA_DURATION))
|
|
||||||
}
|
|
||||||
viewModel.monitorSummary()
|
|
||||||
}
|
|
||||||
viewModel.status.observe(this, {
|
|
||||||
showProgress(false)
|
|
||||||
val (statusColor, statusText) = when (it) {
|
|
||||||
Status.DISABLED -> {
|
|
||||||
binding.enableButton.visibility = View.VISIBLE
|
|
||||||
binding.disableButtons.visibility = View.GONE
|
|
||||||
Pair(R.color.colorDisabled, R.string.status_disabled)
|
|
||||||
}
|
|
||||||
Status.ENABLED -> {
|
|
||||||
binding.enableButton.visibility = View.GONE
|
|
||||||
binding.disableButtons.visibility = View.VISIBLE
|
|
||||||
Pair(R.color.colorEnabled, R.string.status_enabled)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.enableButton.visibility = View.GONE
|
|
||||||
binding.disableButtons.visibility = View.GONE
|
|
||||||
Pair(R.color.colorUnknown, R.string.status_unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val status = getString(statusText)
|
|
||||||
val statusLabel = getString(R.string.label_status, status)
|
|
||||||
val start = statusLabel.indexOf(status)
|
|
||||||
val end = start + status.length
|
|
||||||
val statusSpan = SpannableString(statusLabel)
|
|
||||||
statusSpan[start, end] = StyleSpan(Typeface.BOLD)
|
|
||||||
statusSpan[start, end] =
|
|
||||||
ForegroundColorSpan(getColor(binding.status.context, statusColor))
|
|
||||||
binding.status.text = statusSpan
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentMainBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.main, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
|
||||||
showProgress(true)
|
|
||||||
binding.enableButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.enablePiHole()
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
Log.e("Pi-helper", "Failed to enable Pi-Hole", ignored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.disable10SecondsButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.disablePiHole(10)
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.disable30SecondsButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.disablePiHole(30)
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.disable5MinutesButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.disablePiHole(300)
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.disableCustomTimeButton.setOnClickListener {
|
|
||||||
val dialogView = DialogDisableCustomTimeBinding.inflate(
|
|
||||||
LayoutInflater.from(it.context),
|
|
||||||
view as ViewGroup,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
AlertDialog.Builder(it.context)
|
|
||||||
.setTitle(R.string.action_disable_custom)
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
.setPositiveButton(R.string.action_disable, null)
|
|
||||||
.setView(dialogView.root)
|
|
||||||
.create()
|
|
||||||
.apply {
|
|
||||||
setOnShowListener {
|
|
||||||
getButton(AlertDialog.BUTTON_POSITIVE)
|
|
||||||
.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
try {
|
|
||||||
val rawTime = dialogView.time
|
|
||||||
.text
|
|
||||||
.toString()
|
|
||||||
.toLong()
|
|
||||||
val computedTime =
|
|
||||||
when (dialogView.timeUnit.checkedRadioButtonId) {
|
|
||||||
R.id.seconds -> rawTime
|
|
||||||
R.id.minutes -> rawTime * 60
|
|
||||||
else -> rawTime * 3600
|
|
||||||
}
|
|
||||||
viewModel.disablePiHole(computedTime)
|
|
||||||
dismiss()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
dialogView.time.error = "Failed to disable Pi-hole"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
binding.disablePermanentlyButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.disablePiHole()
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
if (item.itemId == R.id.settings) {
|
|
||||||
findNavController().navigate(R.id.action_mainFragment_to_settingsFragment)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showProgress(show: Boolean) {
|
|
||||||
binding.progressBar.visibility = if (show) {
|
|
||||||
binding.progressBar.startAnimation(RotateAnimation(
|
|
||||||
0f,
|
|
||||||
360f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f
|
|
||||||
).apply {
|
|
||||||
duration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
|
||||||
repeatMode = Animation.RESTART
|
|
||||||
repeatCount = Animation.INFINITE
|
|
||||||
interpolator = LinearInterpolator()
|
|
||||||
fillAfter = true
|
|
||||||
})
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.progressBar.clearAnimation()
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
binding.statusContent.visibility = if (show) {
|
|
||||||
View.GONE
|
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
_binding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ACTION_DISABLE = "com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
|
||||||
const val ACTION_ENABLE = "com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
|
|
||||||
const val EXTRA_DURATION = "com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
|
||||||
}
|
|
||||||
}
|
|
309
app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
Normal file
309
app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||||
|
import com.wbrawner.piholeclient.Status
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(navController: NavController, viewModel: PiHelperViewModel = hiltViewModel()) {
|
||||||
|
LaunchedEffect(key1 = viewModel) {
|
||||||
|
viewModel.monitorSummary()
|
||||||
|
}
|
||||||
|
val status by viewModel.status.collectAsState()
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Pi-helper") },
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
contentColor = MaterialTheme.colors.onBackground,
|
||||||
|
elevation = 0.dp,
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { navController.navigate(Screens.INFO.route) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = "Settings",
|
||||||
|
tint = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
LoadingSpinner(status == Status.LOADING)
|
||||||
|
AnimatedVisibility(visible = status != Status.LOADING) {
|
||||||
|
StatusLabel(status)
|
||||||
|
if (status == Status.ENABLED) {
|
||||||
|
DisableControls(viewModel)
|
||||||
|
} else {
|
||||||
|
EnableControls(viewModel::enablePiHole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun MainScreen_Preview() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
PihelperTheme(false) {
|
||||||
|
MainScreen(navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatusLabel(status: Status) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(color = MaterialTheme.colors.onSurface, text = "Status:")
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
val color = when (status) {
|
||||||
|
Status.ENABLED -> MaterialTheme.colors.secondaryVariant
|
||||||
|
Status.DISABLED -> MaterialTheme.colors.primaryVariant
|
||||||
|
else -> Color(0x00000000)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
color = color,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
text = status.name.toLowerCase(Locale.US).capitalize(Locale.US)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EnableControls(onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
// The strange padding is to work around a bug with the animation
|
||||||
|
.padding(start = 16.dp, top = 48.dp, end = 16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.secondary,
|
||||||
|
contentColor = MaterialTheme.colors.onSecondary
|
||||||
|
),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Text("Enable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisableControls(viewModel: PiHelperViewModel = hiltViewModel()) {
|
||||||
|
val (dialogVisible, setDialogVisible) = remember { mutableStateOf(false) }
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
// The strange padding is to work around a bug with the animation
|
||||||
|
.padding(start = 16.dp, top = 48.dp, end = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
PrimaryButton("Disable for 10 seconds") { viewModel.disablePiHole(10) }
|
||||||
|
PrimaryButton("Disable for 30 seconds") { viewModel.disablePiHole(30) }
|
||||||
|
PrimaryButton("Disable for 5 minutes") { viewModel.disablePiHole(300) }
|
||||||
|
PrimaryButton("Disable for custom time") { /* TODO: Show dialog for custom input */ }
|
||||||
|
PrimaryButton("Disable permanently") { viewModel.disablePiHole() }
|
||||||
|
CustomTimeDialog(dialogVisible, setDialogVisible) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrimaryButton(text: String, onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary,
|
||||||
|
contentColor = MaterialTheme.colors.onPrimary
|
||||||
|
),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Duration {
|
||||||
|
SECONDS,
|
||||||
|
MINUTES,
|
||||||
|
HOURS
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomTimeDialog(
|
||||||
|
visible: Boolean,
|
||||||
|
setVisible: (Boolean) -> Unit,
|
||||||
|
onTimeSelected: (Long) -> Unit
|
||||||
|
) {
|
||||||
|
if (!visible) return
|
||||||
|
val (time: Long, setTime: (Long) -> Unit) = remember { mutableStateOf(0L) }
|
||||||
|
val (duration, selectDuration: (Duration) -> Unit) = remember {
|
||||||
|
mutableStateOf(Duration.SECONDS)
|
||||||
|
}
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { setVisible(false) },
|
||||||
|
buttons = {
|
||||||
|
Row {
|
||||||
|
TextButton({ setVisible(false) }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
TextButton(onClick = {
|
||||||
|
// TODO: Move this math to the viewmodel or repository
|
||||||
|
onTimeSelected(time * (60.0.pow(duration.ordinal)).roundToLong())
|
||||||
|
setVisible(false)
|
||||||
|
}) {
|
||||||
|
Text("Disable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text("Disable for custom time") },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = time.toString(),
|
||||||
|
onValueChange = { setTime(it.toLong()) },
|
||||||
|
placeholder = { Text("Time to disable") }
|
||||||
|
)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DurationRadio(
|
||||||
|
selected = duration == Duration.SECONDS,
|
||||||
|
onClick = { selectDuration(Duration.SECONDS) },
|
||||||
|
text = "Seconds"
|
||||||
|
)
|
||||||
|
DurationRadio(
|
||||||
|
selected = duration == Duration.MINUTES,
|
||||||
|
onClick = { selectDuration(Duration.MINUTES) },
|
||||||
|
text = "Minutes"
|
||||||
|
)
|
||||||
|
DurationRadio(
|
||||||
|
selected = duration == Duration.HOURS,
|
||||||
|
onClick = { selectDuration(Duration.HOURS) },
|
||||||
|
text = "Hours"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DurationRadio(selected: Boolean, onClick: () -> Unit, text: String) {
|
||||||
|
Row(modifier = Modifier.selectable(selected = selected, onClick = onClick)) {
|
||||||
|
RadioButton(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun CustomTimeDialog_Preview() {
|
||||||
|
CustomTimeDialog(true, {}) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun StatusLabelEnabled_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
StatusLabel(Status.ENABLED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun StatusLabelEnabled_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
StatusLabel(Status.ENABLED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun StatusLabelDisabled_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
StatusLabel(Status.DISABLED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun StatusLabelDisabled_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
StatusLabel(Status.DISABLED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun PrimaryButton_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
PrimaryButton("Disable") {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun PrimaryButton_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
PrimaryButton("Disable") {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun EnableControls_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
EnableControls {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun EnableControls_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
EnableControls {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun DisableControls_Preview() {
|
||||||
|
DisableControls()
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
package com.wbrawner.pihelper
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.wbrawner.piholeclient.PiHoleApiService
|
import com.wbrawner.piholeclient.PiHoleApiService
|
||||||
import com.wbrawner.piholeclient.Status
|
import com.wbrawner.piholeclient.Status
|
||||||
import com.wbrawner.piholeclient.StatusProvider
|
import com.wbrawner.piholeclient.StatusProvider
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
@ -15,7 +16,8 @@ import kotlin.coroutines.coroutineContext
|
||||||
class PiHelperViewModel @Inject constructor(
|
class PiHelperViewModel @Inject constructor(
|
||||||
private val apiService: PiHoleApiService
|
private val apiService: PiHoleApiService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val status = MutableLiveData<Status>()
|
private val _status = MutableStateFlow(Status.LOADING)
|
||||||
|
val status = _status.asStateFlow()
|
||||||
private var action: (suspend () -> StatusProvider)? = null
|
private var action: (suspend () -> StatusProvider)? = null
|
||||||
get() = field ?: defaultAction
|
get() = field ?: defaultAction
|
||||||
private var defaultAction = suspend {
|
private var defaultAction = suspend {
|
||||||
|
@ -25,7 +27,7 @@ class PiHelperViewModel @Inject constructor(
|
||||||
suspend fun monitorSummary() {
|
suspend fun monitorSummary() {
|
||||||
while (coroutineContext.isActive) {
|
while (coroutineContext.isActive) {
|
||||||
try {
|
try {
|
||||||
status.postValue(action!!.invoke().status)
|
_status.value = action!!.invoke().status
|
||||||
action = null
|
action = null
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
break
|
break
|
||||||
|
@ -34,13 +36,15 @@ class PiHelperViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun enablePiHole() {
|
fun enablePiHole() {
|
||||||
|
_status.value = Status.LOADING
|
||||||
action = {
|
action = {
|
||||||
apiService.enable()
|
apiService.enable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun disablePiHole(duration: Long? = null) {
|
fun disablePiHole(duration: Long? = null) {
|
||||||
|
_status.value = Status.LOADING
|
||||||
action = {
|
action = {
|
||||||
apiService.disable(duration)
|
apiService.disable(duration)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
package com.wbrawner.pihelper
|
|
||||||
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.transition.TransitionInflater
|
|
||||||
import com.wbrawner.pihelper.databinding.FragmentRetrieveApiKeyBinding
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class RetrieveApiKeyFragment : Fragment() {
|
|
||||||
private val viewModel: AddPiHelperViewModel by activityViewModels()
|
|
||||||
private var _binding: FragmentRetrieveApiKeyBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
sharedElementEnterTransition = TransitionInflater.from(context)
|
|
||||||
.inflateTransition(android.R.transition.move)
|
|
||||||
viewModel.authenticated.observe(this, Observer {
|
|
||||||
if (!it) return@Observer
|
|
||||||
findNavController().navigate(R.id.action_retrieveApiKeyFragment_to_mainFragment)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentRetrieveApiKeyBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.password.setOnEditorActionListener { _, _, _ ->
|
|
||||||
binding.connectWithPasswordButton.performClick()
|
|
||||||
}
|
|
||||||
binding.connectWithPasswordButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.authenticateWithPassword(binding.password.text.toString())
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
Log.e("Pi-helper", "Failed to authenticate with password", ignored)
|
|
||||||
binding.password.error =
|
|
||||||
"Failed to authenticate with given password. Please verify " +
|
|
||||||
"you've entered it correctly and try again."
|
|
||||||
showProgress(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.apiKey.setOnEditorActionListener { _, _, _ ->
|
|
||||||
binding.connectWithApiKeyButton.performClick()
|
|
||||||
}
|
|
||||||
binding.connectWithApiKeyButton.setSuspendingOnClickListener(lifecycleScope) {
|
|
||||||
showProgress(true)
|
|
||||||
try {
|
|
||||||
viewModel.authenticateWithApiKey(binding.apiKey.text.toString())
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
binding.apiKey.error = "Failed to authenticate with given API key. Please verify " +
|
|
||||||
"you've entered it correctly and try again."
|
|
||||||
showProgress(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showProgress(show: Boolean) {
|
|
||||||
if (show) {
|
|
||||||
binding.piHelperLogo.startAnimation(
|
|
||||||
RotateAnimation(
|
|
||||||
0f,
|
|
||||||
360f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f
|
|
||||||
).apply {
|
|
||||||
duration =
|
|
||||||
resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
|
||||||
repeatMode = Animation.RESTART
|
|
||||||
repeatCount = Animation.INFINITE
|
|
||||||
interpolator = LinearInterpolator()
|
|
||||||
fillAfter = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
binding.piHelperLogo.clearAnimation()
|
|
||||||
}
|
|
||||||
binding.authenticationForm.visibility = if (show) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
_binding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,173 +0,0 @@
|
||||||
package com.wbrawner.pihelper
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.transition.Transition
|
|
||||||
import androidx.transition.TransitionInflater
|
|
||||||
import com.wbrawner.pihelper.databinding.FragmentScanNetworkBinding
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.net.Inet4Address
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ScanNetworkFragment : Fragment() {
|
|
||||||
|
|
||||||
private val viewModel: AddPiHelperViewModel by activityViewModels()
|
|
||||||
private var _binding: FragmentScanNetworkBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
sharedElementEnterTransition = TransitionInflater.from(context)
|
|
||||||
.inflateTransition(android.R.transition.move)
|
|
||||||
.addListener(object : Transition.TransitionListener {
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
|
||||||
animatePiHelperLogo()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionResume(transition: Transition) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionPause(transition: Transition) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionCancel(transition: Transition) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionStart(transition: Transition) {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentScanNetworkBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
viewModel.scanningIp.observe(viewLifecycleOwner, Observer {
|
|
||||||
binding.ipAddress.text = it
|
|
||||||
})
|
|
||||||
viewModel.piHoleIpAddress.observe(viewLifecycleOwner, Observer { ipAddress ->
|
|
||||||
if (ipAddress == null) {
|
|
||||||
AlertDialog.Builder(view.context)
|
|
||||||
.setTitle(R.string.scan_failed_title)
|
|
||||||
.setMessage(R.string.scan_failed)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
findNavController().navigateUp()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
return@Observer
|
|
||||||
}
|
|
||||||
binding.piHelperLogo.animation?.let {
|
|
||||||
it.setAnimationListener(object : Animation.AnimationListener {
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
navigateToApiKeyScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
it.repeatCount = 0
|
|
||||||
} ?: navigateToApiKeyScreen()
|
|
||||||
})
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
if (BuildConfig.DEBUG && Build.MODEL == "Android SDK built for x86") {
|
|
||||||
// For emulators, just begin scanning the host machine directly
|
|
||||||
viewModel.beginScanning("10.0.2.2")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
(view.context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager)?.let { connectivityManager ->
|
|
||||||
connectivityManager.allNetworks
|
|
||||||
.filter {
|
|
||||||
connectivityManager.getNetworkCapabilities(it)
|
|
||||||
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|
||||||
?: false
|
|
||||||
}
|
|
||||||
.forEach { network ->
|
|
||||||
connectivityManager.getLinkProperties(network)
|
|
||||||
?.linkAddresses
|
|
||||||
?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address }
|
|
||||||
?.forEach { address ->
|
|
||||||
Log.d(
|
|
||||||
"Pi-helper",
|
|
||||||
"Found link address: ${address.address.hostName}"
|
|
||||||
)
|
|
||||||
viewModel.beginScanning(address.address.hostAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
delay(500)
|
|
||||||
if (binding.piHelperLogo.animation == null) {
|
|
||||||
animatePiHelperLogo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToApiKeyScreen() {
|
|
||||||
val extras = FragmentNavigatorExtras(
|
|
||||||
binding.piHelperLogo to "piHelperLogo"
|
|
||||||
)
|
|
||||||
|
|
||||||
findNavController().navigate(
|
|
||||||
R.id.action_scanNetworkFragment_to_retrieveApiKeyFragment,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
extras
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
binding.piHelperLogo.clearAnimation()
|
|
||||||
_binding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animatePiHelperLogo() {
|
|
||||||
binding.piHelperLogo.startAnimation(
|
|
||||||
RotateAnimation(
|
|
||||||
0f,
|
|
||||||
360f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f,
|
|
||||||
Animation.RELATIVE_TO_SELF,
|
|
||||||
0.5f
|
|
||||||
).apply {
|
|
||||||
duration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
|
||||||
repeatMode = Animation.RESTART
|
|
||||||
repeatCount = Animation.INFINITE
|
|
||||||
interpolator = LinearInterpolator()
|
|
||||||
fillAfter = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
110
app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
Normal file
110
app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.Inet4Address
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScanScreen(navController: NavController, addPiHelperViewModel: AddPiHelperViewModel) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val loadingMessage by addPiHelperViewModel.loadingMessage.collectAsState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(key1 = addPiHelperViewModel) {
|
||||||
|
val onSuccess: () -> Unit = {
|
||||||
|
navController.navigate(Screens.AUTH.route)
|
||||||
|
}
|
||||||
|
val onFailure: () -> Unit = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
if (BuildConfig.DEBUG && Build.MODEL == "Android SDK built for x86") {
|
||||||
|
// For emulators, just begin scanning the host machine directly
|
||||||
|
coroutineScope.launch {
|
||||||
|
addPiHelperViewModel.beginScanning("10.0.2.2", onSuccess, onFailure)
|
||||||
|
}
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
(context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager)?.let { connectivityManager ->
|
||||||
|
connectivityManager.allNetworks
|
||||||
|
.filter {
|
||||||
|
connectivityManager.getNetworkCapabilities(it)
|
||||||
|
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
|
.forEach { network ->
|
||||||
|
connectivityManager.getLinkProperties(network)
|
||||||
|
?.linkAddresses
|
||||||
|
?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address }
|
||||||
|
?.forEach { address ->
|
||||||
|
Log.d(
|
||||||
|
"Pi-helper",
|
||||||
|
"Found link address: ${address.address.hostName}"
|
||||||
|
)
|
||||||
|
addPiHelperViewModel.beginScanning(
|
||||||
|
address.address.hostAddress,
|
||||||
|
onSuccess,
|
||||||
|
onFailure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScanningStatus(loadingMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScanningStatus(
|
||||||
|
loadingMessage: String? = null
|
||||||
|
) {
|
||||||
|
val (host: String, setHost: (String) -> Unit) = remember { mutableStateOf("pi.hole") }
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
|
) {
|
||||||
|
LoadingSpinner(loadingMessage != null)
|
||||||
|
loadingMessage?.let {
|
||||||
|
Text(
|
||||||
|
text = loadingMessage,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun ScanningStatus_Preview() {
|
||||||
|
PihelperTheme(false) {
|
||||||
|
ScanningStatus(loadingMessage = "Scanning 127.0.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun ScanningStatus_DarkPreview() {
|
||||||
|
PihelperTheme(true) {
|
||||||
|
ScanningStatus(loadingMessage = "Scanning 127.0.0.1")
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/java/com/wbrawner/pihelper/ui/Color.kt
Normal file
8
app/src/main/java/com/wbrawner/pihelper/ui/Color.kt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package com.wbrawner.pihelper.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Red500 = Color(0xFFF44336)
|
||||||
|
val Red900 = Color(0xFFB71C1C)
|
||||||
|
val Green500 = Color(0xFF4CAF50)
|
||||||
|
val Green900 = Color(0xFF1B5E20)
|
11
app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt
Normal file
11
app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package com.wbrawner.pihelper.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Shapes
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val Shapes = Shapes(
|
||||||
|
small = RoundedCornerShape(4.dp),
|
||||||
|
medium = RoundedCornerShape(4.dp),
|
||||||
|
large = RoundedCornerShape(0.dp)
|
||||||
|
)
|
47
app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt
Normal file
47
app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package com.wbrawner.pihelper.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.darkColors
|
||||||
|
import androidx.compose.material.lightColors
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
private val DarkColorPalette = darkColors(
|
||||||
|
background = Color.Black,
|
||||||
|
surface = Color.Black,
|
||||||
|
primary = Red500,
|
||||||
|
primaryVariant = Red900,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
secondary = Green500,
|
||||||
|
secondaryVariant = Green900,
|
||||||
|
onSecondary = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightColorPalette = lightColors(
|
||||||
|
primary = Red500,
|
||||||
|
primaryVariant = Red900,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
secondary = Green500,
|
||||||
|
secondaryVariant = Green900,
|
||||||
|
onSecondary = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PihelperTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||||
|
val colors = if (darkTheme) {
|
||||||
|
DarkColorPalette
|
||||||
|
} else {
|
||||||
|
LightColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colors = colors,
|
||||||
|
typography = Typography,
|
||||||
|
shapes = Shapes,
|
||||||
|
content = {
|
||||||
|
Surface(color = MaterialTheme.colors.background, content = content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
16
app/src/main/java/com/wbrawner/pihelper/ui/Type.kt
Normal file
16
app/src/main/java/com/wbrawner/pihelper/ui/Type.kt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package com.wbrawner.pihelper.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography = Typography(
|
||||||
|
body1 = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
)
|
9
app/src/main/res/drawable-v26/ic_shortcut_enable.xml
Normal file
9
app/src/main/res/drawable-v26/ic_shortcut_enable.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_play_arrow"
|
||||||
|
android:inset="15%" />
|
||||||
|
</foreground>
|
||||||
|
<background android:drawable="@color/colorSurface" />
|
||||||
|
</adaptive-icon>
|
9
app/src/main/res/drawable-v26/ic_shortcut_pause.xml
Normal file
9
app/src/main/res/drawable-v26/ic_shortcut_pause.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_pause"
|
||||||
|
android:inset="20%" />
|
||||||
|
</foreground>
|
||||||
|
<background android:drawable="@color/colorSurface" />
|
||||||
|
</adaptive-icon>
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item
|
|
||||||
android:width="500dp"
|
|
||||||
android:height="1dp"
|
|
||||||
android:gravity="center">
|
|
||||||
<color android:color="@color/colorOnSurface" />
|
|
||||||
<shape android:shape="rectangle" />
|
|
||||||
</item>
|
|
||||||
|
|
||||||
</layer-list>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/colorOnSurface"
|
|
||||||
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
|
|
||||||
</vector>
|
|
|
@ -2,8 +2,8 @@
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item>
|
<item>
|
||||||
<shape
|
<shape
|
||||||
android:shape="oval"
|
android:shape="rectangle"
|
||||||
android:tint="@color/colorWhite" />
|
android:tint="@color/colorSurface" />
|
||||||
</item>
|
</item>
|
||||||
<item
|
<item
|
||||||
android:bottom="0dp"
|
android:bottom="0dp"
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item>
|
<item>
|
||||||
<shape
|
<shape
|
||||||
android:shape="oval"
|
android:shape="rectangle"
|
||||||
android:tint="@color/colorWhite" />
|
android:tint="@color/colorSurface" />
|
||||||
</item>
|
</item>
|
||||||
<item
|
<item
|
||||||
android:bottom="0dp"
|
android:bottom="0dp"
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item
|
|
||||||
android:width="1dp"
|
|
||||||
android:height="500dp"
|
|
||||||
android:gravity="center">
|
|
||||||
<color android:color="@color/colorOnSurface" />
|
|
||||||
<shape android:shape="rectangle" />
|
|
||||||
</item>
|
|
||||||
|
|
||||||
</layer-list>
|
|
|
@ -1,118 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView 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:animateLayoutChanges="true"
|
|
||||||
android:fillViewport="true"
|
|
||||||
tools:background="@color/colorSurface"
|
|
||||||
tools:context=".AddPiHoleFragment">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/piHelperLogo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
android:tint="@color/colorOnSurface"
|
|
||||||
android:transitionName="piHelperLogo"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/connectionForm"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/piHelperLogo">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/scanNetwork"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_scan_network"
|
|
||||||
android:textAlignment="center"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/scanNetworkButton"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/scanNetworkButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_scan_network"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/scanNetwork" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/orDivider"
|
|
||||||
layout="@layout/or_divider"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/connectDirectly"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_connect"
|
|
||||||
android:textAlignment="center"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/scanNetworkButton"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/ipAddressContainer"
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_ip_address"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/connectButton"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/connectDirectly"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/ipAddress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionGo"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:text="pi.hole"
|
|
||||||
tools:ignore="HardcodedText" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/connectButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_connect_pihole"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/ipAddressContainer" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</ScrollView>
|
|
|
@ -1,113 +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:animateLayoutChanges="true"
|
|
||||||
tools:background="@color/colorSurface">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/statusContent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="16dp"
|
|
||||||
tools:context=".MainFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/status"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/actionButtons"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Enabled"
|
|
||||||
tools:textColor="@color/colorGreenDark" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/actionButtons"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/status"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/enableButton"
|
|
||||||
style="@style/AppTheme.Button.Green"
|
|
||||||
android:text="@string/action_enable" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/disableButtons"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disable10SecondsButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_10_seconds" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disable30SecondsButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_30_seconds" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disable5MinutesButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_5_minutes" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disableCustomTimeButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_custom" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disablePermanentlyButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_permanently" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/progressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
|
||||||
tools:visibility="gone" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,141 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView 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:animateLayoutChanges="true"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:fillViewport="true"
|
|
||||||
android:padding="16dp"
|
|
||||||
tools:background="@color/colorSurface"
|
|
||||||
tools:context=".RetrieveApiKeyFragment">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minHeight="400dp"
|
|
||||||
android:layout_gravity="center">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/piHelperLogo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/accessibility_description_pi_helper_logo"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
android:tint="@color/colorOnSurface"
|
|
||||||
android:transitionName="piHelperLogo"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/authenticationForm"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/authenticationForm"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintHeight_min="wrap"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/piHelperLogo">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/connectionSuccess"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_connection_success"
|
|
||||||
android:textAlignment="center"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/authRequired"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/authRequired"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_authentication_required"
|
|
||||||
android:textAlignment="center"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/orDivider"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/connectionSuccess" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/passwordContainer"
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_password"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/connectWithPasswordButton"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/orDivider"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionGo"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/connectWithPasswordButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_authenticate_password"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/orDivider"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/passwordContainer" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/orDivider"
|
|
||||||
layout="@layout/or_divider"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHeight_min="200dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/authRequired" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/apiKeyContainer"
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_api_key"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/connectWithApiKeyButton"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/orDivider"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/apiKey"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionGo"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/connectWithApiKeyButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_authenticate_api_key"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/orDivider"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/apiKeyContainer" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</ScrollView>
|
|
|
@ -1,36 +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="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:background="@color/colorSurface">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/vertical_rule"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:text="@string/or"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textAllCaps="true" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/vertical_rule"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/content_main"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
app:defaultNavHost="true"
|
|
||||||
app:navGraph="@navigation/nav_graph"
|
|
||||||
tools:background="@color/colorSurface" />
|
|
|
@ -1,50 +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="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingEnd="16dp"
|
|
||||||
tools:background="@color/colorSurface">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/hint_disable_duration">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/time"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inputType="number|numberSigned" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/timeUnit"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/seconds"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:checked="true"
|
|
||||||
android:text="@string/duration_seconds" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/minutes"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/duration_minutes" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/hours"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/duration_hours" />
|
|
||||||
</RadioGroup>
|
|
||||||
</LinearLayout>
|
|
|
@ -1,78 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
tools:background="@color/colorSurface"
|
|
||||||
tools:context=".AddPiHoleFragment">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/piHelperLogo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
android:tint="@color/colorOnSurface"
|
|
||||||
android:transitionName="piHelperLogo" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/connectionForm"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_scan_network"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/scanNetworkButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_scan_network" />
|
|
||||||
|
|
||||||
<include layout="@layout/or_divider" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_connect"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_ip_address">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/ipAddress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionGo"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:text="pi.hole"
|
|
||||||
tools:ignore="HardcodedText" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/connectButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_connect_pihole" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
|
@ -1,53 +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"
|
|
||||||
tools:background="@color/colorSurface"
|
|
||||||
tools:context=".InfoFragment">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:src="@drawable/ic_app_logo" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/infoContent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/content_info"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/forgetPiHoleButton"
|
|
||||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_forget_pihole" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,96 +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:animateLayoutChanges="true"
|
|
||||||
tools:background="@color/colorSurface">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/statusContent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
tools:context=".MainFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/status"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
tools:text="Enabled"
|
|
||||||
tools:textColor="@color/colorGreenDark" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/enableButton"
|
|
||||||
style="@style/AppTheme.Button.Green"
|
|
||||||
android:text="@string/action_enable" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/disableButtons"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disable10SecondsButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_10_seconds" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disable30SecondsButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_30_seconds" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disable5MinutesButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_5_minutes" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disableCustomTimeButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_custom" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disablePermanentlyButton"
|
|
||||||
style="@style/AppTheme.Button.Red"
|
|
||||||
android:text="@string/action_disable_permanently" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/progressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,102 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView 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:animateLayoutChanges="true"
|
|
||||||
android:fillViewport="true"
|
|
||||||
tools:background="@color/colorSurface"
|
|
||||||
tools:context=".RetrieveApiKeyFragment">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/piHelperLogo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/accessibility_description_pi_helper_logo"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
android:tint="@color/colorOnSurface"
|
|
||||||
android:transitionName="piHelperLogo"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/authenticationForm"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_chainStyle="packed" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/authenticationForm"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/piHelperLogo">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/info_connection_success"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="32dp"
|
|
||||||
android:text="@string/info_authentication_required"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_password">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionGo"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/connectWithPasswordButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_authenticate_password" />
|
|
||||||
|
|
||||||
<include layout="@layout/or_divider" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_api_key">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/apiKey"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionGo"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/connectWithApiKeyButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/action_authenticate_api_key" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
|
@ -1,35 +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="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
tools:background="@color/colorSurface"
|
|
||||||
tools:context=".ScanNetworkFragment">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/piHelperLogo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:src="@drawable/ic_app_logo"
|
|
||||||
android:tint="@color/colorOnSurface"
|
|
||||||
android:transitionName="piHelperLogo"
|
|
||||||
android:contentDescription="@string/accessibility_description_pi_helper_logo" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/ipAddressScanningInfo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/scanning_ip_address"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/ipAddress"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,36 +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="match_parent"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
tools:background="@color/colorSurface">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/horizontal_rule"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:text="@string/or"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textAllCaps="true" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:src="@drawable/horizontal_rule"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
android:id="@+id/settings"
|
|
||||||
android:icon="@drawable/ic_settings"
|
|
||||||
android:title="@string/action_settings"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
</menu>
|
|
|
@ -1,78 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/nav_graph"
|
|
||||||
app:startDestination="@id/placeholder">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/addPiHoleFragment"
|
|
||||||
android:name="com.wbrawner.pihelper.AddPiHoleFragment"
|
|
||||||
android:label="fragment_add_pi_hole"
|
|
||||||
tools:layout="@layout/fragment_add_pi_hole" >
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_addPiHoleFragment_to_scanNetworkFragment"
|
|
||||||
app:destination="@id/scanNetworkFragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_addPiHoleFragment_to_retrieveApiKeyFragment"
|
|
||||||
app:destination="@id/retrieveApiKeyFragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
|
||||||
app:popUpTo="@+id/addPiHoleFragment" />
|
|
||||||
</fragment>
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/placeholder"
|
|
||||||
android:name="androidx.fragment.app.Fragment" />
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/scanNetworkFragment"
|
|
||||||
android:name="com.wbrawner.pihelper.ScanNetworkFragment"
|
|
||||||
android:label="fragment_scan_network"
|
|
||||||
tools:layout="@layout/fragment_scan_network" >
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_scanNetworkFragment_to_retrieveApiKeyFragment"
|
|
||||||
app:destination="@id/retrieveApiKeyFragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
|
||||||
app:popUpTo="@+id/addPiHoleFragment" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/retrieveApiKeyFragment"
|
|
||||||
android:name="com.wbrawner.pihelper.RetrieveApiKeyFragment"
|
|
||||||
android:label="fragment_retrieve_api_key"
|
|
||||||
tools:layout="@layout/fragment_retrieve_api_key" >
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_retrieveApiKeyFragment_to_mainFragment"
|
|
||||||
app:destination="@id/mainFragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/mainFragment"
|
|
||||||
android:name="com.wbrawner.pihelper.MainFragment"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
tools:layout="@layout/fragment_main" >
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_mainFragment_to_settingsFragment"
|
|
||||||
app:destination="@id/settingsFragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/settingsFragment"
|
|
||||||
android:name="com.wbrawner.pihelper.InfoFragment"
|
|
||||||
android:label="@string/action_settings"
|
|
||||||
tools:layout="@layout/fragment_info" />
|
|
||||||
</navigation>
|
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorSurface">#333333</color>
|
<color name="colorSurface">#000000</color>
|
||||||
<color name="colorOnSurface">#f1f1f1</color>
|
<color name="colorOnSurface">#f1f1f1</color>
|
||||||
<color name="colorEnabled">@color/colorGreenLight</color>
|
|
||||||
<color name="colorDisabled">@color/colorRedLight</color>
|
|
||||||
<color name="colorButtonSecondary">#999999</color>
|
|
||||||
</resources>
|
</resources>
|
|
@ -3,11 +3,4 @@
|
||||||
<style name="AppTheme" parent="BaseTheme">
|
<style name="AppTheme" parent="BaseTheme">
|
||||||
<item name="android:windowLightStatusBar">false</item>
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Button.Red" parent="Widget.MaterialComponents.Button">
|
|
||||||
<item name="android:layout_width">match_parent</item>
|
|
||||||
<item name="android:layout_height">wrap_content</item>
|
|
||||||
<item name="backgroundTint">@color/colorRedLight</item>
|
|
||||||
<item name="android:textColor">@color/colorWhite</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,18 +1,15 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">@color/colorRedLight</color>
|
<color name="colorPrimary">@color/colorAccent</color>
|
||||||
<color name="colorPrimaryDark">@color/colorRedDark</color>
|
<color name="colorPrimaryDark">@color/colorRedDark</color>
|
||||||
<color name="colorAccent">@color/colorRedLight</color>
|
<color name="colorAccent">@color/colorRedLight</color>
|
||||||
<color name="colorRedLight">#f60d1a</color>
|
<color name="colorRedLight">#F44336</color>
|
||||||
<color name="colorRedDark">#96060c</color>
|
<color name="colorRedDark">#B71C1C</color>
|
||||||
<color name="colorGreenLight">#30d158</color>
|
<color name="colorGreenLight">#4CAF50</color>
|
||||||
<color name="colorGreenDark">#34c759</color>
|
<color name="colorGreenDark">#1B5E20</color>
|
||||||
|
<color name="colorShortcutBackground">#FFFFFFFF</color>
|
||||||
<color name="colorOnSurface">#000000</color>
|
<color name="colorOnSurface">#000000</color>
|
||||||
<color name="colorWhite">#ffffff</color>
|
<color name="colorWhite">#ffffff</color>
|
||||||
<color name="colorEnabled">@color/colorGreenDark</color>
|
|
||||||
<color name="colorUnknown">@color/colorButtonSecondary</color>
|
|
||||||
<color name="colorDisabled">@color/colorRedDark</color>
|
|
||||||
<color name="colorSurface">@color/colorWhite</color>
|
<color name="colorSurface">@color/colorWhite</color>
|
||||||
<color name="colorTransparent">#00000000</color>
|
<color name="colorTransparent">#00000000</color>
|
||||||
<color name="colorButtonSecondary">#666666</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,50 +1,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Pi-helper</string>
|
<string name="app_name">Pi-helper</string>
|
||||||
<string name="scanning_ip_address">Scanning IP Address:</string>
|
|
||||||
<string name="accessibility_description_pi_helper_logo">Pi-helper Logo</string>
|
|
||||||
<string name="label_status">Status: %1$s</string>
|
|
||||||
<string name="action_enable">Enable</string>
|
<string name="action_enable">Enable</string>
|
||||||
<string name="action_disable">Disable</string>
|
|
||||||
<string name="action_disable_10_seconds">Disable for 10 seconds</string>
|
<string name="action_disable_10_seconds">Disable for 10 seconds</string>
|
||||||
<string name="action_disable_10_seconds_short">Disable for 10 seconds</string>
|
<string name="action_disable_10_seconds_short">Disable for 10 seconds</string>
|
||||||
<string name="action_disable_30_seconds">Disable for 30 seconds</string>
|
<string name="action_disable_30_seconds">Disable for 30 seconds</string>
|
||||||
<string name="action_disable_30_seconds_short">Disable for 30 seconds</string>
|
<string name="action_disable_30_seconds_short">Disable for 30 seconds</string>
|
||||||
<string name="action_disable_5_minutes">Disable for 5 minutes</string>
|
<string name="action_disable_5_minutes">Disable for 5 minutes</string>
|
||||||
<string name="action_disable_5_minutes_short">Disable for 5 minutes</string>
|
<string name="action_disable_5_minutes_short">Disable for 5 minutes</string>
|
||||||
<string name="action_disable_custom">Disable for custom time</string>
|
|
||||||
<string name="action_disable_permanently">Disable Permanently</string>
|
|
||||||
<string name="status_disabled">Disabled</string>
|
|
||||||
<string name="status_enabled">Enabled</string>
|
|
||||||
<string name="scan_failed">Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on and try again, or enter the Pi-Hole\'s IP address manually.</string>
|
|
||||||
<string name="scan_failed_title">Pi-helper failed to find your Pi-Hole</string>
|
|
||||||
<string name="connection_failed">Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on, and that you\'re using the correct IP address and try again.</string>
|
|
||||||
<string name="connection_failed_title">Pi-helper failed to connect to your Pi-Hole</string>
|
|
||||||
<string name="configure_pihelper">Please configure Pi-helper before using shortcuts</string>
|
|
||||||
<string name="or">or</string>
|
|
||||||
<string name="action_settings">Settings</string>
|
|
||||||
<string name="action_forget_pihole">Forget Pi-hole</string>
|
|
||||||
<string name="content_info"><![CDATA[Pi-helper was made with ❤ by <a href=\"https://wbrawner.com\">William Brawner</a>. You can find the source code or report issues on the <a href=\"https://github.com/wbrawner/PiHelperAndroid\">GitHub page</a> for the project.]]></string>
|
|
||||||
<string name="confirm_forget_pihole">Are you sure you want to forget your Pi-hole?</string>
|
|
||||||
<string name="warning_cannot_be_undone">This cannot be undone.</string>
|
|
||||||
<string name="title_crash_notification">Pi-helper Crashed!</string>
|
|
||||||
<string name="text_crash_notification">Would you please consider sending the crash report to me?</string>
|
|
||||||
<string name="channel_crash_notification">Crash Reports</string>
|
|
||||||
<string name="status_unknown">Unknown</string>
|
|
||||||
<string name="duration_seconds">Secs</string>
|
|
||||||
<string name="duration_minutes">Mins</string>
|
|
||||||
<string name="hint_disable_duration">Time to disable</string>
|
|
||||||
<string name="info_scan_network">If you\'re not sure what the IP address for your Pi-Hole is, Pi-helper can attempt to find it for you by scanning your network.</string>
|
|
||||||
<string name="action_scan_network">Scan Network</string>
|
|
||||||
<string name="info_connect">If you already know the IP address or host of your Pi-Hole, you can also enter it below:</string>
|
|
||||||
<string name="prompt_ip_address">Pi-Hole IP Address/Host</string>
|
|
||||||
<string name="action_connect_pihole">Connect to Pi-Hole</string>
|
|
||||||
<string name="info_connection_success">Pi-helper has successfully connected to your Pi-Hole!</string>
|
|
||||||
<string name="info_authentication_required">You\'ll need to authenticate in order to enable and disable the Pi-hole.</string>
|
|
||||||
<string name="prompt_password">Pi-Hole Web Password</string>
|
|
||||||
<string name="action_authenticate_password">Authenticate with Password</string>
|
|
||||||
<string name="prompt_api_key">Pi-Hole API Key</string>
|
|
||||||
<string name="action_authenticate_api_key">Authenticate with API Key</string>
|
|
||||||
<string name="connecting_to_pihole">Connecting to Pi-hole…</string>
|
|
||||||
<string name="action_cancel">Cancel</string>
|
|
||||||
<string name="duration_hours">Hours</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -5,24 +5,9 @@
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
<item name="android:windowBackground">@drawable/background_splash</item>
|
<item name="android:windowBackground">@drawable/background_splash</item>
|
||||||
<item name="android:statusBarColor">@color/colorTransparent</item>
|
<item name="android:statusBarColor">@color/colorTransparent</item>
|
||||||
<item name="android:textColor">@color/colorOnSurface</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme" parent="BaseTheme">
|
<style name="AppTheme" parent="BaseTheme">
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Button" parent="Widget.MaterialComponents.Button">
|
|
||||||
<item name="android:layout_width">match_parent</item>
|
|
||||||
<item name="android:layout_height">wrap_content</item>
|
|
||||||
<item name="android:textColor">@color/colorWhite</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.Button.Green" parent="AppTheme.Button">
|
|
||||||
<item name="backgroundTint">@color/colorGreenDark</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.Button.Red" parent="AppTheme.Button">
|
|
||||||
<item name="backgroundTint">@color/colorAccent</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[versions]
|
[versions]
|
||||||
androidx-core = "1.3.2"
|
androidx-core = "1.3.2"
|
||||||
androidx-appcompat = "1.2.0"
|
androidx-appcompat = "1.2.0"
|
||||||
|
compose = "1.0.0-beta07"
|
||||||
coroutines = "1.4.3"
|
coroutines = "1.4.3"
|
||||||
espresso = "3.3.0"
|
espresso = "3.3.0"
|
||||||
hilt-android = "2.36"
|
hilt-android = "2.36"
|
||||||
|
@ -18,8 +19,15 @@ versionName = "1.0"
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
|
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
|
||||||
|
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
|
||||||
|
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||||
|
compose-activity = { module = "androidx.activity:activity-compose", version = "1.3.0-alpha07" }
|
||||||
|
compose-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
|
||||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
||||||
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
|
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
|
||||||
|
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0-alpha02" }
|
||||||
|
navigation-compose = { module = "androidx.navigation:navigation-compose", version = "2.4.0-alpha01" }
|
||||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||||
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
||||||
|
@ -28,8 +36,6 @@ navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref
|
||||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
||||||
lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
|
||||||
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
|
||||||
junit = { module = "junit:junit", version = "4.12" }
|
junit = { module = "junit:junit", version = "4.12" }
|
||||||
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
|
@ -38,8 +44,7 @@ test-ext = { module = "androidx.test.ext:junit", version = "1.1.2" }
|
||||||
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
compose = ["compose-ui", "compose-material", "compose-tooling", "compose-activity", "navigation-compose"]
|
||||||
coroutines = ["coroutines-core", "coroutines-android"]
|
coroutines = ["coroutines-core", "coroutines-android"]
|
||||||
lifecycle = ["lifecycle-livedata", "lifecycle-viewmodel"]
|
|
||||||
navigation = ["navigation-fragment", "navigation-ui"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,11 @@ enum class Status {
|
||||||
@Json(name = "enabled")
|
@Json(name = "enabled")
|
||||||
ENABLED,
|
ENABLED,
|
||||||
@Json(name = "disabled")
|
@Json(name = "disabled")
|
||||||
DISABLED
|
DISABLED,
|
||||||
|
@Transient
|
||||||
|
LOADING,
|
||||||
|
@Transient
|
||||||
|
UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
Loading…
Reference in a new issue