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" />
|
||||
</configurations>
|
||||
</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">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
|
|
|
@ -59,14 +59,16 @@ android {
|
|||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":piholeclient"))
|
||||
implementation(libs.bundles.coroutines)
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.hilt.android.core)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
kapt(libs.hilt.android.kapt)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
@ -78,7 +80,5 @@ dependencies {
|
|||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.test.ext)
|
||||
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.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.piholeclient.PiHoleApiService
|
||||
import com.wbrawner.piholeclient.VersionResponse
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
|
@ -48,11 +48,10 @@ class AddPiHelperViewModel @Inject constructor(
|
|||
apiService.apiKey = this.apiKey
|
||||
}
|
||||
|
||||
val piHoleIpAddress = MutableLiveData<String?>()
|
||||
val scanningIp = MutableLiveData<String?>()
|
||||
val authenticated = MutableLiveData<Boolean>()
|
||||
val loadingMessage = MutableStateFlow<String?>(null)
|
||||
val errorMessage = MutableStateFlow<String?>(null)
|
||||
|
||||
suspend fun beginScanning(deviceIpAddress: String) {
|
||||
suspend fun beginScanning(deviceIpAddress: String, onSuccess: () -> Unit, onFailure: () -> Unit) {
|
||||
val addressParts = deviceIpAddress.split(".").toMutableList()
|
||||
var chunks = 1
|
||||
// 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
|
||||
}
|
||||
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()) {
|
||||
scanningIp.postValue(null)
|
||||
piHoleIpAddress.postValue(null)
|
||||
return
|
||||
loadingMessage.value = null
|
||||
return false
|
||||
}
|
||||
|
||||
val ipAddress = ipAddresses.removeAt(0)
|
||||
scanningIp.postValue(ipAddress)
|
||||
if (!connectToIpAddress(ipAddress)) {
|
||||
loadingMessage.value = "Scanning $ipAddress..."
|
||||
return if (!connectToIpAddress(ipAddress)) {
|
||||
scan(ipAddresses)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,33 +106,32 @@ class AddPiHelperViewModel @Inject constructor(
|
|||
null
|
||||
}
|
||||
}
|
||||
return if (version == null) {
|
||||
false
|
||||
} else {
|
||||
piHoleIpAddress.postValue(ipAddress)
|
||||
if (version != null) {
|
||||
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
|
||||
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
|
||||
// authentication and is fairly simple to determine whether or not the request was
|
||||
// successful
|
||||
errorMessage.value = null
|
||||
apiService.apiKey = apiKey
|
||||
try {
|
||||
return try {
|
||||
apiService.getTopItems()
|
||||
this.apiKey = apiKey
|
||||
authenticated.postValue(true)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("Pi-helper", "Unable to authenticate with API key", e)
|
||||
authenticated.postValue(false)
|
||||
throw e
|
||||
errorMessage.value = "Authentication failed"
|
||||
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
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
|
||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
|
||||
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
|
||||
import com.wbrawner.pihelper.databinding.ActivityMainBinding
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@AndroidEntryPoint
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
window.setBackgroundDrawable(
|
||||
ColorDrawable(
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
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) }
|
||||
setContent {
|
||||
val navController = rememberNavController()
|
||||
val addPiHoleViewModel: AddPiHelperViewModel = viewModel()
|
||||
val startDestination = when {
|
||||
addPiHoleViewModel.baseUrl == null -> {
|
||||
Screens.ADD
|
||||
}
|
||||
}
|
||||
ACTION_DISABLE -> {
|
||||
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)
|
||||
addPiHoleViewModel.apiKey == null -> {
|
||||
Screens.AUTH
|
||||
}
|
||||
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
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.piholeclient.PiHoleApiService
|
||||
import com.wbrawner.piholeclient.Status
|
||||
import com.wbrawner.piholeclient.StatusProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
@ -15,7 +16,8 @@ import kotlin.coroutines.coroutineContext
|
|||
class PiHelperViewModel @Inject constructor(
|
||||
private val apiService: PiHoleApiService
|
||||
) : ViewModel() {
|
||||
val status = MutableLiveData<Status>()
|
||||
private val _status = MutableStateFlow(Status.LOADING)
|
||||
val status = _status.asStateFlow()
|
||||
private var action: (suspend () -> StatusProvider)? = null
|
||||
get() = field ?: defaultAction
|
||||
private var defaultAction = suspend {
|
||||
|
@ -25,7 +27,7 @@ class PiHelperViewModel @Inject constructor(
|
|||
suspend fun monitorSummary() {
|
||||
while (coroutineContext.isActive) {
|
||||
try {
|
||||
status.postValue(action!!.invoke().status)
|
||||
_status.value = action!!.invoke().status
|
||||
action = null
|
||||
} catch (ignored: Exception) {
|
||||
break
|
||||
|
@ -34,13 +36,15 @@ class PiHelperViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun enablePiHole() {
|
||||
fun enablePiHole() {
|
||||
_status.value = Status.LOADING
|
||||
action = {
|
||||
apiService.enable()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disablePiHole(duration: Long? = null) {
|
||||
fun disablePiHole(duration: Long? = null) {
|
||||
_status.value = Status.LOADING
|
||||
action = {
|
||||
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">
|
||||
<item>
|
||||
<shape
|
||||
android:shape="oval"
|
||||
android:tint="@color/colorWhite" />
|
||||
android:shape="rectangle"
|
||||
android:tint="@color/colorSurface" />
|
||||
</item>
|
||||
<item
|
||||
android:bottom="0dp"
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape
|
||||
android:shape="oval"
|
||||
android:tint="@color/colorWhite" />
|
||||
android:shape="rectangle"
|
||||
android:tint="@color/colorSurface" />
|
||||
</item>
|
||||
<item
|
||||
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"?>
|
||||
<resources>
|
||||
<color name="colorSurface">#333333</color>
|
||||
<color name="colorSurface">#000000</color>
|
||||
<color name="colorOnSurface">#f1f1f1</color>
|
||||
<color name="colorEnabled">@color/colorGreenLight</color>
|
||||
<color name="colorDisabled">@color/colorRedLight</color>
|
||||
<color name="colorButtonSecondary">#999999</color>
|
||||
</resources>
|
|
@ -3,11 +3,4 @@
|
|||
<style name="AppTheme" parent="BaseTheme">
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</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>
|
|
@ -1,18 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">@color/colorRedLight</color>
|
||||
<color name="colorPrimary">@color/colorAccent</color>
|
||||
<color name="colorPrimaryDark">@color/colorRedDark</color>
|
||||
<color name="colorAccent">@color/colorRedLight</color>
|
||||
<color name="colorRedLight">#f60d1a</color>
|
||||
<color name="colorRedDark">#96060c</color>
|
||||
<color name="colorGreenLight">#30d158</color>
|
||||
<color name="colorGreenDark">#34c759</color>
|
||||
<color name="colorRedLight">#F44336</color>
|
||||
<color name="colorRedDark">#B71C1C</color>
|
||||
<color name="colorGreenLight">#4CAF50</color>
|
||||
<color name="colorGreenDark">#1B5E20</color>
|
||||
<color name="colorShortcutBackground">#FFFFFFFF</color>
|
||||
<color name="colorOnSurface">#000000</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="colorTransparent">#00000000</color>
|
||||
<color name="colorButtonSecondary">#666666</color>
|
||||
</resources>
|
||||
|
|
|
@ -1,50 +1,10 @@
|
|||
<resources>
|
||||
<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_disable">Disable</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_30_seconds">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_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>
|
||||
|
|
|
@ -5,24 +5,9 @@
|
|||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowBackground">@drawable/background_splash</item>
|
||||
<item name="android:statusBarColor">@color/colorTransparent</item>
|
||||
<item name="android:textColor">@color/colorOnSurface</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="BaseTheme">
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</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>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
[versions]
|
||||
androidx-core = "1.3.2"
|
||||
androidx-appcompat = "1.2.0"
|
||||
compose = "1.0.0-beta07"
|
||||
coroutines = "1.4.3"
|
||||
espresso = "3.3.0"
|
||||
hilt-android = "2.36"
|
||||
|
@ -18,8 +19,15 @@ versionName = "1.0"
|
|||
[libraries]
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
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-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" }
|
||||
moshi-core = { module = "com.squareup.moshi:moshi", 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-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
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" }
|
||||
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", 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" }
|
||||
|
||||
[bundles]
|
||||
compose = ["compose-ui", "compose-material", "compose-tooling", "compose-activity", "navigation-compose"]
|
||||
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")
|
||||
ENABLED,
|
||||
@Json(name = "disabled")
|
||||
DISABLED
|
||||
DISABLED,
|
||||
@Transient
|
||||
LOADING,
|
||||
@Transient
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
Loading…
Reference in a new issue