Migrate main activity to jetpack

This commit is contained in:
sim 2024-11-15 14:44:35 +00:00
parent 1d206c92f8
commit 9ac56626cb
30 changed files with 1548 additions and 649 deletions

View File

@ -1,13 +1,15 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
kotlin {
jvmToolchain(17)
alias(libs.plugins.compose.compiler)
}
android {
compileOptions {
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
}
compileSdk = 35
defaultConfig {
@ -18,6 +20,10 @@ android {
versionName = "1.9.0"
}
buildFeatures {
compose = true
}
buildTypes {
getByName("release") {
resValue("string", "app_name", "NextPush")
@ -57,10 +63,11 @@ if (project.hasProperty("sign")) {
}
}
dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.coordinatorlayout)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.appcompat)
implementation(libs.kotlin.stdlib)
@ -72,4 +79,7 @@ dependencies {
implementation(libs.retrofit.retrofit)
implementation(libs.rxjava3.rxandroid)
implementation(libs.rxjava3.rxjava)
implementation(libs.androidx.material3.android)
debugImplementation(libs.androidx.ui.tooling.preview.android)
debugImplementation(libs.androidx.ui.tooling)
}

View File

@ -34,11 +34,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.MainActivity"
android:theme="@style/Theme.NextPush.NoActionBar">
<activity android:name=".activities.MainActivity">
</activity>
<activity android:name=".activities.LinkActivity"
android:exported="true">
<intent-filter>

View File

@ -0,0 +1,28 @@
package org.unifiedpush.distributor.nextpush
import android.util.Log
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterIsInstance
object EventBus {
val mutEvents: MutableSharedFlow<Any> = MutableSharedFlow()
val events = mutEvents.asSharedFlow()
suspend inline fun <reified T : Any> publish(event: T) {
if (mutEvents.subscriptionCount.value > 0) {
mutEvents.emit(event)
}
}
suspend inline fun <reified T> subscribe(crossinline onEvent: (T) -> Unit) {
events.filterIsInstance<T>()
.collectLatest { event ->
coroutineContext.ensureActive()
onEvent(event)
}
}
}

View File

@ -0,0 +1,125 @@
package org.unifiedpush.distributor.nextpush.activities
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.unifiedpush.distributor.nextpush.AppStore
import org.unifiedpush.distributor.nextpush.EventBus
import org.unifiedpush.distributor.nextpush.LocalNotification
import org.unifiedpush.distributor.nextpush.account.AccountFactory
import org.unifiedpush.distributor.nextpush.distributor.Distributor
import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteApp
import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteDevice
import org.unifiedpush.distributor.nextpush.services.FailureHandler
import org.unifiedpush.distributor.nextpush.services.RestartWorker
import org.unifiedpush.distributor.nextpush.services.StartService
import org.unifiedpush.distributor.nextpush.utils.TAG
class AppAction(private val type: Type, private val argv: Map<String, Any>? = null) {
enum class Type {
RestartService,
Logout,
AddChannel,
DisableBatteryOptimisation,
CopyEndpoint,
DeleteRegistration,
}
fun handle(context: Context) {
when (type) {
Type.RestartService -> restartService(context)
Type.Logout -> logout(context)
Type.AddChannel -> addChannel(context, argv)
Type.DisableBatteryOptimisation -> disableBatteryOptimisation(context)
Type.CopyEndpoint -> copyEndpoint(context, argv)
Type.DeleteRegistration -> deleteRegistration(context, argv)
}
}
private fun restartService(context: Context) {
Log.d(TAG, "Restarting the Listener")
FailureHandler.clearFails()
StartService.stopService {
RestartWorker.run(context, delay = 0)
}
}
private fun logout(context: Context) {
deleteDevice(context) {
StartService.stopService()
FailureHandler.clearFails()
}
AccountFactory.logout(context)
AppStore(context).wipe()
UiAction.publish(UiAction.Type.Logout)
}
private fun addChannel(context: Context, argv: Map<String, Any>?) {
(argv?.get(ARG_NEW_CHANNEL_TITLE) as String?)?.let {
LocalNotification.createChannel(
context,
it
) {
Log.d(TAG, "Channel \"$it\" created")
UiAction.publish(UiAction.Type.UpdateRegistrations)
}
}
}
@SuppressLint("BatteryLife")
private fun disableBatteryOptimisation(context: Context) {
Log.d(TAG, "Disabling battery optimization")
try {
context.startActivity(
Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${context.packageName}")
)
)
} catch (e: ActivityNotFoundException) {
try {
context.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
} catch (e2: ActivityNotFoundException) {
context.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
}
private fun copyEndpoint(context: Context, argv: Map<String, Any>?) {
val token = argv?.get(ARG_TOKEN) as String? ?: return
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText(
"Endpoint",
Distributor.getEndpoint(context, token)
)
clipboard.setPrimaryClip(clip)
}
private fun deleteRegistration(context: Context, argv: Map<String, Any>?) {
val registrations = argv?.get(ARG_REGISTRATIONS) as List<String>? ?: return
registrations?.forEach {
deleteApp(context, it) {}
}
}
companion object {
const val ARG_NEW_CHANNEL_TITLE = "title"
const val ARG_TOKEN = "token"
const val ARG_REGISTRATIONS = "registrations"
}
}
fun ViewModel.publishAction(action: AppAction) {
viewModelScope.launch {
EventBus.publish(action)
}
}

View File

@ -1,95 +0,0 @@
package org.unifiedpush.distributor.nextpush.activities
import android.content.Context
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.core.view.isGone
import com.google.android.material.color.MaterialColors
import org.unifiedpush.distributor.nextpush.Database.Companion.getDb
import org.unifiedpush.distributor.nextpush.R
import org.unifiedpush.distributor.nextpush.utils.getApplicationName
data class App(
val token: String,
val packageId: String
)
class AppListAdapter(context: Context, private val resource: Int, apps: List<App>) : ArrayAdapter<App>(context, resource, apps) {
private var selectedItemsIds = SparseBooleanArray()
private val inflater = LayoutInflater.from(context)
private val db = getDb(context)
private class ViewHolder {
var name: TextView? = null
var description: TextView? = null
}
override fun getView(position: Int, pConvertView: View?, parent: ViewGroup): View {
var viewHolder: ViewHolder? = null
val convertView = pConvertView?.apply {
viewHolder = tag as ViewHolder
} ?: run {
val rConvertView = inflater.inflate(resource, parent, false)
viewHolder = ViewHolder().apply {
this.name = rConvertView.findViewById(R.id.item_app_name) as TextView
this.description = rConvertView.findViewById(R.id.item_description) as TextView
}
rConvertView.apply {
tag = viewHolder
}
}
getItem(position)?.let {
if (it.packageId == context.packageName) {
setViewHolderForLocalChannel(viewHolder, it)
} else {
setViewHolderForUnifiedPushApp(viewHolder, it)
}
}
if (selectedItemsIds.get(position)) {
convertView?.setBackgroundColor(
MaterialColors.getColor(convertView, com.google.android.material.R.attr.colorOnTertiary)
)
} else {
convertView?.setBackgroundResource(0)
}
return convertView
}
private fun setViewHolderForUnifiedPushApp(viewHolder: ViewHolder?, app: App) {
context.getApplicationName(app.packageId)?.let {
viewHolder?.name?.text = it
viewHolder?.description?.text = app.packageId
} ?: run {
viewHolder?.name?.text = app.packageId
viewHolder?.description?.isGone = true
}
}
private fun setViewHolderForLocalChannel(viewHolder: ViewHolder?, app: App) {
val title = db.getNotificationTitle(app.token)
viewHolder?.name?.text = context.getString(R.string.local_notif_title).format(title)
viewHolder?.description?.text = context.getString(R.string.local_notif_description)
}
fun toggleSelection(position: Int) {
selectView(position, !selectedItemsIds.get(position))
}
fun removeSelection() {
selectedItemsIds = SparseBooleanArray()
notifyDataSetChanged()
}
private fun selectView(position: Int, value: Boolean) {
selectedItemsIds.put(position, value)
notifyDataSetChanged()
}
fun getSelectedIds(): SparseBooleanArray {
return selectedItemsIds
}
}

View File

@ -1,366 +1,82 @@
package org.unifiedpush.distributor.nextpush.activities
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.text.Html
import android.text.InputType
import android.text.SpannableStringBuilder
import android.util.Log
import android.util.TypedValue
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.* // ktlint-disable no-wildcard-imports
import android.widget.AbsListView.MultiChoiceModeListener
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.util.size
import androidx.core.view.isGone
import androidx.core.view.setPadding
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.unifiedpush.distributor.nextpush.AppStore
import org.unifiedpush.distributor.nextpush.Database.Companion.getDb
import org.unifiedpush.distributor.nextpush.LocalNotification
import org.unifiedpush.distributor.nextpush.R
import org.unifiedpush.distributor.nextpush.account.AccountFactory
import org.unifiedpush.distributor.nextpush.activities.PermissionsRequest.requestAppPermissions
import org.unifiedpush.distributor.nextpush.activities.StartActivity.Companion.goToStartActivity
import org.unifiedpush.distributor.nextpush.distributor.Distributor
import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteApp
import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteDevice
import org.unifiedpush.distributor.nextpush.services.FailureHandler
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.unifiedpush.distributor.nextpush.EventBus
import org.unifiedpush.distributor.nextpush.activities.ui.MainUi
import org.unifiedpush.distributor.nextpush.activities.ui.theme.AppTheme
import org.unifiedpush.distributor.nextpush.services.RestartWorker
import org.unifiedpush.distributor.nextpush.services.StartService
import org.unifiedpush.distributor.nextpush.utils.TAG
import org.unifiedpush.distributor.nextpush.utils.copyToClipboard
import org.unifiedpush.distributor.nextpush.utils.getDebugInfo
import java.lang.String.format
class MainActivity : AppCompatActivity() {
private lateinit var listView: ListView
// if the unregister dialog is shown, we prevent the list to be reset
private var preventListReset = false
private var lastClickTime = 0L
private var clickCount = 0
class MainActivity : ComponentActivity() {
private var viewModel: MainViewModel? = null
private var jobs: MutableList<Job> = emptyList<Job>().toMutableList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
this.requestAppPermissions()
if (AccountFactory.getAccount(this)?.connected != true) {
Log.d(TAG, "Not connected: going to StartActivity.")
goToStartActivity(this)
finish()
}
findViewById<TextView>(R.id.main_account_desc).text =
format(getString(R.string.main_account_desc), AccountFactory.getAccount(this)?.name)
invalidateOptionsMenu()
RestartWorker.startPeriodic(this)
setDebugInformationListener()
findViewById<View>(android.R.id.content)?.setOnApplyWindowInsetsListener { _, insets ->
val statusBarSize = insets.systemWindowInsetTop
findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar).setPadding(0, statusBarSize , 0, 0)
return@setOnApplyWindowInsetsListener insets
setContent {
val viewModel =
viewModel {
MainViewModel(this@MainActivity)
}.also {
viewModel = it
}
AppTheme {
MainUi(viewModel)
}
subscribeActions()
}
}
override fun onStart() {
super.onStart()
showOptimisationWarning()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
if (preventListReset) {
preventListReset = false
} else {
setListView()
}
private fun subscribeActions() {
Log.d(TAG, "Subscribing to actions")
jobs += CoroutineScope(Dispatchers.IO).launch {
EventBus.subscribe<AppAction> { it.handle(this@MainActivity) }
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_restart -> {
restart()
return true
}
R.id.action_logout -> {
logout()
return true
}
R.id.action_add_local_channel -> {
addChannel()
return true
}
else -> super.onOptionsItemSelected(item)
}
}
@SuppressLint("BatteryLife")
private fun showOptimisationWarning() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
findViewById<MaterialCardView>(R.id.card_battery_optimization)?.isGone = false
findViewById<Button>(R.id.button_disable_optimisation)?.setOnClickListener {
try {
startActivity(
Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:$packageName")
jobs += CoroutineScope(Dispatchers.IO).launch {
EventBus.subscribe<UiAction> {
it.handle { type ->
when (type) {
UiAction.Type.UpdateRegistrations -> viewModel?.updateRegistrations(
this@MainActivity
)
)
} catch (e: ActivityNotFoundException) {
try {
startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
} catch (e2: ActivityNotFoundException) {
startActivity(Intent(Settings.ACTION_SETTINGS))
UiAction.Type.Logout -> {
StartActivity.goToStartActivity(this@MainActivity)
finish()
}
}
}
findViewById<MaterialCardView>(R.id.card_battery_optimization)?.isGone = true
}
} else {
findViewById<MaterialCardView>(R.id.card_battery_optimization)?.isGone = true
}
}
private fun restart() {
Log.d(TAG, "Restarting the Listener")
FailureHandler.clearFails()
StartService.stopService {
RestartWorker.run(this, delay = 0)
override fun onDestroy() {
Log.d(TAG, "Destroy")
jobs.removeAll {
it.cancel()
true
}
super.onDestroy()
}
private fun logout() {
val alert = AlertDialog.Builder(
this
)
alert.setTitle(getString(R.string.logout_alert_title))
alert.setMessage(R.string.logout_alert_content)
alert.setPositiveButton(R.string.ok) { dialog, _ ->
dialog.dismiss()
deleteDevice(this) {
StartService.stopService()
FailureHandler.clearFails()
}
AccountFactory.logout(this)
AppStore(this).wipe()
finish()
goToStartActivity(this)
}
alert.setNegativeButton(getString(R.string.discard)) { dialog, _ -> dialog.dismiss() }
alert.show()
}
private fun addChannel() {
val builder = AlertDialog.Builder(
this
)
val input = EditText(this)
input.inputType = InputType.TYPE_CLASS_TEXT
input.hint = "My Title"
val pad32 = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_PX,
32F,
resources.displayMetrics
).toInt()
input.setPadding(pad32)
builder.setView(input)
builder.setTitle("Notification Channel")
builder.setMessage(Html.fromHtml(getString(R.string.add_channel_dialog_content), Html.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.ok) { dialog, _ ->
dialog.dismiss()
Log.d(TAG, "title: ${input.text}")
LocalNotification.createChannel(
this,
input.text.toString()
) {
setListView()
}
}
builder.setNegativeButton(getString(R.string.discard)) { dialog, _ -> dialog.dismiss() }
builder.show()
}
private fun shouldShowCopyItem(listView: ListView, appListAdapter: AppListAdapter): Boolean {
if (listView.checkedItemCount == 1) {
val selected = appListAdapter.getSelectedIds()
var i = selected.size - 1
while (i >= 0) {
if (selected.valueAt(i)) {
return appListAdapter.getItem(selected.keyAt(i))?.packageId == packageName
}
i--
}
}
return false
}
private fun setListView() {
listView = findViewById(R.id.applications_list)
val appList = emptyList<App>().toMutableList()
getDb(this).let { db ->
db.listTokens().forEach {
appList.add(
App(token = it, packageId = db.getPackageName(it) ?: it)
)
}
}
val editListAdapter = AppListAdapter(
this,
R.layout.item_app,
appList
)
listView.adapter = editListAdapter
listView.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL
var copyEndpointItem: MenuItem? = null
listView.setMultiChoiceModeListener(object : MultiChoiceModeListener {
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
editListAdapter.removeSelection()
}
override fun onItemCheckedStateChanged(
mode: ActionMode,
position: Int,
id: Long,
checked: Boolean
) {
val checkedCount = listView.checkedItemCount
mode.title = "$checkedCount selected"
editListAdapter.toggleSelection(position)
copyEndpointItem?.isVisible = shouldShowCopyItem(listView, editListAdapter)
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean {
mode.menuInflater.inflate(R.menu.menu_delete, menu)
copyEndpointItem = menu?.findItem(R.id.action_copy_endpoint)
return true
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
Log.d(TAG, "Action clicked")
return when (item.itemId) {
R.id.action_delete -> {
Log.d(TAG, "deleting")
val selected = editListAdapter.getSelectedIds()
val alert = MaterialAlertDialogBuilder(this@MainActivity)
alert.setTitle(getString(R.string.dialog_unregistering_title))
alert.setMessage(getString(R.string.dialog_unregistering_content).format(selected.size))
alert.setPositiveButton(getString(R.string.dialog_yes)) { dialog, _ ->
var i = selected.size - 1
while (i >= 0) {
if (selected.valueAt(i)) {
editListAdapter.getItem(selected.keyAt(i))?.let {
deleteApp(this@MainActivity, it.token) {
Log.d(TAG, "${it.packageId} unregistered")
editListAdapter.remove(it)
this@MainActivity.runOnUiThread {
setListView()
}
}
}
i--
}
}
preventListReset = false
dialog.dismiss()
mode.finish()
}
alert.setNegativeButton(getString(R.string.dialog_no)) { dialog, _ -> dialog.dismiss() }
alert.setOnCancelListener {
Log.d(TAG, "Canceled")
}
preventListReset = true
alert.show()
true
}
R.id.action_copy_endpoint -> {
Log.d(TAG, "Copying endpoint")
val selected = editListAdapter.getSelectedIds()
if (selected.size > 1) {
Log.e(TAG, "Copying endpoint for more than an app")
return true
}
editListAdapter.getItem(selected.keyAt(0))?.let {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("simple text", Distributor.getEndpoint(this@MainActivity, it.token))
clipboard.setPrimaryClip(clip)
mode.finish()
}
true
}
else -> false
}
}
})
}
private fun setDebugInformationListener() {
findViewById<TextView>(R.id.main_account_title).setOnClickListener {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < 500) {
clickCount++
if (clickCount == 5) {
val msg = SpannableStringBuilder(getDebugInfo())
MaterialAlertDialogBuilder(this)
.setTitle("Debug information")
.setMessage(msg)
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
}
.setNeutralButton(android.R.string.copy) { dialog, _ ->
copyToClipboard(this, "Debug Information", msg.toString())
dialog.dismiss()
}
.show()
clickCount = 0 // Reset count after showing the dialog
}
} else {
clickCount = 1
}
lastClickTime = currentTime
}
}
companion object {
fun goToMainActivity(context: Context) {
val intent = Intent(
context,
MainActivity::class.java
)
val intent =
Intent(
context,
MainActivity::class.java
)
context.startActivity(intent)
}
}

View File

@ -0,0 +1,93 @@
package org.unifiedpush.distributor.nextpush.activities
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.unifiedpush.distributor.nextpush.activities.ui.MainUiState
import org.unifiedpush.distributor.nextpush.activities.ui.RegistrationListState
class MainViewModel(
mainUiState: MainUiState,
// We don't need liveData for this little list
registrationsState: RegistrationListState
) : ViewModel() {
constructor(context: Context) : this(
mainUiState = MainUiState(context),
registrationsState = RegistrationListState(context)
)
var mainUiState by mutableStateOf(mainUiState)
private set
var registrationsState by mutableStateOf(registrationsState)
private set
fun disableBatteryOptimization() {
viewModelScope.launch {
mainUiState = mainUiState.copy(requireBatteryOptimization = false)
}
}
fun toggleSelection(token: String) {
viewModelScope.launch {
val newList =
registrationsState.list.toMutableList().apply {
replaceAll {
if (it.token == token) {
it.copy(selected = !it.selected)
} else {
it
}
}
}
registrationsState = RegistrationListState(
list = newList,
hasSelection = newList.any {
it.selected
}
)
}
}
fun updateRegistrations(context: Context) {
viewModelScope.launch {
registrationsState = RegistrationListState(context)
}
}
fun unselectAll() {
viewModelScope.launch {
val newList =
registrationsState.list.toMutableList().apply {
replaceAll {
it.copy(selected = false)
}
}
registrationsState = RegistrationListState(list = newList, hasSelection = false)
}
}
fun deleteSelection() {
viewModelScope.launch {
val tokenList = registrationsState.list.filter { it.selected }.map { it.token }
publishAction(
AppAction(
AppAction.Type.DeleteRegistration,
mapOf(
AppAction.ARG_REGISTRATIONS to tokenList
)
)
)
registrationsState = RegistrationListState(
list = registrationsState.list.filter {
!it.selected
},
hasSelection = false
)
}
}
}

View File

@ -0,0 +1,25 @@
package org.unifiedpush.distributor.nextpush.activities
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.unifiedpush.distributor.nextpush.EventBus
class UiAction(val type: Type) {
enum class Type {
UpdateRegistrations,
Logout,
}
fun handle(action: (Type) -> Unit) {
action(type)
}
companion object {
fun publish(type: Type) {
CoroutineScope(Dispatchers.IO).launch {
EventBus.publish(UiAction(type))
}
}
}
}

View File

@ -0,0 +1,134 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import org.unifiedpush.distributor.nextpush.R
import org.unifiedpush.distributor.nextpush.activities.AppAction
import org.unifiedpush.distributor.nextpush.activities.publishAction
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBarUi(viewModel: ViewModel) {
var expanded by remember { mutableStateOf(false) }
var showNotificationDialog by remember { mutableStateOf(false) }
TopAppBar(
colors = TopAppBarDefaults
.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary
),
title = {
Text(
stringResource(R.string.app_name)
)
},
actions = {
IconButton(
onClick = {
expanded = !expanded
}
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Actions"
)
}
Dropdown(
expanded,
actionHandler = { a -> viewModel.publishAction(a) },
onDismiss = {
expanded = false
},
onNewChannel = {
showNotificationDialog = true
}
)
if (showNotificationDialog) {
AddChannelDialog(
onDismissRequest = {
showNotificationDialog = false
},
onConfirmation = {
viewModel.publishAction(
AppAction(
AppAction.Type.AddChannel,
mapOf(
AppAction.ARG_NEW_CHANNEL_TITLE to it
)
)
)
showNotificationDialog = false
}
)
}
}
)
}
@Composable
fun Dropdown(
expanded: Boolean,
actionHandler: (AppAction) -> Unit,
onDismiss: () -> Unit,
onNewChannel: () -> Unit
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismiss
) {
DropdownMenuItem(
onClick = {
actionHandler(AppAction(AppAction.Type.RestartService, null))
onDismiss()
},
text = {
Text(stringResource(R.string.app_dropdown_restart))
}
)
DropdownMenuItem(
onClick = {
actionHandler(AppAction(AppAction.Type.Logout, null))
onDismiss()
},
text = {
Text(
stringResource(R.string.app_dropdown_logout)
)
}
)
DropdownMenuItem(
onClick = {
onNewChannel()
onDismiss()
},
text = {
Text(
stringResource(R.string.app_dropdown_add_notification_channel)
)
}
)
}
}
@Preview
@Composable
fun AppBarPreview() {
AppBarUi(object : ViewModel() {})
}

View File

@ -0,0 +1,223 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.unifiedpush.distributor.nextpush.R
import org.unifiedpush.distributor.nextpush.activities.AppAction
import org.unifiedpush.distributor.nextpush.activities.MainViewModel
import org.unifiedpush.distributor.nextpush.activities.publishAction
@Composable
fun MainUi(viewModel: MainViewModel) {
Scaffold(
topBar = {
if (viewModel.registrationsState.hasSelection) {
SelectToDeleteBarUi(
count = viewModel.registrationsState.list.count { it.selected },
onBack = { viewModel.unselectAll() },
onDelete = { viewModel.deleteSelection() }
)
} else {
AppBarUi(viewModel)
}
}
) { innerPadding ->
MainUiContent(viewModel, innerPadding)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainUiContent(viewModel: MainViewModel, innerPadding: PaddingValues) {
val mainUiState = viewModel.mainUiState
val registrationsState = viewModel.registrationsState
val haptics = LocalHapticFeedback.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(
0.dp,
innerPadding.calculateTopPadding(),
0.dp,
innerPadding.calculateBottomPadding()
),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Column(
modifier = Modifier
.padding(
16.dp,
0.dp,
16.dp,
0.dp
),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(Modifier)
// ToolBar ?
if (mainUiState.requireBatteryOptimization) {
Card {
Text(
text = stringResource(R.string.card_disable_optimisation_description),
modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)
)
TextButton(
modifier = Modifier.padding(16.dp, 0.dp),
onClick = {
viewModel.publishAction(
AppAction(AppAction.Type.DisableBatteryOptimisation)
)
viewModel.disableBatteryOptimization()
}
) {
Text(
stringResource(R.string.button_disable_optimisation)
)
}
}
}
Text(
text = stringResource(R.string.main_account_desc, mainUiState.accountName),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = stringResource(R.string.main_applications_title),
style = MaterialTheme.typography.headlineMedium
)
}
Column {
registrationsState.list.forEach { app ->
Column(
Modifier
.background(
if (app.selected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.background
}
)
.combinedClickable(
onClick = {
if (registrationsState.hasSelection) {
viewModel.toggleSelection(app.token)
}
},
onLongClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
viewModel.toggleSelection(app.token)
},
onLongClickLabel = stringResource(
R.string.list_registrations_elt_long_click_label
)
)
) {
Row(Modifier.padding(8.dp, 4.dp)) {
Column {
Text(
text = app.title ?: app.description,
style = MaterialTheme.typography.labelLarge
)
Text(
text = app.title?.let { app.description } ?: "",
style = MaterialTheme.typography.bodySmall
)
}
Spacer(Modifier.weight(1f))
if (app.inApp) {
Icon(
modifier = Modifier
.clickable {
viewModel.publishAction(
AppAction(
AppAction.Type.CopyEndpoint,
mapOf(
AppAction.ARG_TOKEN to app.token
)
)
)
}
.align(Alignment.CenterVertically),
painter = painterResource(R.drawable.ic_content_copy_24),
contentDescription = stringResource(
R.string.button_copy_endpoint_description
)
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = Color.LightGray
)
}
}
}
}
}
@Preview
@Composable
fun MainPreview() {
val regList =
listOf(
RegistrationState(
title = "Application 1",
token = "tok1",
description = "tld.app.1",
inApp = false
),
RegistrationState(
title = "My Channel",
token = "tok2",
description = stringResource(R.string.list_registrations_local_description),
inApp = true,
selected = true
),
RegistrationState(
title = null,
token = "tok3",
description = "tld.app.3",
inApp = false
)
)
MainUi(
MainViewModel(
MainUiState(
requireBatteryOptimization = true,
accountName = "account@domain.tld"
),
RegistrationListState(list = regList)
)
)
}

View File

@ -0,0 +1,17 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import android.content.Context
import android.os.PowerManager
import org.unifiedpush.distributor.nextpush.account.AccountFactory
data class MainUiState(
val requireBatteryOptimization: Boolean,
val accountName: String
) {
constructor(context: Context) : this(
requireBatteryOptimization = !(
context.getSystemService(Context.POWER_SERVICE) as PowerManager
).isIgnoringBatteryOptimizations(context.packageName),
accountName = AccountFactory.getAccount(context)?.name ?: ""
)
}

View File

@ -0,0 +1,93 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.unifiedpush.distributor.nextpush.R
@Preview
@Composable
fun NotificationChannelUi(value: String = "", onValueChanged: (String) -> Unit = {}) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
buildAnnotatedString {
append(stringResource(R.string.dialog_add_channel_content))
append(" ")
withLink(
LinkAnnotation.Url(url = stringResource(R.string.hyperlink_to_channel_example))
) {
withStyle(
style =
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
) {
append(stringResource(R.string.hyperlink_to_channel_example_text))
}
}
}
)
TextField(
value = value,
onValueChange = onValueChanged,
label = { Text(stringResource(R.string.dialog_add_channel_input_label)) },
maxLines = 1
)
}
}
@Preview
@Composable
fun AddChannelDialog(onDismissRequest: () -> Unit = {}, onConfirmation: (String) -> Unit = {}) {
var value by remember { mutableStateOf("") }
AlertDialog(
title = {
Text(stringResource(R.string.dialog_add_channel_title))
},
text = {
NotificationChannelUi(value) {
value = it
}
},
onDismissRequest = {
onDismissRequest()
},
confirmButton = {
TextButton(
onClick = {
onConfirmation(value)
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = {
onDismissRequest()
}
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}

View File

@ -0,0 +1,22 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import android.content.Context
import org.unifiedpush.distributor.nextpush.Database.Companion.getDb
data class RegistrationListState(
val hasSelection: Boolean = false,
val list: List<RegistrationState>
) {
constructor(context: Context) : this(
list = emptyList<RegistrationState?>()
.toMutableList().also { appList ->
getDb(context).let { db ->
db.listTokens().forEach {
appList.add(
RegistrationState.get(context, db, it)
)
}
}
}.filterNotNull()
)
}

View File

@ -0,0 +1,45 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import android.content.Context
import org.unifiedpush.distributor.nextpush.Database
import org.unifiedpush.distributor.nextpush.R
import org.unifiedpush.distributor.nextpush.utils.getApplicationName
data class RegistrationState(
val title: String?,
val token: String,
val description: String,
val inApp: Boolean,
val selected: Boolean = false
) {
companion object {
fun get(context: Context, db: Database, token: String): RegistrationState? {
val packageId = db.getPackageName(token) ?: return null
val inApp = packageId == context.packageName
val title =
if (inApp) {
db.getNotificationTitle(token)?.let {
context.getString(R.string.list_registrations_local_title, it)
}
} else {
context.getApplicationName(packageId)
} ?: packageId
val description =
if (inApp) {
context.getString(R.string.list_registrations_local_description)
} else {
if (title == packageId) {
""
} else {
packageId
}
}
return RegistrationState(
title = title,
description = description,
token = token,
inApp = inApp
)
}
}
}

View File

@ -0,0 +1,113 @@
package org.unifiedpush.distributor.nextpush.activities.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.unifiedpush.distributor.nextpush.R
@Preview
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectToDeleteBarUi(count: Int = 1, onBack: () -> Unit = {}, onDelete: () -> Unit = {}) {
var showUnregisterDialog by remember { mutableStateOf(false) }
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary
),
title = {
Text(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.bar_unregister_title, count)
)
},
navigationIcon = {
IconButton(onClick = {
onBack()
}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.bar_unregister_back_description)
)
}
},
actions = {
IconButton(
onClick = {
showUnregisterDialog = true
}
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = stringResource(R.string.bar_unregister_delete_description)
)
}
}
)
if (showUnregisterDialog) {
UnregisterConfirmationDialog(
count,
onConfirmation = onDelete,
onDismissRequest = {
showUnregisterDialog = false
}
)
}
}
@Preview
@Composable
fun UnregisterConfirmationDialog(
count: Int = 1,
onDismissRequest: () -> Unit = {},
onConfirmation: () -> Unit = {}
) {
AlertDialog(
title = {
Text(stringResource(R.string.dialog_unregistering_title))
},
text = { Text(stringResource(R.string.dialog_unregistering_content, count)) },
onDismissRequest = {
onDismissRequest()
},
confirmButton = {
TextButton(
onClick = {
onConfirmation()
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = {
onDismissRequest()
}
) {
Text(
stringResource(android.R.string.cancel)
)
}
}
)
}

View File

@ -0,0 +1,221 @@
package org.unifiedpush.distributor.nextpush.activities.ui.theme
import androidx.compose.ui.graphics.Color
val nextcloud = Color(0xFF0F9AE6)
val primaryLight = Color(0xFF2C638B)
val onPrimaryLight = Color(0xFFFFFFFF)
val primaryContainerLight = Color(0xFFCCE5FF)
val onPrimaryContainerLight = Color(0xFF001D31)
val secondaryLight = Color(0xFF51606F)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFD4E4F6)
val onSecondaryContainerLight = Color(0xFF0D1D2A)
val tertiaryLight = Color(0xFF67587A)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFEDDCFF)
val onTertiaryContainerLight = Color(0xFF221534)
val errorLight = Color(0xFFBA1A1A)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFFFDAD6)
val onErrorContainerLight = Color(0xFF410002)
val backgroundLight = Color(0xFFF7F9FF)
val onBackgroundLight = Color(0xFF181C20)
val surfaceLight = Color(0xFFF7F9FF)
val onSurfaceLight = Color(0xFF181C20)
val surfaceVariantLight = Color(0xFFDEE3EB)
val onSurfaceVariantLight = Color(0xFF42474E)
val outlineLight = Color(0xFF72787E)
val outlineVariantLight = Color(0xFFC2C7CE)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF2D3135)
val inverseOnSurfaceLight = Color(0xFFEEF1F6)
val inversePrimaryLight = Color(0xFF99CCFA)
val surfaceDimLight = Color(0xFFD7DADF)
val surfaceBrightLight = Color(0xFFF7F9FF)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFF1F4F9)
val surfaceContainerLight = Color(0xFFEBEEF3)
val surfaceContainerHighLight = Color(0xFFE6E8EE)
val surfaceContainerHighestLight = Color(0xFFE0E2E8)
val primaryLightMediumContrast = Color(0xFF00476D)
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
val primaryContainerLightMediumContrast = Color(0xFF4579A3)
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val secondaryLightMediumContrast = Color(0xFF354453)
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
val secondaryContainerLightMediumContrast = Color(0xFF677686)
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryLightMediumContrast = Color(0xFF4A3D5D)
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightMediumContrast = Color(0xFF7D6E92)
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val errorLightMediumContrast = Color(0xFF8C0009)
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
val errorContainerLightMediumContrast = Color(0xFFDA342E)
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
val backgroundLightMediumContrast = Color(0xFFF7F9FF)
val onBackgroundLightMediumContrast = Color(0xFF181C20)
val surfaceLightMediumContrast = Color(0xFFF7F9FF)
val onSurfaceLightMediumContrast = Color(0xFF181C20)
val surfaceVariantLightMediumContrast = Color(0xFFDEE3EB)
val onSurfaceVariantLightMediumContrast = Color(0xFF3E434A)
val outlineLightMediumContrast = Color(0xFF5A6066)
val outlineVariantLightMediumContrast = Color(0xFF767B82)
val scrimLightMediumContrast = Color(0xFF000000)
val inverseSurfaceLightMediumContrast = Color(0xFF2D3135)
val inverseOnSurfaceLightMediumContrast = Color(0xFFEEF1F6)
val inversePrimaryLightMediumContrast = Color(0xFF99CCFA)
val surfaceDimLightMediumContrast = Color(0xFFD7DADF)
val surfaceBrightLightMediumContrast = Color(0xFFF7F9FF)
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightMediumContrast = Color(0xFFF1F4F9)
val surfaceContainerLightMediumContrast = Color(0xFFEBEEF3)
val surfaceContainerHighLightMediumContrast = Color(0xFFE6E8EE)
val surfaceContainerHighestLightMediumContrast = Color(0xFFE0E2E8)
val primaryLightHighContrast = Color(0xFF00243B)
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
val primaryContainerLightHighContrast = Color(0xFF00476D)
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
val secondaryLightHighContrast = Color(0xFF142431)
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
val secondaryContainerLightHighContrast = Color(0xFF354453)
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
val tertiaryLightHighContrast = Color(0xFF291C3B)
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightHighContrast = Color(0xFF4A3D5D)
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
val errorLightHighContrast = Color(0xFF4E0002)
val onErrorLightHighContrast = Color(0xFFFFFFFF)
val errorContainerLightHighContrast = Color(0xFF8C0009)
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
val backgroundLightHighContrast = Color(0xFFF7F9FF)
val onBackgroundLightHighContrast = Color(0xFF181C20)
val surfaceLightHighContrast = Color(0xFFF7F9FF)
val onSurfaceLightHighContrast = Color(0xFF000000)
val surfaceVariantLightHighContrast = Color(0xFFDEE3EB)
val onSurfaceVariantLightHighContrast = Color(0xFF1F242A)
val outlineLightHighContrast = Color(0xFF3E434A)
val outlineVariantLightHighContrast = Color(0xFF3E434A)
val scrimLightHighContrast = Color(0xFF000000)
val inverseSurfaceLightHighContrast = Color(0xFF2D3135)
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
val inversePrimaryLightHighContrast = Color(0xFFDEEEFF)
val surfaceDimLightHighContrast = Color(0xFFD7DADF)
val surfaceBrightLightHighContrast = Color(0xFFF7F9FF)
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightHighContrast = Color(0xFFF1F4F9)
val surfaceContainerLightHighContrast = Color(0xFFEBEEF3)
val surfaceContainerHighLightHighContrast = Color(0xFFE6E8EE)
val surfaceContainerHighestLightHighContrast = Color(0xFFE0E2E8)
val primaryDark = Color(0xFF99CCFA)
val onPrimaryDark = Color(0xFF003351)
val primaryContainerDark = Color(0xFF074B72)
val onPrimaryContainerDark = Color(0xFFCCE5FF)
val secondaryDark = Color(0xFFB8C8DA)
val onSecondaryDark = Color(0xFF23323F)
val secondaryContainerDark = Color(0xFF394857)
val onSecondaryContainerDark = Color(0xFFD4E4F6)
val tertiaryDark = Color(0xFFD1BFE7)
val onTertiaryDark = Color(0xFF372A4A)
val tertiaryContainerDark = Color(0xFF4E4161)
val onTertiaryContainerDark = Color(0xFFEDDCFF)
val errorDark = Color(0xFFFFB4AB)
val onErrorDark = Color(0xFF690005)
val errorContainerDark = Color(0xFF93000A)
val onErrorContainerDark = Color(0xFFFFDAD6)
val backgroundDark = Color(0xFF101418)
val onBackgroundDark = Color(0xFFE0E2E8)
val surfaceDark = Color(0xFF101418)
val onSurfaceDark = Color(0xFFE0E2E8)
val surfaceVariantDark = Color(0xFF42474E)
val onSurfaceVariantDark = Color(0xFFC2C7CE)
val outlineDark = Color(0xFF8C9198)
val outlineVariantDark = Color(0xFF42474E)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFE0E2E8)
val inverseOnSurfaceDark = Color(0xFF2D3135)
val inversePrimaryDark = Color(0xFF2C638B)
val surfaceDimDark = Color(0xFF101418)
val surfaceBrightDark = Color(0xFF36393E)
val surfaceContainerLowestDark = Color(0xFF0B0F12)
val surfaceContainerLowDark = Color(0xFF181C20)
val surfaceContainerDark = Color(0xFF1C2024)
val surfaceContainerHighDark = Color(0xFF272A2E)
val surfaceContainerHighestDark = Color(0xFF313539)
val primaryDarkMediumContrast = Color(0xFF9DD0FE)
val onPrimaryDarkMediumContrast = Color(0xFF001829)
val primaryContainerDarkMediumContrast = Color(0xFF6396C1)
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
val secondaryDarkMediumContrast = Color(0xFFBCCCDE)
val onSecondaryDarkMediumContrast = Color(0xFF071824)
val secondaryContainerDarkMediumContrast = Color(0xFF8392A3)
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
val tertiaryDarkMediumContrast = Color(0xFFD6C3EB)
val onTertiaryDarkMediumContrast = Color(0xFF1C102E)
val tertiaryContainerDarkMediumContrast = Color(0xFF9A8AAF)
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
val errorDarkMediumContrast = Color(0xFFFFBAB1)
val onErrorDarkMediumContrast = Color(0xFF370001)
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
val backgroundDarkMediumContrast = Color(0xFF101418)
val onBackgroundDarkMediumContrast = Color(0xFFE0E2E8)
val surfaceDarkMediumContrast = Color(0xFF101418)
val onSurfaceDarkMediumContrast = Color(0xFFF9FBFF)
val surfaceVariantDarkMediumContrast = Color(0xFF42474E)
val onSurfaceVariantDarkMediumContrast = Color(0xFFC6CBD3)
val outlineDarkMediumContrast = Color(0xFF9EA3AB)
val outlineVariantDarkMediumContrast = Color(0xFF7E848B)
val scrimDarkMediumContrast = Color(0xFF000000)
val inverseSurfaceDarkMediumContrast = Color(0xFFE0E2E8)
val inverseOnSurfaceDarkMediumContrast = Color(0xFF272A2E)
val inversePrimaryDarkMediumContrast = Color(0xFF0A4C73)
val surfaceDimDarkMediumContrast = Color(0xFF101418)
val surfaceBrightDarkMediumContrast = Color(0xFF36393E)
val surfaceContainerLowestDarkMediumContrast = Color(0xFF0B0F12)
val surfaceContainerLowDarkMediumContrast = Color(0xFF181C20)
val surfaceContainerDarkMediumContrast = Color(0xFF1C2024)
val surfaceContainerHighDarkMediumContrast = Color(0xFF272A2E)
val surfaceContainerHighestDarkMediumContrast = Color(0xFF313539)
val primaryDarkHighContrast = Color(0xFFF9FBFF)
val onPrimaryDarkHighContrast = Color(0xFF000000)
val primaryContainerDarkHighContrast = Color(0xFF9DD0FE)
val onPrimaryContainerDarkHighContrast = Color(0xFF000000)
val secondaryDarkHighContrast = Color(0xFFF9FBFF)
val onSecondaryDarkHighContrast = Color(0xFF000000)
val secondaryContainerDarkHighContrast = Color(0xFFBCCCDE)
val onSecondaryContainerDarkHighContrast = Color(0xFF000000)
val tertiaryDarkHighContrast = Color(0xFFFFF9FD)
val onTertiaryDarkHighContrast = Color(0xFF000000)
val tertiaryContainerDarkHighContrast = Color(0xFFD6C3EB)
val onTertiaryContainerDarkHighContrast = Color(0xFF000000)
val errorDarkHighContrast = Color(0xFFFFF9F9)
val onErrorDarkHighContrast = Color(0xFF000000)
val errorContainerDarkHighContrast = Color(0xFFFFBAB1)
val onErrorContainerDarkHighContrast = Color(0xFF000000)
val backgroundDarkHighContrast = Color(0xFF101418)
val onBackgroundDarkHighContrast = Color(0xFFE0E2E8)
val surfaceDarkHighContrast = Color(0xFF101418)
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkHighContrast = Color(0xFF42474E)
val onSurfaceVariantDarkHighContrast = Color(0xFFF9FBFF)
val outlineDarkHighContrast = Color(0xFFC6CBD3)
val outlineVariantDarkHighContrast = Color(0xFFC6CBD3)
val scrimDarkHighContrast = Color(0xFF000000)
val inverseSurfaceDarkHighContrast = Color(0xFFE0E2E8)
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
val inversePrimaryDarkHighContrast = Color(0xFF002D47)
val surfaceDimDarkHighContrast = Color(0xFF101418)
val surfaceBrightDarkHighContrast = Color(0xFF36393E)
val surfaceContainerLowestDarkHighContrast = Color(0xFF0B0F12)
val surfaceContainerLowDarkHighContrast = Color(0xFF181C20)
val surfaceContainerDarkHighContrast = Color(0xFF1C2024)
val surfaceContainerHighDarkHighContrast = Color(0xFF272A2E)
val surfaceContainerHighestDarkHighContrast = Color(0xFF313539)

