android: system notifications as in the Final 2026 mockup: connecting with Stop, answers in suggestion pushes, Update / What is new, weekly summary
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8880601ef6
commit
1e32410fca
12 files changed
+252
-6
No files matched your search
@@ -321,6 +321,10 @@
|
|||||||
<receiver
|
<receiver
|
||||||
android:name=".logic.WatchdogReceiver"
|
android:name=".logic.WatchdogReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<!-- «Yes» / «No» of the suggestion notifications -->
|
||||||
|
<receiver
|
||||||
|
android:name=".logic.events.EventAnswerReceiver"
|
||||||
|
android:exported="false" />
|
||||||
<!-- edges of the time windows of the user scenarios -->
|
<!-- edges of the time windows of the user scenarios -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".logic.scenarios.ScenarioReceiver"
|
android:name=".logic.scenarios.ScenarioReceiver"
|
||||||
|
|||||||
+11
-2
@@ -836,6 +836,10 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
{ /* «Protected · Amsterdam» like the mockup shade */
|
{ /* «Protected · Amsterdam» like the mockup shade */
|
||||||
builder.setContentTitle(getString(R.string.notif_protected, name));
|
builder.setContentTitle(getString(R.string.notif_protected, name));
|
||||||
}
|
}
|
||||||
|
else if (state == State.CONNECTING && error == ErrorState.NO_ERROR && !publicVersion && profile != null)
|
||||||
|
{ /* F_SysNotifs «Connecting to Helsinki…» */
|
||||||
|
builder.setContentTitle(getString(R.string.notif_connecting_to, name));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder.setContentTitle(getString(s));
|
builder.setContentTitle(getString(s));
|
||||||
@@ -865,9 +869,14 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
intent.setAction(VpnProfileControlActivity.DISCONNECT);
|
intent.setAction(VpnProfileControlActivity.DISCONNECT);
|
||||||
PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent,
|
PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent,
|
||||||
flags);
|
flags);
|
||||||
builder.addAction(R.drawable.ic_notification_disconnect, getString(R.string.disconnect), pending);
|
builder.addAction(R.drawable.ic_notification_disconnect,
|
||||||
|
getString(state == State.CONNECTING ? R.string.notif_stop : R.string.disconnect), pending);
|
||||||
}
|
}
|
||||||
if (error == ErrorState.NO_ERROR)
|
if (error == ErrorState.NO_ERROR && state == State.CONNECTING)
|
||||||
|
{
|
||||||
|
builder.setContentText(getString(R.string.notif_connecting_text));
|
||||||
|
}
|
||||||
|
else if (error == ErrorState.NO_ERROR)
|
||||||
{
|
{
|
||||||
builder.setContentText(state == State.CONNECTED && isRuSplitOn(this) ? getString(R.string.notif_ru_direct) : name);
|
builder.setContentText(state == State.CONNECTED && isRuSplitOn(this) ? getString(R.string.notif_ru_direct) : name);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -143,6 +143,7 @@ public class StrongSwanApplication extends Application
|
|||||||
org.strongswan.android.logic.profiles.ProfileUpdater.schedule(this);
|
org.strongswan.android.logic.profiles.ProfileUpdater.schedule(this);
|
||||||
/* user scenarios of F_Automation */
|
/* user scenarios of F_Automation */
|
||||||
org.strongswan.android.logic.scenarios.ScenarioEngine.start(this);
|
org.strongswan.android.logic.scenarios.ScenarioEngine.start(this);
|
||||||
|
org.strongswan.android.logic.stats.WeeklySummary.schedule(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadManagedConfigurationAndNotifyListeners()
|
private void reloadManagedConfigurationAndNotifyListeners()
|
||||||
|
|||||||
+45
-3
@@ -235,7 +235,7 @@ class EventLog private constructor(private val context: Context)
|
|||||||
context, 0, Intent(context, InboxActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
context, 0, Intent(context, InboxActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
val notification = NotificationCompat.Builder(context, channel)
|
val builder = NotificationCompat.Builder(context, channel)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(event.title)
|
.setContentTitle(event.title)
|
||||||
.setContentText(event.text)
|
.setContentText(event.text)
|
||||||
@@ -244,7 +244,17 @@ class EventLog private constructor(private val context: Context)
|
|||||||
.setContentIntent(open)
|
.setContentIntent(open)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.build()
|
/* F_SysNotifs: answers right in the notification */
|
||||||
|
if (event.action == EventAction.SET_APP_DIRECT)
|
||||||
|
{
|
||||||
|
builder.addAction(0, context.getString(R.string.notif_yes), answerIntent(event, true))
|
||||||
|
builder.addAction(0, context.getString(R.string.notif_no), answerIntent(event, false))
|
||||||
|
}
|
||||||
|
else if (event.kind == EventKind.WARN)
|
||||||
|
{
|
||||||
|
builder.addAction(0, context.getString(R.string.notif_details), open)
|
||||||
|
}
|
||||||
|
val notification = builder.build()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
manager.notify(TAG, notificationId(event), notification)
|
manager.notify(TAG, notificationId(event), notification)
|
||||||
@@ -255,6 +265,24 @@ class EventLog private constructor(private val context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun answerIntent(event: ShxEvent, yes: Boolean): PendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context, (event.id % 100000).toInt() * 2 + if (yes) 1 else 0,
|
||||||
|
Intent(context, EventAnswerReceiver::class.java).putExtra(EXTRA_ID, event.id).putExtra(EXTRA_YES, yes),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** «Yes» / «No» of a suggestion pushed as a notification. */
|
||||||
|
internal fun answerFromNotification(id: Long, yes: Boolean)
|
||||||
|
{
|
||||||
|
val event = _events.value.firstOrNull { it.id == id } ?: return
|
||||||
|
if (yes && event.action == EventAction.SET_APP_DIRECT)
|
||||||
|
{
|
||||||
|
event.target?.let { org.strongswan.android.logic.routing.RoutingManager.getInstance(context).setAppDirect(it, true) }
|
||||||
|
}
|
||||||
|
answer(id)
|
||||||
|
NotificationManagerCompat.from(context).cancel(TAG, notificationId(event))
|
||||||
|
}
|
||||||
|
|
||||||
/* repeats of a keyed event replace their notification instead of stacking */
|
/* repeats of a keyed event replace their notification instead of stacking */
|
||||||
private fun notificationId(event: ShxEvent): Int = (event.key ?: event.id.toString()).hashCode()
|
private fun notificationId(event: ShxEvent): Int = (event.key ?: event.id.toString()).hashCode()
|
||||||
|
|
||||||
@@ -309,6 +337,8 @@ class EventLog private constructor(private val context: Context)
|
|||||||
private const val FILE_NAME = "events.json"
|
private const val FILE_NAME = "events.json"
|
||||||
const val PROBLEMS_CHANNEL = "org.strongswan.android.problems"
|
const val PROBLEMS_CHANNEL = "org.strongswan.android.problems"
|
||||||
const val HINTS_CHANNEL = "org.strongswan.android.hints"
|
const val HINTS_CHANNEL = "org.strongswan.android.hints"
|
||||||
|
internal const val EXTRA_ID = "org.strongswan.android.extra.EVENT_ID"
|
||||||
|
internal const val EXTRA_YES = "org.strongswan.android.extra.EVENT_YES"
|
||||||
const val RETENTION_MS = 30L * 24 * 60 * 60 * 1000
|
const val RETENTION_MS = 30L * 24 * 60 * 60 * 1000
|
||||||
const val MERGE_WINDOW_MS = 30L * 60 * 1000
|
const val MERGE_WINDOW_MS = 30L * 60 * 1000
|
||||||
private const val MAX_EVENTS = 300
|
private const val MAX_EVENTS = 300
|
||||||
@@ -383,12 +413,24 @@ object NotifPrefs
|
|||||||
const val PROBLEMS = "pref_notif_problems"
|
const val PROBLEMS = "pref_notif_problems"
|
||||||
const val NEW_APPS = "pref_notif_new_apps"
|
const val NEW_APPS = "pref_notif_new_apps"
|
||||||
const val UPDATES = "pref_notif_updates"
|
const val UPDATES = "pref_notif_updates"
|
||||||
|
/** «Weekly summary», the only one off by default */
|
||||||
|
const val WEEKLY = "pref_notif_weekly"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun enabled(context: Context, key: String): Boolean =
|
fun enabled(context: Context, key: String): Boolean =
|
||||||
androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, true)
|
androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, key != WEEKLY)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun set(context: Context, key: String, value: Boolean) =
|
fun set(context: Context, key: String, value: Boolean) =
|
||||||
androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply()
|
androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Receives «Yes» / «No» from a suggestion notification. */
|
||||||
|
class EventAnswerReceiver : android.content.BroadcastReceiver()
|
||||||
|
{
|
||||||
|
override fun onReceive(context: Context, intent: Intent)
|
||||||
|
{
|
||||||
|
val id = intent.getLongExtra(EventLog.EXTRA_ID, 0L)
|
||||||
|
EventLog.getInstance(context).answerFromNotification(id, intent.getBooleanExtra(EventLog.EXTRA_YES, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 SHX VPN
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation; either version 2 of the License, or (at your
|
||||||
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.strongswan.android.logic.stats
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import org.strongswan.android.R
|
||||||
|
import org.strongswan.android.logic.events.NotifPrefs
|
||||||
|
import org.strongswan.android.ui.StatsActivity
|
||||||
|
import org.strongswan.android.ui.compose.formatBytes
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/** «Weekly summary» (F_SysNotifs): last week's traffic on Monday morning, off by default. */
|
||||||
|
object WeeklySummary
|
||||||
|
{
|
||||||
|
private const val WORK_NAME = "weekly-summary"
|
||||||
|
private const val CHANNEL = "org.strongswan.android.weekly"
|
||||||
|
private const val NOTIFICATION_ID = 0x5717
|
||||||
|
|
||||||
|
/** Arms (or cancels) the Monday 10:00 job after the switch. */
|
||||||
|
@JvmStatic
|
||||||
|
fun schedule(context: Context)
|
||||||
|
{
|
||||||
|
val wm = WorkManager.getInstance(context)
|
||||||
|
if (!NotifPrefs.enabled(context, NotifPrefs.WEEKLY))
|
||||||
|
{
|
||||||
|
wm.cancelUniqueWork(WORK_NAME)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val c = Calendar.getInstance()
|
||||||
|
c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
|
||||||
|
c.set(Calendar.HOUR_OF_DAY, 10)
|
||||||
|
c.set(Calendar.MINUTE, 0)
|
||||||
|
c.set(Calendar.SECOND, 0)
|
||||||
|
if (c.timeInMillis <= System.currentTimeMillis())
|
||||||
|
{
|
||||||
|
c.add(Calendar.WEEK_OF_YEAR, 1)
|
||||||
|
}
|
||||||
|
val request = PeriodicWorkRequestBuilder<WeeklySummaryWorker>(7, TimeUnit.DAYS)
|
||||||
|
.setInitialDelay(c.timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
wm.enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun post(context: Context)
|
||||||
|
{
|
||||||
|
if (!NotifPrefs.enabled(context, NotifPrefs.WEEKLY))
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val day = 24L * 60 * 60 * 1000
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val week = TrafficHistory.range(context, now - 7 * day, now - day)
|
||||||
|
val total = week.sumOf { it.total }
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val vpnShare = (week.sumOf { it.vpn } * 100 / total).toInt()
|
||||||
|
val manager = NotificationManagerCompat.from(context)
|
||||||
|
if (!manager.areNotificationsEnabled())
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
{
|
||||||
|
context.getSystemService(NotificationManager::class.java)?.createNotificationChannel(
|
||||||
|
NotificationChannel(CHANNEL, context.getString(R.string.notif_weekly), NotificationManager.IMPORTANCE_LOW)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val open = PendingIntent.getActivity(
|
||||||
|
context, 0, Intent(context, StatsActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
val notification = NotificationCompat.Builder(context, CHANNEL)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle(context.getString(R.string.weekly_title, formatBytes(total)))
|
||||||
|
.setContentText(context.getString(R.string.weekly_text, vpnShare, 100 - vpnShare))
|
||||||
|
.setContentIntent(open)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build()
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manager.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
catch (_: SecurityException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WeeklySummaryWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params)
|
||||||
|
{
|
||||||
|
override suspend fun doWork(): Result
|
||||||
|
{
|
||||||
|
WeeklySummary.post(applicationContext)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -481,6 +481,13 @@ class UpdateManager private constructor(private val context: Context)
|
|||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||||
.setContentIntent(pending)
|
.setContentIntent(pending)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
/* F_SysNotifs: «Update» / «What is new» */
|
||||||
|
.addAction(0, context.getString(R.string.notif_update), pending)
|
||||||
|
.addAction(0, context.getString(R.string.notif_whats_new), PendingIntent.getActivity(
|
||||||
|
context, info.versionCode.toInt() + 1,
|
||||||
|
Intent(context, MainActivity::class.java).putExtra(MainActivity.EXTRA_OPEN_UPDATES, true)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||||
.build()
|
.build()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ public class MainActivity extends AppCompatActivity
|
|||||||
*/
|
*/
|
||||||
public static final String EXTRA_CONNECT_FASTEST = "org.strongswan.android.extra.CONNECT_FASTEST";
|
public static final String EXTRA_CONNECT_FASTEST = "org.strongswan.android.extra.CONNECT_FASTEST";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set as an intent extra to show the Updates page (the update notification)
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_OPEN_UPDATES = "org.strongswan.android.extra.OPEN_UPDATES";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use "bring your own device" (BYOD) features
|
* Use "bring your own device" (BYOD) features
|
||||||
*/
|
*/
|
||||||
@@ -132,6 +137,11 @@ public class MainActivity extends AppCompatActivity
|
|||||||
intent.removeExtra(EXTRA_OPEN_SERVERS);
|
intent.removeExtra(EXTRA_OPEN_SERVERS);
|
||||||
ShxNavigation.INSTANCE.request(ShxTab.PROFILES);
|
ShxNavigation.INSTANCE.request(ShxTab.PROFILES);
|
||||||
}
|
}
|
||||||
|
if (intent != null && intent.getBooleanExtra(EXTRA_OPEN_UPDATES, false))
|
||||||
|
{
|
||||||
|
intent.removeExtra(EXTRA_OPEN_UPDATES);
|
||||||
|
ShxNavigation.INSTANCE.requestUpdatesPage();
|
||||||
|
}
|
||||||
if (intent != null && intent.getBooleanExtra(EXTRA_CONNECT_FASTEST, false))
|
if (intent != null && intent.getBooleanExtra(EXTRA_CONNECT_FASTEST, false))
|
||||||
{
|
{
|
||||||
intent.removeExtra(EXTRA_CONNECT_FASTEST);
|
intent.removeExtra(EXTRA_CONNECT_FASTEST);
|
||||||
|
|||||||
+3
-1
@@ -446,7 +446,8 @@ internal fun NotifSettingsPage()
|
|||||||
}
|
}
|
||||||
NotifGroup(stringResource(R.string.notif_group_other))
|
NotifGroup(stringResource(R.string.notif_group_other))
|
||||||
{
|
{
|
||||||
NotifRow(stringResource(R.string.notif_updates), stringResource(R.string.notif_updates_sub), NotifTag.QUIET, NotifPrefs.UPDATES)
|
NotifRow(stringResource(R.string.notif_updates), stringResource(R.string.notif_updates_sub), NotifTag.QUIET, NotifPrefs.UPDATES, divider = true)
|
||||||
|
NotifRow(stringResource(R.string.notif_weekly), stringResource(R.string.notif_weekly_sub), NotifTag.QUIET, NotifPrefs.WEEKLY)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -516,6 +517,7 @@ private fun NotifRow(title: String, sub: String, tag: NotifTag, key: String?, di
|
|||||||
onCheckedChange = if (key == null) null else { value ->
|
onCheckedChange = if (key == null) null else { value ->
|
||||||
on = value
|
on = value
|
||||||
NotifPrefs.set(context, key, value)
|
NotifPrefs.set(context, key, value)
|
||||||
|
if (key == NotifPrefs.WEEKLY) org.strongswan.android.logic.stats.WeeklySummary.schedule(context)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -213,6 +213,15 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pageState by rememberSaveable { mutableStateOf(SettingsPage.ROOT) }
|
var pageState by rememberSaveable { mutableStateOf(SettingsPage.ROOT) }
|
||||||
|
val updatesRequested by ShxNavigation.updatesPage.collectAsStateWithLifecycle()
|
||||||
|
androidx.compose.runtime.LaunchedEffect(updatesRequested)
|
||||||
|
{
|
||||||
|
if (updatesRequested)
|
||||||
|
{
|
||||||
|
pageState = SettingsPage.UPDATES
|
||||||
|
ShxNavigation.consumeUpdatesPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
/* from a sub page "back" returns to the settings list, not out of the tab */
|
/* from a sub page "back" returns to the settings list, not out of the tab */
|
||||||
val effects = MaterialTheme.motionScheme.defaultEffectsSpec<Float>()
|
val effects = MaterialTheme.motionScheme.defaultEffectsSpec<Float>()
|
||||||
val spatial = MaterialTheme.motionScheme.defaultSpatialSpec<androidx.compose.ui.unit.IntOffset>()
|
val spatial = MaterialTheme.motionScheme.defaultSpatialSpec<androidx.compose.ui.unit.IntOffset>()
|
||||||
|
|||||||
+15
@@ -63,4 +63,19 @@ object ShxNavigation
|
|||||||
{
|
{
|
||||||
_fastest.value = false
|
_fastest.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* «What is new» of the update notification: the Updates page of Settings */
|
||||||
|
private val _updatesPage = MutableStateFlow(false)
|
||||||
|
val updatesPage: StateFlow<Boolean> = _updatesPage
|
||||||
|
|
||||||
|
fun requestUpdatesPage()
|
||||||
|
{
|
||||||
|
request(ShxTab.SETTINGS)
|
||||||
|
_updatesPage.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeUpdatesPage()
|
||||||
|
{
|
||||||
|
_updatesPage.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1318,4 +1318,16 @@
|
|||||||
<string name="prot_list_ads">реклама</string>
|
<string name="prot_list_ads">реклама</string>
|
||||||
<string name="prot_list_trackers">трекеры</string>
|
<string name="prot_list_trackers">трекеры</string>
|
||||||
<string name="prot_list_phishing">фишинг</string>
|
<string name="prot_list_phishing">фишинг</string>
|
||||||
|
<string name="notif_connecting_to">Подключаемся к %1$s…</string>
|
||||||
|
<string name="notif_connecting_text">Сеть, сервер, вход, туннель</string>
|
||||||
|
<string name="notif_stop">Стоп</string>
|
||||||
|
<string name="notif_yes">Да</string>
|
||||||
|
<string name="notif_no">Нет</string>
|
||||||
|
<string name="notif_details">Подробнее</string>
|
||||||
|
<string name="notif_update">Обновить</string>
|
||||||
|
<string name="notif_whats_new">Что нового</string>
|
||||||
|
<string name="notif_weekly">Итоги недели</string>
|
||||||
|
<string name="notif_weekly_sub">Трафик за неделю, по понедельникам</string>
|
||||||
|
<string name="weekly_title">За неделю: %1$s</string>
|
||||||
|
<string name="weekly_text">%1$d%% через VPN · %2$d%% напрямую</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1304,4 +1304,16 @@
|
|||||||
<string name="prot_list_ads">ads</string>
|
<string name="prot_list_ads">ads</string>
|
||||||
<string name="prot_list_trackers">trackers</string>
|
<string name="prot_list_trackers">trackers</string>
|
||||||
<string name="prot_list_phishing">phishing</string>
|
<string name="prot_list_phishing">phishing</string>
|
||||||
|
<string name="notif_connecting_to">Connecting to %1$s…</string>
|
||||||
|
<string name="notif_connecting_text">Network, server, sign-in, tunnel</string>
|
||||||
|
<string name="notif_stop">Stop</string>
|
||||||
|
<string name="notif_yes">Yes</string>
|
||||||
|
<string name="notif_no">No</string>
|
||||||
|
<string name="notif_details">Details</string>
|
||||||
|
<string name="notif_update">Update</string>
|
||||||
|
<string name="notif_whats_new">What is new</string>
|
||||||
|
<string name="notif_weekly">Weekly summary</string>
|
||||||
|
<string name="notif_weekly_sub">Traffic of the week, on Mondays</string>
|
||||||
|
<string name="weekly_title">Last week: %1$s</string>
|
||||||
|
<string name="weekly_text">%1$d%% through the VPN · %2$d%% direct</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user