diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..60eed15
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index a740c72..89ababe 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -5,6 +5,48 @@
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 721fc23..1a060f9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt b/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt
index 3e824ae..e829370 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt
@@ -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()
- val scanningIp = MutableLiveData()
- val authenticated = MutableLiveData()
+ val loadingMessage = MutableStateFlow(null)
+ val errorMessage = MutableStateFlow(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) {
+ private suspend fun scan(ipAddresses: MutableList): 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
}
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt b/app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt
deleted file mode 100644
index cbe6f6b..0000000
--- a/app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt
+++ /dev/null
@@ -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
- }
-}
diff --git a/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt b/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
new file mode 100644
index 0000000..5f11e0e
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt b/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
new file mode 100644
index 0000000..1478af6
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt b/app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt
deleted file mode 100644
index bc4e0e1..0000000
--- a/app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt
+++ /dev/null
@@ -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
- }
-}
diff --git a/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt b/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
new file mode 100644
index 0000000..c06a53e
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
@@ -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")
+ }
+ }
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
index 2bf92d3..f870519 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
@@ -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
+ }
+ addPiHoleViewModel.apiKey == null -> {
+ Screens.AUTH
+ }
+ else -> {
+ Screens.MAIN
}
}
- 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())
+ 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
+ )
}
}
}
- ACTION_FORGET_PIHOLE -> {
- if (intent.component?.packageName == packageName) {
- while (navController.popBackStack()) {
- // Do nothing, just pop all the items off the back stack
- }
- // Just return an empty bundle so that the navigation branch below will load
- // the correct screen
- Bundle()
- } else {
- null
- }
- }
- else -> null
}
- when {
- navController.currentDestination?.id != R.id.placeholder && args == null -> {
- return
- }
- addPiHoleViewModel.baseUrl.isNullOrBlank() -> {
- navController.navigate(R.id.addPiHoleFragment, args)
- }
- addPiHoleViewModel.apiKey.isNullOrBlank() -> {
- navController.navigate(R.id.addPiHoleFragment)
- navController.navigate(R.id.retrieveApiKeyFragment, args)
- }
- else -> {
- navController.navigate(R.id.mainFragment, args)
- }
- }
- }
-
- 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()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainFragment.kt b/app/src/main/java/com/wbrawner/pihelper/MainFragment.kt
deleted file mode 100644
index aa0dc2b..0000000
--- a/app/src/main/java/com/wbrawner/pihelper/MainFragment.kt
+++ /dev/null
@@ -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"
- }
-}
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt b/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
new file mode 100644
index 0000000..bb85343
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
@@ -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()
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt
index 384b405..af61f11 100644
--- a/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt
@@ -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()
+ 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)
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt b/app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt
deleted file mode 100644
index 0d2a2f7..0000000
--- a/app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt
+++ /dev/null
@@ -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()
- }
-}
diff --git a/app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt b/app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt
deleted file mode 100644
index 21e746a..0000000
--- a/app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt
+++ /dev/null
@@ -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
- }
- )
- }
-}
diff --git a/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt b/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
new file mode 100644
index 0000000..0876ab0
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
@@ -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")
+ }
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Color.kt b/app/src/main/java/com/wbrawner/pihelper/ui/Color.kt
new file mode 100644
index 0000000..19502cf
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/ui/Color.kt
@@ -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)
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt b/app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt
new file mode 100644
index 0000000..a9354b7
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt
@@ -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)
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt b/app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt
new file mode 100644
index 0000000..0bcb2bf
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt
@@ -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)
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Type.kt b/app/src/main/java/com/wbrawner/pihelper/ui/Type.kt
new file mode 100644
index 0000000..dde6ff2
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/ui/Type.kt
@@ -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
+ )
+)
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v26/ic_shortcut_enable.xml b/app/src/main/res/drawable-v26/ic_shortcut_enable.xml
new file mode 100644
index 0000000..59967ab
--- /dev/null
+++ b/app/src/main/res/drawable-v26/ic_shortcut_enable.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v26/ic_shortcut_pause.xml b/app/src/main/res/drawable-v26/ic_shortcut_pause.xml
new file mode 100644
index 0000000..9300e9f
--- /dev/null
+++ b/app/src/main/res/drawable-v26/ic_shortcut_pause.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/horizontal_rule.xml b/app/src/main/res/drawable/horizontal_rule.xml
deleted file mode 100644
index 7f702b0..0000000
--- a/app/src/main/res/drawable/horizontal_rule.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- -
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
deleted file mode 100644
index f673399..0000000
--- a/app/src/main/res/drawable/ic_settings.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_shortcut_enable.xml b/app/src/main/res/drawable/ic_shortcut_enable.xml
index 5e57f74..604dbc2 100644
--- a/app/src/main/res/drawable/ic_shortcut_enable.xml
+++ b/app/src/main/res/drawable/ic_shortcut_enable.xml
@@ -2,8 +2,8 @@
-
+ android:shape="rectangle"
+ android:tint="@color/colorSurface" />
-
-
+ android:shape="rectangle"
+ android:tint="@color/colorSurface" />
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout-w800dp/fragment_add_pi_hole.xml b/app/src/main/res/layout-w800dp/fragment_add_pi_hole.xml
deleted file mode 100644
index 8017952..0000000
--- a/app/src/main/res/layout-w800dp/fragment_add_pi_hole.xml
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout-w800dp/fragment_main.xml b/app/src/main/res/layout-w800dp/fragment_main.xml
deleted file mode 100644
index cdda992..0000000
--- a/app/src/main/res/layout-w800dp/fragment_main.xml
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout-w800dp/fragment_retrieve_api_key.xml b/app/src/main/res/layout-w800dp/fragment_retrieve_api_key.xml
deleted file mode 100644
index 87d42bb..0000000
--- a/app/src/main/res/layout-w800dp/fragment_retrieve_api_key.xml
+++ /dev/null
@@ -1,141 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout-w800dp/or_divider.xml b/app/src/main/res/layout-w800dp/or_divider.xml
deleted file mode 100644
index b35939b..0000000
--- a/app/src/main/res/layout-w800dp/or_divider.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 8c354b6..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/dialog_disable_custom_time.xml b/app/src/main/res/layout/dialog_disable_custom_time.xml
deleted file mode 100644
index dc6c14a..0000000
--- a/app/src/main/res/layout/dialog_disable_custom_time.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_add_pi_hole.xml b/app/src/main/res/layout/fragment_add_pi_hole.xml
deleted file mode 100644
index a34476a..0000000
--- a/app/src/main/res/layout/fragment_add_pi_hole.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml
deleted file mode 100644
index 7f7bbb9..0000000
--- a/app/src/main/res/layout/fragment_info.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
deleted file mode 100644
index fdf3abc..0000000
--- a/app/src/main/res/layout/fragment_main.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_retrieve_api_key.xml b/app/src/main/res/layout/fragment_retrieve_api_key.xml
deleted file mode 100644
index d9f9d84..0000000
--- a/app/src/main/res/layout/fragment_retrieve_api_key.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_scan_network.xml b/app/src/main/res/layout/fragment_scan_network.xml
deleted file mode 100644
index 408f2cb..0000000
--- a/app/src/main/res/layout/fragment_scan_network.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/or_divider.xml b/app/src/main/res/layout/or_divider.xml
deleted file mode 100644
index fd429ef..0000000
--- a/app/src/main/res/layout/or_divider.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
deleted file mode 100644
index 9cc65f6..0000000
--- a/app/src/main/res/menu/main.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
deleted file mode 100644
index da9eb6f..0000000
--- a/app/src/main/res/navigation/nav_graph.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 175ae8f..578d767 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -1,8 +1,5 @@
- #333333
+ #000000
#f1f1f1
- @color/colorGreenLight
- @color/colorRedLight
- #999999
\ No newline at end of file
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
index c754cb1..6bb7e56 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values-night/styles.xml
@@ -3,11 +3,4 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3faa826..e0ec3a2 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,18 +1,15 @@
- @color/colorRedLight
+ @color/colorAccent
@color/colorRedDark
@color/colorRedLight
- #f60d1a
- #96060c
- #30d158
- #34c759
+ #F44336
+ #B71C1C
+ #4CAF50
+ #1B5E20
+ #FFFFFFFF
#000000
#ffffff
- @color/colorGreenDark
- @color/colorButtonSecondary
- @color/colorRedDark
@color/colorWhite
#00000000
- #666666
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5106b73..bede7b7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,50 +1,10 @@
Pi-helper
- Scanning IP Address:
- Pi-helper Logo
- Status: %1$s
Enable
- Disable
Disable for 10 seconds
Disable for 10 seconds
Disable for 30 seconds
Disable for 30 seconds
Disable for 5 minutes
Disable for 5 minutes
- Disable for custom time
- Disable Permanently
- Disabled
- Enabled
- 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.
- Pi-helper failed to find your Pi-Hole
- 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.
- Pi-helper failed to connect to your Pi-Hole
- Please configure Pi-helper before using shortcuts
- or
- Settings
- Forget Pi-hole
- William Brawner. You can find the source code or report issues on the GitHub page for the project.]]>
- Are you sure you want to forget your Pi-hole?
- This cannot be undone.
- Pi-helper Crashed!
- Would you please consider sending the crash report to me?
- Crash Reports
- Unknown
- Secs
- Mins
- Time to disable
- 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.
- Scan Network
- If you already know the IP address or host of your Pi-Hole, you can also enter it below:
- Pi-Hole IP Address/Host
- Connect to Pi-Hole
- Pi-helper has successfully connected to your Pi-Hole!
- You\'ll need to authenticate in order to enable and disable the Pi-hole.
- Pi-Hole Web Password
- Authenticate with Password
- Pi-Hole API Key
- Authenticate with API Key
- Connecting to Pi-hole…
- Cancel
- Hours
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 2551663..47db077 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -5,24 +5,9 @@
- @color/colorAccent
- @drawable/background_splash
- @color/colorTransparent
- - @color/colorOnSurface
-
-
-
-
-
-
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c94defd..4ad83e9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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"]
diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt
index 91061a1..c6bb17c 100644
--- a/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt
+++ b/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt
@@ -48,7 +48,11 @@ enum class Status {
@Json(name = "enabled")
ENABLED,
@Json(name = "disabled")
- DISABLED
+ DISABLED,
+ @Transient
+ LOADING,
+ @Transient
+ UNKNOWN
}
@JsonClass(generateAdapter = true)