View File

@ -0,0 +1,280 @@
package org.unifiedpush.distributor.nextpush.activities.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val lightScheme =
lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight
)
private val darkScheme =
darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark
)
private val mediumContrastLightColorScheme =
lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast
)
private val highContrastLightColorScheme =
lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast
)
private val mediumContrastDarkColorScheme =
darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast
)
private val highContrastDarkColorScheme =
darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast
)
@Immutable
data class ColorFamily(
val color: Color,
val onColor: Color,
val colorContainer: Color,
val onColorContainer: Color
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkScheme
else -> lightScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}

View File

@ -0,0 +1,5 @@
package org.unifiedpush.distributor.nextpush.activities.ui.theme
import androidx.compose.material3.Typography
val AppTypography = Typography()

View File

@ -12,6 +12,7 @@ import org.unifiedpush.distributor.nextpush.AppCompanion
import org.unifiedpush.distributor.nextpush.Database.Companion.getDb
import org.unifiedpush.distributor.nextpush.WakeLock
import org.unifiedpush.distributor.nextpush.account.AccountFactory
import org.unifiedpush.distributor.nextpush.activities.UiAction
import org.unifiedpush.distributor.nextpush.distributor.* // ktlint-disable no-wildcard-imports
import org.unifiedpush.distributor.nextpush.distributor.Distributor.checkRegistration
import org.unifiedpush.distributor.nextpush.distributor.Distributor.createApp
@ -169,7 +170,7 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
private fun onRegisterUpdatedToken(context: Context, connectorToken: String, application: String, vapid: String?) {
Log.d(TAG, "Updating registration for $application")
deleteApp(context, connectorToken, false) {
onRegisterNewToken(context, connectorToken, application, vapid, toastOnSuccess = false)
saveApplication(context, connectorToken, application, vapid, newRegistration = false)
}
}
@ -178,7 +179,25 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
*
* If we are not connected to Nextcloud, we send registration failed with [FailedReason.ACTION_REQUIRED]
*/
private fun onRegisterNewToken(context: Context, connectorToken: String, application: String, vapid: String?, toastOnSuccess: Boolean = true) {
private fun onRegisterNewToken(context: Context, connectorToken: String, application: String, vapid: String?) {
Log.d(TAG, "New registration for $application")
saveApplication(context, connectorToken, application, vapid, newRegistration = true)
}
/**
* Save the registration, consumed by [onRegisterNewToken] and [onRegisterUpdatedToken]
*
* If we are not connected to Nextcloud, we send registration failed with [FailedReason.ACTION_REQUIRED]
*
* @param newRegistration if this is a new registration, we inform user with a toast and update registrationsUi
*/
private fun saveApplication(
context: Context,
connectorToken: String,
application: String,
vapid: String?,
newRegistration: Boolean = true,
) {
val appName = context.getApplicationName(application) ?: application
when {
AccountFactory.getAccount(context)?.connected != true -> registrationFailedWithToast(
@ -204,7 +223,8 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
when (success) {
true -> {
sendEndpoint(context, connectorToken)
if (toastOnSuccess) {
if (newRegistration) {
UiAction.publish(UiAction.Type.UpdateRegistrations)
Toast.makeText(context, "$appName registered.", Toast.LENGTH_SHORT)
.show()
}
@ -251,6 +271,7 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
try {
deleteApp(context, connectorToken) {
Log.d(TAG, "Unregistration is finished")
UiAction.publish(UiAction.Type.UpdateRegistrations)
AppCompanion.delQueue.removeToken(connectorToken)
}
} catch (e: Exception) {

View File

@ -1,12 +0,0 @@
package org.unifiedpush.distributor.nextpush.utils
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.core.content.ContextCompat.getSystemService
fun copyToClipboard(context: Context, label: String, text: String) {
val clipboard: ClipboardManager? = getSystemService(context, ClipboardManager::class.java)
val clip = ClipData.newPlainText(label, text)
clipboard?.setPrimaryClip(clip)
}

View File

@ -286,7 +286,7 @@ class FromPushNotification(context: Context, title: String, content: String) : A
"${context.getString(R.string.app_name)}.Push.$title",
"(Push) $title",
NotificationManager.IMPORTANCE_HIGH,
context.getString(R.string.local_notif_description)
context.getString(R.string.list_registrations_local_description)
)
)

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:context="org.unifiedpush.distributor.nextpush.activities.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.NextPush.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:popupTheme="@style/Theme.NextPush.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include
android:id="@+id/sub_main"
layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,94 +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"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_battery_optimization"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:checkable="true"
android:clickable="true"
android:focusable="true"
android:padding="16dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="0dp"
android:text="@string/card_disable_optimisation_description" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_disable_optimisation"
style="?attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:text="@string/button_disable_optimisation" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/main_account_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/main_account_title"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_battery_optimization" />
<TextView
android:id="@+id/main_account_desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/main_account_desc"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/main_account_title" />
<TextView
android:id="@+id/main_applications_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/main_applications_title"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_account_desc" />
<ListView
android:id="@+id/applications_list"
android:layout_width="match_parent"
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_toBottomOf="@+id/main_applications_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:minHeight="?android:attr/listPreferredItemHeightSmall" >
<TextView
android:id="@+id/item_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"/>
<TextView
android:id="@+id/item_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:gravity="center_vertical" />
</LinearLayout>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_copy_endpoint"
android:icon="@drawable/ic_content_copy_24"
android:title="@string/copy_endpoint"
android:iconTint="@color/white" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_24"
android:title="@string/action_delete"
android:iconTint="@color/white" />
</menu>

View File

@ -1,20 +0,0 @@
<menu 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"
tools:context="org.unifiedpush.distributor.nextpush.activities.MainActivity">
<item
android:id="@+id/action_restart"
android:orderInCategory="100"
android:title="@string/action_restart"
app:showAsAction="never" />
<item
android:id="@+id/action_logout"
android:orderInCategory="105"
android:title="@string/action_logout"
app:showAsAction="never" />
<item
android:id="@+id/action_add_local_channel"
android:orderInCategory="110"
android:title="Add notification channel"
app:showAsAction="never" />
</menu>

View File

@ -1,22 +1,15 @@
<resources>
<string name="action_settings">Settings</string>
<!-- Strings used on the main page -->
<string name="main_applications_title">Registered applications</string>
<string name="help"></string>
<string name="foreground_notif_description">Notification to run in the foreground</string>
<string name="foreground_notif_content_no_reg">Waiting for registration to connect</string>
<string name="foreground_notif_content_with_reg">Connected for %s registration(s)</string>
<string name="sso_connection_button">Login with the Nextcloud File application</string>
<string name="action_logout">Logout</string>
<string name="logout_alert_title">Logout</string>
<string name="logout_alert_content">Confirm to logout</string>
<string name="ok">OK</string>
<string name="discard">Discard</string>
<string name="main_account_title">Account</string>
<string name="main_account_desc">You are connected as: %s</string>
<string name="action_restart">Restart Service</string>
<string name="main_account_desc">You are connected as %s</string>
<string name="app_dropdown_logout">Logout</string>
<string name="app_dropdown_restart">Restart Service</string>
<string name="app_dropdown_add_notification_channel">Add notification channel</string>
<string name="warning_notif_content">NextPush is disconnected</string>
<string name="warning_notif_description">Warn when NextPush is disconnected or an issue occurred.</string>
<string name="warning_notif_ticker">Warning</string>
@ -42,13 +35,18 @@
<string name="login_show_password_img_description">Show password</string>
<string name="button_disable_optimisation">Disable optimization</string>
<string name="card_disable_optimisation_description">To ensure the app functions properly, it is important to disable battery optimization. This will prevent the app from being put to sleep and causing delays in notifications.</string>
<string name="action_delete">Delete</string>
<string name="dialog_yes">YES</string>
<string name="dialog_no">NO</string>
<string name="dialog_unregistering_title">Unregistering</string>
<string name="dialog_unregistering_content">Are you sure to unregister %d app(s)?</string>
<string name="add_channel_dialog_content"><![CDATA[Notification channels are an endpoint that you can use to create custom push notifications for services outside of UnifiedPush, such as a script failures or ssh logins. After creating a channel, long tap it to copy its endpoint URL. Example scripts are provided <a href="https://codeberg.org/NextPush/nextpush-android/src/branch/main/docs/notification_channel_examples.md"here</a><br><br><h4>Give your notification a Title:</h4>]]></string>
<string name="local_notif_title">(Notif) %s</string>
<string name="local_notif_description">Push to be notified. Not related to UnifiedPush: messages aren\'t forwarded to any other app.</string>
<string name="copy_endpoint">Copy Endpoint</string>
<string name="dialog_add_channel_title">Notification Channel</string>
<string name="dialog_add_channel_content">Notification channels are an endpoint you can use to create customized push notifications for services outside UnifiedPush, such as script failures or ssh connections. After creating a channel, long-press it to copy its endpoint URL. Sample scripts are provided</string>
<string name="dialog_add_channel_input_label">Channel title</string>
<string name="hyperlink_to_channel_example" translatable="false">https://codeberg.org/NextPush/nextpush-android/src/branch/main/docs/notification_channel_examples.md</string>
<string name="hyperlink_to_channel_example_text">on Codeberg.</string>
<string name="list_registrations_local_title">(Channel) %s</string>
<string name="list_registrations_local_description">Local channel. Not related to UnifiedPush.</string>
<string name="button_copy_endpoint_description">Copy Endpoint</string>
<string name="list_registrations_elt_long_click_label">Select registration to delete</string>
<string name="bar_unregister_title">%d selected</string>
<string name="bar_unregister_back_description">Unselect all</string>
<string name="bar_unregister_delete_description">Unregister selection</string>
</resources>

View File

@ -16,7 +16,8 @@ buildscript {
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ktlint)
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ktlint) apply false
}
tasks.register<Delete>("clean") {

View File

@ -1,21 +1,29 @@
[versions]
android-gradle-plugin = "8.7.2"
androidx-activityCompose = "1.9.3"
androidx-constraintlayout = "2.2.0"
androidx-coordinatorlayout = "1.2.0"
androidx-lifecycle = "2.8.7"
androidx-work = "2.10.0"
appcompat = "1.7.0"
kotlin = "2.0.21"
ktlint = "12.1.1"
material = "1.12.0"
nextcloud-sso = "1.3.2"
#nextcloud-sso = "1.3.2.patch3"
nextcloud-sso = "1.3.2.nodes1"
#nextcloud-sso = "1.3.2"
okhttp-sse = "5.0.0.SSEPATCH1-SNAPSHOT"
retrofit = "2.11.0"
rxjava3-rxandroid = "3.0.2"
rxjava3-rxjava = "3.1.9"
material3Android = "1.3.1"
uiTooling = "1.7.5"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "androidx-coordinatorlayout" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
@ -29,8 +37,12 @@ retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", ve
retrofit-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
rxjava3-rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxjava3-rxandroid" }
rxjava3-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava3-rxjava" }
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiTooling" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@ -25,6 +25,7 @@ dependencyResolutionManagement {
includeModule("com.squareup.okhttp3", "okhttp-sse")
}
}
mavenLocal()
}
}