Apply Spotless formatting to Java and Kotlin files
This commit is contained in:
@@ -25,7 +25,7 @@ object RootInstaller {
|
|||||||
handle.service.installPackage(
|
handle.service.installPackage(
|
||||||
pfd,
|
pfd,
|
||||||
apkFile.length(),
|
apkFile.length(),
|
||||||
android.os.Process.myUserHandle().hashCode()
|
android.os.Process.myUserHandle().hashCode(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,10 +69,7 @@ object RootInstaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RootServiceHandle(
|
private class RootServiceHandle(val connection: ServiceConnection, val service: IRootService) : java.io.Closeable {
|
||||||
val connection: ServiceConnection,
|
|
||||||
val service: IRootService
|
|
||||||
) : java.io.Closeable {
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
RootService.unbind(connection)
|
RootService.unbind(connection)
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
|||||||
|
|
||||||
object SystemPackageInstaller {
|
object SystemPackageInstaller {
|
||||||
|
|
||||||
fun canSystemSilentInstall(): Boolean {
|
fun canSystemSilentInstall(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
||||||
}
|
|
||||||
|
|
||||||
fun install(context: Context, apkFile: File) {
|
fun install(context: Context, apkFile: File) {
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
@@ -38,7 +36,7 @@ object SystemPackageInstaller {
|
|||||||
context,
|
context,
|
||||||
sessionId,
|
sessionId,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
session.commit(pendingIntent.intentSender)
|
session.commit(pendingIntent.intentSender)
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ import io.nekohasekai.sfa.update.UpdateState
|
|||||||
import io.nekohasekai.sfa.update.UpdateTrack
|
import io.nekohasekai.sfa.update.UpdateTrack
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UpdateWorker(
|
class UpdateWorker(private val appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
|
||||||
private val appContext: Context,
|
|
||||||
params: WorkerParameters
|
|
||||||
) : CoroutineWorker(appContext, params) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WORK_NAME = "AutoUpdate"
|
private const val WORK_NAME = "AutoUpdate"
|
||||||
@@ -37,7 +34,8 @@ class UpdateWorker(
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val workRequest = PeriodicWorkRequestBuilder<UpdateWorker>(
|
val workRequest = PeriodicWorkRequestBuilder<UpdateWorker>(
|
||||||
24, TimeUnit.HOURS
|
24,
|
||||||
|
TimeUnit.HOURS,
|
||||||
)
|
)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
|
||||||
@@ -46,7 +44,7 @@ class UpdateWorker(
|
|||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||||
WORK_NAME,
|
WORK_NAME,
|
||||||
ExistingPeriodicWorkPolicy.KEEP,
|
ExistingPeriodicWorkPolicy.KEEP,
|
||||||
workRequest
|
workRequest,
|
||||||
)
|
)
|
||||||
Log.d(TAG, "Auto update scheduled")
|
Log.d(TAG, "Auto update scheduled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import android.os.Bundle;
|
|||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
public interface IIntentReceiver extends IInterface {
|
public interface IIntentReceiver extends IInterface {
|
||||||
void performReceive(Intent intent, int resultCode, String data, Bundle extras,
|
void performReceive(
|
||||||
boolean ordered, boolean sticky, int sendingUser);
|
Intent intent,
|
||||||
|
int resultCode,
|
||||||
|
String data,
|
||||||
|
Bundle extras,
|
||||||
|
boolean ordered,
|
||||||
|
boolean sticky,
|
||||||
|
int sendingUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,23 @@ import android.os.IInterface;
|
|||||||
|
|
||||||
public interface IIntentSender extends IInterface {
|
public interface IIntentSender extends IInterface {
|
||||||
|
|
||||||
void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
|
void send(
|
||||||
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options);
|
int code,
|
||||||
|
Intent intent,
|
||||||
|
String resolvedType,
|
||||||
|
IBinder whitelistToken,
|
||||||
|
IIntentReceiver finishedReceiver,
|
||||||
|
String requiredPermission,
|
||||||
|
Bundle options);
|
||||||
|
|
||||||
abstract class Stub extends Binder implements IIntentSender {
|
abstract class Stub extends Binder implements IIntentSender {
|
||||||
public static IIntentSender asInterface(IBinder binder) {
|
public static IIntentSender asInterface(IBinder binder) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder asBinder() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder asBinder() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ import android.os.RemoteException;
|
|||||||
|
|
||||||
public interface IPackageInstaller extends IInterface {
|
public interface IPackageInstaller extends IInterface {
|
||||||
|
|
||||||
int createSession(PackageInstaller.SessionParams params, String installerPackageName, String installerAttributionTag, int userId) throws RemoteException;
|
int createSession(
|
||||||
|
PackageInstaller.SessionParams params,
|
||||||
|
String installerPackageName,
|
||||||
|
String installerAttributionTag,
|
||||||
|
int userId)
|
||||||
|
throws RemoteException;
|
||||||
|
|
||||||
IPackageInstallerSession openSession(int sessionId) throws RemoteException;
|
IPackageInstallerSession openSession(int sessionId) throws RemoteException;
|
||||||
|
|
||||||
void abandonSession(int sessionId) throws RemoteException;
|
void abandonSession(int sessionId) throws RemoteException;
|
||||||
|
|
||||||
abstract class Stub extends Binder implements IPackageInstaller {
|
abstract class Stub extends Binder implements IPackageInstaller {
|
||||||
public static IPackageInstaller asInterface(IBinder binder) {
|
public static IPackageInstaller asInterface(IBinder binder) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import android.os.IInterface;
|
|||||||
|
|
||||||
public interface IPackageInstallerSession extends IInterface {
|
public interface IPackageInstallerSession extends IInterface {
|
||||||
|
|
||||||
abstract class Stub extends Binder implements IPackageInstallerSession {
|
abstract class Stub extends Binder implements IPackageInstallerSession {
|
||||||
public static IPackageInstallerSession asInterface(IBinder binder) {
|
public static IPackageInstallerSession asInterface(IBinder binder) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -26,211 +24,215 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public final class RemotePreferences implements SharedPreferences {
|
public final class RemotePreferences implements SharedPreferences {
|
||||||
|
|
||||||
private static final String TAG = "RemotePreferences";
|
private static final String TAG = "RemotePreferences";
|
||||||
private static final Object CONTENT = new Object();
|
private static final Object CONTENT = new Object();
|
||||||
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
|
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
private final XposedService mService;
|
private final XposedService mService;
|
||||||
private final String mGroup;
|
private final String mGroup;
|
||||||
private final Lock mLock = new ReentrantLock();
|
private final Lock mLock = new ReentrantLock();
|
||||||
private final Map<String, Object> mMap = new ConcurrentHashMap<>();
|
private final Map<String, Object> mMap = new ConcurrentHashMap<>();
|
||||||
private final Map<OnSharedPreferenceChangeListener, Object> mListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
private final Map<OnSharedPreferenceChangeListener, Object> mListeners =
|
||||||
|
Collections.synchronizedMap(new WeakHashMap<>());
|
||||||
|
|
||||||
private volatile boolean isDeleted = false;
|
private volatile boolean isDeleted = false;
|
||||||
|
|
||||||
private RemotePreferences(XposedService service, String group) {
|
private RemotePreferences(XposedService service, String group) {
|
||||||
this.mService = service;
|
this.mService = service;
|
||||||
this.mGroup = group;
|
this.mGroup = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static RemotePreferences newInstance(XposedService service, String group) throws RemoteException {
|
||||||
|
Bundle output = service.getRaw().requestRemotePreferences(group);
|
||||||
|
if (output == null) return null;
|
||||||
|
RemotePreferences prefs = new RemotePreferences(service, group);
|
||||||
|
if (output.containsKey("map")) {
|
||||||
|
prefs.mMap.putAll((Map<String, Object>) output.getSerializable("map"));
|
||||||
}
|
}
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
void setDeleted() {
|
||||||
static RemotePreferences newInstance(XposedService service, String group) throws RemoteException {
|
this.isDeleted = true;
|
||||||
Bundle output = service.getRaw().requestRemotePreferences(group);
|
}
|
||||||
if (output == null) return null;
|
|
||||||
RemotePreferences prefs = new RemotePreferences(service, group);
|
|
||||||
if (output.containsKey("map")) {
|
|
||||||
prefs.mMap.putAll((Map<String, Object>) output.getSerializable("map"));
|
|
||||||
}
|
|
||||||
return prefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDeleted() {
|
@Override
|
||||||
this.isDeleted = true;
|
public Map<String, ?> getAll() {
|
||||||
|
return new TreeMap<>(mMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getString(String key, @Nullable String defValue) {
|
||||||
|
return (String) mMap.getOrDefault(key, defValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
|
||||||
|
return (Set<String>) mMap.getOrDefault(key, defValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInt(String key, int defValue) {
|
||||||
|
Integer v = (Integer) mMap.getOrDefault(key, defValue);
|
||||||
|
assert v != null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLong(String key, long defValue) {
|
||||||
|
Long v = (Long) mMap.getOrDefault(key, defValue);
|
||||||
|
assert v != null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFloat(String key, float defValue) {
|
||||||
|
Float v = (Float) mMap.getOrDefault(key, defValue);
|
||||||
|
assert v != null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
Boolean v = (Boolean) mMap.getOrDefault(key, defValue);
|
||||||
|
assert v != null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(String key) {
|
||||||
|
return mMap.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||||
|
mListeners.put(listener, CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterOnSharedPreferenceChangeListener(
|
||||||
|
OnSharedPreferenceChangeListener listener) {
|
||||||
|
mListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Editor edit() {
|
||||||
|
return new Editor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Editor implements SharedPreferences.Editor {
|
||||||
|
|
||||||
|
private final HashSet<String> mDelete = new HashSet<>();
|
||||||
|
private final HashMap<String, Object> mPut = new HashMap<>();
|
||||||
|
|
||||||
|
private void put(String key, @NonNull Object value) {
|
||||||
|
mDelete.remove(key);
|
||||||
|
mPut.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, ?> getAll() {
|
public SharedPreferences.Editor putString(String key, @Nullable String value) {
|
||||||
return new TreeMap<>(mMap);
|
if (value == null) remove(key);
|
||||||
}
|
else put(key, value);
|
||||||
|
return this;
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String getString(String key, @Nullable String defValue) {
|
|
||||||
return (String) mMap.getOrDefault(key, defValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
|
|
||||||
return (Set<String>) mMap.getOrDefault(key, defValues);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getInt(String key, int defValue) {
|
public SharedPreferences.Editor putStringSet(String key, @Nullable Set<String> values) {
|
||||||
Integer v = (Integer) mMap.getOrDefault(key, defValue);
|
if (values == null) remove(key);
|
||||||
assert v != null;
|
else put(key, values);
|
||||||
return v;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getLong(String key, long defValue) {
|
public SharedPreferences.Editor putInt(String key, int value) {
|
||||||
Long v = (Long) mMap.getOrDefault(key, defValue);
|
put(key, value);
|
||||||
assert v != null;
|
return this;
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getFloat(String key, float defValue) {
|
public SharedPreferences.Editor putLong(String key, long value) {
|
||||||
Float v = (Float) mMap.getOrDefault(key, defValue);
|
put(key, value);
|
||||||
assert v != null;
|
return this;
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean getBoolean(String key, boolean defValue) {
|
public SharedPreferences.Editor putFloat(String key, float value) {
|
||||||
Boolean v = (Boolean) mMap.getOrDefault(key, defValue);
|
put(key, value);
|
||||||
assert v != null;
|
return this;
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean contains(String key) {
|
public SharedPreferences.Editor putBoolean(String key, boolean value) {
|
||||||
return mMap.containsKey(key);
|
put(key, value);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
public SharedPreferences.Editor remove(String key) {
|
||||||
mListeners.put(listener, CONTENT);
|
mDelete.add(key);
|
||||||
|
mPut.remove(key);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
public SharedPreferences.Editor clear() {
|
||||||
mListeners.remove(listener);
|
mDelete.clear();
|
||||||
|
mPut.clear();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doUpdate(boolean throwing) {
|
||||||
|
mService.deletionLock.readLock().lock();
|
||||||
|
try {
|
||||||
|
if (isDeleted) {
|
||||||
|
throw new IllegalStateException("This preferences group has been deleted");
|
||||||
|
}
|
||||||
|
mDelete.forEach(mMap::remove);
|
||||||
|
mMap.putAll(mPut);
|
||||||
|
List<String> changes = new ArrayList<>(mDelete.size() + mMap.size());
|
||||||
|
changes.addAll(mDelete);
|
||||||
|
changes.addAll(mMap.keySet());
|
||||||
|
for (String key : changes) {
|
||||||
|
mListeners
|
||||||
|
.keySet()
|
||||||
|
.forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putSerializable("delete", mDelete);
|
||||||
|
bundle.putSerializable("put", mPut);
|
||||||
|
try {
|
||||||
|
mService.getRaw().updateRemotePreferences(mGroup, bundle);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
if (throwing) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to update remote preferences", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
mService.deletionLock.readLock().unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Editor edit() {
|
public boolean commit() {
|
||||||
return new Editor();
|
if (!mLock.tryLock()) return false;
|
||||||
|
try {
|
||||||
|
doUpdate(true);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Editor implements SharedPreferences.Editor {
|
@Override
|
||||||
|
public void apply() {
|
||||||
private final HashSet<String> mDelete = new HashSet<>();
|
HANDLER.post(() -> doUpdate(false));
|
||||||
private final HashMap<String, Object> mPut = new HashMap<>();
|
|
||||||
|
|
||||||
private void put(String key, @NonNull Object value) {
|
|
||||||
mDelete.remove(key);
|
|
||||||
mPut.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor putString(String key, @Nullable String value) {
|
|
||||||
if (value == null) remove(key);
|
|
||||||
else put(key, value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor putStringSet(String key, @Nullable Set<String> values) {
|
|
||||||
if (values == null) remove(key);
|
|
||||||
else put(key, values);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor putInt(String key, int value) {
|
|
||||||
put(key, value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor putLong(String key, long value) {
|
|
||||||
put(key, value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor putFloat(String key, float value) {
|
|
||||||
put(key, value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor putBoolean(String key, boolean value) {
|
|
||||||
put(key, value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor remove(String key) {
|
|
||||||
mDelete.add(key);
|
|
||||||
mPut.remove(key);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences.Editor clear() {
|
|
||||||
mDelete.clear();
|
|
||||||
mPut.clear();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doUpdate(boolean throwing) {
|
|
||||||
mService.deletionLock.readLock().lock();
|
|
||||||
try {
|
|
||||||
if (isDeleted) {
|
|
||||||
throw new IllegalStateException("This preferences group has been deleted");
|
|
||||||
}
|
|
||||||
mDelete.forEach(mMap::remove);
|
|
||||||
mMap.putAll(mPut);
|
|
||||||
List<String> changes = new ArrayList<>(mDelete.size() + mMap.size());
|
|
||||||
changes.addAll(mDelete);
|
|
||||||
changes.addAll(mMap.keySet());
|
|
||||||
for (String key : changes) {
|
|
||||||
mListeners.keySet().forEach(listener -> listener.onSharedPreferenceChanged(RemotePreferences.this, key));
|
|
||||||
}
|
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putSerializable("delete", mDelete);
|
|
||||||
bundle.putSerializable("put", mPut);
|
|
||||||
try {
|
|
||||||
mService.getRaw().updateRemotePreferences(mGroup, bundle);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
if (throwing) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Failed to update remote preferences", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
mService.deletionLock.readLock().unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean commit() {
|
|
||||||
if (!mLock.tryLock()) return false;
|
|
||||||
try {
|
|
||||||
doUpdate(true);
|
|
||||||
return true;
|
|
||||||
} finally {
|
|
||||||
mLock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply() {
|
|
||||||
HANDLER.post(() -> doUpdate(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,58 +7,67 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public final class XposedProvider extends ContentProvider {
|
public final class XposedProvider extends ContentProvider {
|
||||||
|
|
||||||
private static final String TAG = "XposedProvider";
|
private static final String TAG = "XposedProvider";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
public Cursor query(
|
||||||
return null;
|
@NonNull Uri uri,
|
||||||
}
|
@Nullable String[] projection,
|
||||||
|
@Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs,
|
||||||
|
@Nullable String sortOrder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getType(@NonNull Uri uri) {
|
public String getType(@NonNull Uri uri) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
|
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
|
public int delete(
|
||||||
return 0;
|
@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||||
}
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
|
public int update(
|
||||||
return 0;
|
@NonNull Uri uri,
|
||||||
}
|
@Nullable ContentValues values,
|
||||||
|
@Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
|
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
|
||||||
if (method.equals(IXposedService.SEND_BINDER) && extras != null) {
|
if (method.equals(IXposedService.SEND_BINDER) && extras != null) {
|
||||||
IBinder binder = extras.getBinder("binder");
|
IBinder binder = extras.getBinder("binder");
|
||||||
if (binder != null) {
|
if (binder != null) {
|
||||||
Log.d(TAG, "binder received: " + binder);
|
Log.d(TAG, "binder received: " + binder);
|
||||||
XposedServiceHelper.onBinderReceived(binder);
|
XposedServiceHelper.onBinderReceived(binder);
|
||||||
}
|
}
|
||||||
return new Bundle();
|
return new Bundle();
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package io.github.libxposed.service;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -16,363 +14,359 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class XposedService {
|
public final class XposedService {
|
||||||
|
|
||||||
public final static class ServiceException extends RuntimeException {
|
public static final class ServiceException extends RuntimeException {
|
||||||
ServiceException(String message) {
|
ServiceException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
|
||||||
|
|
||||||
ServiceException(RemoteException e) {
|
|
||||||
super("Xposed service error", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static Map<OnScopeEventListener, IXposedScopeCallback> scopeCallbacks = new WeakHashMap<>();
|
ServiceException(RemoteException e) {
|
||||||
|
super("Xposed service error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<OnScopeEventListener, IXposedScopeCallback> scopeCallbacks =
|
||||||
|
new WeakHashMap<>();
|
||||||
|
|
||||||
|
/** Callback interface for module scope request. */
|
||||||
|
public interface OnScopeEventListener {
|
||||||
|
/**
|
||||||
|
* Callback when the request notification / window prompted.
|
||||||
|
*
|
||||||
|
* @param packageName Package name of requested app
|
||||||
|
*/
|
||||||
|
default void onScopeRequestPrompted(String packageName) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback interface for module scope request.
|
* Callback when the request is approved.
|
||||||
|
*
|
||||||
|
* @param packageName Package name of requested app
|
||||||
*/
|
*/
|
||||||
public interface OnScopeEventListener {
|
default void onScopeRequestApproved(String packageName) {}
|
||||||
/**
|
|
||||||
* Callback when the request notification / window prompted.
|
|
||||||
*
|
|
||||||
* @param packageName Package name of requested app
|
|
||||||
*/
|
|
||||||
default void onScopeRequestPrompted(String packageName) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback when the request is approved.
|
* Callback when the request is denied.
|
||||||
*
|
*
|
||||||
* @param packageName Package name of requested app
|
* @param packageName Package name of requested app
|
||||||
*/
|
*/
|
||||||
default void onScopeRequestApproved(String packageName) {
|
default void onScopeRequestDenied(String packageName) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback when the request is denied.
|
* Callback when the request is timeout or revoked.
|
||||||
*
|
*
|
||||||
* @param packageName Package name of requested app
|
* @param packageName Package name of requested app
|
||||||
*/
|
*/
|
||||||
default void onScopeRequestDenied(String packageName) {
|
default void onScopeRequestTimeout(String packageName) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback when the request is timeout or revoked.
|
* Callback when the request is failed.
|
||||||
*
|
*
|
||||||
* @param packageName Package name of requested app
|
* @param packageName Package name of requested app
|
||||||
*/
|
* @param message Error message
|
||||||
default void onScopeRequestTimeout(String packageName) {
|
*/
|
||||||
}
|
default void onScopeRequestFailed(String packageName, String message) {}
|
||||||
|
|
||||||
/**
|
private IXposedScopeCallback asInterface() {
|
||||||
* Callback when the request is failed.
|
return scopeCallbacks.computeIfAbsent(
|
||||||
*
|
this,
|
||||||
* @param packageName Package name of requested app
|
(listener) ->
|
||||||
* @param message Error message
|
new IXposedScopeCallback.Stub() {
|
||||||
*/
|
|
||||||
default void onScopeRequestFailed(String packageName, String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private IXposedScopeCallback asInterface() {
|
|
||||||
return scopeCallbacks.computeIfAbsent(this, (listener) -> new IXposedScopeCallback.Stub() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onScopeRequestPrompted(String packageName) {
|
public void onScopeRequestPrompted(String packageName) {
|
||||||
listener.onScopeRequestPrompted(packageName);
|
listener.onScopeRequestPrompted(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScopeRequestApproved(String packageName) {
|
public void onScopeRequestApproved(String packageName) {
|
||||||
listener.onScopeRequestApproved(packageName);
|
listener.onScopeRequestApproved(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScopeRequestDenied(String packageName) {
|
public void onScopeRequestDenied(String packageName) {
|
||||||
listener.onScopeRequestDenied(packageName);
|
listener.onScopeRequestDenied(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScopeRequestTimeout(String packageName) {
|
public void onScopeRequestTimeout(String packageName) {
|
||||||
listener.onScopeRequestTimeout(packageName);
|
listener.onScopeRequestTimeout(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScopeRequestFailed(String packageName, String message) {
|
public void onScopeRequestFailed(String packageName, String message) {
|
||||||
listener.onScopeRequestFailed(packageName, message);
|
listener.onScopeRequestFailed(packageName, message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Privilege {
|
public enum Privilege {
|
||||||
/**
|
/** Unknown privilege value. */
|
||||||
* Unknown privilege value.
|
FRAMEWORK_PRIVILEGE_UNKNOWN,
|
||||||
*/
|
|
||||||
FRAMEWORK_PRIVILEGE_UNKNOWN,
|
|
||||||
|
|
||||||
/**
|
/** The framework is running as root. */
|
||||||
* The framework is running as root.
|
FRAMEWORK_PRIVILEGE_ROOT,
|
||||||
*/
|
|
||||||
FRAMEWORK_PRIVILEGE_ROOT,
|
|
||||||
|
|
||||||
/**
|
/** The framework is running in a container with a fake system_server. */
|
||||||
* The framework is running in a container with a fake system_server.
|
FRAMEWORK_PRIVILEGE_CONTAINER,
|
||||||
*/
|
|
||||||
FRAMEWORK_PRIVILEGE_CONTAINER,
|
|
||||||
|
|
||||||
/**
|
/** The framework is running as a different app, which may have at most shell permission. */
|
||||||
* The framework is running as a different app, which may have at most shell permission.
|
FRAMEWORK_PRIVILEGE_APP,
|
||||||
*/
|
|
||||||
FRAMEWORK_PRIVILEGE_APP,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will be null and remote file is unsupported.
|
|
||||||
*/
|
|
||||||
FRAMEWORK_PRIVILEGE_EMBEDDED
|
|
||||||
}
|
|
||||||
|
|
||||||
private final IXposedService mService;
|
|
||||||
private final Map<String, RemotePreferences> mRemotePrefs = new HashMap<>();
|
|
||||||
|
|
||||||
final ReentrantReadWriteLock deletionLock = new ReentrantReadWriteLock();
|
|
||||||
|
|
||||||
XposedService(IXposedService service) {
|
|
||||||
mService = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
IXposedService getRaw() {
|
|
||||||
return mService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Xposed API version of current implementation.
|
* The framework is embedded in the hooked app, which means {@link #getRemotePreferences} will
|
||||||
*
|
* be null and remote file is unsupported.
|
||||||
* @return API version
|
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
|
||||||
*/
|
*/
|
||||||
public int getAPIVersion() {
|
FRAMEWORK_PRIVILEGE_EMBEDDED
|
||||||
try {
|
}
|
||||||
return mService.getAPIVersion();
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private final IXposedService mService;
|
||||||
* Get the Xposed framework name of current implementation.
|
private final Map<String, RemotePreferences> mRemotePrefs = new HashMap<>();
|
||||||
*
|
|
||||||
* @return Framework name
|
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public String getFrameworkName() {
|
|
||||||
try {
|
|
||||||
return mService.getFrameworkName();
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
final ReentrantReadWriteLock deletionLock = new ReentrantReadWriteLock();
|
||||||
* Get the Xposed framework version of current implementation.
|
|
||||||
*
|
|
||||||
* @return Framework version
|
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public String getFrameworkVersion() {
|
|
||||||
try {
|
|
||||||
return mService.getFrameworkVersion();
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
XposedService(IXposedService service) {
|
||||||
* Get the Xposed framework version code of current implementation.
|
mService = service;
|
||||||
*
|
}
|
||||||
* @return Framework version code
|
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
|
||||||
*/
|
|
||||||
public long getFrameworkVersionCode() {
|
|
||||||
try {
|
|
||||||
return mService.getFrameworkVersionCode();
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
IXposedService getRaw() {
|
||||||
* Get the Xposed framework privilege of current implementation.
|
return mService;
|
||||||
*
|
}
|
||||||
* @return Framework privilege
|
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public Privilege getFrameworkPrivilege() {
|
|
||||||
try {
|
|
||||||
int value = mService.getFrameworkPrivilege();
|
|
||||||
return (value >= 0 && value <= 3) ? Privilege.values()[value + 1] : Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the application scope of current module.
|
* Get the Xposed API version of current implementation.
|
||||||
*
|
*
|
||||||
* @return Module scope
|
* @return API version
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public int getAPIVersion() {
|
||||||
public List<String> getScope() {
|
try {
|
||||||
try {
|
return mService.getAPIVersion();
|
||||||
return mService.getScope();
|
} catch (RemoteException e) {
|
||||||
} catch (RemoteException e) {
|
throw new ServiceException(e);
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request to add a new app to the module scope.
|
* Get the Xposed framework name of current implementation.
|
||||||
*
|
*
|
||||||
* @param packageName Package name of the app to be added
|
* @return Framework name
|
||||||
* @param callback Callback to be invoked when the request is completed or error occurred
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
*/
|
||||||
*/
|
@NonNull
|
||||||
public void requestScope(@NonNull String packageName, @NonNull OnScopeEventListener callback) {
|
public String getFrameworkName() {
|
||||||
try {
|
try {
|
||||||
mService.requestScope(packageName, callback.asInterface());
|
return mService.getFrameworkName();
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new ServiceException(e);
|
throw new ServiceException(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an app from the module scope.
|
* Get the Xposed framework version of current implementation.
|
||||||
*
|
*
|
||||||
* @param packageName Package name of the app to be added
|
* @return Framework version
|
||||||
* @return null if successful, or non-null with error message
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
*/
|
||||||
*/
|
@NonNull
|
||||||
@Nullable
|
public String getFrameworkVersion() {
|
||||||
public String removeScope(@NonNull String packageName) {
|
try {
|
||||||
try {
|
return mService.getFrameworkVersion();
|
||||||
return mService.removeScope(packageName);
|
} catch (RemoteException e) {
|
||||||
} catch (RemoteException e) {
|
throw new ServiceException(e);
|
||||||
throw new ServiceException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get remote preferences from Xposed framework. If the group does not exist, it will be created.
|
* Get the Xposed framework version code of current implementation.
|
||||||
*
|
*
|
||||||
* @param group Group name
|
* @return Framework version code
|
||||||
* @return The preferences
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
*/
|
||||||
* @throws UnsupportedOperationException If the framework is embedded
|
public long getFrameworkVersionCode() {
|
||||||
*/
|
try {
|
||||||
@NonNull
|
return mService.getFrameworkVersionCode();
|
||||||
public SharedPreferences getRemotePreferences(@NonNull String group) {
|
} catch (RemoteException e) {
|
||||||
return mRemotePrefs.computeIfAbsent(group, k -> {
|
throw new ServiceException(e);
|
||||||
try {
|
}
|
||||||
RemotePreferences instance = RemotePreferences.newInstance(this, k);
|
}
|
||||||
if (instance == null) {
|
|
||||||
throw new ServiceException("Framework returns null");
|
/**
|
||||||
}
|
* Get the Xposed framework privilege of current implementation.
|
||||||
return instance;
|
*
|
||||||
} catch (RemoteException e) {
|
* @return Framework privilege
|
||||||
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
throw cause;
|
*/
|
||||||
}
|
@NonNull
|
||||||
throw new ServiceException(e);
|
public Privilege getFrameworkPrivilege() {
|
||||||
|
try {
|
||||||
|
int value = mService.getFrameworkPrivilege();
|
||||||
|
return (value >= 0 && value <= 3)
|
||||||
|
? Privilege.values()[value + 1]
|
||||||
|
: Privilege.FRAMEWORK_PRIVILEGE_UNKNOWN;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new ServiceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the application scope of current module.
|
||||||
|
*
|
||||||
|
* @return Module scope
|
||||||
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public List<String> getScope() {
|
||||||
|
try {
|
||||||
|
return mService.getScope();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new ServiceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to add a new app to the module scope.
|
||||||
|
*
|
||||||
|
* @param packageName Package name of the app to be added
|
||||||
|
* @param callback Callback to be invoked when the request is completed or error occurred
|
||||||
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
|
*/
|
||||||
|
public void requestScope(@NonNull String packageName, @NonNull OnScopeEventListener callback) {
|
||||||
|
try {
|
||||||
|
mService.requestScope(packageName, callback.asInterface());
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new ServiceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an app from the module scope.
|
||||||
|
*
|
||||||
|
* @param packageName Package name of the app to be added
|
||||||
|
* @return null if successful, or non-null with error message
|
||||||
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public String removeScope(@NonNull String packageName) {
|
||||||
|
try {
|
||||||
|
return mService.removeScope(packageName);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new ServiceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remote preferences from Xposed framework. If the group does not exist, it will be created.
|
||||||
|
*
|
||||||
|
* @param group Group name
|
||||||
|
* @return The preferences
|
||||||
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
|
* @throws UnsupportedOperationException If the framework is embedded
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public SharedPreferences getRemotePreferences(@NonNull String group) {
|
||||||
|
return mRemotePrefs.computeIfAbsent(
|
||||||
|
group,
|
||||||
|
k -> {
|
||||||
|
try {
|
||||||
|
RemotePreferences instance = RemotePreferences.newInstance(this, k);
|
||||||
|
if (instance == null) {
|
||||||
|
throw new ServiceException("Framework returns null");
|
||||||
}
|
}
|
||||||
|
return instance;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
throw new ServiceException(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a group of remote preferences.
|
* Delete a group of remote preferences.
|
||||||
*
|
*
|
||||||
* @param group Group name
|
* @param group Group name
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws UnsupportedOperationException If the framework is embedded
|
* @throws UnsupportedOperationException If the framework is embedded
|
||||||
*/
|
*/
|
||||||
public void deleteRemotePreferences(@NonNull String group) {
|
public void deleteRemotePreferences(@NonNull String group) {
|
||||||
deletionLock.writeLock().lock();
|
deletionLock.writeLock().lock();
|
||||||
try {
|
try {
|
||||||
mService.deleteRemotePreferences(group);
|
mService.deleteRemotePreferences(group);
|
||||||
mRemotePrefs.computeIfPresent(group, (k, v) -> {
|
mRemotePrefs.computeIfPresent(
|
||||||
v.setDeleted();
|
group,
|
||||||
return null;
|
(k, v) -> {
|
||||||
});
|
v.setDeleted();
|
||||||
} catch (RemoteException e) {
|
return null;
|
||||||
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
});
|
||||||
throw cause;
|
} catch (RemoteException e) {
|
||||||
}
|
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
||||||
throw new ServiceException(e);
|
throw cause;
|
||||||
} finally {
|
}
|
||||||
deletionLock.writeLock().unlock();
|
throw new ServiceException(e);
|
||||||
}
|
} finally {
|
||||||
|
deletionLock.writeLock().unlock();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all files in the module's shared data directory.
|
* List all files in the module's shared data directory.
|
||||||
*
|
*
|
||||||
* @return The file list
|
* @return The file list
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws UnsupportedOperationException If the framework is embedded
|
* @throws UnsupportedOperationException If the framework is embedded
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public String[] listRemoteFiles() {
|
public String[] listRemoteFiles() {
|
||||||
try {
|
try {
|
||||||
String[] files = mService.listRemoteFiles();
|
String[] files = mService.listRemoteFiles();
|
||||||
if (files == null) throw new ServiceException("Framework returns null");
|
if (files == null) throw new ServiceException("Framework returns null");
|
||||||
return files;
|
return files;
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
||||||
throw cause;
|
throw cause;
|
||||||
}
|
}
|
||||||
throw new ServiceException(e);
|
throw new ServiceException(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a file in the module's shared data directory. The file will be created if not exists.
|
* Open a file in the module's shared data directory. The file will be created if not exists.
|
||||||
*
|
*
|
||||||
* @param name File name, must not contain path separators and . or ..
|
* @param name File name, must not contain path separators and . or ..
|
||||||
* @return The file descriptor
|
* @return The file descriptor
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws UnsupportedOperationException If the framework is embedded
|
* @throws UnsupportedOperationException If the framework is embedded
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public ParcelFileDescriptor openRemoteFile(@NonNull String name) {
|
public ParcelFileDescriptor openRemoteFile(@NonNull String name) {
|
||||||
try {
|
try {
|
||||||
ParcelFileDescriptor file = mService.openRemoteFile(name);
|
ParcelFileDescriptor file = mService.openRemoteFile(name);
|
||||||
if (file == null) throw new ServiceException("Framework returns null");
|
if (file == null) throw new ServiceException("Framework returns null");
|
||||||
return file;
|
return file;
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
||||||
throw cause;
|
throw cause;
|
||||||
}
|
}
|
||||||
throw new ServiceException(e);
|
throw new ServiceException(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a file in the module's shared data directory.
|
* Delete a file in the module's shared data directory.
|
||||||
*
|
*
|
||||||
* @param name File name, must not contain path separators and . or ..
|
* @param name File name, must not contain path separators and . or ..
|
||||||
* @return true if successful, false if the file does not exist
|
* @return true if successful, false if the file does not exist
|
||||||
* @throws ServiceException If the service is dead or an error occurred
|
* @throws ServiceException If the service is dead or an error occurred
|
||||||
* @throws UnsupportedOperationException If the framework is embedded
|
* @throws UnsupportedOperationException If the framework is embedded
|
||||||
*/
|
*/
|
||||||
public boolean deleteRemoteFile(@NonNull String name) {
|
public boolean deleteRemoteFile(@NonNull String name) {
|
||||||
try {
|
try {
|
||||||
return mService.deleteRemoteFile(name);
|
return mService.deleteRemoteFile(name);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
if (e.getCause() instanceof UnsupportedOperationException cause) {
|
||||||
throw cause;
|
throw cause;
|
||||||
}
|
}
|
||||||
throw new ServiceException(e);
|
throw new ServiceException(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package io.github.libxposed.service;
|
|||||||
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -12,67 +10,63 @@ import java.util.Set;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class XposedServiceHelper {
|
public final class XposedServiceHelper {
|
||||||
|
|
||||||
|
/** Callback interface for Xposed service. */
|
||||||
|
public interface OnServiceListener {
|
||||||
/**
|
/**
|
||||||
* Callback interface for Xposed service.
|
* Callback when the service is connected.<br>
|
||||||
*/
|
* This method could be called multiple times if multiple Xposed frameworks exist.
|
||||||
public interface OnServiceListener {
|
|
||||||
/**
|
|
||||||
* Callback when the service is connected.<br/>
|
|
||||||
* This method could be called multiple times if multiple Xposed frameworks exist.
|
|
||||||
*
|
|
||||||
* @param service Service instance
|
|
||||||
*/
|
|
||||||
void onServiceBind(@NonNull XposedService service);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback when the service is dead.
|
|
||||||
*/
|
|
||||||
void onServiceDied(@NonNull XposedService service);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String TAG = "XposedServiceHelper";
|
|
||||||
private static final Set<XposedService> mCache = new HashSet<>();
|
|
||||||
private static OnServiceListener mListener = null;
|
|
||||||
|
|
||||||
static void onBinderReceived(IBinder binder) {
|
|
||||||
if (binder == null) return;
|
|
||||||
synchronized (mCache) {
|
|
||||||
try {
|
|
||||||
XposedService service = new XposedService(IXposedService.Stub.asInterface(binder));
|
|
||||||
if (mListener == null) {
|
|
||||||
mCache.add(service);
|
|
||||||
} else {
|
|
||||||
binder.linkToDeath(() -> mListener.onServiceDied(service), 0);
|
|
||||||
mListener.onServiceBind(service);
|
|
||||||
}
|
|
||||||
} catch (Throwable t) {
|
|
||||||
Log.e(TAG, "onBinderReceived", t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a ServiceListener to receive service binders from Xposed frameworks.<br/>
|
|
||||||
* This method should only be called once.
|
|
||||||
*
|
*
|
||||||
* @param listener Listener to register
|
* @param service Service instance
|
||||||
*/
|
*/
|
||||||
public static void registerListener(OnServiceListener listener) {
|
void onServiceBind(@NonNull XposedService service);
|
||||||
synchronized (mCache) {
|
|
||||||
mListener = listener;
|
/** Callback when the service is dead. */
|
||||||
if (!mCache.isEmpty()) {
|
void onServiceDied(@NonNull XposedService service);
|
||||||
for (Iterator<XposedService> it = mCache.iterator(); it.hasNext(); ) {
|
}
|
||||||
try {
|
|
||||||
XposedService service = it.next();
|
private static final String TAG = "XposedServiceHelper";
|
||||||
service.getRaw().asBinder().linkToDeath(() -> mListener.onServiceDied(service), 0);
|
private static final Set<XposedService> mCache = new HashSet<>();
|
||||||
mListener.onServiceBind(service);
|
private static OnServiceListener mListener = null;
|
||||||
} catch (Throwable t) {
|
|
||||||
Log.e(TAG, "registerListener", t);
|
static void onBinderReceived(IBinder binder) {
|
||||||
it.remove();
|
if (binder == null) return;
|
||||||
}
|
synchronized (mCache) {
|
||||||
}
|
try {
|
||||||
mCache.clear();
|
XposedService service = new XposedService(IXposedService.Stub.asInterface(binder));
|
||||||
}
|
if (mListener == null) {
|
||||||
|
mCache.add(service);
|
||||||
|
} else {
|
||||||
|
binder.linkToDeath(() -> mListener.onServiceDied(service), 0);
|
||||||
|
mListener.onServiceBind(service);
|
||||||
}
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.e(TAG, "onBinderReceived", t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a ServiceListener to receive service binders from Xposed frameworks.<br>
|
||||||
|
* This method should only be called once.
|
||||||
|
*
|
||||||
|
* @param listener Listener to register
|
||||||
|
*/
|
||||||
|
public static void registerListener(OnServiceListener listener) {
|
||||||
|
synchronized (mCache) {
|
||||||
|
mListener = listener;
|
||||||
|
if (!mCache.isEmpty()) {
|
||||||
|
for (Iterator<XposedService> it = mCache.iterator(); it.hasNext(); ) {
|
||||||
|
try {
|
||||||
|
XposedService service = it.next();
|
||||||
|
service.getRaw().asBinder().linkToDeath(() -> mListener.onServiceDied(service), 0);
|
||||||
|
mListener.onServiceBind(service);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.e(TAG, "registerListener", t);
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import io.nekohasekai.sfa.bg.UpdateProfileWork
|
|||||||
import io.nekohasekai.sfa.constant.Bugs
|
import io.nekohasekai.sfa.constant.Bugs
|
||||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||||
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
||||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
|
||||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||||
|
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||||
import io.nekohasekai.sfa.vendor.Vendor
|
import io.nekohasekai.sfa.vendor.Vendor
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.provider.DocumentsContract
|
|||||||
import android.provider.DocumentsProvider
|
import android.provider.DocumentsProvider
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
class WorkingDirectoryProvider : DocumentsProvider() {
|
class WorkingDirectoryProvider : DocumentsProvider() {
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ class WorkingDirectoryProvider : DocumentsProvider() {
|
|||||||
add(
|
add(
|
||||||
DocumentsContract.Root.COLUMN_FLAGS,
|
DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
|
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
|
||||||
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD,
|
||||||
)
|
)
|
||||||
add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||||
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
|
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
|
||||||
@@ -64,11 +63,7 @@ class WorkingDirectoryProvider : DocumentsProvider() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun queryChildDocuments(
|
override fun queryChildDocuments(parentDocumentId: String, projection: Array<out String>?, sortOrder: String?): Cursor {
|
||||||
parentDocumentId: String,
|
|
||||||
projection: Array<out String>?,
|
|
||||||
sortOrder: String?
|
|
||||||
): Cursor {
|
|
||||||
val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
val parent = getFileForDocId(parentDocumentId)
|
val parent = getFileForDocId(parentDocumentId)
|
||||||
parent.listFiles()?.forEach { file ->
|
parent.listFiles()?.forEach { file ->
|
||||||
@@ -77,21 +72,13 @@ class WorkingDirectoryProvider : DocumentsProvider() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openDocument(
|
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor {
|
||||||
documentId: String,
|
|
||||||
mode: String,
|
|
||||||
signal: CancellationSignal?
|
|
||||||
): ParcelFileDescriptor {
|
|
||||||
val file = getFileForDocId(documentId)
|
val file = getFileForDocId(documentId)
|
||||||
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||||
return ParcelFileDescriptor.open(file, accessMode)
|
return ParcelFileDescriptor.open(file, accessMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createDocument(
|
override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String {
|
||||||
parentDocumentId: String,
|
|
||||||
mimeType: String,
|
|
||||||
displayName: String
|
|
||||||
): String {
|
|
||||||
val parent = getFileForDocId(parentDocumentId)
|
val parent = getFileForDocId(parentDocumentId)
|
||||||
val file = File(parent, displayName)
|
val file = File(parent, displayName)
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.database.Settings
|
|
||||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -21,10 +21,7 @@ class AppChangeReceiver : BroadcastReceiver() {
|
|||||||
private const val TAG = "AppChangeReceiver"
|
private const val TAG = "AppChangeReceiver"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
) {
|
|
||||||
Log.d(TAG, "onReceive: ${intent.action}")
|
Log.d(TAG, "onReceive: ${intent.action}")
|
||||||
if (!Settings.perAppProxyEnabled) {
|
if (!Settings.perAppProxyEnabled) {
|
||||||
Log.d(TAG, "per app proxy disabled")
|
Log.d(TAG, "per app proxy disabled")
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
) {
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ import io.nekohasekai.libbox.PlatformInterface
|
|||||||
import io.nekohasekai.libbox.SystemProxyStatus
|
import io.nekohasekai.libbox.SystemProxyStatus
|
||||||
import io.nekohasekai.sfa.Application
|
import io.nekohasekai.sfa.Application
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.compose.MainActivity
|
||||||
import io.nekohasekai.sfa.constant.Action
|
import io.nekohasekai.sfa.constant.Action
|
||||||
import io.nekohasekai.sfa.constant.Alert
|
import io.nekohasekai.sfa.constant.Alert
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.database.ProfileManager
|
import io.nekohasekai.sfa.database.ProfileManager
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.ktx.hasPermission
|
import io.nekohasekai.sfa.ktx.hasPermission
|
||||||
import io.nekohasekai.sfa.compose.MainActivity
|
|
||||||
import io.nekohasekai.sfa.vendor.Vendor
|
import io.nekohasekai.sfa.vendor.Vendor
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -44,10 +44,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class BoxService(
|
class BoxService(private val service: Service, private val platformInterface: PlatformInterface) : CommandServerHandler {
|
||||||
private val service: Service,
|
|
||||||
private val platformInterface: PlatformInterface,
|
|
||||||
) : CommandServerHandler {
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PROFILE_UPDATE_INTERVAL = 15L * 60 * 1000 // 15 minutes in milliseconds
|
private const val PROFILE_UPDATE_INTERVAL = 15L * 60 * 1000 // 15 minutes in milliseconds
|
||||||
private const val TAG = "BoxService"
|
private const val TAG = "BoxService"
|
||||||
@@ -81,10 +78,7 @@ class BoxService(
|
|||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
private val receiver =
|
private val receiver =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
) {
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.SERVICE_CLOSE -> {
|
Action.SERVICE_CLOSE -> {
|
||||||
stopService()
|
stopService()
|
||||||
@@ -316,10 +310,7 @@ class BoxService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopAndAlert(
|
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
|
||||||
type: Alert,
|
|
||||||
message: String? = null,
|
|
||||||
) {
|
|
||||||
Settings.startedByUser = false
|
Settings.startedByUser = false
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (receiverRegistered) {
|
if (receiverRegistered) {
|
||||||
@@ -368,9 +359,7 @@ class BoxService(
|
|||||||
return Service.START_NOT_STICKY
|
return Service.START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun onBind(): IBinder {
|
internal fun onBind(): IBinder = binder
|
||||||
return binder
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun onDestroy() {
|
internal fun onDestroy() {
|
||||||
binder.close()
|
binder.close()
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import java.io.StringWriter
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.zip.Deflater
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
@@ -134,11 +133,7 @@ object DebugInfoExporter {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addLogEntries(
|
private fun addLogEntries(zip: ZipOutputStream, warnings: MutableList<String>, context: Context): Int {
|
||||||
zip: ZipOutputStream,
|
|
||||||
warnings: MutableList<String>,
|
|
||||||
context: Context,
|
|
||||||
): Int {
|
|
||||||
var count = 0
|
var count = 0
|
||||||
if (streamCommandToZip(zip, "logs/logcat.txt", warnings, listOf("logcat", "-d", "-b", "all")) != null) count++
|
if (streamCommandToZip(zip, "logs/logcat.txt", warnings, listOf("logcat", "-d", "-b", "all")) != null) count++
|
||||||
if (streamCommandToZip(zip, "logs/dmesg.txt", warnings, listOf("dmesg")) != null) count++
|
if (streamCommandToZip(zip, "logs/dmesg.txt", warnings, listOf("dmesg")) != null) count++
|
||||||
@@ -185,11 +180,7 @@ object DebugInfoExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addSystemEntries(
|
private fun addSystemEntries(zip: ZipOutputStream, warnings: MutableList<String>, packageName: String): Int {
|
||||||
zip: ZipOutputStream,
|
|
||||||
warnings: MutableList<String>,
|
|
||||||
packageName: String,
|
|
||||||
): Int {
|
|
||||||
var count = 0
|
var count = 0
|
||||||
if (streamCommandToZip(zip, "system/getprop.txt", warnings, listOf("getprop")) != null) count++
|
if (streamCommandToZip(zip, "system/getprop.txt", warnings, listOf("getprop")) != null) count++
|
||||||
if (streamCommandToZip(zip, "system/uname.txt", warnings, listOf("uname", "-a")) != null) count++
|
if (streamCommandToZip(zip, "system/uname.txt", warnings, listOf("uname", "-a")) != null) count++
|
||||||
@@ -210,27 +201,28 @@ object DebugInfoExporter {
|
|||||||
if (cmdPackages != null) count++
|
if (cmdPackages != null) count++
|
||||||
if ((cmdPackages == null || cmdPackages.bytes == 0L) && (cmdPackages?.exitCode ?: 1) != 0) {
|
if ((cmdPackages == null || cmdPackages.bytes == 0L) && (cmdPackages?.exitCode ?: 1) != 0) {
|
||||||
if (streamCommandToZip(
|
if (streamCommandToZip(
|
||||||
zip,
|
zip,
|
||||||
"system/packages_pm.txt",
|
"system/packages_pm.txt",
|
||||||
warnings,
|
warnings,
|
||||||
listOf("pm", "list", "packages", "-f"),
|
listOf("pm", "list", "packages", "-f"),
|
||||||
) != null) count++
|
) != null
|
||||||
|
) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (streamCommandToZip(
|
if (streamCommandToZip(
|
||||||
zip,
|
zip,
|
||||||
"system/dumpsys_package_${packageName}.txt",
|
"system/dumpsys_package_$packageName.txt",
|
||||||
warnings,
|
warnings,
|
||||||
listOf("dumpsys", "package", packageName),
|
listOf("dumpsys", "package", packageName),
|
||||||
) != null) count++
|
) != null
|
||||||
|
) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addFileEntry(
|
private fun addFileEntry(zip: ZipOutputStream, file: File, entryName: String, warnings: MutableList<String>): Boolean {
|
||||||
zip: ZipOutputStream,
|
|
||||||
file: File,
|
|
||||||
entryName: String,
|
|
||||||
warnings: MutableList<String>,
|
|
||||||
): Boolean {
|
|
||||||
if (!file.isFile) {
|
if (!file.isFile) {
|
||||||
warnings.add("missing file: ${file.path}")
|
warnings.add("missing file: ${file.path}")
|
||||||
return false
|
return false
|
||||||
@@ -262,51 +254,40 @@ object DebugInfoExporter {
|
|||||||
zip.closeEntry()
|
zip.closeEntry()
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class CommandResult(
|
private data class CommandResult(val exitCode: Int, val bytes: Long)
|
||||||
val exitCode: Int,
|
|
||||||
val bytes: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun streamCommandToZip(
|
private fun streamCommandToZip(
|
||||||
zip: ZipOutputStream,
|
zip: ZipOutputStream,
|
||||||
entryName: String,
|
entryName: String,
|
||||||
warnings: MutableList<String>,
|
warnings: MutableList<String>,
|
||||||
command: List<String>,
|
command: List<String>,
|
||||||
): CommandResult? {
|
): CommandResult? = try {
|
||||||
return try {
|
val process = ProcessBuilder(command).redirectErrorStream(true).start()
|
||||||
val process = ProcessBuilder(command).redirectErrorStream(true).start()
|
val entry = ZipEntry(entryName)
|
||||||
val entry = ZipEntry(entryName)
|
zip.putNextEntry(entry)
|
||||||
zip.putNextEntry(entry)
|
var bytes = 0L
|
||||||
var bytes = 0L
|
process.inputStream.use { input ->
|
||||||
process.inputStream.use { input ->
|
val buffer = ByteArray(16 * 1024)
|
||||||
val buffer = ByteArray(16 * 1024)
|
while (true) {
|
||||||
while (true) {
|
val read = input.read(buffer)
|
||||||
val read = input.read(buffer)
|
if (read <= 0) break
|
||||||
if (read <= 0) break
|
zip.write(buffer, 0, read)
|
||||||
zip.write(buffer, 0, read)
|
bytes += read
|
||||||
bytes += read
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
zip.closeEntry()
|
|
||||||
val code = process.waitFor()
|
|
||||||
if (code != 0) {
|
|
||||||
warnings.add("command failed (${command.joinToString(" ")}): exit=$code")
|
|
||||||
}
|
|
||||||
CommandResult(code, bytes)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
warnings.add("command failed (${command.joinToString(" ")}): ${e.message}")
|
|
||||||
runCatching { zip.closeEntry() }
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
zip.closeEntry()
|
||||||
|
val code = process.waitFor()
|
||||||
|
if (code != 0) {
|
||||||
|
warnings.add("command failed (${command.joinToString(" ")}): exit=$code")
|
||||||
|
}
|
||||||
|
CommandResult(code, bytes)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
warnings.add("command failed (${command.joinToString(" ")}): ${e.message}")
|
||||||
|
runCatching { zip.closeEntry() }
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildError(
|
private fun buildError(stage: String, detail: String, throwable: Throwable?, warnings: List<String>, outputPath: String?): String {
|
||||||
stage: String,
|
|
||||||
detail: String,
|
|
||||||
throwable: Throwable?,
|
|
||||||
warnings: List<String>,
|
|
||||||
outputPath: String?,
|
|
||||||
): String {
|
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("stage=").append(stage).append('\n')
|
sb.append("stage=").append(stage).append('\n')
|
||||||
if (!outputPath.isNullOrBlank()) {
|
if (!outputPath.isNullOrBlank()) {
|
||||||
|
|||||||
@@ -60,103 +60,102 @@ object DefaultNetworkListener {
|
|||||||
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
||||||
var network: Network? = null
|
var network: Network? = null
|
||||||
val pendingRequests = arrayListOf<NetworkMessage.Get>()
|
val pendingRequests = arrayListOf<NetworkMessage.Get>()
|
||||||
for (message in channel) when (message) {
|
for (message in channel) {
|
||||||
is NetworkMessage.Start -> {
|
when (message) {
|
||||||
if (listeners.isEmpty()) register()
|
is NetworkMessage.Start -> {
|
||||||
listeners[message.key] = message.listener
|
if (listeners.isEmpty()) register()
|
||||||
if (network != null) message.listener(network)
|
listeners[message.key] = message.listener
|
||||||
}
|
if (network != null) message.listener(network)
|
||||||
|
|
||||||
is NetworkMessage.Get -> {
|
|
||||||
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
|
|
||||||
if (network == null) {
|
|
||||||
pendingRequests += message
|
|
||||||
} else {
|
|
||||||
message.response.complete(
|
|
||||||
network,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkMessage.Stop ->
|
|
||||||
if (listeners.isNotEmpty() && // was not empty
|
|
||||||
listeners.remove(message.key) != null && listeners.isEmpty()
|
|
||||||
) {
|
|
||||||
network = null
|
|
||||||
unregister()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkMessage.Put -> {
|
is NetworkMessage.Get -> {
|
||||||
network = message.network
|
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
|
||||||
pendingRequests.forEach { it.response.complete(message.network) }
|
if (network == null) {
|
||||||
pendingRequests.clear()
|
pendingRequests += message
|
||||||
listeners.values.forEach { it(network) }
|
} else {
|
||||||
}
|
message.response.complete(
|
||||||
|
|
||||||
is NetworkMessage.Update ->
|
|
||||||
if (network == message.network) {
|
|
||||||
listeners.values.forEach {
|
|
||||||
it(
|
|
||||||
network,
|
network,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkMessage.Lost ->
|
is NetworkMessage.Stop ->
|
||||||
if (network == message.network) {
|
if (listeners.isNotEmpty() &&
|
||||||
network = null
|
// was not empty
|
||||||
listeners.values.forEach { it(null) }
|
listeners.remove(message.key) != null &&
|
||||||
|
listeners.isEmpty()
|
||||||
|
) {
|
||||||
|
network = null
|
||||||
|
unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkMessage.Put -> {
|
||||||
|
network = message.network
|
||||||
|
pendingRequests.forEach { it.response.complete(message.network) }
|
||||||
|
pendingRequests.clear()
|
||||||
|
listeners.values.forEach { it(network) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is NetworkMessage.Update ->
|
||||||
|
if (network == message.network) {
|
||||||
|
listeners.values.forEach {
|
||||||
|
it(
|
||||||
|
network,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkMessage.Lost ->
|
||||||
|
if (network == message.network) {
|
||||||
|
network = null
|
||||||
|
listeners.values.forEach { it(null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun start(
|
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(
|
||||||
key: Any,
|
|
||||||
listener: (Network?) -> Unit,
|
|
||||||
) = networkActor.send(
|
|
||||||
NetworkMessage.Start(
|
NetworkMessage.Start(
|
||||||
key,
|
key,
|
||||||
listener,
|
listener,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun get(): Network = if (fallback) @TargetApi(23) {
|
suspend fun get(): Network = if (fallback) {
|
||||||
|
@TargetApi(23)
|
||||||
Application.connectivity.activeNetwork
|
Application.connectivity.activeNetwork
|
||||||
?: error("missing default network") // failed to listen, return current if available
|
?: error("missing default network") // failed to listen, return current if available
|
||||||
} else NetworkMessage.Get().run {
|
} else {
|
||||||
networkActor.send(this)
|
NetworkMessage.Get().run {
|
||||||
response.await()
|
networkActor.send(this)
|
||||||
|
response.await()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
|
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
|
||||||
|
|
||||||
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
||||||
private object Callback : ConnectivityManager.NetworkCallback() {
|
private object Callback : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) =
|
override fun onAvailable(network: Network) = runBlocking {
|
||||||
runBlocking {
|
networkActor.send(
|
||||||
networkActor.send(
|
NetworkMessage.Put(
|
||||||
NetworkMessage.Put(
|
network,
|
||||||
network,
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
network: Network,
|
|
||||||
networkCapabilities: NetworkCapabilities,
|
|
||||||
) {
|
|
||||||
// it's a good idea to refresh capabilities
|
// it's a good idea to refresh capabilities
|
||||||
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
|
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) =
|
override fun onLost(network: Network) = runBlocking {
|
||||||
runBlocking {
|
networkActor.send(
|
||||||
networkActor.send(
|
NetworkMessage.Lost(
|
||||||
NetworkMessage.Lost(
|
network,
|
||||||
network,
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fallback = false
|
private var fallback = false
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ object DefaultNetworkMonitor {
|
|||||||
checkDefaultInterfaceUpdate(defaultNetwork)
|
checkDefaultInterfaceUpdate(defaultNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkDefaultInterfaceUpdate(
|
private fun checkDefaultInterfaceUpdate(newNetwork: Network?) {
|
||||||
newNetwork: Network?
|
|
||||||
) {
|
|
||||||
val listener = listener ?: return
|
val listener = listener ?: return
|
||||||
if (newNetwork != null) {
|
if (newNetwork != null) {
|
||||||
val interfaceName =
|
val interfaceName =
|
||||||
@@ -61,5 +59,4 @@ object DefaultNetworkMonitor {
|
|||||||
listener.updateDefaultInterface("", -1, false, false)
|
listener.updateDefaultInterface("", -1, false, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,15 +19,10 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
object LocalResolver : LocalDNSTransport {
|
object LocalResolver : LocalDNSTransport {
|
||||||
private const val RCODE_NXDOMAIN = 3
|
private const val RCODE_NXDOMAIN = 3
|
||||||
|
|
||||||
override fun raw(): Boolean {
|
override fun raw(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
override fun exchange(
|
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
|
||||||
ctx: ExchangeContext,
|
|
||||||
message: ByteArray,
|
|
||||||
) {
|
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
val defaultNetwork = DefaultNetworkMonitor.require()
|
val defaultNetwork = DefaultNetworkMonitor.require()
|
||||||
suspendCoroutine { continuation ->
|
suspendCoroutine { continuation ->
|
||||||
@@ -35,10 +30,7 @@ object LocalResolver : LocalDNSTransport {
|
|||||||
ctx.onCancel(signal::cancel)
|
ctx.onCancel(signal::cancel)
|
||||||
val callback =
|
val callback =
|
||||||
object : DnsResolver.Callback<ByteArray> {
|
object : DnsResolver.Callback<ByteArray> {
|
||||||
override fun onAnswer(
|
override fun onAnswer(answer: ByteArray, rcode: Int) {
|
||||||
answer: ByteArray,
|
|
||||||
rcode: Int,
|
|
||||||
) {
|
|
||||||
if (rcode == 0) {
|
if (rcode == 0) {
|
||||||
ctx.rawSuccess(answer)
|
ctx.rawSuccess(answer)
|
||||||
} else {
|
} else {
|
||||||
@@ -70,11 +62,7 @@ object LocalResolver : LocalDNSTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun lookup(
|
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
|
||||||
ctx: ExchangeContext,
|
|
||||||
network: String,
|
|
||||||
domain: String,
|
|
||||||
) {
|
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
val defaultNetwork = DefaultNetworkMonitor.require()
|
val defaultNetwork = DefaultNetworkMonitor.require()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -84,10 +72,7 @@ object LocalResolver : LocalDNSTransport {
|
|||||||
val callback =
|
val callback =
|
||||||
object : DnsResolver.Callback<Collection<InetAddress>> {
|
object : DnsResolver.Callback<Collection<InetAddress>> {
|
||||||
@Suppress("ThrowableNotThrown")
|
@Suppress("ThrowableNotThrown")
|
||||||
override fun onAnswer(
|
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
|
||||||
answer: Collection<InetAddress>,
|
|
||||||
rcode: Int,
|
|
||||||
) {
|
|
||||||
if (rcode == 0) {
|
if (rcode == 0) {
|
||||||
ctx.success(
|
ctx.success(
|
||||||
(answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
|
(answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
|
||||||
|
|||||||
@@ -2,64 +2,66 @@ package io.nekohasekai.sfa.bg;
|
|||||||
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class LogEntry implements Parcelable {
|
public class LogEntry implements Parcelable {
|
||||||
public static final int LEVEL_DEBUG = 0;
|
public static final int LEVEL_DEBUG = 0;
|
||||||
public static final int LEVEL_INFO = 1;
|
public static final int LEVEL_INFO = 1;
|
||||||
public static final int LEVEL_WARN = 2;
|
public static final int LEVEL_WARN = 2;
|
||||||
public static final int LEVEL_ERROR = 3;
|
public static final int LEVEL_ERROR = 3;
|
||||||
|
|
||||||
public final int level;
|
public final int level;
|
||||||
public final long timestamp;
|
public final long timestamp;
|
||||||
@NonNull
|
@NonNull public final String source;
|
||||||
public final String source;
|
@NonNull public final String message;
|
||||||
@NonNull
|
@Nullable public final String stackTrace;
|
||||||
public final String message;
|
|
||||||
@Nullable
|
|
||||||
public final String stackTrace;
|
|
||||||
|
|
||||||
public LogEntry(int level, long timestamp, @NonNull String source, @NonNull String message, @Nullable String stackTrace) {
|
public LogEntry(
|
||||||
this.level = level;
|
int level,
|
||||||
this.timestamp = timestamp;
|
long timestamp,
|
||||||
this.source = source;
|
@NonNull String source,
|
||||||
this.message = message;
|
@NonNull String message,
|
||||||
this.stackTrace = stackTrace;
|
@Nullable String stackTrace) {
|
||||||
}
|
this.level = level;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.source = source;
|
||||||
|
this.message = message;
|
||||||
|
this.stackTrace = stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
protected LogEntry(Parcel in) {
|
protected LogEntry(Parcel in) {
|
||||||
level = in.readInt();
|
level = in.readInt();
|
||||||
timestamp = in.readLong();
|
timestamp = in.readLong();
|
||||||
source = in.readString();
|
source = in.readString();
|
||||||
message = in.readString();
|
message = in.readString();
|
||||||
stackTrace = in.readString();
|
stackTrace = in.readString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
dest.writeInt(level);
|
dest.writeInt(level);
|
||||||
dest.writeLong(timestamp);
|
dest.writeLong(timestamp);
|
||||||
dest.writeString(source);
|
dest.writeString(source);
|
||||||
dest.writeString(message);
|
dest.writeString(message);
|
||||||
dest.writeString(stackTrace);
|
dest.writeString(stackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<LogEntry> CREATOR = new Creator<>() {
|
public static final Creator<LogEntry> CREATOR =
|
||||||
|
new Creator<>() {
|
||||||
@Override
|
@Override
|
||||||
public LogEntry createFromParcel(Parcel in) {
|
public LogEntry createFromParcel(Parcel in) {
|
||||||
return new LogEntry(in);
|
return new LogEntry(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LogEntry[] newArray(int size) {
|
public LogEntry[] newArray(int size) {
|
||||||
return new LogEntry[size];
|
return new LogEntry[size];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,40 +2,39 @@ package io.nekohasekai.sfa.bg;
|
|||||||
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class PackageEntry implements Parcelable {
|
public class PackageEntry implements Parcelable {
|
||||||
@NonNull
|
@NonNull public final String packageName;
|
||||||
public final String packageName;
|
|
||||||
|
|
||||||
public PackageEntry(@NonNull String packageName) {
|
public PackageEntry(@NonNull String packageName) {
|
||||||
this.packageName = packageName;
|
this.packageName = packageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected PackageEntry(Parcel in) {
|
protected PackageEntry(Parcel in) {
|
||||||
packageName = in.readString();
|
packageName = in.readString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
dest.writeString(packageName);
|
dest.writeString(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<PackageEntry> CREATOR = new Creator<>() {
|
public static final Creator<PackageEntry> CREATOR =
|
||||||
|
new Creator<>() {
|
||||||
@Override
|
@Override
|
||||||
public PackageEntry createFromParcel(Parcel in) {
|
public PackageEntry createFromParcel(Parcel in) {
|
||||||
return new PackageEntry(in);
|
return new PackageEntry(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PackageEntry[] newArray(int size) {
|
public PackageEntry[] newArray(int size) {
|
||||||
return new PackageEntry[size];
|
return new PackageEntry[size];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,132 +21,132 @@ import android.os.IBinder;
|
|||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ParceledListSlice<T extends Parcelable> implements Parcelable {
|
public class ParceledListSlice<T extends Parcelable> implements Parcelable {
|
||||||
private static final int MAX_IPC_SIZE = 64 * 1024;
|
private static final int MAX_IPC_SIZE = 64 * 1024;
|
||||||
|
|
||||||
private final List<T> mList;
|
private final List<T> mList;
|
||||||
|
|
||||||
public ParceledListSlice(List<T> list) {
|
public ParceledListSlice(List<T> list) {
|
||||||
mList = list;
|
mList = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParceledListSlice(Parcel in, ClassLoader loader) {
|
||||||
|
final int n = in.readInt();
|
||||||
|
mList = new ArrayList<>(n);
|
||||||
|
if (n <= 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ParceledListSlice(Parcel in, ClassLoader loader) {
|
int i = 0;
|
||||||
final int n = in.readInt();
|
while (i < n) {
|
||||||
mList = new ArrayList<>(n);
|
if (in.readInt() == 0) {
|
||||||
if (n <= 0) {
|
break;
|
||||||
return;
|
}
|
||||||
}
|
@SuppressWarnings("unchecked")
|
||||||
|
T item = (T) in.readParcelable(loader);
|
||||||
|
mList.add(item);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i >= n) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final IBinder retriever = in.readStrongBinder();
|
||||||
|
while (i < n) {
|
||||||
|
Parcel data = Parcel.obtain();
|
||||||
|
Parcel reply = Parcel.obtain();
|
||||||
|
data.writeInt(i);
|
||||||
|
try {
|
||||||
|
retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
reply.recycle();
|
||||||
|
data.recycle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (i < n && reply.readInt() != 0) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T item = (T) reply.readParcelable(loader);
|
||||||
|
mList.add(item);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
reply.recycle();
|
||||||
|
data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int i = 0;
|
public List<T> getList() {
|
||||||
while (i < n) {
|
return mList;
|
||||||
if (in.readInt() == 0) {
|
}
|
||||||
break;
|
|
||||||
}
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
public int describeContents() {
|
||||||
T item = (T) in.readParcelable(loader);
|
int contents = 0;
|
||||||
mList.add(item);
|
for (int i = 0; i < mList.size(); i++) {
|
||||||
i++;
|
contents |= mList.get(i).describeContents();
|
||||||
}
|
}
|
||||||
if (i >= n) {
|
return contents;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
final IBinder retriever = in.readStrongBinder();
|
@Override
|
||||||
while (i < n) {
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
Parcel data = Parcel.obtain();
|
final int n = mList.size();
|
||||||
Parcel reply = Parcel.obtain();
|
dest.writeInt(n);
|
||||||
data.writeInt(i);
|
if (n <= 0) {
|
||||||
try {
|
return;
|
||||||
retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
|
}
|
||||||
} catch (RemoteException e) {
|
int i = 0;
|
||||||
reply.recycle();
|
while (i < n && dest.dataSize() < MAX_IPC_SIZE) {
|
||||||
data.recycle();
|
dest.writeInt(1);
|
||||||
return;
|
dest.writeParcelable(mList.get(i), flags);
|
||||||
}
|
i++;
|
||||||
while (i < n && reply.readInt() != 0) {
|
}
|
||||||
@SuppressWarnings("unchecked")
|
if (i < n) {
|
||||||
T item = (T) reply.readParcelable(loader);
|
dest.writeInt(0);
|
||||||
mList.add(item);
|
final int start = i;
|
||||||
|
Binder retriever =
|
||||||
|
new Binder() {
|
||||||
|
@Override
|
||||||
|
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
|
||||||
|
throws RemoteException {
|
||||||
|
if (code != FIRST_CALL_TRANSACTION) {
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
int i = data.readInt();
|
||||||
|
if (i < start || i > n) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (i < n && reply.dataSize() < MAX_IPC_SIZE) {
|
||||||
|
reply.writeInt(1);
|
||||||
|
reply.writeParcelable(mList.get(i), flags);
|
||||||
i++;
|
i++;
|
||||||
|
}
|
||||||
|
if (i < n) {
|
||||||
|
reply.writeInt(0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
reply.recycle();
|
};
|
||||||
data.recycle();
|
dest.writeStrongBinder(retriever);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<T> getList() {
|
public static final Parcelable.ClassLoaderCreator<ParceledListSlice> CREATOR =
|
||||||
return mList;
|
new Parcelable.ClassLoaderCreator<ParceledListSlice>() {
|
||||||
}
|
@Override
|
||||||
|
public ParceledListSlice createFromParcel(Parcel in) {
|
||||||
@Override
|
return new ParceledListSlice(in, null);
|
||||||
public int describeContents() {
|
|
||||||
int contents = 0;
|
|
||||||
for (int i = 0; i < mList.size(); i++) {
|
|
||||||
contents |= mList.get(i).describeContents();
|
|
||||||
}
|
}
|
||||||
return contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) {
|
||||||
final int n = mList.size();
|
return new ParceledListSlice(in, loader);
|
||||||
dest.writeInt(n);
|
|
||||||
if (n <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
int i = 0;
|
|
||||||
while (i < n && dest.dataSize() < MAX_IPC_SIZE) {
|
@Override
|
||||||
dest.writeInt(1);
|
public ParceledListSlice[] newArray(int size) {
|
||||||
dest.writeParcelable(mList.get(i), flags);
|
return new ParceledListSlice[size];
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
if (i < n) {
|
};
|
||||||
dest.writeInt(0);
|
|
||||||
final int start = i;
|
|
||||||
Binder retriever = new Binder() {
|
|
||||||
@Override
|
|
||||||
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
|
|
||||||
throws RemoteException {
|
|
||||||
if (code != FIRST_CALL_TRANSACTION) {
|
|
||||||
return super.onTransact(code, data, reply, flags);
|
|
||||||
}
|
|
||||||
int i = data.readInt();
|
|
||||||
if (i < start || i > n) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
while (i < n && reply.dataSize() < MAX_IPC_SIZE) {
|
|
||||||
reply.writeInt(1);
|
|
||||||
reply.writeParcelable(mList.get(i), flags);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (i < n) {
|
|
||||||
reply.writeInt(0);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
dest.writeStrongBinder(retriever);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Parcelable.ClassLoaderCreator<ParceledListSlice> CREATOR =
|
|
||||||
new Parcelable.ClassLoaderCreator<ParceledListSlice>() {
|
|
||||||
@Override
|
|
||||||
public ParceledListSlice createFromParcel(Parcel in) {
|
|
||||||
return new ParceledListSlice(in, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) {
|
|
||||||
return new ParceledListSlice(in, loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParceledListSlice[] newArray(int size) {
|
|
||||||
return new ParceledListSlice[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
|
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
|
||||||
|
|
||||||
interface PlatformInterfaceWrapper : PlatformInterface {
|
interface PlatformInterfaceWrapper : PlatformInterface {
|
||||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
|
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun autoDetectInterfaceControl(fd: Int) {
|
override fun autoDetectInterfaceControl(fd: Int) {
|
||||||
}
|
}
|
||||||
@@ -38,9 +36,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
error("invalid argument")
|
error("invalid argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun useProcFS(): Boolean {
|
override fun useProcFS(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
override fun findConnectionOwner(
|
override fun findConnectionOwner(
|
||||||
@@ -136,13 +132,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
return InterfaceArray(interfaces.iterator())
|
return InterfaceArray(interfaces.iterator())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun underNetworkExtension(): Boolean {
|
override fun underNetworkExtension(): Boolean = false
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun includeAllNetworks(): Boolean {
|
override fun includeAllNetworks(): Boolean = false
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearDNSCache() {
|
override fun clearDNSCache() {
|
||||||
}
|
}
|
||||||
@@ -161,9 +153,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
return WIFIState(ssid, wifiInfo.bssid)
|
return WIFIState(ssid, wifiInfo.bssid)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun localDNSTransport(): LocalDNSTransport? {
|
override fun localDNSTransport(): LocalDNSTransport? = LocalResolver
|
||||||
return LocalResolver
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
override fun systemCertificates(): StringIterator {
|
override fun systemCertificates(): StringIterator {
|
||||||
@@ -182,15 +172,10 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
return StringArray(certificates.iterator())
|
return StringArray(certificates.iterator())
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) :
|
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) : NetworkInterfaceIterator {
|
||||||
NetworkInterfaceIterator {
|
override fun hasNext(): Boolean = iterator.hasNext()
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return iterator.hasNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): LibboxNetworkInterface {
|
override fun next(): LibboxNetworkInterface = iterator.next()
|
||||||
return iterator.next()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StringArray(private val iterator: Iterator<String>) : StringIterator {
|
class StringArray(private val iterator: Iterator<String>) : StringIterator {
|
||||||
@@ -199,21 +184,15 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean = iterator.hasNext()
|
||||||
return iterator.hasNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): String {
|
override fun next(): String = iterator.next()
|
||||||
return iterator.next()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun InterfaceAddress.toPrefix(): String {
|
private fun InterfaceAddress.toPrefix(): String = if (address is Inet6Address) {
|
||||||
return if (address is Inet6Address) {
|
"${Inet6Address.getByAddress(address.address).hostAddress}/$networkPrefixLength"
|
||||||
"${Inet6Address.getByAddress(address.address).hostAddress}/$networkPrefixLength"
|
} else {
|
||||||
} else {
|
"${address.hostAddress}/$networkPrefixLength"
|
||||||
"${address.hostAddress}/$networkPrefixLength"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val NetworkInterface.flags: Int
|
private val NetworkInterface.flags: Int
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import io.nekohasekai.libbox.Notification
|
import io.nekohasekai.libbox.Notification
|
||||||
|
|
||||||
class ProxyService : Service(), PlatformInterfaceWrapper {
|
class ProxyService :
|
||||||
|
Service(),
|
||||||
|
PlatformInterfaceWrapper {
|
||||||
private val service = BoxService(this, this)
|
private val service = BoxService(this, this)
|
||||||
|
|
||||||
override fun onStartCommand(
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand()
|
||||||
intent: Intent?,
|
|
||||||
flags: Int,
|
|
||||||
startId: Int,
|
|
||||||
) = service.onStartCommand()
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent) = service.onBind()
|
override fun onBind(intent: Intent) = service.onBind()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ object RootClient {
|
|||||||
Shell.setDefaultBuilder(
|
Shell.setDefaultBuilder(
|
||||||
Shell.Builder.create()
|
Shell.Builder.create()
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
.setTimeout(10)
|
.setTimeout(10),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@ object RootClient {
|
|||||||
val svc = bindService()
|
val svc = bindService()
|
||||||
return try {
|
return try {
|
||||||
val slice = svc.getInstalledPackages(flags, userId)
|
val slice = svc.getInstalledPackages(flags, userId)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = slice.list as List<PackageInfo>
|
val list = slice.list as List<PackageInfo>
|
||||||
list
|
list
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package io.nekohasekai.sfa.bg
|
package io.nekohasekai.sfa.bg
|
||||||
|
|
||||||
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
|
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import io.nekohasekai.sfa.BuildConfig
|
import io.nekohasekai.sfa.BuildConfig
|
||||||
|
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class RootServer : RootService() {
|
class RootServer : RootService() {
|
||||||
@@ -17,10 +16,7 @@ class RootServer : RootService() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInstalledPackages(
|
override fun getInstalledPackages(flags: Int, userId: Int): ParceledListSlice<PackageInfo> {
|
||||||
flags: Int,
|
|
||||||
userId: Int
|
|
||||||
): ParceledListSlice<PackageInfo> {
|
|
||||||
val allPackages = PrivilegedServiceUtils.getInstalledPackages(flags, userId)
|
val allPackages = PrivilegedServiceUtils.getInstalledPackages(flags, userId)
|
||||||
return ParceledListSlice(allPackages)
|
return ParceledListSlice(allPackages)
|
||||||
}
|
}
|
||||||
@@ -30,16 +26,12 @@ class RootServer : RootService() {
|
|||||||
PrivilegedServiceUtils.installPackage(apk, size, userId)
|
PrivilegedServiceUtils.installPackage(apk, size, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exportDebugInfo(outputPath: String?): String {
|
override fun exportDebugInfo(outputPath: String?): String = DebugInfoExporter.export(
|
||||||
return DebugInfoExporter.export(
|
this@RootServer,
|
||||||
this@RootServer,
|
outputPath!!,
|
||||||
outputPath!!,
|
BuildConfig.APPLICATION_ID,
|
||||||
BuildConfig.APPLICATION_ID
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder = binder
|
||||||
return binder
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(): Int {
|
override fun getStatus(): Int = (status.value ?: Status.Stopped).ordinal
|
||||||
return (status.value ?: Status.Stopped).ordinal
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerCallback(callback: IServiceCallback) {
|
override fun registerCallback(callback: IServiceCallback) {
|
||||||
callbacks.register(callback)
|
callbacks.register(callback)
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ServiceConnection(
|
class ServiceConnection(private val context: Context, callback: Callback, private val register: Boolean = true) : ServiceConnection {
|
||||||
private val context: Context,
|
|
||||||
callback: Callback,
|
|
||||||
private val register: Boolean = true,
|
|
||||||
) : ServiceConnection {
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ServiceConnection"
|
private const val TAG = "ServiceConnection"
|
||||||
}
|
}
|
||||||
@@ -66,10 +62,7 @@ class ServiceConnection(
|
|||||||
Log.d(TAG, "request reconnect")
|
Log.d(TAG, "request reconnect")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(
|
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||||
name: ComponentName,
|
|
||||||
binder: IBinder,
|
|
||||||
) {
|
|
||||||
val service = IService.Stub.asInterface(binder)
|
val service = IService.Stub.asInterface(binder)
|
||||||
this.service = service
|
this.service = service
|
||||||
try {
|
try {
|
||||||
@@ -98,10 +91,7 @@ class ServiceConnection(
|
|||||||
interface Callback {
|
interface Callback {
|
||||||
fun onServiceStatusChanged(status: Status)
|
fun onServiceStatusChanged(status: Status)
|
||||||
|
|
||||||
fun onServiceAlert(
|
fun onServiceAlert(type: Alert, message: String?) {
|
||||||
type: Alert,
|
|
||||||
message: String?,
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +100,7 @@ class ServiceConnection(
|
|||||||
callback.onServiceStatusChanged(Status.values()[status])
|
callback.onServiceStatusChanged(Status.values()[status])
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceAlert(
|
override fun onServiceAlert(type: Int, message: String?) {
|
||||||
type: Int,
|
|
||||||
message: String?,
|
|
||||||
) {
|
|
||||||
callback.onServiceAlert(Alert.values()[type], message)
|
callback.onServiceAlert(Alert.values()[type], message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.libbox.StatusMessage
|
import io.nekohasekai.libbox.StatusMessage
|
||||||
import io.nekohasekai.sfa.Application
|
import io.nekohasekai.sfa.Application
|
||||||
import io.nekohasekai.sfa.compose.MainActivity
|
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.compose.MainActivity
|
||||||
import io.nekohasekai.sfa.constant.Action
|
import io.nekohasekai.sfa.constant.Action
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
@@ -27,10 +27,9 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ServiceNotification(
|
class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) :
|
||||||
private val status: MutableLiveData<Status>,
|
BroadcastReceiver(),
|
||||||
private val service: Service,
|
CommandClient.Handler {
|
||||||
) : BroadcastReceiver(), CommandClient.Handler {
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val notificationId = 1
|
private const val notificationId = 1
|
||||||
private const val notificationChannel = "service"
|
private const val notificationChannel = "service"
|
||||||
@@ -82,10 +81,7 @@ class ServiceNotification(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(
|
fun show(lastProfileName: String, @StringRes contentTextId: Int) {
|
||||||
lastProfileName: String,
|
|
||||||
@StringRes contentTextId: Int,
|
|
||||||
) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
Application.notification.createNotificationChannel(
|
Application.notification.createNotificationChannel(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
@@ -132,10 +128,7 @@ class ServiceNotification(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
) {
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
commandClient.connect()
|
commandClient.connect()
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import androidx.annotation.RequiresApi
|
|||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class TileService : TileService(), ServiceConnection.Callback {
|
class TileService :
|
||||||
|
TileService(),
|
||||||
|
ServiceConnection.Callback {
|
||||||
private val connection = ServiceConnection(this, this)
|
private val connection = ServiceConnection(this, this)
|
||||||
|
|
||||||
override fun onServiceStatusChanged(status: Status) {
|
override fun onServiceStatusChanged(status: Status) {
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ class UpdateProfileWork {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateTask(
|
class UpdateTask(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
|
||||||
appContext: Context,
|
|
||||||
params: WorkerParameters,
|
|
||||||
) : CoroutineWorker(appContext, params) {
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
var selectedProfileUpdated = false
|
var selectedProfileUpdated = false
|
||||||
val remoteProfiles =
|
val remoteProfiles =
|
||||||
|
|||||||
@@ -15,18 +15,16 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class VPNService : VpnService(), PlatformInterfaceWrapper {
|
class VPNService :
|
||||||
|
VpnService(),
|
||||||
|
PlatformInterfaceWrapper {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "VPNService"
|
private const val TAG = "VPNService"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val service = BoxService(this, this)
|
private val service = BoxService(this, this)
|
||||||
|
|
||||||
override fun onStartCommand(
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand()
|
||||||
intent: Intent?,
|
|
||||||
flags: Int,
|
|
||||||
startId: Int,
|
|
||||||
) = service.onStartCommand()
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
val binder = super.onBind(intent)
|
val binder = super.onBind(intent)
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ fun LineChart(
|
|||||||
|
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(80.dp),
|
.height(80.dp),
|
||||||
) {
|
) {
|
||||||
val width = size.width
|
val width = size.width
|
||||||
val height = size.height
|
val height = size.height
|
||||||
@@ -96,11 +96,11 @@ fun LineChart(
|
|||||||
path = path,
|
path = path,
|
||||||
color = lineColor,
|
color = lineColor,
|
||||||
style =
|
style =
|
||||||
Stroke(
|
Stroke(
|
||||||
width = 2.dp.toPx(),
|
width = 2.dp.toPx(),
|
||||||
cap = StrokeCap.Round,
|
cap = StrokeCap.Round,
|
||||||
join = StrokeJoin.Round,
|
join = StrokeJoin.Round,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Draw gradient fill under the line
|
// Draw gradient fill under the line
|
||||||
|
|||||||
@@ -16,34 +16,29 @@ import androidx.compose.animation.scaleIn
|
|||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material.icons.filled.UnfoldLess
|
import androidx.compose.material.icons.filled.UnfoldLess
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -53,9 +48,9 @@ import androidx.compose.material3.NavigationBarItem
|
|||||||
import androidx.compose.material3.NavigationRail
|
import androidx.compose.material3.NavigationRail
|
||||||
import androidx.compose.material3.NavigationRailItem
|
import androidx.compose.material3.NavigationRailItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -69,9 +64,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@@ -81,6 +79,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
|||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.Application
|
import io.nekohasekai.sfa.Application
|
||||||
import io.nekohasekai.sfa.BuildConfig
|
import io.nekohasekai.sfa.BuildConfig
|
||||||
@@ -91,25 +90,24 @@ import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
|||||||
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
|
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
|
||||||
import io.nekohasekai.sfa.compose.base.UiEvent
|
import io.nekohasekai.sfa.compose.base.UiEvent
|
||||||
import io.nekohasekai.sfa.compose.component.ServiceStatusBar
|
import io.nekohasekai.sfa.compose.component.ServiceStatusBar
|
||||||
import io.nekohasekai.sfa.compose.component.UptimeText
|
|
||||||
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
||||||
|
import io.nekohasekai.sfa.compose.component.UptimeText
|
||||||
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
|
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
|
||||||
import io.nekohasekai.sfa.compose.navigation.ProfileRoutes
|
import io.nekohasekai.sfa.compose.navigation.ProfileRoutes
|
||||||
import io.nekohasekai.sfa.compose.navigation.SFANavHost
|
import io.nekohasekai.sfa.compose.navigation.SFANavHost
|
||||||
import io.nekohasekai.sfa.compose.navigation.Screen
|
import io.nekohasekai.sfa.compose.navigation.Screen
|
||||||
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
||||||
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
|
|
||||||
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
|
|
||||||
import io.nekohasekai.sfa.compose.topbar.TopBarController
|
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.CardGroup
|
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||||
|
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||||
|
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||||
import io.nekohasekai.sfa.compose.theme.SFATheme
|
import io.nekohasekai.sfa.compose.theme.SFATheme
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.TopBarController
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
|
||||||
import io.nekohasekai.sfa.constant.Alert
|
import io.nekohasekai.sfa.constant.Alert
|
||||||
import io.nekohasekai.sfa.constant.ServiceMode
|
import io.nekohasekai.sfa.constant.ServiceMode
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
@@ -119,10 +117,13 @@ import io.nekohasekai.sfa.ktx.launchCustomTab
|
|||||||
import io.nekohasekai.sfa.update.UpdateState
|
import io.nekohasekai.sfa.update.UpdateState
|
||||||
import io.nekohasekai.sfa.vendor.Vendor
|
import io.nekohasekai.sfa.vendor.Vendor
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
class MainActivity :
|
||||||
|
ComponentActivity(),
|
||||||
|
ServiceConnection.Callback {
|
||||||
private val connection = ServiceConnection(this, this)
|
private val connection = ServiceConnection(this, this)
|
||||||
private lateinit var dashboardViewModel: DashboardViewModel
|
private lateinit var dashboardViewModel: DashboardViewModel
|
||||||
private var currentServiceStatus by mutableStateOf(Status.Stopped)
|
private var currentServiceStatus by mutableStateOf(Status.Stopped)
|
||||||
@@ -253,21 +254,20 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun prepare() =
|
private suspend fun prepare() = withContext(Dispatchers.Main) {
|
||||||
withContext(Dispatchers.Main) {
|
try {
|
||||||
try {
|
val intent = VpnService.prepare(this@MainActivity)
|
||||||
val intent = VpnService.prepare(this@MainActivity)
|
if (intent != null) {
|
||||||
if (intent != null) {
|
prepareLauncher.launch(intent)
|
||||||
prepareLauncher.launch(intent)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onServiceAlert(Alert.RequestVPNPermission, e.message)
|
|
||||||
true
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onServiceAlert(Alert.RequestVPNPermission, e.message)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -388,8 +388,11 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
text = {
|
text = {
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
markdown = stringResource(
|
markdown = stringResource(
|
||||||
if (BuildConfig.FLAVOR == "play") R.string.check_update_prompt_play
|
if (BuildConfig.FLAVOR == "play") {
|
||||||
else R.string.check_update_prompt_github
|
R.string.check_update_prompt_play
|
||||||
|
} else {
|
||||||
|
R.string.check_update_prompt_github
|
||||||
|
},
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
@@ -534,7 +537,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -729,17 +732,17 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (isRunning || isStopping) {
|
if (isRunning || isStopping) {
|
||||||
Icons.Default.Stop
|
Icons.Default.Stop
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.PlayArrow
|
Icons.Default.PlayArrow
|
||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (isRunning || isStopping) {
|
if (isRunning || isStopping) {
|
||||||
stringResource(R.string.stop)
|
stringResource(R.string.stop)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.action_start)
|
stringResource(R.string.action_start)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -873,9 +876,9 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
},
|
},
|
||||||
label = { Text(stringResource(screen.titleRes)) },
|
label = { Text(stringResource(screen.titleRes)) },
|
||||||
selected =
|
selected =
|
||||||
currentDestination?.hierarchy?.any {
|
currentDestination?.hierarchy?.any {
|
||||||
it.route == screen.route
|
it.route == screen.route
|
||||||
} == true,
|
} == true,
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(screen.route) {
|
navController.navigate(screen.route) {
|
||||||
// Pop up to the start destination of the graph to
|
// Pop up to the start destination of the graph to
|
||||||
@@ -909,7 +912,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
val groupsUiState by groupsViewModel.uiState.collectAsState()
|
val groupsUiState by groupsViewModel.uiState.collectAsState()
|
||||||
val allCollapsed = groupsUiState.expandedGroups.isEmpty()
|
val allCollapsed = groupsUiState.expandedGroups.isEmpty()
|
||||||
@@ -943,12 +946,16 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
if (groupsUiState.groups.isNotEmpty()) {
|
if (groupsUiState.groups.isNotEmpty()) {
|
||||||
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (allCollapsed) Icons.Default.UnfoldMore
|
imageVector = if (allCollapsed) {
|
||||||
else Icons.Default.UnfoldLess,
|
Icons.Default.UnfoldMore
|
||||||
contentDescription = if (allCollapsed)
|
} else {
|
||||||
|
Icons.Default.UnfoldLess
|
||||||
|
},
|
||||||
|
contentDescription = if (allCollapsed) {
|
||||||
stringResource(R.string.expand_all)
|
stringResource(R.string.expand_all)
|
||||||
else
|
} else {
|
||||||
stringResource(R.string.collapse_all),
|
stringResource(R.string.collapse_all)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1032,10 +1039,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
connection.reconnect()
|
connection.reconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceAlert(
|
override fun onServiceAlert(type: Alert, message: String?) {
|
||||||
type: Alert,
|
|
||||||
message: String?,
|
|
||||||
) {
|
|
||||||
when (type) {
|
when (type) {
|
||||||
Alert.RequestLocationPermission -> {
|
Alert.RequestLocationPermission -> {
|
||||||
return requestLocationPermission()
|
return requestLocationPermission()
|
||||||
@@ -1071,11 +1075,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ServiceAlertDialog(
|
private fun ServiceAlertDialog(alertType: Alert, message: String?, onDismiss: () -> Unit) {
|
||||||
alertType: Alert,
|
|
||||||
message: String?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
val title =
|
val title =
|
||||||
when (alertType) {
|
when (alertType) {
|
||||||
Alert.RequestNotificationPermission -> stringResource(R.string.notification_permission_title)
|
Alert.RequestNotificationPermission -> stringResource(R.string.notification_permission_title)
|
||||||
@@ -1106,10 +1106,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LocationPermissionDialog(
|
private fun LocationPermissionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||||
onConfirm: () -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.location_permission_title)) },
|
title = { Text(stringResource(R.string.location_permission_title)) },
|
||||||
@@ -1128,10 +1125,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BackgroundLocationPermissionDialog(
|
private fun BackgroundLocationPermissionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||||
onConfirm: () -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.location_permission_title)) },
|
title = { Text(stringResource(R.string.location_permission_title)) },
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ abstract class BaseViewModel<State, Event> : ViewModel() {
|
|||||||
sendGlobalEvent(UiEvent.ErrorMessage(message))
|
sendGlobalEvent(UiEvent.ErrorMessage(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun launch(
|
protected fun launch(onError: ((Throwable) -> Unit)? = null, block: suspend CoroutineScope.() -> Unit) {
|
||||||
onError: ((Throwable) -> Unit)? = null,
|
|
||||||
block: suspend CoroutineScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
val errorHandler =
|
val errorHandler =
|
||||||
CoroutineExceptionHandler { _, throwable ->
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
onError?.invoke(throwable) ?: sendError(throwable)
|
onError?.invoke(throwable) ?: sendError(throwable)
|
||||||
|
|||||||
@@ -29,7 +29,5 @@ object GlobalEventBus {
|
|||||||
* Try to emit an event without suspending.
|
* Try to emit an event without suspending.
|
||||||
* Returns true if the event was emitted successfully.
|
* Returns true if the event was emitted successfully.
|
||||||
*/
|
*/
|
||||||
fun tryEmit(event: UiEvent): Boolean {
|
fun tryEmit(event: UiEvent): Boolean = _events.tryEmit(event)
|
||||||
return _events.tryEmit(event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectableMessageDialog(
|
fun SelectableMessageDialog(title: String, message: String, onDismiss: () -> Unit) {
|
||||||
title: String,
|
|
||||||
message: String,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|||||||
@@ -8,8 +8,4 @@ sealed class UiState<out T> {
|
|||||||
data class Error(val exception: Throwable, val message: String? = null) : UiState<Nothing>()
|
data class Error(val exception: Throwable, val message: String? = null) : UiState<Nothing>()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BaseUiState<T>(
|
data class BaseUiState<T>(val isLoading: Boolean = false, val data: T? = null, val error: String? = null)
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val data: T? = null,
|
|
||||||
val error: String? = null,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ fun ServiceStatusBar(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -85,11 +85,11 @@ fun ServiceStatusBar(
|
|||||||
// Connections button
|
// Connections button
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||||
.clickable(onClick = onConnectionsClick)
|
.clickable(onClick = onConnectionsClick)
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
@@ -112,11 +112,11 @@ fun ServiceStatusBar(
|
|||||||
if (hasGroups) {
|
if (hasGroups) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||||
.clickable(onClick = onGroupsClick)
|
.clickable(onClick = onGroupsClick)
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
@@ -139,11 +139,11 @@ fun ServiceStatusBar(
|
|||||||
// Stop button
|
// Stop button
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
.clickable(onClick = onStopClick)
|
.clickable(onClick = onStopClick)
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
@@ -164,10 +164,7 @@ fun ServiceStatusBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatusItem(
|
private fun StatusItem(text: String, modifier: Modifier = Modifier) {
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -178,10 +175,7 @@ private fun StatusItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UptimeText(
|
fun UptimeText(startTime: Long, modifier: Modifier = Modifier) {
|
||||||
startTime: Long,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
var currentTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
var currentTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||||
|
|
||||||
LaunchedEffect(startTime) {
|
LaunchedEffect(startTime) {
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ import org.kodein.emoji.EmojiTemplateCatalog
|
|||||||
import org.kodein.emoji.all
|
import org.kodein.emoji.all
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateAvailableDialog(
|
fun UpdateAvailableDialog(updateInfo: UpdateInfo, onDismiss: () -> Unit, onUpdate: () -> Unit) {
|
||||||
updateInfo: UpdateInfo,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onUpdate: () -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val emojiCatalog = remember { EmojiTemplateCatalog(Emoji.all()) }
|
val emojiCatalog = remember { EmojiTemplateCatalog(Emoji.all()) }
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -24,24 +23,21 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QRCodeDialog(
|
fun QRCodeDialog(bitmap: Bitmap, onDismiss: () -> Unit) {
|
||||||
bitmap: Bitmap,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -52,9 +48,9 @@ fun QRCodeDialog(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class QRSBitmapGenerator(
|
|||||||
private val actualBufferSize = bufferSize.coerceAtMost(frames.size)
|
private val actualBufferSize = bufferSize.coerceAtMost(frames.size)
|
||||||
private val bitmapBuffer = arrayOfNulls<Bitmap>(actualBufferSize)
|
private val bitmapBuffer = arrayOfNulls<Bitmap>(actualBufferSize)
|
||||||
private var generationJob: Job? = null
|
private var generationJob: Job? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var currentFrameIndex = 0
|
private var currentFrameIndex = 0
|
||||||
private var generatedUpTo = -1
|
private var generatedUpTo = -1
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package io.nekohasekai.sfa.compose.component.qr
|
package io.nekohasekai.sfa.compose.component.qr
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -58,11 +58,7 @@ import io.nekohasekai.sfa.qrs.QRSEncoder
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QRSDialog(
|
fun QRSDialog(profileData: ByteArray, profileName: String, onDismiss: () -> Unit) {
|
||||||
profileData: ByteArray,
|
|
||||||
profileName: String,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val isTablet = configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
val isTablet = configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||||
@@ -126,16 +122,16 @@ fun QRSDialog(
|
|||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(0.85f)
|
.fillMaxWidth(0.85f)
|
||||||
.sizeIn(maxWidth = 960.dp)
|
.sizeIn(maxWidth = 960.dp)
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -54,18 +54,14 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
||||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
||||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanViewModel
|
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun QRScanSheet(
|
fun QRScanSheet(onDismiss: () -> Unit, onScanResult: (QRScanResult) -> Unit, viewModel: QRScanViewModel = viewModel()) {
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onScanResult: (QRScanResult) -> Unit,
|
|
||||||
viewModel: QRScanViewModel = viewModel(),
|
|
||||||
) {
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
@@ -74,12 +70,12 @@ fun QRScanSheet(
|
|||||||
var hasPermission by remember {
|
var hasPermission by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
||||||
PackageManager.PERMISSION_GRANTED
|
PackageManager.PERMISSION_GRANTED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
) { isGranted ->
|
) { isGranted ->
|
||||||
if (isGranted) {
|
if (isGranted) {
|
||||||
hasPermission = true
|
hasPermission = true
|
||||||
@@ -113,7 +109,7 @@ fun QRScanSheet(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.9f)
|
.fillMaxHeight(0.9f),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -138,44 +134,44 @@ fun QRScanSheet(
|
|||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
onDismissRequest = { showMenu = false }
|
onDismissRequest = { showMenu = false },
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
(if (uiState.useFrontCamera) "✓ " else " ") +
|
(if (uiState.useFrontCamera) "✓ " else " ") +
|
||||||
stringResource(R.string.profile_add_scan_use_front_camera)
|
stringResource(R.string.profile_add_scan_use_front_camera),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.toggleFrontCamera(lifecycleOwner)
|
viewModel.toggleFrontCamera(lifecycleOwner)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
(if (uiState.torchEnabled) "✓ " else " ") +
|
(if (uiState.torchEnabled) "✓ " else " ") +
|
||||||
stringResource(R.string.profile_add_scan_enable_torch)
|
stringResource(R.string.profile_add_scan_enable_torch),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.toggleTorch()
|
viewModel.toggleTorch()
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
if (uiState.vendorAnalyzerAvailable) {
|
if (uiState.vendorAnalyzerAvailable) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
(if (uiState.useVendorAnalyzer) "✓ " else " ") +
|
(if (uiState.useVendorAnalyzer) "✓ " else " ") +
|
||||||
stringResource(R.string.profile_add_scan_use_vendor_analyzer)
|
stringResource(R.string.profile_add_scan_use_vendor_analyzer),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.toggleVendorAnalyzer()
|
viewModel.toggleVendorAnalyzer()
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +181,7 @@ fun QRScanSheet(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
CameraPreview(
|
CameraPreview(
|
||||||
@@ -201,7 +197,7 @@ fun QRScanSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
@@ -214,7 +210,7 @@ fun QRScanSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.Black.copy(alpha = 0.5f)),
|
.background(Color.Black.copy(alpha = 0.5f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
@@ -228,18 +224,18 @@ fun QRScanSheet(
|
|||||||
Text(
|
Text(
|
||||||
text = "${minOf(99, (progress * 100).toInt())}%",
|
text = "${minOf(99, (progress * 100).toInt())}%",
|
||||||
style = MaterialTheme.typography.titleLarge.copy(
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold,
|
||||||
),
|
),
|
||||||
color = Color.White
|
color = Color.White,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "QRS",
|
text = "QRS",
|
||||||
style = MaterialTheme.typography.headlineLarge.copy(
|
style = MaterialTheme.typography.headlineLarge.copy(
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold,
|
||||||
),
|
),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
modifier = Modifier.offset(y = (-88).dp)
|
modifier = Modifier.offset(y = (-88).dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +253,7 @@ fun QRScanSheet(
|
|||||||
TextButton(onClick = { viewModel.dismissError() }) {
|
TextButton(onClick = { viewModel.dismissError() }) {
|
||||||
Text(stringResource(android.R.string.ok))
|
Text(stringResource(android.R.string.ok))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +290,7 @@ private fun CameraPreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
@@ -309,11 +305,7 @@ private fun CameraPreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapCropAreaToPreview(
|
private fun mapCropAreaToPreview(area: QRCodeCropArea, viewWidth: Float, viewHeight: Float): Rect? {
|
||||||
area: QRCodeCropArea,
|
|
||||||
viewWidth: Float,
|
|
||||||
viewHeight: Float,
|
|
||||||
): Rect? {
|
|
||||||
if (viewWidth <= 0f || viewHeight <= 0f) return null
|
if (viewWidth <= 0f || viewHeight <= 0f) return null
|
||||||
|
|
||||||
val rotation = ((area.rotationDegrees % 360) + 360) % 360
|
val rotation = ((area.rotationDegrees % 360) + 360) % 360
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
package io.nekohasekai.sfa.compose.model
|
package io.nekohasekai.sfa.compose.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.nekohasekai.sfa.ktx.toList
|
||||||
import io.nekohasekai.libbox.Connection as LibboxConnection
|
import io.nekohasekai.libbox.Connection as LibboxConnection
|
||||||
import io.nekohasekai.libbox.ProcessInfo as LibboxProcessInfo
|
import io.nekohasekai.libbox.ProcessInfo as LibboxProcessInfo
|
||||||
import io.nekohasekai.sfa.ktx.toList
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ProcessInfo(
|
data class ProcessInfo(val processId: Long, val userId: Int, val userName: String, val processPath: String, val packageName: String) {
|
||||||
val processId: Long,
|
|
||||||
val userId: Int,
|
|
||||||
val userName: String,
|
|
||||||
val processPath: String,
|
|
||||||
val packageName: String,
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(processInfo: LibboxProcessInfo?): ProcessInfo? {
|
fun from(processInfo: LibboxProcessInfo?): ProcessInfo? {
|
||||||
if (processInfo == null) return null
|
if (processInfo == null) return null
|
||||||
@@ -68,59 +62,53 @@ data class Connection(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performSearchPlain(content: String): Boolean {
|
private fun performSearchPlain(content: String): Boolean = destination.contains(content, ignoreCase = true) ||
|
||||||
return destination.contains(content, ignoreCase = true) ||
|
domain.contains(content, ignoreCase = true) ||
|
||||||
domain.contains(content, ignoreCase = true) ||
|
outbound.contains(content, ignoreCase = true) ||
|
||||||
outbound.contains(content, ignoreCase = true) ||
|
rule.contains(content, ignoreCase = true) ||
|
||||||
rule.contains(content, ignoreCase = true) ||
|
processInfo?.packageName?.contains(content, ignoreCase = true) == true
|
||||||
processInfo?.packageName?.contains(content, ignoreCase = true) == true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performSearchType(type: String, value: String): Boolean {
|
private fun performSearchType(type: String, value: String): Boolean = when (type) {
|
||||||
return when (type) {
|
"network" -> network.equals(value, ignoreCase = true)
|
||||||
"network" -> network.equals(value, ignoreCase = true)
|
"inbound" -> inbound.contains(value, ignoreCase = true)
|
||||||
"inbound" -> inbound.contains(value, ignoreCase = true)
|
"inbound.type" -> inboundType.equals(value, ignoreCase = true)
|
||||||
"inbound.type" -> inboundType.equals(value, ignoreCase = true)
|
"source" -> source.contains(value, ignoreCase = true)
|
||||||
"source" -> source.contains(value, ignoreCase = true)
|
"destination" -> destination.contains(value, ignoreCase = true)
|
||||||
"destination" -> destination.contains(value, ignoreCase = true)
|
"outbound" -> outbound.contains(value, ignoreCase = true)
|
||||||
"outbound" -> outbound.contains(value, ignoreCase = true)
|
"outbound.type" -> outboundType.equals(value, ignoreCase = true)
|
||||||
"outbound.type" -> outboundType.equals(value, ignoreCase = true)
|
"rule" -> rule.contains(value, ignoreCase = true)
|
||||||
"rule" -> rule.contains(value, ignoreCase = true)
|
"protocol" -> protocolName.equals(value, ignoreCase = true)
|
||||||
"protocol" -> protocolName.equals(value, ignoreCase = true)
|
"user" -> user.contains(value, ignoreCase = true)
|
||||||
"user" -> user.contains(value, ignoreCase = true)
|
"package" -> processInfo?.packageName?.contains(value, ignoreCase = true) == true
|
||||||
"package" -> processInfo?.packageName?.contains(value, ignoreCase = true) == true
|
"chain" -> chain.any { it.contains(value, ignoreCase = true) }
|
||||||
"chain" -> chain.any { it.contains(value, ignoreCase = true) }
|
else -> false
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(connection: LibboxConnection): Connection {
|
fun from(connection: LibboxConnection): Connection = Connection(
|
||||||
return Connection(
|
id = connection.id,
|
||||||
id = connection.id,
|
inbound = connection.inbound,
|
||||||
inbound = connection.inbound,
|
inboundType = connection.inboundType,
|
||||||
inboundType = connection.inboundType,
|
ipVersion = connection.ipVersion,
|
||||||
ipVersion = connection.ipVersion,
|
network = connection.network,
|
||||||
network = connection.network,
|
source = connection.source,
|
||||||
source = connection.source,
|
destination = connection.destination,
|
||||||
destination = connection.destination,
|
domain = connection.domain,
|
||||||
domain = connection.domain,
|
displayDestination = connection.displayDestination(),
|
||||||
displayDestination = connection.displayDestination(),
|
protocolName = connection.protocol,
|
||||||
protocolName = connection.protocol,
|
user = connection.user,
|
||||||
user = connection.user,
|
fromOutbound = connection.fromOutbound,
|
||||||
fromOutbound = connection.fromOutbound,
|
createdAt = connection.createdAt,
|
||||||
createdAt = connection.createdAt,
|
closedAt = if (connection.closedAt > 0) connection.closedAt else null,
|
||||||
closedAt = if (connection.closedAt > 0) connection.closedAt else null,
|
upload = connection.uplink,
|
||||||
upload = connection.uplink,
|
download = connection.downlink,
|
||||||
download = connection.downlink,
|
uploadTotal = connection.uplinkTotal,
|
||||||
uploadTotal = connection.uplinkTotal,
|
downloadTotal = connection.downlinkTotal,
|
||||||
downloadTotal = connection.downlinkTotal,
|
rule = connection.rule,
|
||||||
rule = connection.rule,
|
outbound = connection.outbound,
|
||||||
outbound = connection.outbound,
|
outboundType = connection.outboundType,
|
||||||
outboundType = connection.outboundType,
|
chain = connection.chain().toList(),
|
||||||
chain = connection.chain().toList(),
|
processInfo = ProcessInfo.from(connection.processInfo),
|
||||||
processInfo = ProcessInfo.from(connection.processInfo),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,7 @@ data class Group(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class GroupItem(
|
data class GroupItem(val tag: String, val type: String, val urlTestTime: Long, val urlTestDelay: Int) {
|
||||||
val tag: String,
|
|
||||||
val type: String,
|
|
||||||
val urlTestTime: Long,
|
|
||||||
val urlTestDelay: Int,
|
|
||||||
) {
|
|
||||||
constructor(item: OutboundGroupItem) : this(
|
constructor(item: OutboundGroupItem) : this(
|
||||||
item.tag,
|
item.tag,
|
||||||
item.type,
|
item.type,
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ import androidx.compose.material.icons.filled.SwapVert
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
|
||||||
sealed class Screen(
|
sealed class Screen(val route: String, @StringRes val titleRes: Int, val icon: ImageVector) {
|
||||||
val route: String,
|
|
||||||
@StringRes val titleRes: Int,
|
|
||||||
val icon: ImageVector,
|
|
||||||
) {
|
|
||||||
object Dashboard : Screen(
|
object Dashboard : Screen(
|
||||||
route = "dashboard",
|
route = "dashboard",
|
||||||
titleRes = R.string.title_dashboard,
|
titleRes = R.string.title_dashboard,
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package io.nekohasekai.sfa.compose.navigation
|
package io.nekohasekai.sfa.compose.navigation
|
||||||
|
|
||||||
data class NewProfileArgs(
|
data class NewProfileArgs(val importName: String? = null, val importUrl: String? = null, val qrsData: ByteArray? = null)
|
||||||
val importName: String? = null,
|
|
||||||
val importUrl: String? = null,
|
|
||||||
val qrsData: ByteArray? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
object ProfileRoutes {
|
object ProfileRoutes {
|
||||||
const val NewProfile = "profile/new"
|
const val NewProfile = "profile/new"
|
||||||
|
|||||||
@@ -12,18 +12,20 @@ import androidx.navigation.NavType
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
|
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
|
||||||
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsRoute
|
||||||
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||||
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
|
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsRoute
|
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
|
||||||
import io.nekohasekai.sfa.compose.screen.log.HookLogScreen
|
import io.nekohasekai.sfa.compose.screen.log.HookLogScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogScreen
|
import io.nekohasekai.sfa.compose.screen.log.LogScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.configuration.NewProfileScreen
|
|
||||||
import io.nekohasekai.sfa.compose.screen.profile.EditProfileRoute
|
import io.nekohasekai.sfa.compose.screen.profile.EditProfileRoute
|
||||||
|
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen
|
||||||
@@ -31,8 +33,6 @@ import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
|||||||
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.SettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.SettingsScreen
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.compose.screen.privilegesettings.PrivilegeSettingsManageScreen
|
|
||||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
|
|
||||||
|
|
||||||
private val slideInFromRight: AnimatedContentTransitionScope<*>.() -> androidx.compose.animation.EnterTransition = {
|
private val slideInFromRight: AnimatedContentTransitionScope<*>.() -> androidx.compose.animation.EnterTransition = {
|
||||||
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300))
|
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(300))
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
@@ -60,6 +59,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -149,9 +149,9 @@ fun NewProfileScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,20 +164,20 @@ fun NewProfileScreen(
|
|||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.padding(bottom = bottomBarPadding),
|
.padding(bottom = bottomBarPadding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
// Profile Name
|
// Profile Name
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
@@ -211,9 +211,9 @@ fun NewProfileScreen(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
@@ -233,30 +233,30 @@ fun NewProfileScreen(
|
|||||||
onClick = { viewModel.updateProfileType(ProfileType.Local) },
|
onClick = { viewModel.updateProfileType(ProfileType.Local) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape =
|
shape =
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 12.dp,
|
topStart = 12.dp,
|
||||||
bottomStart = 12.dp,
|
bottomStart = 12.dp,
|
||||||
topEnd = 0.dp,
|
topEnd = 0.dp,
|
||||||
bottomEnd = 0.dp,
|
bottomEnd = 0.dp,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
if (uiState.profileType == ProfileType.Local) {
|
if (uiState.profileType == ProfileType.Local) {
|
||||||
ButtonDefaults.buttonColors(
|
ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ButtonDefaults.outlinedButtonColors()
|
ButtonDefaults.outlinedButtonColors()
|
||||||
},
|
},
|
||||||
border =
|
border =
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
1.dp,
|
1.dp,
|
||||||
if (uiState.profileType == ProfileType.Local) {
|
if (uiState.profileType == ProfileType.Local) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline
|
MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.profile_type_local))
|
Text(stringResource(R.string.profile_type_local))
|
||||||
}
|
}
|
||||||
@@ -264,30 +264,30 @@ fun NewProfileScreen(
|
|||||||
onClick = { viewModel.updateProfileType(ProfileType.Remote) },
|
onClick = { viewModel.updateProfileType(ProfileType.Remote) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape =
|
shape =
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 0.dp,
|
topStart = 0.dp,
|
||||||
bottomStart = 0.dp,
|
bottomStart = 0.dp,
|
||||||
topEnd = 12.dp,
|
topEnd = 12.dp,
|
||||||
bottomEnd = 12.dp,
|
bottomEnd = 12.dp,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
if (uiState.profileType == ProfileType.Remote) {
|
if (uiState.profileType == ProfileType.Remote) {
|
||||||
ButtonDefaults.buttonColors(
|
ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ButtonDefaults.outlinedButtonColors()
|
ButtonDefaults.outlinedButtonColors()
|
||||||
},
|
},
|
||||||
border =
|
border =
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
1.dp,
|
1.dp,
|
||||||
if (uiState.profileType == ProfileType.Remote) {
|
if (uiState.profileType == ProfileType.Remote) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline
|
MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.profile_type_remote))
|
Text(stringResource(R.string.profile_type_remote))
|
||||||
}
|
}
|
||||||
@@ -304,9 +304,9 @@ fun NewProfileScreen(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f),
|
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
@@ -326,30 +326,30 @@ fun NewProfileScreen(
|
|||||||
onClick = { viewModel.updateProfileSource(ProfileSource.CreateNew) },
|
onClick = { viewModel.updateProfileSource(ProfileSource.CreateNew) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape =
|
shape =
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 12.dp,
|
topStart = 12.dp,
|
||||||
bottomStart = 12.dp,
|
bottomStart = 12.dp,
|
||||||
topEnd = 0.dp,
|
topEnd = 0.dp,
|
||||||
bottomEnd = 0.dp,
|
bottomEnd = 0.dp,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
if (uiState.profileSource == ProfileSource.CreateNew) {
|
if (uiState.profileSource == ProfileSource.CreateNew) {
|
||||||
ButtonDefaults.buttonColors(
|
ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ButtonDefaults.outlinedButtonColors()
|
ButtonDefaults.outlinedButtonColors()
|
||||||
},
|
},
|
||||||
border =
|
border =
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
1.dp,
|
1.dp,
|
||||||
if (uiState.profileSource == ProfileSource.CreateNew) {
|
if (uiState.profileSource == ProfileSource.CreateNew) {
|
||||||
MaterialTheme.colorScheme.secondary
|
MaterialTheme.colorScheme.secondary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline
|
MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CreateNewFolder,
|
Icons.Default.CreateNewFolder,
|
||||||
@@ -363,30 +363,30 @@ fun NewProfileScreen(
|
|||||||
onClick = { viewModel.updateProfileSource(ProfileSource.Import) },
|
onClick = { viewModel.updateProfileSource(ProfileSource.Import) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
shape =
|
shape =
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 0.dp,
|
topStart = 0.dp,
|
||||||
bottomStart = 0.dp,
|
bottomStart = 0.dp,
|
||||||
topEnd = 12.dp,
|
topEnd = 12.dp,
|
||||||
bottomEnd = 12.dp,
|
bottomEnd = 12.dp,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
if (uiState.profileSource == ProfileSource.Import) {
|
if (uiState.profileSource == ProfileSource.Import) {
|
||||||
ButtonDefaults.buttonColors(
|
ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ButtonDefaults.outlinedButtonColors()
|
ButtonDefaults.outlinedButtonColors()
|
||||||
},
|
},
|
||||||
border =
|
border =
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
1.dp,
|
1.dp,
|
||||||
if (uiState.profileSource == ProfileSource.Import) {
|
if (uiState.profileSource == ProfileSource.Import) {
|
||||||
MaterialTheme.colorScheme.secondary
|
MaterialTheme.colorScheme.secondary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline
|
MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.FileUpload,
|
Icons.Default.FileUpload,
|
||||||
@@ -408,20 +408,20 @@ fun NewProfileScreen(
|
|||||||
onClick = { filePickerLauncher.launch("*/*") },
|
onClick = { filePickerLauncher.launch("*/*") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
border =
|
border =
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
1.dp,
|
1.dp,
|
||||||
if (uiState.importError != null) {
|
if (uiState.importError != null) {
|
||||||
MaterialTheme.colorScheme.error
|
MaterialTheme.colorScheme.error
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline
|
MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
@@ -429,11 +429,11 @@ fun NewProfileScreen(
|
|||||||
Icons.Default.FileUpload,
|
Icons.Default.FileUpload,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (uiState.importError != null) {
|
if (uiState.importError != null) {
|
||||||
MaterialTheme.colorScheme.error
|
MaterialTheme.colorScheme.error
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -473,9 +473,9 @@ fun NewProfileScreen(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
@@ -550,18 +550,18 @@ fun NewProfileScreen(
|
|||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 3.dp,
|
tonalElevation = 3.dp,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.validateAndCreateProfile() },
|
onClick = { viewModel.validateAndCreateProfile() },
|
||||||
|
|||||||
@@ -124,21 +124,18 @@ class NewProfileViewModel(application: Application) : AndroidViewModel(applicati
|
|||||||
_uiState.update { it.copy(autoUpdateInterval = intValue.coerceAtLeast(15)) }
|
_uiState.update { it.copy(autoUpdateInterval = intValue.coerceAtLeast(15)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setImportUri(
|
fun setImportUri(uri: Uri, fileName: String?) {
|
||||||
uri: Uri,
|
|
||||||
fileName: String?,
|
|
||||||
) {
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
importUri = uri,
|
importUri = uri,
|
||||||
importFileName = fileName,
|
importFileName = fileName,
|
||||||
importError = null, // Clear error when file is selected
|
importError = null, // Clear error when file is selected
|
||||||
name =
|
name =
|
||||||
if (it.name.isEmpty()) {
|
if (it.name.isEmpty()) {
|
||||||
fileName?.substringBeforeLast(".") ?: "Imported Profile"
|
fileName?.substringBeforeLast(".") ?: "Imported Profile"
|
||||||
} else {
|
} else {
|
||||||
it.name
|
it.name
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ class ProfileImportHandler(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class QRCodeParseResult {
|
sealed class QRCodeParseResult {
|
||||||
data class RemoteProfile(val name: String, val host: String, val url: String) :
|
data class RemoteProfile(val name: String, val host: String, val url: String) : QRCodeParseResult()
|
||||||
QRCodeParseResult()
|
|
||||||
|
|
||||||
data class LocalProfile(val name: String) : QRCodeParseResult()
|
data class LocalProfile(val name: String) : QRCodeParseResult()
|
||||||
|
|
||||||
@@ -43,188 +42,182 @@ class ProfileImportHandler(private val context: Context) {
|
|||||||
data class Error(val message: String) : UriParseResult()
|
data class Error(val message: String) : UriParseResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importFromUri(uri: Uri): ImportResult =
|
suspend fun importFromUri(uri: Uri): ImportResult = withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
try {
|
||||||
try {
|
val data =
|
||||||
val data =
|
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
?: return@withContext ImportResult.Error(context.getString(R.string.error_empty_file))
|
||||||
?: return@withContext ImportResult.Error(context.getString(R.string.error_empty_file))
|
|
||||||
|
|
||||||
// Get the filename from the URI
|
// Get the filename from the URI
|
||||||
val filename = getFileNameFromUri(uri)
|
val filename = getFileNameFromUri(uri)
|
||||||
|
|
||||||
// Try to detect if it's a JSON configuration file
|
// Try to detect if it's a JSON configuration file
|
||||||
val dataString = String(data)
|
val dataString = String(data)
|
||||||
if (isJsonConfiguration(dataString)) {
|
if (isJsonConfiguration(dataString)) {
|
||||||
// It's a JSON configuration, import it directly as a local profile
|
// It's a JSON configuration, import it directly as a local profile
|
||||||
return@withContext importJsonConfiguration(dataString, filename)
|
return@withContext importJsonConfiguration(dataString, filename)
|
||||||
}
|
|
||||||
|
|
||||||
// Try to decode as ProfileContent (the old way)
|
|
||||||
val content =
|
|
||||||
try {
|
|
||||||
Libbox.decodeProfileContent(data)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If it fails, try one more time as JSON
|
|
||||||
if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) {
|
|
||||||
return@withContext importJsonConfiguration(dataString, filename)
|
|
||||||
}
|
|
||||||
return@withContext ImportResult.Error(
|
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
importProfile(content)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ImportResult.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun parseUri(uri: Uri): UriParseResult =
|
// Try to decode as ProfileContent (the old way)
|
||||||
withContext(Dispatchers.IO) {
|
val content =
|
||||||
try {
|
try {
|
||||||
val data =
|
|
||||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
|
||||||
?: return@withContext UriParseResult.Error(context.getString(R.string.error_empty_file))
|
|
||||||
|
|
||||||
val filename = getFileNameFromUri(uri)
|
|
||||||
val dataString = String(data)
|
|
||||||
|
|
||||||
if (isJsonConfiguration(dataString)) {
|
|
||||||
return@withContext UriParseResult.Success(name = filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
val content =
|
|
||||||
try {
|
|
||||||
Libbox.decodeProfileContent(data)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) {
|
|
||||||
return@withContext UriParseResult.Success(name = filename)
|
|
||||||
}
|
|
||||||
return@withContext UriParseResult.Error(
|
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
UriParseResult.Success(name = content.name)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
UriParseResult.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun parseQRCode(data: String): QRCodeParseResult =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
// Check if it's a sing-box remote profile import link
|
|
||||||
if (data.startsWith("sing-box://import-remote-profile")) {
|
|
||||||
try {
|
|
||||||
val profileInfo = Libbox.parseRemoteProfileImportLink(data)
|
|
||||||
return@withContext QRCodeParseResult.RemoteProfile(
|
|
||||||
name = profileInfo.name,
|
|
||||||
host = profileInfo.host,
|
|
||||||
url = profileInfo.url,
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@withContext QRCodeParseResult.Error(
|
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a direct URL
|
|
||||||
if (data.startsWith("http://") || data.startsWith("https://")) {
|
|
||||||
val profileName = extractProfileNameFromUrl(data)
|
|
||||||
return@withContext QRCodeParseResult.RemoteProfile(
|
|
||||||
name = profileName,
|
|
||||||
host = extractHostFromUrl(data),
|
|
||||||
url = data,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to decode as profile content
|
|
||||||
val content =
|
|
||||||
try {
|
|
||||||
Libbox.decodeProfileContent(data.toByteArray())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@withContext QRCodeParseResult.Error(
|
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@withContext QRCodeParseResult.LocalProfile(name = content.name)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
QRCodeParseResult.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun importFromQRCode(data: String): ImportResult =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
// Check if it's a sing-box remote profile import link
|
|
||||||
if (data.startsWith("sing-box://import-remote-profile")) {
|
|
||||||
try {
|
|
||||||
val profileInfo = Libbox.parseRemoteProfileImportLink(data)
|
|
||||||
return@withContext importRemoteProfile(profileInfo.name, profileInfo.url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@withContext ImportResult.Error(
|
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a URL or direct profile content
|
|
||||||
if (data.startsWith("http://") || data.startsWith("https://")) {
|
|
||||||
// Handle remote profile URL
|
|
||||||
val profileName = extractProfileNameFromUrl(data)
|
|
||||||
importRemoteProfile(profileName, data)
|
|
||||||
} else {
|
|
||||||
// Try to decode as profile content
|
|
||||||
val content =
|
|
||||||
try {
|
|
||||||
Libbox.decodeProfileContent(data.toByteArray())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@withContext ImportResult.Error(
|
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
importProfile(content)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ImportResult.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun parseQRSData(data: ByteArray): QRSParseResult =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val content = try {
|
|
||||||
Libbox.decodeProfileContent(data)
|
Libbox.decodeProfileContent(data)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@withContext QRSParseResult.Error(
|
// If it fails, try one more time as JSON
|
||||||
|
if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) {
|
||||||
|
return@withContext importJsonConfiguration(dataString, filename)
|
||||||
|
}
|
||||||
|
return@withContext ImportResult.Error(
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
QRSParseResult.Success(name = content.name)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
QRSParseResult.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun importFromQRSData(data: ByteArray): ImportResult =
|
importProfile(content)
|
||||||
withContext(Dispatchers.IO) {
|
} catch (e: Exception) {
|
||||||
try {
|
ImportResult.Error(e.message ?: "Unknown error")
|
||||||
val content = try {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun parseUri(uri: Uri): UriParseResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val data =
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||||
|
?: return@withContext UriParseResult.Error(context.getString(R.string.error_empty_file))
|
||||||
|
|
||||||
|
val filename = getFileNameFromUri(uri)
|
||||||
|
val dataString = String(data)
|
||||||
|
|
||||||
|
if (isJsonConfiguration(dataString)) {
|
||||||
|
return@withContext UriParseResult.Success(name = filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
val content =
|
||||||
|
try {
|
||||||
Libbox.decodeProfileContent(data)
|
Libbox.decodeProfileContent(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (dataString.trimStart().startsWith("{") || dataString.trimStart().startsWith("[")) {
|
||||||
|
return@withContext UriParseResult.Success(name = filename)
|
||||||
|
}
|
||||||
|
return@withContext UriParseResult.Error(
|
||||||
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UriParseResult.Success(name = content.name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
UriParseResult.Error(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun parseQRCode(data: String): QRCodeParseResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Check if it's a sing-box remote profile import link
|
||||||
|
if (data.startsWith("sing-box://import-remote-profile")) {
|
||||||
|
try {
|
||||||
|
val profileInfo = Libbox.parseRemoteProfileImportLink(data)
|
||||||
|
return@withContext QRCodeParseResult.RemoteProfile(
|
||||||
|
name = profileInfo.name,
|
||||||
|
host = profileInfo.host,
|
||||||
|
url = profileInfo.url,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@withContext QRCodeParseResult.Error(
|
||||||
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a direct URL
|
||||||
|
if (data.startsWith("http://") || data.startsWith("https://")) {
|
||||||
|
val profileName = extractProfileNameFromUrl(data)
|
||||||
|
return@withContext QRCodeParseResult.RemoteProfile(
|
||||||
|
name = profileName,
|
||||||
|
host = extractHostFromUrl(data),
|
||||||
|
url = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to decode as profile content
|
||||||
|
val content =
|
||||||
|
try {
|
||||||
|
Libbox.decodeProfileContent(data.toByteArray())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@withContext QRCodeParseResult.Error(
|
||||||
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext QRCodeParseResult.LocalProfile(name = content.name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
QRCodeParseResult.Error(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun importFromQRCode(data: String): ImportResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Check if it's a sing-box remote profile import link
|
||||||
|
if (data.startsWith("sing-box://import-remote-profile")) {
|
||||||
|
try {
|
||||||
|
val profileInfo = Libbox.parseRemoteProfileImportLink(data)
|
||||||
|
return@withContext importRemoteProfile(profileInfo.name, profileInfo.url)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@withContext ImportResult.Error(
|
return@withContext ImportResult.Error(
|
||||||
context.getString(R.string.error_decode_profile, e.message),
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
importProfile(content)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ImportResult.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a URL or direct profile content
|
||||||
|
if (data.startsWith("http://") || data.startsWith("https://")) {
|
||||||
|
// Handle remote profile URL
|
||||||
|
val profileName = extractProfileNameFromUrl(data)
|
||||||
|
importRemoteProfile(profileName, data)
|
||||||
|
} else {
|
||||||
|
// Try to decode as profile content
|
||||||
|
val content =
|
||||||
|
try {
|
||||||
|
Libbox.decodeProfileContent(data.toByteArray())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@withContext ImportResult.Error(
|
||||||
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
importProfile(content)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ImportResult.Error(e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun parseQRSData(data: ByteArray): QRSParseResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val content = try {
|
||||||
|
Libbox.decodeProfileContent(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@withContext QRSParseResult.Error(
|
||||||
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
QRSParseResult.Success(name = content.name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
QRSParseResult.Error(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun importFromQRSData(data: ByteArray): ImportResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val content = try {
|
||||||
|
Libbox.decodeProfileContent(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@withContext ImportResult.Error(
|
||||||
|
context.getString(R.string.error_decode_profile, e.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
importProfile(content)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ImportResult.Error(e.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun importProfile(content: ProfileContent): ImportResult {
|
private suspend fun importProfile(content: ProfileContent): ImportResult {
|
||||||
val typedProfile = TypedProfile()
|
val typedProfile = TypedProfile()
|
||||||
@@ -259,10 +252,7 @@ class ProfileImportHandler(private val context: Context) {
|
|||||||
return ImportResult.Success(profile)
|
return ImportResult.Success(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun importRemoteProfile(
|
private suspend fun importRemoteProfile(name: String, url: String): ImportResult {
|
||||||
name: String,
|
|
||||||
url: String,
|
|
||||||
): ImportResult {
|
|
||||||
val typedProfile =
|
val typedProfile =
|
||||||
TypedProfile().apply {
|
TypedProfile().apply {
|
||||||
type = TypedProfile.Type.Remote
|
type = TypedProfile.Type.Remote
|
||||||
@@ -297,13 +287,11 @@ class ProfileImportHandler(private val context: Context) {
|
|||||||
?: "Remote Profile"
|
?: "Remote Profile"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractHostFromUrl(url: String): String {
|
private fun extractHostFromUrl(url: String): String = try {
|
||||||
return try {
|
val uri = Uri.parse(url)
|
||||||
val uri = Uri.parse(url)
|
uri.host ?: url
|
||||||
uri.host ?: url
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
url
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameFromUri(uri: Uri): String {
|
private fun getFileNameFromUri(uri: Uri): String {
|
||||||
@@ -354,10 +342,7 @@ class ProfileImportHandler(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun importJsonConfiguration(
|
private suspend fun importJsonConfiguration(jsonContent: String, profileName: String): ImportResult {
|
||||||
jsonContent: String,
|
|
||||||
profileName: String,
|
|
||||||
): ImportResult {
|
|
||||||
return try {
|
return try {
|
||||||
// Validate the JSON configuration using sing-box
|
// Validate the JSON configuration using sing-box
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.connections
|
package io.nekohasekai.sfa.compose.screen.connections
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -10,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.ScrollState
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -32,14 +32,14 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
@@ -286,10 +286,7 @@ fun ConnectionDetailsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DetailSection(
|
private fun DetailSection(title: String, content: @Composable ColumnScope.() -> Unit) {
|
||||||
title: String,
|
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -317,12 +314,7 @@ private fun DetailSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DetailRow(
|
private fun DetailRow(label: String, value: String, monospace: Boolean = false, valueColor: Color = MaterialTheme.colorScheme.onSurface) {
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
monospace: Boolean = false,
|
|
||||||
valueColor: Color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -346,20 +338,10 @@ private fun DetailRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberBounceBlockingNestedScrollConnection(
|
private fun rememberBounceBlockingNestedScrollConnection(scrollState: ScrollState): NestedScrollConnection = remember(scrollState) {
|
||||||
scrollState: ScrollState
|
|
||||||
): NestedScrollConnection = remember(scrollState) {
|
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset {
|
|
||||||
return if (available.y < 0) available else Offset.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
|
||||||
return if (available.y < 0) available else Velocity.Zero
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,16 +45,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.compose.model.Connection
|
import io.nekohasekai.sfa.compose.model.Connection
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
private fun Drawable.toBitmap(): Bitmap {
|
private fun Drawable.toBitmap(): Bitmap {
|
||||||
if (this is BitmapDrawable) return bitmap
|
if (this is BitmapDrawable) return bitmap
|
||||||
val bitmap = Bitmap.createBitmap(
|
val bitmap = Bitmap.createBitmap(
|
||||||
intrinsicWidth.coerceAtLeast(1),
|
intrinsicWidth.coerceAtLeast(1),
|
||||||
intrinsicHeight.coerceAtLeast(1),
|
intrinsicHeight.coerceAtLeast(1),
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.ARGB_8888,
|
||||||
)
|
)
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
setBounds(0, 0, canvas.width, canvas.height)
|
setBounds(0, 0, canvas.width, canvas.height)
|
||||||
@@ -62,10 +59,7 @@ private fun Drawable.toBitmap(): Bitmap {
|
|||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AppInfo(
|
data class AppInfo(val icon: ImageBitmap, val label: String)
|
||||||
val icon: ImageBitmap,
|
|
||||||
val label: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberAppInfo(packageName: String): AppInfo? {
|
private fun rememberAppInfo(packageName: String): AppInfo? {
|
||||||
@@ -86,12 +80,7 @@ private fun rememberAppInfo(packageName: String): AppInfo? {
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionItem(
|
fun ConnectionItem(connection: Connection, onClick: () -> Unit, onClose: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
connection: Connection,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onClose: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
var showContextMenu by remember { mutableStateOf(false) }
|
var showContextMenu by remember { mutableStateOf(false) }
|
||||||
val packageName = connection.processInfo?.packageName?.takeIf { it.isNotEmpty() }
|
val packageName = connection.processInfo?.packageName?.takeIf { it.isNotEmpty() }
|
||||||
val appInfo = packageName?.let { rememberAppInfo(it) }
|
val appInfo = packageName?.let { rememberAppInfo(it) }
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
@@ -54,15 +49,20 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
import io.nekohasekai.sfa.compose.model.Connection
|
||||||
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
||||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.compose.model.Connection
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -118,7 +118,7 @@ fun ConnectionsPage(
|
|||||||
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
||||||
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
||||||
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -230,7 +230,7 @@ fun ConnectionsPage(
|
|||||||
stringResource(R.string.close_search)
|
stringResource(R.string.close_search)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.search)
|
stringResource(R.string.search)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -433,20 +433,10 @@ fun ConnectionsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberBounceBlockingNestedScrollConnection(
|
private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
|
||||||
lazyListState: LazyListState
|
|
||||||
): NestedScrollConnection = remember(lazyListState) {
|
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = if (available.y < 0) available else Offset.Zero
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset {
|
|
||||||
return if (available.y < 0) available else Offset.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = if (available.y < 0) available else Velocity.Zero
|
||||||
return if (available.y < 0) available else Velocity.Zero
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import io.nekohasekai.libbox.Connections
|
|||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
||||||
import io.nekohasekai.sfa.compose.base.ScreenEvent
|
import io.nekohasekai.sfa.compose.base.ScreenEvent
|
||||||
import io.nekohasekai.sfa.constant.Status
|
|
||||||
import io.nekohasekai.sfa.ktx.toList
|
|
||||||
import io.nekohasekai.sfa.compose.model.Connection
|
import io.nekohasekai.sfa.compose.model.Connection
|
||||||
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
||||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||||
|
import io.nekohasekai.sfa.constant.Status
|
||||||
|
import io.nekohasekai.sfa.ktx.toList
|
||||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||||
import io.nekohasekai.sfa.utils.CommandClient
|
import io.nekohasekai.sfa.utils.CommandClient
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -22,6 +21,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
data class ConnectionsUiState(
|
data class ConnectionsUiState(
|
||||||
val connections: List<Connection> = emptyList(),
|
val connections: List<Connection> = emptyList(),
|
||||||
@@ -38,7 +38,9 @@ sealed class ConnectionsEvent : ScreenEvent {
|
|||||||
data object AllConnectionsClosed : ConnectionsEvent()
|
data object AllConnectionsClosed : ConnectionsEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler {
|
class ConnectionsViewModel :
|
||||||
|
BaseViewModel<ConnectionsUiState, ConnectionsEvent>(),
|
||||||
|
CommandClient.Handler {
|
||||||
private val commandClient = CommandClient(
|
private val commandClient = CommandClient(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
CommandClient.ConnectionType.Connections,
|
CommandClient.ConnectionType.Connections,
|
||||||
@@ -62,7 +64,7 @@ class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>
|
|||||||
combine(
|
combine(
|
||||||
AppLifecycleObserver.isForeground,
|
AppLifecycleObserver.isForeground,
|
||||||
_isVisible,
|
_isVisible,
|
||||||
_serviceStatus
|
_serviceStatus,
|
||||||
) { foreground, visible, status ->
|
) { foreground, visible, status ->
|
||||||
Triple(foreground, visible, status)
|
Triple(foreground, visible, status)
|
||||||
}.collect { (foreground, visible, status) ->
|
}.collect { (foreground, visible, status) ->
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -14,8 +13,8 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.material.icons.outlined.Tune
|
import androidx.compose.material.icons.outlined.Tune
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -44,20 +43,15 @@ import io.nekohasekai.sfa.R
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ClashModeCard(
|
fun ClashModeCard(modes: List<String>, selectedMode: String, onModeSelected: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||||
modes: List<String>,
|
|
||||||
selectedMode: String,
|
|
||||||
onModeSelected: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -109,10 +103,10 @@ fun ClashModeCard(
|
|||||||
modes.forEachIndexed { index, mode ->
|
modes.forEachIndexed { index, mode ->
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
shape =
|
shape =
|
||||||
SegmentedButtonDefaults.itemShape(
|
SegmentedButtonDefaults.itemShape(
|
||||||
index = index,
|
index = index,
|
||||||
count = modes.size,
|
count = modes.size,
|
||||||
),
|
),
|
||||||
onClick = { onModeSelected(mode) },
|
onClick = { onModeSelected(mode) },
|
||||||
selected = mode == selectedMode,
|
selected = mode == selectedMode,
|
||||||
) {
|
) {
|
||||||
@@ -127,11 +121,7 @@ fun ClashModeCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ModeDropdown(
|
private fun ModeDropdown(modes: List<String>, selectedMode: String, onModeSelected: (String) -> Unit) {
|
||||||
modes: List<String>,
|
|
||||||
selectedMode: String,
|
|
||||||
onModeSelected: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
@@ -147,9 +137,9 @@ private fun ModeDropdown(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -24,19 +24,15 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionsCard(
|
fun ConnectionsCard(connectionsIn: String, connectionsOut: String, modifier: Modifier = Modifier) {
|
||||||
connectionsIn: String,
|
|
||||||
connectionsOut: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -37,10 +36,7 @@ import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
|||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
data class CardRenderItem(
|
data class CardRenderItem(val cards: List<CardGroup>, val isRow: Boolean)
|
||||||
val cards: List<CardGroup>,
|
|
||||||
val isRow: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -87,18 +83,18 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton =
|
dismissButton =
|
||||||
if (!note.migrationLink.isNullOrBlank()) {
|
if (!note.migrationLink.isNullOrBlank()) {
|
||||||
{
|
{
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
viewModel.sendGlobalEvent(UiEvent.OpenUrl(note.migrationLink))
|
viewModel.sendGlobalEvent(UiEvent.OpenUrl(note.migrationLink))
|
||||||
viewModel.dismissDeprecatedNote()
|
viewModel.dismissDeprecatedNote()
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(R.string.error_deprecated_documentation))
|
Text(stringResource(R.string.error_deprecated_documentation))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
null
|
} else {
|
||||||
},
|
null
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,9 +130,9 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(bottom = bottomPadding),
|
contentPadding = PaddingValues(bottom = bottomPadding),
|
||||||
) {
|
) {
|
||||||
@@ -172,8 +168,8 @@ fun DashboardScreen(
|
|||||||
DashboardCardRenderer(
|
DashboardCardRenderer(
|
||||||
cardGroup = cardGroup,
|
cardGroup = cardGroup,
|
||||||
cardWidth =
|
cardWidth =
|
||||||
uiState.cardWidths[cardGroup]
|
uiState.cardWidths[cardGroup]
|
||||||
?: CardWidth.Full,
|
?: CardWidth.Full,
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onClashModeSelected = viewModel::selectClashMode,
|
onClashModeSelected = viewModel::selectClashMode,
|
||||||
onSystemProxyToggle = viewModel::toggleSystemProxy,
|
onSystemProxyToggle = viewModel::toggleSystemProxy,
|
||||||
@@ -199,9 +195,9 @@ fun DashboardScreen(
|
|||||||
onOpenNewProfile = onOpenNewProfile,
|
onOpenNewProfile = onOpenNewProfile,
|
||||||
commandClient = viewModel.commandClient,
|
commandClient = viewModel.commandClient,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,8 +207,8 @@ fun DashboardScreen(
|
|||||||
DashboardCardRenderer(
|
DashboardCardRenderer(
|
||||||
cardGroup = cardGroup,
|
cardGroup = cardGroup,
|
||||||
cardWidth =
|
cardWidth =
|
||||||
uiState.cardWidths[cardGroup]
|
uiState.cardWidths[cardGroup]
|
||||||
?: CardWidth.Full,
|
?: CardWidth.Full,
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
serviceStatus = serviceStatus,
|
serviceStatus = serviceStatus,
|
||||||
onClashModeSelected = viewModel::selectClashMode,
|
onClashModeSelected = viewModel::selectClashMode,
|
||||||
@@ -307,17 +303,12 @@ fun processCardsForRendering(
|
|||||||
* This function is only relevant when the service is running.
|
* This function is only relevant when the service is running.
|
||||||
* Note: Profiles card is always available and should not use this function.
|
* Note: Profiles card is always available and should not use this function.
|
||||||
*/
|
*/
|
||||||
fun isCardAvailableWhenServiceRunning(
|
fun isCardAvailableWhenServiceRunning(cardGroup: CardGroup, uiState: DashboardUiState): Boolean = when (cardGroup) {
|
||||||
cardGroup: CardGroup,
|
CardGroup.ClashMode -> uiState.clashModeVisible
|
||||||
uiState: DashboardUiState,
|
CardGroup.UploadTraffic -> uiState.trafficVisible
|
||||||
): Boolean {
|
CardGroup.DownloadTraffic -> uiState.trafficVisible
|
||||||
return when (cardGroup) {
|
CardGroup.Debug -> true // Debug info is always available when service is running
|
||||||
CardGroup.ClashMode -> uiState.clashModeVisible
|
CardGroup.Connections -> uiState.trafficVisible
|
||||||
CardGroup.UploadTraffic -> uiState.trafficVisible
|
CardGroup.SystemProxy -> uiState.systemProxyVisible
|
||||||
CardGroup.DownloadTraffic -> uiState.trafficVisible
|
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety
|
||||||
CardGroup.Debug -> true // Debug info is always available when service is running
|
|
||||||
CardGroup.Connections -> uiState.trafficVisible
|
|
||||||
CardGroup.SystemProxy -> uiState.systemProxyVisible
|
|
||||||
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.DragHandle
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import io.nekohasekai.sfa.compat.animateItemCompat
|
|
||||||
import androidx.compose.material.icons.outlined.BugReport
|
import androidx.compose.material.icons.outlined.BugReport
|
||||||
import androidx.compose.material.icons.outlined.Cable
|
import androidx.compose.material.icons.outlined.Cable
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
import androidx.compose.material.icons.outlined.Folder
|
|
||||||
import androidx.compose.material.icons.outlined.Person
|
import androidx.compose.material.icons.outlined.Person
|
||||||
import androidx.compose.material.icons.outlined.Route
|
import androidx.compose.material.icons.outlined.Route
|
||||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||||
@@ -65,6 +63,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.compat.animateItemCompat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -96,12 +95,12 @@ fun DashboardSettingsBottomSheet(
|
|||||||
var dragOffset by remember { mutableStateOf(0f) }
|
var dragOffset by remember { mutableStateOf(0f) }
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
fun onMove(
|
fun onMove(fromIndex: Int, toIndex: Int) {
|
||||||
fromIndex: Int,
|
if (fromIndex != toIndex &&
|
||||||
toIndex: Int,
|
fromIndex >= 0 &&
|
||||||
) {
|
toIndex >= 0 &&
|
||||||
if (fromIndex != toIndex && fromIndex >= 0 && toIndex >= 0 &&
|
fromIndex < reorderedList.size &&
|
||||||
fromIndex < reorderedList.size && toIndex < reorderedList.size
|
toIndex < reorderedList.size
|
||||||
) {
|
) {
|
||||||
val newList = reorderedList.toMutableList()
|
val newList = reorderedList.toMutableList()
|
||||||
val item = newList.removeAt(fromIndex)
|
val item = newList.removeAt(fromIndex)
|
||||||
@@ -135,17 +134,17 @@ fun DashboardSettingsBottomSheet(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.8f),
|
.fillMaxHeight(0.8f),
|
||||||
) {
|
) {
|
||||||
// Header with reset button
|
// Header with reset button
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
.padding(bottom = 16.dp),
|
.padding(bottom = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -199,18 +198,18 @@ fun DashboardSettingsBottomSheet(
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
.padding(bottom = 12.dp),
|
.padding(bottom = 12.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reorderable list
|
// Reorderable list
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
@@ -275,13 +274,13 @@ fun DashboardSettingsBottomSheet(
|
|||||||
dragOffset = 0f
|
dragOffset = 0f
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
animateItemCompat(
|
animateItemCompat(
|
||||||
placementSpec =
|
placementSpec =
|
||||||
spring(
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow,
|
stiffness = Spring.StiffnessLow,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,40 +314,40 @@ fun DashboardItemCard(
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.offset(y = with(LocalDensity.current) { offsetY.value.toDp() })
|
.offset(y = with(LocalDensity.current) { offsetY.value.toDp() })
|
||||||
.zIndex(if (isDragging) 1f else 0f)
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
elevation =
|
elevation =
|
||||||
CardDefaults.cardElevation(
|
CardDefaults.cardElevation(
|
||||||
defaultElevation = cardElevation,
|
defaultElevation = cardElevation,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
MaterialTheme.colorScheme.surface.copy(alpha = 0.95f)
|
MaterialTheme.colorScheme.surface.copy(alpha = 0.95f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.surface
|
MaterialTheme.colorScheme.surface
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
border =
|
border =
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color =
|
color =
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.12f)
|
MaterialTheme.colorScheme.outline.copy(alpha = 0.12f)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
// Drag handle
|
// Drag handle
|
||||||
@@ -361,66 +360,66 @@ fun DashboardItemCard(
|
|||||||
imageVector = Icons.Default.DragHandle,
|
imageVector = Icons.Default.DragHandle,
|
||||||
contentDescription = stringResource(R.string.drag_to_reorder),
|
contentDescription = stringResource(R.string.drag_to_reorder),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.draggable(
|
.draggable(
|
||||||
state = draggableState,
|
state = draggableState,
|
||||||
orientation = Orientation.Vertical,
|
orientation = Orientation.Vertical,
|
||||||
onDragStarted = { onDragStart() },
|
onDragStarted = { onDragStart() },
|
||||||
onDragStopped = { onDragEnd() },
|
onDragStopped = { onDragEnd() },
|
||||||
)
|
)
|
||||||
.padding(4.dp),
|
.padding(4.dp),
|
||||||
tint =
|
tint =
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Card icon
|
// Card icon
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
when (cardGroup) {
|
when (cardGroup) {
|
||||||
CardGroup.Debug -> Icons.Outlined.BugReport
|
CardGroup.Debug -> Icons.Outlined.BugReport
|
||||||
CardGroup.Connections -> Icons.Outlined.Cable
|
CardGroup.Connections -> Icons.Outlined.Cable
|
||||||
CardGroup.UploadTraffic -> Icons.Outlined.Upload
|
CardGroup.UploadTraffic -> Icons.Outlined.Upload
|
||||||
CardGroup.DownloadTraffic -> Icons.Outlined.Download
|
CardGroup.DownloadTraffic -> Icons.Outlined.Download
|
||||||
CardGroup.ClashMode -> Icons.Outlined.Route
|
CardGroup.ClashMode -> Icons.Outlined.Route
|
||||||
CardGroup.SystemProxy -> Icons.Outlined.SettingsEthernet
|
CardGroup.SystemProxy -> Icons.Outlined.SettingsEthernet
|
||||||
CardGroup.Profiles -> Icons.Outlined.Person
|
CardGroup.Profiles -> Icons.Outlined.Person
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.padding(horizontal = 4.dp),
|
.padding(horizontal = 4.dp),
|
||||||
tint =
|
tint =
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Card info
|
// Card info
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
when (cardGroup) {
|
when (cardGroup) {
|
||||||
CardGroup.Debug -> stringResource(R.string.title_debug)
|
CardGroup.Debug -> stringResource(R.string.title_debug)
|
||||||
CardGroup.Connections -> stringResource(R.string.title_connections)
|
CardGroup.Connections -> stringResource(R.string.title_connections)
|
||||||
CardGroup.UploadTraffic -> stringResource(R.string.upload)
|
CardGroup.UploadTraffic -> stringResource(R.string.upload)
|
||||||
CardGroup.DownloadTraffic -> stringResource(R.string.download)
|
CardGroup.DownloadTraffic -> stringResource(R.string.download)
|
||||||
CardGroup.ClashMode -> stringResource(R.string.clash_mode)
|
CardGroup.ClashMode -> stringResource(R.string.clash_mode)
|
||||||
CardGroup.SystemProxy -> stringResource(R.string.system_proxy)
|
CardGroup.SystemProxy -> stringResource(R.string.system_proxy)
|
||||||
CardGroup.Profiles -> stringResource(R.string.title_configuration)
|
CardGroup.Profiles -> stringResource(R.string.title_configuration)
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|||||||
@@ -114,16 +114,15 @@ data class DashboardUiState(
|
|||||||
),
|
),
|
||||||
val showCardSettingsDialog: Boolean = false,
|
val showCardSettingsDialog: Boolean = false,
|
||||||
) {
|
) {
|
||||||
data class DeprecatedNote(
|
data class DeprecatedNote(val message: String, val migrationLink: String?)
|
||||||
val message: String,
|
|
||||||
val migrationLink: String?,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardViewModel now only uses UiEvent for all events
|
// DashboardViewModel now only uses UiEvent for all events
|
||||||
// No need for DashboardEvent anymore as all events are handled globally
|
// No need for DashboardEvent anymore as all events are handled globally
|
||||||
|
|
||||||
class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandClient.Handler {
|
class DashboardViewModel :
|
||||||
|
BaseViewModel<DashboardUiState, UiEvent>(),
|
||||||
|
CommandClient.Handler {
|
||||||
private val _serviceStatus = MutableStateFlow(Status.Stopped)
|
private val _serviceStatus = MutableStateFlow(Status.Stopped)
|
||||||
val serviceStatus: StateFlow<Status> = _serviceStatus.asStateFlow()
|
val serviceStatus: StateFlow<Status> = _serviceStatus.asStateFlow()
|
||||||
|
|
||||||
@@ -395,10 +394,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveProfile(
|
fun moveProfile(from: Int, to: Int) {
|
||||||
from: Int,
|
|
||||||
to: Int,
|
|
||||||
) {
|
|
||||||
val currentProfiles = currentState.profiles.toMutableList()
|
val currentProfiles = currentState.profiles.toMutableList()
|
||||||
|
|
||||||
if (from < to) {
|
if (from < to) {
|
||||||
@@ -614,10 +610,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initializeClashMode(
|
override fun initializeClashMode(modeList: List<String>, currentMode: String) {
|
||||||
modeList: List<String>,
|
|
||||||
currentMode: String,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
updateState {
|
updateState {
|
||||||
copy(
|
copy(
|
||||||
@@ -702,16 +695,15 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for serialization
|
// Helper functions for serialization
|
||||||
private fun getDefaultItemOrder() =
|
private fun getDefaultItemOrder() = listOf(
|
||||||
listOf(
|
CardGroup.UploadTraffic,
|
||||||
CardGroup.UploadTraffic,
|
CardGroup.DownloadTraffic,
|
||||||
CardGroup.DownloadTraffic,
|
CardGroup.Debug,
|
||||||
CardGroup.Debug,
|
CardGroup.Connections,
|
||||||
CardGroup.Connections,
|
CardGroup.SystemProxy,
|
||||||
CardGroup.SystemProxy,
|
CardGroup.ClashMode,
|
||||||
CardGroup.ClashMode,
|
CardGroup.Profiles,
|
||||||
CardGroup.Profiles,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
private fun loadItemOrder(): List<CardGroup> {
|
private fun loadItemOrder(): List<CardGroup> {
|
||||||
val savedOrder = Settings.dashboardItemOrder
|
val savedOrder = Settings.dashboardItemOrder
|
||||||
@@ -766,11 +758,9 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
|
|
||||||
private fun cardGroupToString(card: CardGroup): String = card.name
|
private fun cardGroupToString(card: CardGroup): String = card.name
|
||||||
|
|
||||||
private fun stringToCardGroup(name: String): CardGroup? {
|
private fun stringToCardGroup(name: String): CardGroup? = try {
|
||||||
return try {
|
CardGroup.valueOf(name)
|
||||||
CardGroup.valueOf(name)
|
} catch (e: IllegalArgumentException) {
|
||||||
} catch (e: IllegalArgumentException) {
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,19 +24,15 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DebugCard(
|
fun DebugCard(memory: String, goroutines: String, modifier: Modifier = Modifier) {
|
||||||
memory: String,
|
|
||||||
goroutines: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -24,20 +24,15 @@ import io.nekohasekai.sfa.R
|
|||||||
import io.nekohasekai.sfa.compose.LineChart
|
import io.nekohasekai.sfa.compose.LineChart
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadTrafficCard(
|
fun DownloadTrafficCard(downlink: String, downlinkTotal: String, downlinkHistory: List<Float>, modifier: Modifier = Modifier) {
|
||||||
downlink: String,
|
|
||||||
downlinkTotal: String,
|
|
||||||
downlinkHistory: List<Float>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.UnfoldLess
|
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.Speed
|
import androidx.compose.material.icons.filled.Speed
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldLess
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -58,19 +58,19 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
|
||||||
import io.nekohasekai.sfa.constant.Status
|
|
||||||
import io.nekohasekai.sfa.compose.model.Group
|
import io.nekohasekai.sfa.compose.model.Group
|
||||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||||
|
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.utils.CommandClient
|
import io.nekohasekai.sfa.utils.CommandClient
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -84,12 +84,12 @@ fun GroupsCard(
|
|||||||
) {
|
) {
|
||||||
val actualViewModel: GroupsViewModel = viewModel ?: viewModel(
|
val actualViewModel: GroupsViewModel = viewModel ?: viewModel(
|
||||||
factory =
|
factory =
|
||||||
object : ViewModelProvider.Factory {
|
object : ViewModelProvider.Factory {
|
||||||
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return GroupsViewModel(commandClient) as T
|
return GroupsViewModel(commandClient) as T
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val uiState by actualViewModel.uiState.collectAsState()
|
val uiState by actualViewModel.uiState.collectAsState()
|
||||||
@@ -104,17 +104,17 @@ fun GroupsCard(
|
|||||||
IconButton(onClick = { actualViewModel.toggleAllGroups() }) {
|
IconButton(onClick = { actualViewModel.toggleAllGroups() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (allCollapsed) {
|
if (allCollapsed) {
|
||||||
Icons.Default.UnfoldMore
|
Icons.Default.UnfoldMore
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.UnfoldLess
|
Icons.Default.UnfoldLess
|
||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (allCollapsed) {
|
if (allCollapsed) {
|
||||||
stringResource(R.string.expand_all)
|
stringResource(R.string.expand_all)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.collapse_all)
|
stringResource(R.string.collapse_all)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,9 +186,9 @@ private fun GroupsCardContent(
|
|||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(200.dp),
|
.height(200.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -196,9 +196,9 @@ private fun GroupsCardContent(
|
|||||||
} else if (uiState.groups.isEmpty()) {
|
} else if (uiState.groups.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(100.dp),
|
.height(100.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -216,12 +216,12 @@ private fun GroupsCardContent(
|
|||||||
.nestedScroll(bounceBlockingConnection),
|
.nestedScroll(bounceBlockingConnection),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding =
|
contentPadding =
|
||||||
PaddingValues(
|
PaddingValues(
|
||||||
start = 16.dp,
|
start = 16.dp,
|
||||||
end = 16.dp,
|
end = 16.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = 16.dp,
|
bottom = 16.dp,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
@@ -347,17 +347,17 @@ private fun ProxyGroupItem(
|
|||||||
imageVector = Icons.Default.ExpandMore,
|
imageVector = Icons.Default.ExpandMore,
|
||||||
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.graphicsLayer { rotationZ = rotationAngle },
|
.graphicsLayer { rotationZ = rotationAngle },
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,21 +365,21 @@ private fun ProxyGroupItem(
|
|||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded && group.items.isNotEmpty(),
|
visible = isExpanded && group.items.isNotEmpty(),
|
||||||
enter =
|
enter =
|
||||||
expandVertically(animationSpec = tween(300)) +
|
expandVertically(animationSpec = tween(300)) +
|
||||||
fadeIn(
|
fadeIn(
|
||||||
animationSpec =
|
animationSpec =
|
||||||
tween(
|
tween(
|
||||||
300,
|
300,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
exit =
|
exit =
|
||||||
shrinkVertically(animationSpec = tween(300)) +
|
shrinkVertically(animationSpec = tween(300)) +
|
||||||
fadeOut(
|
fadeOut(
|
||||||
animationSpec =
|
animationSpec =
|
||||||
tween(
|
tween(
|
||||||
300,
|
300,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -401,12 +401,7 @@ private fun ProxyGroupItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyItemsList(
|
private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
|
||||||
items: List<GroupItem>,
|
|
||||||
selectedTag: String,
|
|
||||||
isSelectable: Boolean,
|
|
||||||
onItemSelected: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
val itemsPerRow = 2
|
val itemsPerRow = 2
|
||||||
val chunkedItems =
|
val chunkedItems =
|
||||||
remember(items) {
|
remember(items) {
|
||||||
@@ -415,9 +410,9 @@ private fun ProxyItemsList(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
chunkedItems.forEach { rowItems ->
|
chunkedItems.forEach { rowItems ->
|
||||||
@@ -450,13 +445,7 @@ private fun ProxyItemsList(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyChip(
|
private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
item: GroupItem,
|
|
||||||
isSelected: Boolean,
|
|
||||||
isSelectable: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
// Use simpler, faster animations
|
// Use simpler, faster animations
|
||||||
val animatedElevation by animateFloatAsState(
|
val animatedElevation by animateFloatAsState(
|
||||||
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
|
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
|
||||||
@@ -475,18 +464,18 @@ private fun ProxyChip(
|
|||||||
androidx.compose.foundation.BorderStroke(
|
androidx.compose.foundation.BorderStroke(
|
||||||
width = if (isSelected) 2.dp else 1.dp,
|
width = if (isSelected) 2.dp else 1.dp,
|
||||||
color =
|
color =
|
||||||
when {
|
when {
|
||||||
isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
||||||
else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
|
else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val content: @Composable () -> Unit = {
|
val content: @Composable () -> Unit = {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -500,11 +489,11 @@ private fun ProxyChip(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -520,11 +509,11 @@ private fun ProxyChip(
|
|||||||
text = Libbox.proxyDisplayType(item.type),
|
text = Libbox.proxyDisplayType(item.type),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Latency
|
// Latency
|
||||||
@@ -566,11 +555,7 @@ private fun ProxyChip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyLatencyBadge(
|
private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
|
||||||
delay: Int,
|
|
||||||
isSelected: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
// Direct color calculation without animation for better performance
|
// Direct color calculation without animation for better performance
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val latencyColor =
|
val latencyColor =
|
||||||
@@ -624,15 +609,9 @@ private fun ProxyLatencyBadge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberBounceBlockingNestedScrollConnection(
|
private fun rememberBounceBlockingNestedScrollConnection(lazyListState: LazyListState): NestedScrollConnection = remember(lazyListState) {
|
||||||
lazyListState: LazyListState
|
|
||||||
): NestedScrollConnection = remember(lazyListState) {
|
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset {
|
|
||||||
// Only block upward scroll (y < 0) at bottom to prevent sheet expansion
|
// Only block upward scroll (y < 0) at bottom to prevent sheet expansion
|
||||||
// Allow downward scroll (y > 0) at top to let sheet collapse
|
// Allow downward scroll (y > 0) at top to let sheet collapse
|
||||||
return if (available.y < 0) available else Offset.Zero
|
return if (available.y < 0) available else Offset.Zero
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -42,9 +43,7 @@ import androidx.compose.material3.ModalBottomSheet
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -52,6 +51,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -16,7 +17,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -29,11 +29,7 @@ import io.nekohasekai.sfa.compose.util.ProfileIcons
|
|||||||
import io.nekohasekai.sfa.database.Profile
|
import io.nekohasekai.sfa.database.Profile
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileSelectorButton(
|
fun ProfileSelectorButton(selectedProfile: Profile?, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
selectedProfile: Profile?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier.fillMaxWidth().height(48.dp),
|
modifier = modifier.fillMaxWidth().height(48.dp),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -21,33 +22,30 @@ import androidx.compose.material.icons.filled.AccessTime
|
|||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Cloud
|
import androidx.compose.material.icons.filled.Cloud
|
||||||
|
import androidx.compose.material.icons.filled.DataObject
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.IosShare
|
import androidx.compose.material.icons.filled.IosShare
|
||||||
import androidx.compose.material.icons.filled.QrCode2
|
import androidx.compose.material.icons.filled.QrCode2
|
||||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.DataObject
|
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material.icons.outlined.CreateNewFolder
|
import androidx.compose.material.icons.outlined.CreateNewFolder
|
||||||
import androidx.compose.material.icons.outlined.Description
|
import androidx.compose.material.icons.outlined.Description
|
||||||
import androidx.compose.material.icons.outlined.FileUpload
|
import androidx.compose.material.icons.outlined.FileUpload
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -55,6 +53,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -64,11 +63,11 @@ import io.nekohasekai.libbox.Libbox
|
|||||||
import io.nekohasekai.libbox.ProfileContent
|
import io.nekohasekai.libbox.ProfileContent
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.compose.component.qr.QRCodeDialog
|
import io.nekohasekai.sfa.compose.component.qr.QRCodeDialog
|
||||||
import io.nekohasekai.sfa.compose.component.qr.QRScanSheet
|
|
||||||
import io.nekohasekai.sfa.compose.component.qr.QRSDialog
|
import io.nekohasekai.sfa.compose.component.qr.QRSDialog
|
||||||
|
import io.nekohasekai.sfa.compose.component.qr.QRScanSheet
|
||||||
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
|
import io.nekohasekai.sfa.compose.navigation.NewProfileArgs
|
||||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
|
||||||
import io.nekohasekai.sfa.compose.screen.configuration.ProfileImportHandler
|
import io.nekohasekai.sfa.compose.screen.configuration.ProfileImportHandler
|
||||||
|
import io.nekohasekai.sfa.compose.screen.qrscan.QRScanResult
|
||||||
import io.nekohasekai.sfa.compose.util.QRCodeGenerator
|
import io.nekohasekai.sfa.compose.util.QRCodeGenerator
|
||||||
import io.nekohasekai.sfa.compose.util.RelativeTimeFormatter
|
import io.nekohasekai.sfa.compose.util.RelativeTimeFormatter
|
||||||
import io.nekohasekai.sfa.database.Profile
|
import io.nekohasekai.sfa.database.Profile
|
||||||
|
|||||||
@@ -23,20 +23,15 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SystemProxyCard(
|
fun SystemProxyCard(enabled: Boolean, isSwitching: Boolean, onToggle: (Boolean) -> Unit, modifier: Modifier = Modifier) {
|
||||||
enabled: Boolean,
|
|
||||||
isSwitching: Boolean,
|
|
||||||
onToggle: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -24,20 +24,15 @@ import io.nekohasekai.sfa.R
|
|||||||
import io.nekohasekai.sfa.compose.LineChart
|
import io.nekohasekai.sfa.compose.LineChart
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UploadTrafficCard(
|
fun UploadTrafficCard(uplink: String, uplinkTotal: String, uplinkHistory: List<Float>, modifier: Modifier = Modifier) {
|
||||||
uplink: String,
|
|
||||||
uplinkTotal: String,
|
|
||||||
uplinkHistory: List<Float>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.constant.Status
|
|
||||||
import io.nekohasekai.sfa.compose.model.Group
|
import io.nekohasekai.sfa.compose.model.Group
|
||||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||||
|
import io.nekohasekai.sfa.constant.Status
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupsScreen(
|
fun GroupsScreen(
|
||||||
@@ -121,12 +121,12 @@ fun GroupsScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
PaddingValues(
|
PaddingValues(
|
||||||
start = 16.dp,
|
start = 16.dp,
|
||||||
end = 16.dp,
|
end = 16.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = 16.dp,
|
bottom = 16.dp,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
@@ -254,17 +254,17 @@ private fun ProxyGroupCard(
|
|||||||
imageVector = Icons.Default.ExpandMore,
|
imageVector = Icons.Default.ExpandMore,
|
||||||
contentDescription = if (isExpanded) collapseContentDescription else expandContentDescription,
|
contentDescription = if (isExpanded) collapseContentDescription else expandContentDescription,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.graphicsLayer { rotationZ = rotationAngle },
|
.graphicsLayer { rotationZ = rotationAngle },
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,12 +294,7 @@ private fun ProxyGroupCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyItemsList(
|
private fun ProxyItemsList(items: List<GroupItem>, selectedTag: String, isSelectable: Boolean, onItemSelected: (String) -> Unit) {
|
||||||
items: List<GroupItem>,
|
|
||||||
selectedTag: String,
|
|
||||||
isSelectable: Boolean,
|
|
||||||
onItemSelected: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
// Cache the chunked items to avoid re-chunking on every recomposition
|
// Cache the chunked items to avoid re-chunking on every recomposition
|
||||||
val itemsPerRow = 2
|
val itemsPerRow = 2
|
||||||
val chunkedItems =
|
val chunkedItems =
|
||||||
@@ -310,9 +305,9 @@ private fun ProxyItemsList(
|
|||||||
// Use Column with Rows for better control over item sizing
|
// Use Column with Rows for better control over item sizing
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
chunkedItems.forEach { rowItems ->
|
chunkedItems.forEach { rowItems ->
|
||||||
@@ -344,13 +339,7 @@ private fun ProxyItemsList(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyChip(
|
private fun ProxyChip(item: GroupItem, isSelected: Boolean, isSelectable: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
item: GroupItem,
|
|
||||||
isSelected: Boolean,
|
|
||||||
isSelectable: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
// Use simpler, faster animations
|
// Use simpler, faster animations
|
||||||
val animatedElevation by animateFloatAsState(
|
val animatedElevation by animateFloatAsState(
|
||||||
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
|
targetValue = if (isSelected) 6.dp.value else 1.dp.value,
|
||||||
@@ -369,18 +358,18 @@ private fun ProxyChip(
|
|||||||
androidx.compose.foundation.BorderStroke(
|
androidx.compose.foundation.BorderStroke(
|
||||||
width = if (isSelected) 2.dp else 1.dp,
|
width = if (isSelected) 2.dp else 1.dp,
|
||||||
color =
|
color =
|
||||||
when {
|
when {
|
||||||
isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
isSelected -> MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
||||||
else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
|
else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val content: @Composable () -> Unit = {
|
val content: @Composable () -> Unit = {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -394,11 +383,11 @@ private fun ProxyChip(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -414,11 +403,11 @@ private fun ProxyChip(
|
|||||||
text = Libbox.proxyDisplayType(item.type),
|
text = Libbox.proxyDisplayType(item.type),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Latency
|
// Latency
|
||||||
@@ -460,11 +449,7 @@ private fun ProxyChip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProxyLatencyBadge(
|
private fun ProxyLatencyBadge(delay: Int, isSelected: Boolean, modifier: Modifier = Modifier) {
|
||||||
delay: Int,
|
|
||||||
isSelected: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
// Direct color calculation without animation for better performance
|
// Direct color calculation without animation for better performance
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val latencyColor =
|
val latencyColor =
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import io.nekohasekai.libbox.Libbox
|
|||||||
import io.nekohasekai.libbox.OutboundGroup
|
import io.nekohasekai.libbox.OutboundGroup
|
||||||
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
||||||
import io.nekohasekai.sfa.compose.base.ScreenEvent
|
import io.nekohasekai.sfa.compose.base.ScreenEvent
|
||||||
import io.nekohasekai.sfa.constant.Status
|
|
||||||
import io.nekohasekai.sfa.compose.model.Group
|
import io.nekohasekai.sfa.compose.model.Group
|
||||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||||
import io.nekohasekai.sfa.compose.model.toList
|
import io.nekohasekai.sfa.compose.model.toList
|
||||||
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||||
import io.nekohasekai.sfa.utils.CommandClient
|
import io.nekohasekai.sfa.utils.CommandClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -28,9 +28,9 @@ sealed class GroupsEvent : ScreenEvent {
|
|||||||
data class GroupSelected(val groupTag: String, val itemTag: String) : GroupsEvent()
|
data class GroupSelected(val groupTag: String, val itemTag: String) : GroupsEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupsViewModel(
|
class GroupsViewModel(private val sharedCommandClient: CommandClient? = null) :
|
||||||
private val sharedCommandClient: CommandClient? = null,
|
BaseViewModel<GroupsUiState, GroupsEvent>(),
|
||||||
) : BaseViewModel<GroupsUiState, GroupsEvent>(), CommandClient.Handler {
|
CommandClient.Handler {
|
||||||
private val commandClient: CommandClient
|
private val commandClient: CommandClient
|
||||||
private val isUsingSharedClient: Boolean
|
private val isUsingSharedClient: Boolean
|
||||||
|
|
||||||
@@ -154,10 +154,7 @@ class GroupsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectGroupItem(
|
fun selectGroupItem(groupTag: String, itemTag: String) {
|
||||||
groupTag: String,
|
|
||||||
itemTag: String,
|
|
||||||
) {
|
|
||||||
// Check if this is actually a different selection
|
// Check if this is actually a different selection
|
||||||
val currentGroup = uiState.value.groups.find { it.tag == groupTag }
|
val currentGroup = uiState.value.groups.find { it.tag == groupTag }
|
||||||
if (currentGroup?.selected == itemTag) {
|
if (currentGroup?.selected == itemTag) {
|
||||||
@@ -175,13 +172,13 @@ class GroupsViewModel(
|
|||||||
updateState {
|
updateState {
|
||||||
copy(
|
copy(
|
||||||
groups =
|
groups =
|
||||||
groups.map { group ->
|
groups.map { group ->
|
||||||
if (group.tag == groupTag) {
|
if (group.tag == groupTag) {
|
||||||
group.copy(selected = itemTag)
|
group.copy(selected = itemTag)
|
||||||
} else {
|
} else {
|
||||||
group
|
group
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showCloseConnectionsSnackbar = true,
|
showCloseConnectionsSnackbar = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import java.util.LinkedList
|
|||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
abstract class BaseLogViewModel : ViewModel(), LogViewerViewModel {
|
abstract class BaseLogViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
LogViewerViewModel {
|
||||||
protected val _uiState = MutableStateFlow(LogUiState())
|
protected val _uiState = MutableStateFlow(LogUiState())
|
||||||
override val uiState: StateFlow<LogUiState> = _uiState.asStateFlow()
|
override val uiState: StateFlow<LogUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -119,9 +121,7 @@ abstract class BaseLogViewModel : ViewModel(), LogViewerViewModel {
|
|||||||
.joinToString("\n")
|
.joinToString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllLogsText(): String {
|
override fun getAllLogsText(): String = _uiState.value.logs.joinToString("\n") { AnsiColorUtils.stripAnsi(it.entry.message) }
|
||||||
return _uiState.value.logs.joinToString("\n") { AnsiColorUtils.stripAnsi(it.entry.message) }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun updateDisplayedLogs() {
|
protected fun updateDisplayedLogs() {
|
||||||
val currentState = _uiState.value
|
val currentState = _uiState.value
|
||||||
|
|||||||
@@ -3,16 +3,9 @@ package io.nekohasekai.sfa.compose.screen.log
|
|||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
|
|
||||||
data class LogEntryData(
|
data class LogEntryData(val level: LogLevel, val message: String)
|
||||||
val level: LogLevel,
|
|
||||||
val message: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ProcessedLogEntry(
|
data class ProcessedLogEntry(val id: Long, val entry: LogEntryData, val annotatedString: AnnotatedString)
|
||||||
val id: Long,
|
|
||||||
val entry: LogEntryData,
|
|
||||||
val annotatedString: AnnotatedString,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class LogLevel(val label: String, val priority: Int) {
|
enum class LogLevel(val label: String, val priority: Int) {
|
||||||
Default("Default", 7),
|
Default("Default", 7),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.log
|
package io.nekohasekai.sfa.compose.screen.log
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.os.Build
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -144,17 +144,17 @@ fun LogScreen(
|
|||||||
IconButton(onClick = { resolvedViewModel.togglePause() }) {
|
IconButton(onClick = { resolvedViewModel.togglePause() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (uiState.isPaused) {
|
if (uiState.isPaused) {
|
||||||
Icons.Default.PlayArrow
|
Icons.Default.PlayArrow
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.Pause
|
Icons.Default.Pause
|
||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (uiState.isPaused) {
|
if (uiState.isPaused) {
|
||||||
stringResource(R.string.content_description_resume_logs)
|
stringResource(R.string.content_description_resume_logs)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.content_description_pause_logs)
|
stringResource(R.string.content_description_pause_logs)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,23 +162,23 @@ fun LogScreen(
|
|||||||
IconButton(onClick = { resolvedViewModel.toggleSearch() }) {
|
IconButton(onClick = { resolvedViewModel.toggleSearch() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (uiState.isSearchActive) {
|
if (uiState.isSearchActive) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.Search
|
Icons.Default.Search
|
||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (uiState.isSearchActive) {
|
if (uiState.isSearchActive) {
|
||||||
stringResource(R.string.content_description_collapse_search)
|
stringResource(R.string.content_description_collapse_search)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.content_description_search_logs)
|
stringResource(R.string.content_description_search_logs)
|
||||||
},
|
},
|
||||||
tint =
|
tint =
|
||||||
if (uiState.isSearchActive) {
|
if (uiState.isSearchActive) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSurface
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,9 +281,9 @@ fun LogScreen(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -298,10 +298,10 @@ fun LogScreen(
|
|||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.selected_count,
|
R.string.selected_count,
|
||||||
uiState.selectedLogIndices.size,
|
uiState.selectedLogIndices.size,
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
@@ -343,18 +343,18 @@ fun LogScreen(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.filter_label,
|
R.string.filter_label,
|
||||||
uiState.filterLogLevel.label,
|
uiState.filterLogLevel.label,
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -375,19 +375,19 @@ fun LogScreen(
|
|||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = uiState.isSearchActive,
|
visible = uiState.isSearchActive,
|
||||||
enter =
|
enter =
|
||||||
expandVertically(
|
expandVertically(
|
||||||
|
animationSpec = tween(300),
|
||||||
|
) +
|
||||||
|
fadeIn(
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
) +
|
),
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(300),
|
|
||||||
),
|
|
||||||
exit =
|
exit =
|
||||||
shrinkVertically(
|
shrinkVertically(
|
||||||
|
animationSpec = tween(300),
|
||||||
|
) +
|
||||||
|
fadeOut(
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
) +
|
),
|
||||||
fadeOut(
|
|
||||||
animationSpec = tween(300),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -405,10 +405,10 @@ fun LogScreen(
|
|||||||
value = uiState.searchQuery,
|
value = uiState.searchQuery,
|
||||||
onValueChange = { resolvedViewModel.updateSearchQuery(it) },
|
onValueChange = { resolvedViewModel.updateSearchQuery(it) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
placeholder = { Text(stringResource(R.string.search_logs_placeholder)) },
|
placeholder = { Text(stringResource(R.string.search_logs_placeholder)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -429,11 +429,11 @@ fun LogScreen(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions =
|
keyboardActions =
|
||||||
KeyboardActions(
|
KeyboardActions(
|
||||||
onSearch = {
|
onSearch = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,12 +495,12 @@ fun LogScreen(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
PaddingValues(
|
PaddingValues(
|
||||||
start = 8.dp,
|
start = 8.dp,
|
||||||
end = 8.dp,
|
end = 8.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = bottomPadding,
|
bottom = bottomPadding,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
@@ -532,9 +532,9 @@ fun LogScreen(
|
|||||||
// Options Menu - Material 3 style
|
// Options Menu - Material 3 style
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(end = 8.dp),
|
.padding(end = 8.dp),
|
||||||
) {
|
) {
|
||||||
var expandedLogLevel by remember { mutableStateOf(false) }
|
var expandedLogLevel by remember { mutableStateOf(false) }
|
||||||
var expandedSave by remember { mutableStateOf(false) }
|
var expandedSave by remember { mutableStateOf(false) }
|
||||||
@@ -595,11 +595,11 @@ fun LogScreen(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (expandedLogLevel) {
|
if (expandedLogLevel) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -620,23 +620,23 @@ fun LogScreen(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (uiState.filterLogLevel == level) {
|
if (uiState.filterLogLevel == level) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (uiState.filterLogLevel == level) {
|
if (uiState.filterLogLevel == level) {
|
||||||
stringResource(R.string.group_selected_title)
|
stringResource(R.string.group_selected_title)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
tint =
|
tint =
|
||||||
if (uiState.filterLogLevel == level) {
|
if (uiState.filterLogLevel == level) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -665,11 +665,11 @@ fun LogScreen(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (expandedSave) {
|
if (expandedSave) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -841,9 +841,9 @@ fun LogScreen(
|
|||||||
val fabEndPadding = if (isTablet) 20.dp else 16.dp
|
val fabEndPadding = if (isTablet) 20.dp else 16.dp
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(bottom = fabBottomPadding, end = fabEndPadding, top = 16.dp),
|
.padding(bottom = fabBottomPadding, end = fabEndPadding, top = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
// Scroll to bottom FAB
|
// Scroll to bottom FAB
|
||||||
@@ -880,34 +880,34 @@ fun LogItem(
|
|||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(4.dp),
|
shape = RoundedCornerShape(4.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (isSelected) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
border =
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
CardDefaults.outlinedCardBorder().copy(
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
width = 2.dp,
|
|
||||||
brush =
|
|
||||||
androidx.compose.ui.graphics.SolidColor(
|
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
null
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
border =
|
||||||
|
if (isSelected) {
|
||||||
|
CardDefaults.outlinedCardBorder().copy(
|
||||||
|
width = 2.dp,
|
||||||
|
brush =
|
||||||
|
androidx.compose.ui.graphics.SolidColor(
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -917,13 +917,13 @@ fun LogItem(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isSelected) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank,
|
imageVector = if (isSelected) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
stringResource(R.string.group_selected_title)
|
stringResource(R.string.group_selected_title)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.not_selected,
|
R.string.not_selected,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 4.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 4.dp),
|
||||||
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -931,14 +931,14 @@ fun LogItem(
|
|||||||
Text(
|
Text(
|
||||||
text = annotatedString,
|
text = annotatedString,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(
|
.padding(
|
||||||
start = if (isSelectionMode) 4.dp else 12.dp,
|
start = if (isSelectionMode) 4.dp else 12.dp,
|
||||||
end = 12.dp,
|
end = 12.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = 8.dp,
|
bottom = 8.dp,
|
||||||
),
|
),
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
lineHeight = 18.sp,
|
lineHeight = 18.sp,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
|
||||||
class LogViewModel : BaseLogViewModel(), CommandClient.Handler {
|
class LogViewModel :
|
||||||
|
BaseLogViewModel(),
|
||||||
|
CommandClient.Handler {
|
||||||
companion object {
|
companion object {
|
||||||
private val maxLines = 3000
|
private val maxLines = 3000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import android.content.pm.PackageManager
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -53,13 +53,13 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.database.Settings
|
|
||||||
import io.nekohasekai.sfa.ktx.clipboardText
|
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
|
||||||
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
|
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
|
||||||
import io.nekohasekai.sfa.compose.shared.PackageCache
|
import io.nekohasekai.sfa.compose.shared.PackageCache
|
||||||
import io.nekohasekai.sfa.compose.shared.SortMode
|
import io.nekohasekai.sfa.compose.shared.SortMode
|
||||||
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
|
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
|
import io.nekohasekai.sfa.ktx.clipboardText
|
||||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
||||||
@@ -68,11 +68,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
private data class LoadResult(val packages: List<PackageCache>, val selectedUids: Set<Int>)
|
||||||
private data class LoadResult(
|
|
||||||
val packages: List<PackageCache>,
|
|
||||||
val selectedUids: Set<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
private const val VPN_SERVICE_PERMISSION = "android.permission.BIND_VPN_SERVICE"
|
private const val VPN_SERVICE_PERMISSION = "android.permission.BIND_VPN_SERVICE"
|
||||||
|
|
||||||
@@ -126,10 +122,11 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
|||||||
val hasManagement = permissions.any { it in managementPermissions }
|
val hasManagement = permissions.any { it in managementPermissions }
|
||||||
val isSelf = packageCache.packageName == context.packageName
|
val isSelf = packageCache.packageName == context.packageName
|
||||||
val hasVpnService =
|
val hasVpnService =
|
||||||
!isSelf && (
|
!isSelf &&
|
||||||
permissions.any { it == VPN_SERVICE_PERMISSION } ||
|
(
|
||||||
packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true
|
permissions.any { it == VPN_SERVICE_PERMISSION } ||
|
||||||
)
|
packageCache.info.services?.any { it.permission == VPN_SERVICE_PERMISSION } == true
|
||||||
|
)
|
||||||
return when {
|
return when {
|
||||||
hasManagement && hasVpnService -> RiskCategory.BOTH
|
hasManagement && hasVpnService -> RiskCategory.BOTH
|
||||||
hasManagement -> RiskCategory.MANAGEMENT_APP
|
hasManagement -> RiskCategory.MANAGEMENT_APP
|
||||||
@@ -138,11 +135,9 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildPackageList(newUids: Set<Int>): Set<String> {
|
fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
|
||||||
return newUids.mapNotNull { uid ->
|
packages.find { it.uid == uid }?.packageName
|
||||||
packages.find { it.uid == uid }?.packageName
|
}.toSet()
|
||||||
}.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCurrentPackages(filterQuery: String) {
|
fun updateCurrentPackages(filterQuery: String) {
|
||||||
currentPackages =
|
currentPackages =
|
||||||
@@ -443,10 +438,10 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,10 +487,10 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
|||||||
updateCurrentPackages(it)
|
updateCurrentPackages(it)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
placeholder = { Text(stringResource(R.string.search)) },
|
placeholder = { Text(stringResource(R.string.search)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -524,10 +519,10 @@ fun PrivilegeSettingsManageScreen(onBack: () -> Unit) {
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
androidx.compose.foundation.layout.PaddingValues(
|
androidx.compose.foundation.layout.PaddingValues(
|
||||||
horizontal = 16.dp,
|
horizontal = 16.dp,
|
||||||
vertical = 12.dp,
|
vertical = 12.dp,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
items(currentPackages, key = { it.packageName }) { packageCache ->
|
items(currentPackages, key = { it.packageName }) { packageCache ->
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -38,11 +38,7 @@ data class EditProfileContentUiState(
|
|||||||
val profileName: String = "", // Add profile name
|
val profileName: String = "", // Add profile name
|
||||||
)
|
)
|
||||||
|
|
||||||
class EditProfileContentViewModel(
|
class EditProfileContentViewModel(private val profileId: Long, initialProfileName: String = "", initialIsReadOnly: Boolean = false) : ViewModel() {
|
||||||
private val profileId: Long,
|
|
||||||
initialProfileName: String = "",
|
|
||||||
initialIsReadOnly: Boolean = false,
|
|
||||||
) : ViewModel() {
|
|
||||||
private val _uiState =
|
private val _uiState =
|
||||||
MutableStateFlow(
|
MutableStateFlow(
|
||||||
EditProfileContentUiState(
|
EditProfileContentUiState(
|
||||||
@@ -56,10 +52,7 @@ class EditProfileContentViewModel(
|
|||||||
private var editor: ManualScrollTextProcessor? = null
|
private var editor: ManualScrollTextProcessor? = null
|
||||||
private var configCheckJob: Job? = null
|
private var configCheckJob: Job? = null
|
||||||
|
|
||||||
fun setEditor(
|
fun setEditor(textProcessor: ManualScrollTextProcessor, isReadOnly: Boolean = false) {
|
||||||
textProcessor: ManualScrollTextProcessor,
|
|
||||||
isReadOnly: Boolean = false,
|
|
||||||
) {
|
|
||||||
val isNewEditor = editor != textProcessor
|
val isNewEditor = editor != textProcessor
|
||||||
editor = textProcessor
|
editor = textProcessor
|
||||||
textProcessor.resumeAutoScroll()
|
textProcessor.resumeAutoScroll()
|
||||||
@@ -89,18 +82,12 @@ class EditProfileContentViewModel(
|
|||||||
// Customize text selection to remove Cut and Paste options
|
// Customize text selection to remove Cut and Paste options
|
||||||
textProcessor.customSelectionActionModeCallback =
|
textProcessor.customSelectionActionModeCallback =
|
||||||
object : android.view.ActionMode.Callback {
|
object : android.view.ActionMode.Callback {
|
||||||
override fun onCreateActionMode(
|
override fun onCreateActionMode(mode: android.view.ActionMode?, menu: android.view.Menu?): Boolean {
|
||||||
mode: android.view.ActionMode?,
|
|
||||||
menu: android.view.Menu?,
|
|
||||||
): Boolean {
|
|
||||||
// Allow the action mode to be created
|
// Allow the action mode to be created
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(
|
override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: android.view.Menu?): Boolean {
|
||||||
mode: android.view.ActionMode?,
|
|
||||||
menu: android.view.Menu?,
|
|
||||||
): Boolean {
|
|
||||||
// Remove editing-related menu items, keep only Copy and Select All
|
// Remove editing-related menu items, keep only Copy and Select All
|
||||||
menu?.let { m ->
|
menu?.let { m ->
|
||||||
// Remove all editing-related items
|
// Remove all editing-related items
|
||||||
@@ -116,10 +103,7 @@ class EditProfileContentViewModel(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(
|
override fun onActionItemClicked(mode: android.view.ActionMode?, item: android.view.MenuItem?): Boolean {
|
||||||
mode: android.view.ActionMode?,
|
|
||||||
item: android.view.MenuItem?,
|
|
||||||
): Boolean {
|
|
||||||
// Let the default implementation handle allowed actions (copy, select all)
|
// Let the default implementation handle allowed actions (copy, select all)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditProfileRoute(
|
fun EditProfileRoute(profileId: Long, onNavigateBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
profileId: Long,
|
|
||||||
onNavigateBack: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
if (profileId == -1L) {
|
if (profileId == -1L) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
@@ -84,12 +80,12 @@ fun EditProfileRoute(
|
|||||||
composable(
|
composable(
|
||||||
route = "icon_selection/{currentIconId}",
|
route = "icon_selection/{currentIconId}",
|
||||||
arguments =
|
arguments =
|
||||||
listOf(
|
listOf(
|
||||||
navArgument("currentIconId") {
|
navArgument("currentIconId") {
|
||||||
type = NavType.StringType
|
type = NavType.StringType
|
||||||
nullable = true
|
nullable = true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
slideIntoContainer(
|
slideIntoContainer(
|
||||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||||
@@ -134,16 +130,16 @@ fun EditProfileRoute(
|
|||||||
composable(
|
composable(
|
||||||
route = "edit_content/{profileName}/{isReadOnly}",
|
route = "edit_content/{profileName}/{isReadOnly}",
|
||||||
arguments =
|
arguments =
|
||||||
listOf(
|
listOf(
|
||||||
navArgument("profileName") {
|
navArgument("profileName") {
|
||||||
type = NavType.StringType
|
type = NavType.StringType
|
||||||
defaultValue = ""
|
defaultValue = ""
|
||||||
},
|
},
|
||||||
navArgument("isReadOnly") {
|
navArgument("isReadOnly") {
|
||||||
type = NavType.BoolType
|
type = NavType.BoolType
|
||||||
defaultValue = false
|
defaultValue = false
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
slideIntoContainer(
|
slideIntoContainer(
|
||||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
@@ -61,6 +60,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -179,9 +179,9 @@ fun EditProfileScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,324 +199,324 @@ fun EditProfileScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
// Progress indicator at top (only for initial loading)
|
// Progress indicator at top (only for initial loading)
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uiState.isLoading) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp)
|
||||||
|
.padding(bottom = bottomBarPadding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
// Basic Information Card
|
||||||
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
colors =
|
||||||
}
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||||
if (!uiState.isLoading) {
|
),
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(16.dp)
|
|
||||||
.padding(bottom = bottomBarPadding),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
) {
|
||||||
// Basic Information Card
|
Column(
|
||||||
Card(
|
modifier = Modifier.padding(16.dp),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Text(
|
||||||
modifier = Modifier.padding(16.dp),
|
text = stringResource(R.string.basic_information),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
style = MaterialTheme.typography.titleSmall,
|
||||||
) {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
Text(
|
)
|
||||||
text = stringResource(R.string.basic_information),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = uiState.name,
|
value = uiState.name,
|
||||||
onValueChange = viewModel::updateName,
|
onValueChange = viewModel::updateName,
|
||||||
label = { Text(stringResource(R.string.profile_name)) },
|
label = { Text(stringResource(R.string.profile_name)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(vertical = 4.dp),
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Icon selection with Material You style
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.icon),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(bottom = 4.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.clickable { viewModel.showIconDialog() },
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
// Display current icon
|
|
||||||
val currentIcon =
|
|
||||||
ProfileIcons.getIconById(uiState.icon)
|
|
||||||
?: Icons.AutoMirrored.Filled.InsertDriveFile
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
imageVector = currentIcon,
|
|
||||||
contentDescription = stringResource(R.string.profile_icon),
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
uiState.icon?.let { iconId ->
|
|
||||||
MaterialIconsLibrary.getAllIcons()
|
|
||||||
.find { it.id == iconId }?.label
|
|
||||||
} ?: stringResource(R.string.default_text),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
|
||||||
contentDescription = stringResource(R.string.select_icon),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote Profile Options
|
|
||||||
if (uiState.profileType == TypedProfile.Type.Remote) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
singleLine = true,
|
||||||
CardDefaults.cardColors(
|
)
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.CloudDownload,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.remote_configuration),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
)
|
|
||||||
uiState.lastUpdated?.let { lastUpdated ->
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
stringResource(
|
|
||||||
R.string.last_updated_format,
|
|
||||||
RelativeTimeFormatter.format(
|
|
||||||
context,
|
|
||||||
lastUpdated,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update button in top-right corner
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.updateRemoteProfile() },
|
|
||||||
enabled = !uiState.isUpdating && !uiState.showUpdateSuccess,
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
uiState.isUpdating -> {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
uiState.showUpdateSuccess -> {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Check,
|
|
||||||
contentDescription = stringResource(R.string.success),
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Update,
|
|
||||||
contentDescription = stringResource(R.string.profile_update),
|
|
||||||
tint = MaterialTheme.colorScheme.tertiary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
HorizontalDivider(
|
||||||
value = uiState.remoteUrl,
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
onValueChange = viewModel::updateRemoteUrl,
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||||
label = { Text(stringResource(R.string.profile_url)) },
|
)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
// Icon selection with Material You style
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.icon),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.clickable { viewModel.showIconDialog() },
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
// Display current icon
|
||||||
|
val currentIcon =
|
||||||
|
ProfileIcons.getIconById(uiState.icon)
|
||||||
|
?: Icons.AutoMirrored.Filled.InsertDriveFile
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = currentIcon,
|
||||||
|
contentDescription = stringResource(R.string.profile_icon),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider()
|
Text(
|
||||||
|
text =
|
||||||
|
uiState.icon?.let { iconId ->
|
||||||
|
MaterialIconsLibrary.getAllIcons()
|
||||||
|
.find { it.id == iconId }?.label
|
||||||
|
} ?: stringResource(R.string.default_text),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
// Auto Update Toggle
|
Icon(
|
||||||
Row(
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
contentDescription = stringResource(R.string.select_icon),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.size(20.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
) {
|
)
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.profile_auto_update),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
)
|
|
||||||
Switch(
|
|
||||||
checked = uiState.autoUpdate,
|
|
||||||
onCheckedChange = viewModel::updateAutoUpdate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(visible = uiState.autoUpdate) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.autoUpdateInterval.toString(),
|
|
||||||
onValueChange = viewModel::updateAutoUpdateInterval,
|
|
||||||
label = { Text(stringResource(R.string.profile_auto_update_interval)) },
|
|
||||||
supportingText = {
|
|
||||||
uiState.autoUpdateIntervalError?.let { error ->
|
|
||||||
Text(
|
|
||||||
text = error,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
|
||||||
} ?: Text(stringResource(R.string.profile_auto_update_interval_minimum_hint))
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
isError = uiState.autoUpdateIntervalError != null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Content Card (for both Local and Remote profiles) - placed at the end
|
// Remote Profile Options
|
||||||
|
if (uiState.profileType == TypedProfile.Type.Remote) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f),
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CloudDownload,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.remote_configuration),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
)
|
||||||
|
uiState.lastUpdated?.let { lastUpdated ->
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.last_updated_format,
|
||||||
|
RelativeTimeFormatter.format(
|
||||||
|
context,
|
||||||
|
lastUpdated,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update button in top-right corner
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.updateRemoteProfile() },
|
||||||
|
enabled = !uiState.isUpdating && !uiState.showUpdateSuccess,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
uiState.isUpdating -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiState.showUpdateSuccess -> {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(R.string.success),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Update,
|
||||||
|
contentDescription = stringResource(R.string.profile_update),
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.remoteUrl,
|
||||||
|
onValueChange = viewModel::updateRemoteUrl,
|
||||||
|
label = { Text(stringResource(R.string.profile_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Auto Update Toggle
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.InsertDriveFile,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.content),
|
text = stringResource(R.string.profile_auto_update),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
)
|
||||||
|
Switch(
|
||||||
|
checked = uiState.autoUpdate,
|
||||||
|
onCheckedChange = viewModel::updateAutoUpdate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON Editor/Viewer option
|
AnimatedVisibility(visible = uiState.autoUpdate) {
|
||||||
Surface(
|
OutlinedTextField(
|
||||||
modifier =
|
value = uiState.autoUpdateInterval.toString(),
|
||||||
Modifier
|
onValueChange = viewModel::updateAutoUpdateInterval,
|
||||||
.fillMaxWidth()
|
label = { Text(stringResource(R.string.profile_auto_update_interval)) },
|
||||||
.clip(RoundedCornerShape(12.dp))
|
supportingText = {
|
||||||
.clickable {
|
uiState.autoUpdateIntervalError?.let { error ->
|
||||||
onNavigateToEditContent(
|
Text(
|
||||||
uiState.name,
|
text = error,
|
||||||
uiState.profileType == TypedProfile.Type.Remote,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
},
|
} ?: Text(stringResource(R.string.profile_auto_update_interval_minimum_hint))
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
},
|
||||||
shape = RoundedCornerShape(12.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
isError = uiState.autoUpdateIntervalError != null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content Card (for both Local and Remote profiles) - placed at the end
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.content),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON Editor/Viewer option
|
||||||
|
Surface(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.clickable {
|
||||||
|
onNavigateToEditContent(
|
||||||
|
uiState.name,
|
||||||
|
uiState.profileType == TypedProfile.Type.Remote,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Icon(
|
||||||
modifier =
|
imageVector = Icons.Default.Code,
|
||||||
Modifier
|
contentDescription = null,
|
||||||
.fillMaxWidth()
|
modifier = Modifier.size(24.dp),
|
||||||
.padding(16.dp),
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
)
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
Text(
|
||||||
) {
|
text =
|
||||||
Icon(
|
if (uiState.profileType == TypedProfile.Type.Remote) {
|
||||||
imageVector = Icons.Default.Code,
|
stringResource(R.string.json_viewer)
|
||||||
contentDescription = null,
|
} else {
|
||||||
modifier = Modifier.size(24.dp),
|
stringResource(R.string.json_editor)
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
},
|
||||||
)
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
Text(
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
text =
|
modifier = Modifier.weight(1f),
|
||||||
if (uiState.profileType == TypedProfile.Type.Remote) {
|
)
|
||||||
stringResource(R.string.json_viewer)
|
Icon(
|
||||||
} else {
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
stringResource(R.string.json_editor)
|
contentDescription = null,
|
||||||
},
|
modifier = Modifier.size(20.dp),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
)
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = uiState.hasChanges,
|
visible = uiState.hasChanges,
|
||||||
enter = fadeIn() + expandVertically(),
|
enter = fadeIn() + expandVertically(),
|
||||||
@@ -530,10 +530,10 @@ fun EditProfileScreen(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.saveChanges() },
|
onClick = { viewModel.saveChanges() },
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
state.copy(
|
state.copy(
|
||||||
name = name,
|
name = name,
|
||||||
hasChanges =
|
hasChanges =
|
||||||
checkHasChanges(
|
checkHasChanges(
|
||||||
state.copy(name = name),
|
state.copy(name = name),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,9 +121,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
state.copy(
|
state.copy(
|
||||||
icon = icon,
|
icon = icon,
|
||||||
hasChanges =
|
hasChanges =
|
||||||
checkHasChanges(
|
checkHasChanges(
|
||||||
state.copy(icon = icon),
|
state.copy(icon = icon),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,9 +141,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
state.copy(
|
state.copy(
|
||||||
remoteUrl = url,
|
remoteUrl = url,
|
||||||
hasChanges =
|
hasChanges =
|
||||||
checkHasChanges(
|
checkHasChanges(
|
||||||
state.copy(remoteUrl = url),
|
state.copy(remoteUrl = url),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,9 +153,9 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
state.copy(
|
state.copy(
|
||||||
autoUpdate = enabled,
|
autoUpdate = enabled,
|
||||||
hasChanges =
|
hasChanges =
|
||||||
checkHasChanges(
|
checkHasChanges(
|
||||||
state.copy(autoUpdate = enabled),
|
state.copy(autoUpdate = enabled),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,22 +174,20 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
autoUpdateInterval = intValue,
|
autoUpdateInterval = intValue,
|
||||||
autoUpdateIntervalError = error,
|
autoUpdateIntervalError = error,
|
||||||
hasChanges =
|
hasChanges =
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
checkHasChanges(state.copy(autoUpdateInterval = intValue))
|
checkHasChanges(state.copy(autoUpdateInterval = intValue))
|
||||||
} else {
|
} else {
|
||||||
state.hasChanges
|
state.hasChanges
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkHasChanges(state: EditProfileUiState): Boolean {
|
private fun checkHasChanges(state: EditProfileUiState): Boolean = state.name != state.originalName ||
|
||||||
return state.name != state.originalName ||
|
state.icon != state.originalIcon ||
|
||||||
state.icon != state.originalIcon ||
|
state.remoteUrl != state.originalRemoteUrl ||
|
||||||
state.remoteUrl != state.originalRemoteUrl ||
|
state.autoUpdate != state.originalAutoUpdate ||
|
||||||
state.autoUpdate != state.originalAutoUpdate ||
|
state.autoUpdateInterval != state.originalAutoUpdateInterval
|
||||||
state.autoUpdateInterval != state.originalAutoUpdateInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveChanges() {
|
fun saveChanges() {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
@@ -343,10 +341,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveExportToUri(
|
fun saveExportToUri(context: Context, uri: Uri) {
|
||||||
context: Context,
|
|
||||||
uri: Uri,
|
|
||||||
) {
|
|
||||||
val content = pendingExportContent ?: return
|
val content = pendingExportContent ?: return
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -37,17 +37,13 @@ import io.nekohasekai.sfa.compose.util.ProfileIcon
|
|||||||
import io.nekohasekai.sfa.compose.util.ProfileIcons
|
import io.nekohasekai.sfa.compose.util.ProfileIcons
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IconSelectionDialog(
|
fun IconSelectionDialog(currentIconId: String?, onIconSelected: (String?) -> Unit, onDismiss: () -> Unit) {
|
||||||
currentIconId: String?,
|
|
||||||
onIconSelected: (String?) -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 500.dp),
|
.heightIn(max = 500.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -65,9 +61,9 @@ fun IconSelectionDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
// Add option to remove custom icon (use default)
|
// Add option to remove custom icon (use default)
|
||||||
item {
|
item {
|
||||||
@@ -110,40 +106,35 @@ fun IconSelectionDialog(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun IconOption(
|
private fun IconOption(icon: ProfileIcon?, label: String, isSelected: Boolean, onClick: () -> Unit) {
|
||||||
icon: ProfileIcon?,
|
|
||||||
label: String,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable { onClick() },
|
.clickable { onClick() },
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (isSelected) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
border =
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
CardDefaults.outlinedCardBorder()
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
} else {
|
} else {
|
||||||
null
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
border =
|
||||||
|
if (isSelected) {
|
||||||
|
CardDefaults.outlinedCardBorder()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@@ -153,11 +144,11 @@ private fun IconOption(
|
|||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
tint =
|
tint =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Default icon indicator
|
// Default icon indicator
|
||||||
@@ -165,11 +156,11 @@ private fun IconOption(
|
|||||||
text = stringResource(R.string.auto),
|
text = stringResource(R.string.auto),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,11 +170,11 @@ private fun IconOption(
|
|||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -60,6 +59,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -74,11 +74,7 @@ import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun IconSelectionScreen(
|
fun IconSelectionScreen(currentIconId: String?, onIconSelected: (String?) -> Unit, onNavigateBack: () -> Unit) {
|
||||||
currentIconId: String?,
|
|
||||||
onIconSelected: (String?) -> Unit,
|
|
||||||
onNavigateBack: () -> Unit,
|
|
||||||
) {
|
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var selectedCategory by remember { mutableStateOf<String?>(null) }
|
var selectedCategory by remember { mutableStateOf<String?>(null) }
|
||||||
var viewMode by remember { mutableStateOf(IconViewMode.CATEGORIES) }
|
var viewMode by remember { mutableStateOf(IconViewMode.CATEGORIES) }
|
||||||
@@ -126,26 +122,26 @@ fun IconSelectionScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Search,
|
imageVector = Icons.Default.Search,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (isSearchActive) {
|
if (isSearchActive) {
|
||||||
stringResource(R.string.close_search)
|
stringResource(R.string.close_search)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.search_icons,
|
R.string.search_icons,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
tint =
|
tint =
|
||||||
if (isSearchActive) {
|
if (isSearchActive) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSurface
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,27 +163,27 @@ fun IconSelectionScreen(
|
|||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(bottom = bottomBarPadding),
|
.padding(bottom = bottomBarPadding),
|
||||||
) {
|
) {
|
||||||
// Show search bar with animation
|
// Show search bar with animation
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isSearchActive,
|
visible = isSearchActive,
|
||||||
enter =
|
enter =
|
||||||
expandVertically(
|
expandVertically(
|
||||||
|
animationSpec = tween(300),
|
||||||
|
) +
|
||||||
|
fadeIn(
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
) +
|
),
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(300),
|
|
||||||
),
|
|
||||||
exit =
|
exit =
|
||||||
shrinkVertically(
|
shrinkVertically(
|
||||||
|
animationSpec = tween(300),
|
||||||
|
) +
|
||||||
|
fadeOut(
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
) +
|
),
|
||||||
fadeOut(
|
|
||||||
animationSpec = tween(300),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -212,10 +208,10 @@ fun IconSelectionScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
placeholder = { Text(stringResource(R.string.search_icons_placeholder)) },
|
placeholder = { Text(stringResource(R.string.search_icons_placeholder)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -240,20 +236,20 @@ fun IconSelectionScreen(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions =
|
keyboardActions =
|
||||||
KeyboardActions(
|
KeyboardActions(
|
||||||
onSearch = {
|
onSearch = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
// View mode tabs (only show when not searching)
|
// View mode tabs (only show when not searching)
|
||||||
AnimatedVisibility(visible = searchQuery.isEmpty()) {
|
AnimatedVisibility(visible = searchQuery.isEmpty()) {
|
||||||
@@ -269,11 +265,11 @@ fun IconSelectionScreen(
|
|||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.categories)) },
|
label = { Text(stringResource(R.string.categories)) },
|
||||||
leadingIcon =
|
leadingIcon =
|
||||||
if (viewMode == IconViewMode.CATEGORIES && selectedCategory == null) {
|
if (viewMode == IconViewMode.CATEGORIES && selectedCategory == null) {
|
||||||
{ Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) }
|
{ Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
@@ -284,11 +280,11 @@ fun IconSelectionScreen(
|
|||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.all_icons)) },
|
label = { Text(stringResource(R.string.all_icons)) },
|
||||||
leadingIcon =
|
leadingIcon =
|
||||||
if (viewMode == IconViewMode.ALL) {
|
if (viewMode == IconViewMode.ALL) {
|
||||||
{ Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) }
|
{ Icon(Icons.Default.Check, contentDescription = null, Modifier.size(16.dp)) }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
@@ -329,9 +325,9 @@ fun IconSelectionScreen(
|
|||||||
// Main content area
|
// Main content area
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
// Search results
|
// Search results
|
||||||
@@ -387,21 +383,21 @@ fun IconSelectionScreen(
|
|||||||
currentIcon?.let { (id, icon) ->
|
currentIcon?.let { (id, icon) ->
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f),
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -415,10 +411,10 @@ fun IconSelectionScreen(
|
|||||||
val iconInfo = MaterialIconsLibrary.getAllIcons().find { it.id == id }
|
val iconInfo = MaterialIconsLibrary.getAllIcons().find { it.id == id }
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.current_icon_format,
|
R.string.current_icon_format,
|
||||||
iconInfo?.label ?: id,
|
iconInfo?.label ?: id,
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
MaterialIconsLibrary.getCategoryForIcon(id)?.let { category ->
|
MaterialIconsLibrary.getCategoryForIcon(id)?.let { category ->
|
||||||
@@ -436,11 +432,7 @@ fun IconSelectionScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CategoryList(
|
private fun CategoryList(categories: List<IconCategory>, currentIconId: String?, onCategoryClick: (IconCategory) -> Unit) {
|
||||||
categories: List<IconCategory>,
|
|
||||||
currentIconId: String?,
|
|
||||||
onCategoryClick: (IconCategory) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
@@ -456,29 +448,25 @@ private fun CategoryList(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun CategoryCard(
|
private fun CategoryCard(category: IconCategory, hasSelectedIcon: Boolean, onClick: () -> Unit) {
|
||||||
category: IconCategory,
|
|
||||||
hasSelectedIcon: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (hasSelectedIcon) {
|
if (hasSelectedIcon) {
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -517,11 +505,7 @@ private fun CategoryCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IconGrid(
|
private fun IconGrid(icons: List<ProfileIcon>, currentIconId: String?, onIconClick: (ProfileIcon) -> Unit) {
|
||||||
icons: List<ProfileIcon>,
|
|
||||||
currentIconId: String?,
|
|
||||||
onIconClick: (ProfileIcon) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 72.dp),
|
columns = GridCells.Adaptive(minSize = 72.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -539,38 +523,34 @@ private fun IconGrid(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun IconGridItem(
|
private fun IconGridItem(icon: ProfileIcon, isSelected: Boolean, onClick: () -> Unit) {
|
||||||
icon: ProfileIcon,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (isSelected) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
border =
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
CardDefaults.outlinedCardBorder()
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
} else {
|
} else {
|
||||||
null
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
border =
|
||||||
|
if (isSelected) {
|
||||||
|
CardDefaults.outlinedCardBorder()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@@ -579,11 +559,11 @@ private fun IconGridItem(
|
|||||||
contentDescription = icon.label,
|
contentDescription = icon.label,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
tint =
|
tint =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@@ -592,11 +572,11 @@ private fun IconGridItem(
|
|||||||
text = icon.label,
|
text = icon.label,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color =
|
color =
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
@@ -610,9 +590,9 @@ private fun IconGridItem(
|
|||||||
private fun EmptySearchResult(query: String) {
|
private fun EmptySearchResult(query: String) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(32.dp),
|
.padding(32.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -4,129 +4,129 @@ import android.content.Context;
|
|||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.ViewConfiguration;
|
import android.view.ViewConfiguration;
|
||||||
|
|
||||||
import com.blacksquircle.ui.editorkit.widget.TextProcessor;
|
import com.blacksquircle.ui.editorkit.widget.TextProcessor;
|
||||||
|
|
||||||
public class ManualScrollTextProcessor extends TextProcessor {
|
public class ManualScrollTextProcessor extends TextProcessor {
|
||||||
|
|
||||||
private final int touchSlop;
|
private final int touchSlop;
|
||||||
private boolean allowCursorAutoScroll = true;
|
private boolean allowCursorAutoScroll = true;
|
||||||
private float downX;
|
private float downX;
|
||||||
private float downY;
|
private float downY;
|
||||||
private boolean userDragging;
|
private boolean userDragging;
|
||||||
private int downSelectionStart = -1;
|
private int downSelectionStart = -1;
|
||||||
private int downSelectionEnd = -1;
|
private int downSelectionEnd = -1;
|
||||||
private boolean restoringSelection;
|
private boolean restoringSelection;
|
||||||
|
|
||||||
public ManualScrollTextProcessor(Context context) {
|
public ManualScrollTextProcessor(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManualScrollTextProcessor(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, android.R.attr.autoCompleteTextViewStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManualScrollTextProcessor(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resumeAutoScroll() {
|
||||||
|
allowCursorAutoScroll = true;
|
||||||
|
userDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean bringPointIntoView(int offset) {
|
||||||
|
if (allowCursorAutoScroll) {
|
||||||
|
return super.bringPointIntoView(offset);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public ManualScrollTextProcessor(Context context, AttributeSet attrs) {
|
@Override
|
||||||
this(context, attrs, android.R.attr.autoCompleteTextViewStyle);
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
}
|
int action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
public ManualScrollTextProcessor(Context context, AttributeSet attrs, int defStyleAttr) {
|
case MotionEvent.ACTION_DOWN:
|
||||||
super(context, attrs, defStyleAttr);
|
downX = event.getX();
|
||||||
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
downY = event.getY();
|
||||||
}
|
|
||||||
|
|
||||||
public void resumeAutoScroll() {
|
|
||||||
allowCursorAutoScroll = true;
|
|
||||||
userDragging = false;
|
userDragging = false;
|
||||||
|
restoringSelection = false;
|
||||||
|
downSelectionStart = getSelectionStart();
|
||||||
|
downSelectionEnd = getSelectionEnd();
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (!userDragging) {
|
||||||
|
float dx = Math.abs(event.getX() - downX);
|
||||||
|
float dy = Math.abs(event.getY() - downY);
|
||||||
|
if (dx > touchSlop || dy > touchSlop) {
|
||||||
|
userDragging = true;
|
||||||
|
allowCursorAutoScroll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
boolean handled = super.onTouchEvent(event);
|
||||||
public boolean bringPointIntoView(int offset) {
|
|
||||||
if (allowCursorAutoScroll) {
|
|
||||||
return super.bringPointIntoView(offset);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
|
||||||
int action = event.getActionMasked();
|
|
||||||
switch (action) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
downX = event.getX();
|
|
||||||
downY = event.getY();
|
|
||||||
userDragging = false;
|
|
||||||
restoringSelection = false;
|
|
||||||
downSelectionStart = getSelectionStart();
|
|
||||||
downSelectionEnd = getSelectionEnd();
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
if (!userDragging) {
|
|
||||||
float dx = Math.abs(event.getX() - downX);
|
|
||||||
float dy = Math.abs(event.getY() - downY);
|
|
||||||
if (dx > touchSlop || dy > touchSlop) {
|
|
||||||
userDragging = true;
|
|
||||||
allowCursorAutoScroll = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean handled = super.onTouchEvent(event);
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
if (userDragging) {
|
|
||||||
maybeRestoreSelection();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
if (userDragging) {
|
|
||||||
maybeRestoreSelection();
|
|
||||||
} else {
|
|
||||||
resumeAutoScroll();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeRestoreSelection() {
|
|
||||||
if (userDragging && !restoringSelection) {
|
|
||||||
int selStart = getSelectionStart();
|
|
||||||
int selEnd = getSelectionEnd();
|
|
||||||
if (selStart != downSelectionStart || selEnd != downSelectionEnd) {
|
|
||||||
restoringSelection = true;
|
|
||||||
int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart;
|
|
||||||
setSelection(downSelectionStart, targetEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
|
||||||
if (restoringSelection) {
|
|
||||||
restoringSelection = false;
|
|
||||||
super.onSelectionChanged(selStart, selEnd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
if (userDragging) {
|
if (userDragging) {
|
||||||
if (downSelectionStart >= 0 && (selStart != downSelectionStart || selEnd != downSelectionEnd)) {
|
maybeRestoreSelection();
|
||||||
restoringSelection = true;
|
|
||||||
int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart;
|
|
||||||
setSelection(downSelectionStart, targetEnd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
downSelectionStart = selStart;
|
case MotionEvent.ACTION_UP:
|
||||||
downSelectionEnd = selEnd;
|
case MotionEvent.ACTION_CANCEL:
|
||||||
super.onSelectionChanged(selStart, selEnd);
|
if (userDragging) {
|
||||||
|
maybeRestoreSelection();
|
||||||
|
} else {
|
||||||
|
resumeAutoScroll();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeRestoreSelection() {
|
||||||
|
if (userDragging && !restoringSelection) {
|
||||||
|
int selStart = getSelectionStart();
|
||||||
|
int selEnd = getSelectionEnd();
|
||||||
|
if (selStart != downSelectionStart || selEnd != downSelectionEnd) {
|
||||||
|
restoringSelection = true;
|
||||||
|
int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart;
|
||||||
|
setSelection(downSelectionStart, targetEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||||
|
if (restoringSelection) {
|
||||||
|
restoringSelection = false;
|
||||||
|
super.onSelectionChanged(selStart, selEnd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDragging) {
|
||||||
|
if (downSelectionStart >= 0
|
||||||
|
&& (selStart != downSelectionStart || selEnd != downSelectionEnd)) {
|
||||||
|
restoringSelection = true;
|
||||||
|
int targetEnd = downSelectionEnd >= 0 ? downSelectionEnd : downSelectionStart;
|
||||||
|
setSelection(downSelectionStart, targetEnd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downSelectionStart = selStart;
|
||||||
|
downSelectionEnd = selEnd;
|
||||||
|
super.onSelectionChanged(selStart, selEnd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -54,7 +54,6 @@ import androidx.compose.material3.LinearProgressIndicator
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -66,9 +65,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -79,13 +76,13 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.database.Settings
|
|
||||||
import io.nekohasekai.sfa.ktx.clipboardText
|
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
|
||||||
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
|
import io.nekohasekai.sfa.compose.shared.AppSelectionCard
|
||||||
import io.nekohasekai.sfa.compose.shared.PackageCache
|
import io.nekohasekai.sfa.compose.shared.PackageCache
|
||||||
import io.nekohasekai.sfa.compose.shared.SortMode
|
import io.nekohasekai.sfa.compose.shared.SortMode
|
||||||
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
|
import io.nekohasekai.sfa.compose.shared.buildDisplayPackages
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
|
import io.nekohasekai.sfa.ktx.clipboardText
|
||||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -97,16 +94,9 @@ import java.io.File
|
|||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
private data class LoadResult(
|
private data class LoadResult(val proxyMode: Int, val packages: List<PackageCache>, val selectedUids: Set<Int>)
|
||||||
val proxyMode: Int,
|
|
||||||
val packages: List<PackageCache>,
|
|
||||||
val selectedUids: Set<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class ScanProgress(
|
private data class ScanProgress(val current: Int, val max: Int)
|
||||||
val current: Int,
|
|
||||||
val max: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
private sealed class ScanResult {
|
private sealed class ScanResult {
|
||||||
data object Empty : ScanResult()
|
data object Empty : ScanResult()
|
||||||
@@ -139,11 +129,9 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
|||||||
var scanProgress by remember { mutableStateOf<ScanProgress?>(null) }
|
var scanProgress by remember { mutableStateOf<ScanProgress?>(null) }
|
||||||
var scanResult by remember { mutableStateOf<ScanResult?>(null) }
|
var scanResult by remember { mutableStateOf<ScanResult?>(null) }
|
||||||
|
|
||||||
fun buildPackageList(newUids: Set<Int>): Set<String> {
|
fun buildPackageList(newUids: Set<Int>): Set<String> = newUids.mapNotNull { uid ->
|
||||||
return newUids.mapNotNull { uid ->
|
packages.find { it.uid == uid }?.packageName
|
||||||
packages.find { it.uid == uid }?.packageName
|
}.toSet()
|
||||||
}.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCurrentPackages(filterQuery: String) {
|
fun updateCurrentPackages(filterQuery: String) {
|
||||||
currentPackages =
|
currentPackages =
|
||||||
@@ -411,10 +399,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,11 +423,11 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||||
stringResource(R.string.per_app_proxy_mode_include_description)
|
stringResource(R.string.per_app_proxy_mode_include_description)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.per_app_proxy_mode_exclude_description)
|
stringResource(R.string.per_app_proxy_mode_exclude_description)
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
@@ -465,10 +453,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
|||||||
updateCurrentPackages(it)
|
updateCurrentPackages(it)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
placeholder = { Text(stringResource(R.string.search)) },
|
placeholder = { Text(stringResource(R.string.search)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -497,10 +485,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
androidx.compose.foundation.layout.PaddingValues(
|
androidx.compose.foundation.layout.PaddingValues(
|
||||||
horizontal = 16.dp,
|
horizontal = 16.dp,
|
||||||
vertical = 12.dp,
|
vertical = 12.dp,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
items(currentPackages, key = { it.packageName }) { packageCache ->
|
items(currentPackages, key = { it.packageName }) { packageCache ->
|
||||||
@@ -609,10 +597,10 @@ fun PerAppProxyScreen(onBack: () -> Unit) {
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 360.dp)
|
.heightIn(max = 360.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = dialogContent,
|
text = dialogContent,
|
||||||
@@ -722,11 +710,11 @@ private fun PerAppProxyMenus(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showModeMenu) {
|
if (showModeMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -742,18 +730,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
if (proxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -768,18 +756,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
|
if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
|
if (proxyMode == Settings.PER_APP_PROXY_EXCLUDE) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -799,11 +787,11 @@ private fun PerAppProxyMenus(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showSortMenu) {
|
if (showSortMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -819,18 +807,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (sortMode == SortMode.NAME) {
|
if (sortMode == SortMode.NAME) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (sortMode == SortMode.NAME) {
|
if (sortMode == SortMode.NAME) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -845,18 +833,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (sortMode == SortMode.PACKAGE_NAME) {
|
if (sortMode == SortMode.PACKAGE_NAME) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (sortMode == SortMode.PACKAGE_NAME) {
|
if (sortMode == SortMode.PACKAGE_NAME) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -871,18 +859,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (sortMode == SortMode.UID) {
|
if (sortMode == SortMode.UID) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (sortMode == SortMode.UID) {
|
if (sortMode == SortMode.UID) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -897,18 +885,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (sortMode == SortMode.INSTALL_TIME) {
|
if (sortMode == SortMode.INSTALL_TIME) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (sortMode == SortMode.INSTALL_TIME) {
|
if (sortMode == SortMode.INSTALL_TIME) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -923,18 +911,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (sortMode == SortMode.UPDATE_TIME) {
|
if (sortMode == SortMode.UPDATE_TIME) {
|
||||||
Icons.Default.RadioButtonChecked
|
Icons.Default.RadioButtonChecked
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (sortMode == SortMode.UPDATE_TIME) {
|
if (sortMode == SortMode.UPDATE_TIME) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -949,18 +937,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (sortReverse) {
|
if (sortReverse) {
|
||||||
Icons.Default.Check
|
Icons.Default.Check
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (sortReverse) {
|
if (sortReverse) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -980,11 +968,11 @@ private fun PerAppProxyMenus(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showFilterMenu) {
|
if (showFilterMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1000,18 +988,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (hideSystemApps) {
|
if (hideSystemApps) {
|
||||||
Icons.Default.Check
|
Icons.Default.Check
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (hideSystemApps) {
|
if (hideSystemApps) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1026,18 +1014,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (hideOfflineApps) {
|
if (hideOfflineApps) {
|
||||||
Icons.Default.Check
|
Icons.Default.Check
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (hideOfflineApps) {
|
if (hideOfflineApps) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1052,18 +1040,18 @@ private fun PerAppProxyMenus(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (hideDisabledApps) {
|
if (hideDisabledApps) {
|
||||||
Icons.Default.Check
|
Icons.Default.Check
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.RadioButtonUnchecked
|
Icons.Default.RadioButtonUnchecked
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint =
|
tint =
|
||||||
if (hideDisabledApps) {
|
if (hideDisabledApps) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 24.dp),
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1083,11 +1071,11 @@ private fun PerAppProxyMenus(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showSelectMenu) {
|
if (showSelectMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1140,11 +1128,11 @@ private fun PerAppProxyMenus(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showBackupMenu) {
|
if (showBackupMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1197,11 +1185,11 @@ private fun PerAppProxyMenus(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showScanMenu) {
|
if (showScanMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1331,7 +1319,7 @@ object PerAppProxyScanner {
|
|||||||
if (!(
|
if (!(
|
||||||
packageEntry.name.startsWith("classes") &&
|
packageEntry.name.startsWith("classes") &&
|
||||||
packageEntry.name.endsWith(".dex")
|
packageEntry.name.endsWith(".dex")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ data class QRCodeCropArea(
|
|||||||
)
|
)
|
||||||
|
|
||||||
object QRCodeSmartCrop {
|
object QRCodeSmartCrop {
|
||||||
fun findCropArea(
|
fun findCropArea(yData: ByteArray, width: Int, height: Int, rotationDegrees: Int): QRCodeCropArea? {
|
||||||
yData: ByteArray,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
rotationDegrees: Int,
|
|
||||||
): QRCodeCropArea? {
|
|
||||||
val minDim = min(width, height)
|
val minDim = min(width, height)
|
||||||
if (minDim <= 0) return null
|
if (minDim <= 0) return null
|
||||||
|
|
||||||
@@ -94,14 +89,7 @@ object QRCodeSmartCrop {
|
|||||||
return bestArea
|
return bestArea
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class CropComponent(
|
private data class CropComponent(val minX: Int, val minY: Int, val maxX: Int, val maxY: Int, val count: Int, val score: Float)
|
||||||
val minX: Int,
|
|
||||||
val minY: Int,
|
|
||||||
val maxX: Int,
|
|
||||||
val maxY: Int,
|
|
||||||
val count: Int,
|
|
||||||
val score: Float,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun findBestComponent(
|
private fun findBestComponent(
|
||||||
yData: ByteArray,
|
yData: ByteArray,
|
||||||
@@ -233,13 +221,7 @@ object QRCodeSmartCrop {
|
|||||||
return best
|
return best
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildCropArea(
|
private fun buildCropArea(component: CropComponent, step: Int, width: Int, height: Int, rotationDegrees: Int): QRCodeCropArea? {
|
||||||
component: CropComponent,
|
|
||||||
step: Int,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
rotationDegrees: Int,
|
|
||||||
): QRCodeCropArea? {
|
|
||||||
val left = component.minX * step
|
val left = component.minX * step
|
||||||
val top = component.minY * step
|
val top = component.minY * step
|
||||||
val right = min(width, (component.maxX + 1) * step)
|
val right = min(width, (component.maxX + 1) * step)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class QRScanViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
vendorAnalyzerAvailable = vendorAnalyzer != null,
|
vendorAnalyzerAvailable = vendorAnalyzer != null,
|
||||||
useVendorAnalyzer = vendorAnalyzer != null
|
useVendorAnalyzer = vendorAnalyzer != null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ class QRScanViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
cameraSelector,
|
cameraSelector,
|
||||||
preview,
|
preview,
|
||||||
analysis
|
analysis,
|
||||||
)
|
)
|
||||||
val maxZoom = camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f
|
val maxZoom = camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f
|
||||||
_uiState.update { it.copy(maxZoomRatio = maxZoom, zoomRatio = 1f) }
|
_uiState.update { it.copy(maxZoomRatio = maxZoom, zoomRatio = 1f) }
|
||||||
|
|||||||
@@ -109,12 +109,10 @@ class ZxingQRCodeAnalyzer(
|
|||||||
return yData
|
return yData
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryDecode(bitmap: BinaryBitmap): Result? {
|
private fun tryDecode(bitmap: BinaryBitmap): Result? = try {
|
||||||
return try {
|
qrCodeReader.decode(bitmap)
|
||||||
qrCodeReader.decode(bitmap)
|
} catch (_: NotFoundException) {
|
||||||
} catch (_: NotFoundException) {
|
qrCodeReader.reset()
|
||||||
qrCodeReader.reset()
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.provider.Settings as AndroidSettings
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -29,7 +28,6 @@ import androidx.compose.material.icons.outlined.NewReleases
|
|||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.SystemUpdateAlt
|
import androidx.compose.material.icons.outlined.SystemUpdateAlt
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -42,6 +40,7 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -53,8 +52,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -62,22 +59,25 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import io.nekohasekai.sfa.BuildConfig
|
import io.nekohasekai.sfa.BuildConfig
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.database.Settings
|
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
|
||||||
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||||
import io.nekohasekai.sfa.update.UpdateState
|
import io.nekohasekai.sfa.update.UpdateState
|
||||||
import io.nekohasekai.sfa.update.UpdateTrack
|
import io.nekohasekai.sfa.update.UpdateTrack
|
||||||
import io.nekohasekai.sfa.vendor.Vendor
|
|
||||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||||
|
import io.nekohasekai.sfa.vendor.Vendor
|
||||||
import io.nekohasekai.sfa.xposed.XposedActivation
|
import io.nekohasekai.sfa.xposed.XposedActivation
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import android.provider.Settings as AndroidSettings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -136,10 +136,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
isMethodAvailable = success
|
isMethodAvailable = success
|
||||||
silentInstallError = if (success) {
|
silentInstallError = if (success) {
|
||||||
null
|
null
|
||||||
} else when (silentInstallMethod) {
|
} else {
|
||||||
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
when (silentInstallMethod) {
|
||||||
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
||||||
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
|
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
||||||
|
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,10 +226,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
isMethodAvailable = success
|
isMethodAvailable = success
|
||||||
silentInstallError = if (success) {
|
silentInstallError = if (success) {
|
||||||
null
|
null
|
||||||
} else when (method) {
|
} else {
|
||||||
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
when (method) {
|
||||||
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
||||||
else -> context.getString(R.string.silent_install_verify_failed, method)
|
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
||||||
|
else -> context.getString(R.string.silent_install_verify_failed, method)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -259,22 +263,22 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
// Info Card
|
// Info Card
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -303,12 +307,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,13 +328,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
val updateItemCount =
|
val updateItemCount =
|
||||||
@@ -393,12 +397,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
updateItemModifier()
|
updateItemModifier()
|
||||||
.clickable { showTrackDialog = true },
|
.clickable { showTrackDialog = true },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,9 +433,9 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = updateItemModifier(),
|
modifier = updateItemModifier(),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (Vendor.supportsSilentInstall()) {
|
if (Vendor.supportsSilentInstall()) {
|
||||||
@@ -478,10 +482,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
isMethodAvailable = success
|
isMethodAvailable = success
|
||||||
silentInstallError = if (success) {
|
silentInstallError = if (success) {
|
||||||
null
|
null
|
||||||
} else when (silentInstallMethod) {
|
} else {
|
||||||
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
when (silentInstallMethod) {
|
||||||
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
"PACKAGE_INSTALLER" -> context.getString(R.string.package_installer_not_available)
|
||||||
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
|
"SHIZUKU" -> context.getString(R.string.shizuku_not_available)
|
||||||
|
else -> context.getString(R.string.silent_install_verify_failed, silentInstallMethod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -493,9 +499,9 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = updateItemModifier(),
|
modifier = updateItemModifier(),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (silentInstallEnabled) {
|
if (silentInstallEnabled) {
|
||||||
@@ -510,11 +516,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
Text(
|
Text(
|
||||||
if (xposedActivated) {
|
if (xposedActivated) {
|
||||||
stringResource(R.string.install_method_root)
|
stringResource(R.string.install_method_root)
|
||||||
} else when (silentInstallMethod) {
|
} else {
|
||||||
"PACKAGE_INSTALLER" -> stringResource(R.string.install_method_package_installer)
|
when (silentInstallMethod) {
|
||||||
"SHIZUKU" -> stringResource(R.string.install_method_shizuku)
|
"PACKAGE_INSTALLER" -> stringResource(R.string.install_method_package_installer)
|
||||||
"ROOT" -> stringResource(R.string.install_method_root)
|
"SHIZUKU" -> stringResource(R.string.install_method_shizuku)
|
||||||
else -> silentInstallMethod
|
"ROOT" -> stringResource(R.string.install_method_root)
|
||||||
|
else -> silentInstallMethod
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
@@ -527,12 +535,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
updateItemModifier()
|
updateItemModifier()
|
||||||
.let { if (!xposedActivated) it.clickable { showInstallMethodMenu = true } else it },
|
.let { if (!xposedActivated) it.clickable { showInstallMethodMenu = true } else it },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (silentInstallMethod == "SHIZUKU" && !isMethodAvailable) {
|
if (silentInstallMethod == "SHIZUKU" && !isMethodAvailable) {
|
||||||
@@ -558,15 +566,15 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
updateItemModifier()
|
updateItemModifier()
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/"))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/"))
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,18 +601,18 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
updateItemModifier()
|
updateItemModifier()
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
Uri.parse("package:${context.packageName}")
|
Uri.parse("package:${context.packageName}"),
|
||||||
)
|
)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -646,9 +654,9 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = updateItemModifier(),
|
modifier = updateItemModifier(),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -666,13 +674,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -698,40 +706,40 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(
|
.clip(
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
} else {
|
} else {
|
||||||
RoundedCornerShape(12.dp)
|
RoundedCornerShape(12.dp)
|
||||||
},
|
|
||||||
)
|
|
||||||
.clickable(enabled = !isChecking) {
|
|
||||||
if (hasUpdate && updateInfo != null) {
|
|
||||||
showUpdateAvailableDialog = true
|
|
||||||
} else {
|
|
||||||
scope.launch {
|
|
||||||
UpdateState.isChecking.value = true
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val result = Vendor.checkUpdateAsync()
|
|
||||||
UpdateState.setUpdate(result)
|
|
||||||
if (result == null) {
|
|
||||||
showErrorDialog = R.string.no_updates_available
|
|
||||||
}
|
|
||||||
} catch (_: UpdateCheckException.TrackNotSupported) {
|
|
||||||
showErrorDialog = R.string.update_track_not_supported
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UpdateState.isChecking.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
.clickable(enabled = !isChecking) {
|
||||||
|
if (hasUpdate && updateInfo != null) {
|
||||||
|
showUpdateAvailableDialog = true
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
UpdateState.isChecking.value = true
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val result = Vendor.checkUpdateAsync()
|
||||||
|
UpdateState.setUpdate(result)
|
||||||
|
if (result == null) {
|
||||||
|
showErrorDialog = R.string.no_updates_available
|
||||||
|
}
|
||||||
|
} catch (_: UpdateCheckException.TrackNotSupported) {
|
||||||
|
showErrorDialog = R.string.update_track_not_supported
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateState.isChecking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasUpdate && updateInfo != null) {
|
if (hasUpdate && updateInfo != null) {
|
||||||
@@ -756,15 +764,15 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
showUpdateAvailableDialog = true
|
showUpdateAvailableDialog = true
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -791,11 +799,11 @@ private fun UpdateTrackDialog(
|
|||||||
tracks.forEach { (value, label) ->
|
tracks.forEach { (value, label) ->
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable { onTrackSelected(value) }
|
.clickable { onTrackSelected(value) }
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
@@ -841,11 +849,11 @@ private fun InstallMethodDialog(
|
|||||||
methods.forEach { (value, label) ->
|
methods.forEach { (value, label) ->
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable { onMethodSelected(value) }
|
.clickable { onMethodSelected(value) }
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.settings
|
package io.nekohasekai.sfa.compose.screen.settings
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -11,11 +16,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.DeleteForever
|
import androidx.compose.material.icons.outlined.DeleteForever
|
||||||
@@ -95,22 +95,22 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
// Core Information Card
|
// Core Information Card
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// Version Info
|
// Version Info
|
||||||
@@ -138,9 +138,9 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
|
modifier = Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data Size
|
// Data Size
|
||||||
@@ -167,16 +167,16 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clip(
|
Modifier.clip(
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
bottomStart = 12.dp,
|
bottomStart = 12.dp,
|
||||||
bottomEnd = 12.dp,
|
bottomEnd = 12.dp,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,13 +193,13 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -228,9 +228,9 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,13 +246,13 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
// Working Directory Card
|
// Working Directory Card
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
// Browse
|
// Browse
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -270,15 +270,15 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
openInFileManager(context)
|
openInFileManager(context)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Destroy
|
// Destroy
|
||||||
@@ -298,28 +298,28 @@ fun CoreSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
|
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
|
||||||
filesDir.deleteRecursively()
|
filesDir.deleteRecursively()
|
||||||
filesDir.mkdirs()
|
filesDir.mkdirs()
|
||||||
|
|
||||||
// Recalculate data size
|
// Recalculate data size
|
||||||
val newSize =
|
val newSize =
|
||||||
filesDir.walkTopDown()
|
filesDir.walkTopDown()
|
||||||
.filter { it.isFile }
|
.filter { it.isFile }
|
||||||
.map { it.length() }
|
.map { it.length() }
|
||||||
.sum()
|
.sum()
|
||||||
val formattedSize = Libbox.formatBytes(newSize)
|
val formattedSize = Libbox.formatBytes(newSize)
|
||||||
dataSize = formattedSize
|
dataSize = formattedSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ private fun openInFileManager(context: Context) {
|
|||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.no_file_manager),
|
context.getString(R.string.no_file_manager),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT,
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.settings
|
package io.nekohasekai.sfa.compose.screen.settings
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -61,18 +60,18 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import io.nekohasekai.sfa.R
|
|
||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
||||||
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
|
import io.nekohasekai.sfa.compose.base.SelectableMessageDialog
|
||||||
import io.nekohasekai.sfa.compose.base.UiEvent
|
import io.nekohasekai.sfa.compose.base.UiEvent
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
import io.nekohasekai.sfa.database.Settings
|
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.utils.DetectionResult
|
import io.nekohasekai.sfa.utils.DetectionResult
|
||||||
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
||||||
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
|
||||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||||
|
import io.nekohasekai.sfa.utils.PrivilegeSettingsClient
|
||||||
import io.nekohasekai.sfa.utils.VpnDetectionTest
|
import io.nekohasekai.sfa.utils.VpnDetectionTest
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -122,7 +121,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
var messageDialogMessage by remember { mutableStateOf("") }
|
var messageDialogMessage by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val saveFileLauncher = rememberLauncherForActivityResult(
|
val saveFileLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.CreateDocument("application/zip")
|
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
||||||
) { uri ->
|
) { uri ->
|
||||||
val file = exportedFile
|
val file = exportedFile
|
||||||
if (uri != null && file != null) {
|
if (uri != null && file != null) {
|
||||||
@@ -146,9 +145,9 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
HookStatusClient.refresh()
|
HookStatusClient.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasPendingDowngrade = HookModuleUpdateNotifier.isDowngrade(systemHookStatus)
|
val hasPendingDowngrade = HookModuleUpdateNotifier.isDowngrade(systemHookStatus)
|
||||||
val hasPendingUpdate = HookModuleUpdateNotifier.isUpgrade(systemHookStatus)
|
val hasPendingUpdate = HookModuleUpdateNotifier.isUpgrade(systemHookStatus)
|
||||||
val hasPendingChange = hasPendingDowngrade || hasPendingUpdate
|
val hasPendingChange = hasPendingDowngrade || hasPendingUpdate
|
||||||
androidx.compose.runtime.LaunchedEffect(systemHookStatus) {
|
androidx.compose.runtime.LaunchedEffect(systemHookStatus) {
|
||||||
HookModuleUpdateNotifier.maybeNotify(context, systemHookStatus)
|
HookModuleUpdateNotifier.maybeNotify(context, systemHookStatus)
|
||||||
}
|
}
|
||||||
@@ -231,8 +230,11 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
if (exportError != null) exportError!!
|
if (exportError != null) {
|
||||||
else stringResource(R.string.exporting)
|
exportError!!
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.exporting)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -273,7 +275,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
"${context.packageName}.cache",
|
"${context.packageName}.cache",
|
||||||
file
|
file,
|
||||||
)
|
)
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
type = "application/zip"
|
type = "application/zip"
|
||||||
@@ -283,7 +285,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
context.startActivity(Intent.createChooser(intent, null))
|
context.startActivity(Intent.createChooser(intent, null))
|
||||||
showExportSuccessDialog = false
|
showExportSuccessDialog = false
|
||||||
exportedFile = null
|
exportedFile = null
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.menu_share))
|
Text(stringResource(R.string.menu_share))
|
||||||
}
|
}
|
||||||
@@ -293,7 +295,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
onClick = {
|
onClick = {
|
||||||
val file = exportedFile ?: return@TextButton
|
val file = exportedFile ?: return@TextButton
|
||||||
saveFileLauncher.launch(file.name)
|
saveFileLauncher.launch(file.name)
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.save))
|
Text(stringResource(R.string.save))
|
||||||
}
|
}
|
||||||
@@ -413,11 +415,11 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(logItemShape)
|
.clip(logItemShape)
|
||||||
.clickable {
|
.clickable {
|
||||||
navController.navigate("settings/privilege/logs")
|
navController.navigate("settings/privilege/logs")
|
||||||
},
|
},
|
||||||
colors = ListItemDefaults.colors(
|
colors = ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
@@ -439,42 +441,42 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
val exportBase = File(context.cacheDir, "debug")
|
val exportBase = File(context.cacheDir, "debug")
|
||||||
if (!exportBase.exists()) {
|
if (!exportBase.exists()) {
|
||||||
exportBase.mkdirs()
|
exportBase.mkdirs()
|
||||||
|
}
|
||||||
|
val timestamp =
|
||||||
|
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
val outZip = File(exportBase, "sing-box-lsposed-debug-$timestamp.zip")
|
||||||
|
exportCancelled = false
|
||||||
|
exportError = null
|
||||||
|
showExportProgressDialog = true
|
||||||
|
scope.launch {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
PrivilegeSettingsClient.exportDebugInfo(outZip.absolutePath)
|
||||||
}
|
}
|
||||||
val timestamp =
|
if (exportCancelled) {
|
||||||
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
outZip.delete()
|
||||||
val outZip = File(exportBase, "sing-box-lsposed-debug-${timestamp}.zip")
|
return@launch
|
||||||
exportCancelled = false
|
|
||||||
exportError = null
|
|
||||||
showExportProgressDialog = true
|
|
||||||
scope.launch {
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
|
||||||
PrivilegeSettingsClient.exportDebugInfo(outZip.absolutePath)
|
|
||||||
}
|
|
||||||
if (exportCancelled) {
|
|
||||||
outZip.delete()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
showExportProgressDialog = false
|
|
||||||
val failure = result.error
|
|
||||||
if (failure == null) {
|
|
||||||
exportedFile = outZip
|
|
||||||
showExportSuccessDialog = true
|
|
||||||
} else {
|
|
||||||
messageDialogTitle = context.getString(R.string.error_title)
|
|
||||||
messageDialogMessage = context.getString(
|
|
||||||
R.string.privilege_settings_export_debug_failed,
|
|
||||||
failure
|
|
||||||
)
|
|
||||||
showMessageDialog = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
showExportProgressDialog = false
|
||||||
|
val failure = result.error
|
||||||
|
if (failure == null) {
|
||||||
|
exportedFile = outZip
|
||||||
|
showExportSuccessDialog = true
|
||||||
|
} else {
|
||||||
|
messageDialogTitle = context.getString(R.string.error_title)
|
||||||
|
messageDialogMessage = context.getString(
|
||||||
|
R.string.privilege_settings_export_debug_failed,
|
||||||
|
failure,
|
||||||
|
)
|
||||||
|
showMessageDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = ListItemDefaults.colors(
|
colors = ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
@@ -496,44 +498,44 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val failure = withContext(Dispatchers.IO) {
|
val failure = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val process = Runtime.getRuntime().exec(
|
val process = Runtime.getRuntime().exec(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"su",
|
"su",
|
||||||
"-c",
|
"-c",
|
||||||
"/system/bin/svc power reboot || /system/bin/reboot",
|
"/system/bin/svc power reboot || /system/bin/reboot",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val error = process.errorStream.bufferedReader().use { it.readText().trim() }
|
val error = process.errorStream.bufferedReader().use { it.readText().trim() }
|
||||||
process.inputStream.close()
|
process.inputStream.close()
|
||||||
process.outputStream.close()
|
process.outputStream.close()
|
||||||
process.errorStream.close()
|
process.errorStream.close()
|
||||||
val code = process.waitFor()
|
val code = process.waitFor()
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
error.ifBlank { "exit=$code" }
|
error.ifBlank { "exit=$code" }
|
||||||
}
|
}
|
||||||
}.getOrElse { it.message ?: "unknown" }
|
}.getOrElse { it.message ?: "unknown" }
|
||||||
}
|
|
||||||
if (failure != null) {
|
|
||||||
val message =
|
|
||||||
if (failure == "unknown" || failure.startsWith("exit=")) {
|
|
||||||
context.getString(R.string.root_access_required)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.privilege_module_restart_failed, failure)
|
|
||||||
}
|
|
||||||
messageDialogTitle = context.getString(R.string.error_title)
|
|
||||||
messageDialogMessage = message
|
|
||||||
showMessageDialog = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
if (failure != null) {
|
||||||
|
val message =
|
||||||
|
if (failure == "unknown" || failure.startsWith("exit=")) {
|
||||||
|
context.getString(R.string.root_access_required)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.privilege_module_restart_failed, failure)
|
||||||
|
}
|
||||||
|
messageDialogTitle = context.getString(R.string.error_title)
|
||||||
|
messageDialogMessage = message
|
||||||
|
showMessageDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = ListItemDefaults.colors(
|
colors = ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
@@ -621,7 +623,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
} else {
|
} else {
|
||||||
RoundedCornerShape(12.dp)
|
RoundedCornerShape(12.dp)
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
colors = ListItemDefaults.colors(
|
colors = ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
@@ -662,7 +664,6 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,7 +731,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
} else {
|
} else {
|
||||||
RoundedCornerShape(12.dp)
|
RoundedCornerShape(12.dp)
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
colors = ListItemDefaults.colors(
|
colors = ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
@@ -847,11 +848,7 @@ fun PrivilegeSettingsScreen(navController: NavController, serviceStatus: Status
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelfTestDialog(
|
private fun SelfTestDialog(isRunning: Boolean, result: DetectionResult?, onDismiss: () -> Unit) {
|
||||||
isRunning: Boolean,
|
|
||||||
result: DetectionResult?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
val notDetectedText = stringResource(R.string.privilege_settings_hide_test_not_detected)
|
val notDetectedText = stringResource(R.string.privilege_settings_hide_test_not_detected)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.navigation.NavController
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.bg.RootClient
|
import io.nekohasekai.sfa.bg.RootClient
|
||||||
|
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScanner
|
|
||||||
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
import io.nekohasekai.sfa.vendor.PackageQueryManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -162,22 +162,22 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
// Card 1: Auto Redirect
|
// Card 1: Auto Redirect
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -232,9 +232,9 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,13 +254,13 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// Mode selector (only when privileged query is needed)
|
// Mode selector (only when privileged query is needed)
|
||||||
@@ -272,32 +272,44 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
Text(
|
Text(
|
||||||
stringResource(R.string.per_app_proxy_package_query_mode),
|
stringResource(R.string.per_app_proxy_package_query_mode),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = if (modeEnabled) Color.Unspecified
|
color = if (modeEnabled) {
|
||||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
Color.Unspecified
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
if (useRootMode) "ROOT" else "Shizuku",
|
if (useRootMode) "ROOT" else "Shizuku",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (modeEnabled) {
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Tune,
|
imageVector = Icons.Outlined.Tune,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (modeEnabled) MaterialTheme.colorScheme.primary
|
tint = if (modeEnabled) {
|
||||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
|
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
|
tint = if (modeEnabled) {
|
||||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -355,19 +367,19 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clip(
|
Modifier.clip(
|
||||||
if (showModeSelector) {
|
if (showModeSelector) {
|
||||||
RoundedCornerShape(0.dp)
|
RoundedCornerShape(0.dp)
|
||||||
} else if (perAppProxyEnabled && canUsePerAppProxy) {
|
} else if (perAppProxyEnabled && canUsePerAppProxy) {
|
||||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
} else {
|
} else {
|
||||||
RoundedCornerShape(12.dp)
|
RoundedCornerShape(12.dp)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (perAppProxyEnabled && canUsePerAppProxy) {
|
if (perAppProxyEnabled && canUsePerAppProxy) {
|
||||||
@@ -409,13 +421,13 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable(enabled = manageEnabled) {
|
Modifier.clickable(enabled = manageEnabled) {
|
||||||
navController.navigate("settings/profile_override/manage")
|
navController.navigate("settings/profile_override/manage")
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Managed Mode toggle
|
// Managed Mode toggle
|
||||||
@@ -477,9 +489,9 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)),
|
modifier = Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,7 +613,7 @@ fun ProfileOverrideScreen(navController: NavController) {
|
|||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
R.string.root_access_denied,
|
R.string.root_access_denied,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG,
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +718,7 @@ private suspend fun scanAllChinaApps(): Set<String> = withContext(Dispatchers.De
|
|||||||
val chinaApps = mutableSetOf<String>()
|
val chinaApps = mutableSetOf<String>()
|
||||||
installedPackages.map { packageInfo ->
|
installedPackages.map { packageInfo ->
|
||||||
async {
|
async {
|
||||||
if (PerAppProxyScanner.scanChinaPackage(packageInfo)) {
|
if (PerAppProxyScanner.scanChinaPackage(packageInfo)) {
|
||||||
synchronized(chinaApps) {
|
synchronized(chinaApps) {
|
||||||
chinaApps.add(packageInfo.packageName)
|
chinaApps.add(packageInfo.packageName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ServiceSettingsScreen(
|
fun ServiceSettingsScreen(navController: NavController, serviceConnection: ServiceConnection? = null) {
|
||||||
navController: NavController,
|
|
||||||
serviceConnection: ServiceConnection? = null,
|
|
||||||
) {
|
|
||||||
OverrideTopBar {
|
OverrideTopBar {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.service)) },
|
title = { Text(stringResource(R.string.service)) },
|
||||||
@@ -113,23 +110,23 @@ fun ServiceSettingsScreen(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
// Background Permission Card (only show if battery optimization is not ignored)
|
// Background Permission Card (only show if battery optimization is not ignored)
|
||||||
if (!isBatteryOptimizationIgnored && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (!isBatteryOptimizationIgnored && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f),
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
@@ -193,13 +190,13 @@ fun ServiceSettingsScreen(
|
|||||||
// Options Section
|
// Options Section
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -234,9 +231,9 @@ fun ServiceSettingsScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||||
|
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||||
import androidx.compose.material.icons.outlined.Code
|
import androidx.compose.material.icons.outlined.Code
|
||||||
import androidx.compose.material.icons.outlined.Description
|
import androidx.compose.material.icons.outlined.Description
|
||||||
import androidx.compose.material.icons.outlined.Favorite
|
import androidx.compose.material.icons.outlined.Favorite
|
||||||
import androidx.compose.material.icons.outlined.FilterAlt
|
import androidx.compose.material.icons.outlined.FilterAlt
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.SwapHoriz
|
|
||||||
import androidx.compose.material.icons.outlined.Tune
|
import androidx.compose.material.icons.outlined.Tune
|
||||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -49,15 +48,12 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import io.nekohasekai.sfa.BuildConfig
|
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.update.UpdateState
|
import io.nekohasekai.sfa.update.UpdateState
|
||||||
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
import io.nekohasekai.sfa.utils.HookModuleUpdateNotifier
|
||||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -87,22 +83,22 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
// General Settings Group
|
// General Settings Group
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -125,13 +121,13 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
.clickable { navController.navigate("settings/app") },
|
.clickable { navController.navigate("settings/app") },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -149,12 +145,12 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clickable { navController.navigate("settings/core") },
|
.clickable { navController.navigate("settings/core") },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -178,9 +174,9 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.clickable { navController.navigate("settings/service") },
|
modifier = Modifier.clickable { navController.navigate("settings/service") },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -198,12 +194,12 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clickable { navController.navigate("settings/profile_override") },
|
.clickable { navController.navigate("settings/profile_override") },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -228,13 +224,13 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
.clickable { navController.navigate("settings/privilege") },
|
.clickable { navController.navigate("settings/privilege") },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,13 +245,13 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -280,17 +276,17 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
||||||
intent.data = android.net.Uri.parse("https://sing-box.sagernet.org/")
|
intent.data = android.net.Uri.parse("https://sing-box.sagernet.org/")
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -315,17 +311,17 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
||||||
intent.data =
|
intent.data =
|
||||||
android.net.Uri.parse("https://github.com/SagerNet/sing-box-for-android")
|
android.net.Uri.parse("https://github.com/SagerNet/sing-box-for-android")
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -350,17 +346,17 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
||||||
intent.data = android.net.Uri.parse("https://sekai.icu/sponsors/")
|
intent.data = android.net.Uri.parse("https://sekai.icu/sponsors/")
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,9 +178,9 @@ fun AppSelectionCard(
|
|||||||
modifier = cardModifier,
|
modifier = cardModifier,
|
||||||
shape = cardShape,
|
shape = cardShape,
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier.padding(12.dp),
|
||||||
@@ -236,11 +236,11 @@ fun AppSelectionCard(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showCopyMenu) {
|
if (showCopyMenu) {
|
||||||
Icons.Default.ExpandLess
|
Icons.Default.ExpandLess
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ExpandMore
|
Icons.Default.ExpandMore
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,127 +11,127 @@ val Typography =
|
|||||||
Typography(
|
Typography(
|
||||||
// Display styles
|
// Display styles
|
||||||
displayLarge =
|
displayLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 57.sp,
|
fontSize = 57.sp,
|
||||||
lineHeight = 64.sp,
|
lineHeight = 64.sp,
|
||||||
letterSpacing = (-0.25).sp,
|
letterSpacing = (-0.25).sp,
|
||||||
),
|
),
|
||||||
displayMedium =
|
displayMedium =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 45.sp,
|
fontSize = 45.sp,
|
||||||
lineHeight = 52.sp,
|
lineHeight = 52.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
displaySmall =
|
displaySmall =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 36.sp,
|
fontSize = 36.sp,
|
||||||
lineHeight = 44.sp,
|
lineHeight = 44.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
// Headline styles
|
// Headline styles
|
||||||
headlineLarge =
|
headlineLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 32.sp,
|
fontSize = 32.sp,
|
||||||
lineHeight = 40.sp,
|
lineHeight = 40.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
headlineMedium =
|
headlineMedium =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 28.sp,
|
fontSize = 28.sp,
|
||||||
lineHeight = 36.sp,
|
lineHeight = 36.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
headlineSmall =
|
headlineSmall =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
lineHeight = 32.sp,
|
lineHeight = 32.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
// Title styles
|
// Title styles
|
||||||
titleLarge =
|
titleLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
lineHeight = 28.sp,
|
lineHeight = 28.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
titleMedium =
|
titleMedium =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.15.sp,
|
letterSpacing = 0.15.sp,
|
||||||
),
|
),
|
||||||
titleSmall =
|
titleSmall =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.1.sp,
|
letterSpacing = 0.1.sp,
|
||||||
),
|
),
|
||||||
// Body styles
|
// Body styles
|
||||||
bodyLarge =
|
bodyLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
bodyMedium =
|
bodyMedium =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.25.sp,
|
letterSpacing = 0.25.sp,
|
||||||
),
|
),
|
||||||
bodySmall =
|
bodySmall =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.4.sp,
|
letterSpacing = 0.4.sp,
|
||||||
),
|
),
|
||||||
// Label styles
|
// Label styles
|
||||||
labelLarge =
|
labelLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.1.sp,
|
letterSpacing = 0.1.sp,
|
||||||
),
|
),
|
||||||
labelMedium =
|
labelMedium =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
labelSmall =
|
labelSmall =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,20 +7,12 @@ import androidx.compose.runtime.compositionLocalOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
|
||||||
internal data class TopBarEntry(
|
internal data class TopBarEntry(val key: Any, val content: @Composable () -> Unit)
|
||||||
val key: Any,
|
|
||||||
val content: @Composable () -> Unit,
|
|
||||||
)
|
|
||||||
|
|
||||||
class TopBarController internal constructor(
|
class TopBarController internal constructor(private val state: MutableState<List<TopBarEntry>>) {
|
||||||
private val state: MutableState<List<TopBarEntry>>,
|
|
||||||
) {
|
|
||||||
val current: (@Composable () -> Unit)? get() = state.value.lastOrNull()?.content
|
val current: (@Composable () -> Unit)? get() = state.value.lastOrNull()?.content
|
||||||
|
|
||||||
fun set(
|
fun set(key: Any, content: @Composable () -> Unit) {
|
||||||
key: Any,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
state.value = state.value.filterNot { it.key == key } + TopBarEntry(key, content)
|
state.value = state.value.filterNot { it.key == key } + TopBarEntry(key, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import androidx.compose.material.icons.sharp.*
|
|||||||
import androidx.compose.material.icons.twotone.*
|
import androidx.compose.material.icons.twotone.*
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
data class IconCategory(
|
data class IconCategory(val name: String, val icons: List<ProfileIcon>)
|
||||||
val name: String,
|
|
||||||
val icons: List<ProfileIcon>,
|
|
||||||
)
|
|
||||||
|
|
||||||
object MaterialIconsLibrary {
|
object MaterialIconsLibrary {
|
||||||
val categories =
|
val categories =
|
||||||
@@ -416,20 +413,16 @@ object MaterialIconsLibrary {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getAllIcons(): List<ProfileIcon> {
|
fun getAllIcons(): List<ProfileIcon> = categories.flatMap { it.icons }
|
||||||
return categories.flatMap { it.icons }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIconById(id: String?): ImageVector? {
|
fun getIconById(id: String?): ImageVector? {
|
||||||
if (id == null) return null
|
if (id == null) return null
|
||||||
return getAllIcons().find { it.id == id }?.icon
|
return getAllIcons().find { it.id == id }?.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCategoryForIcon(iconId: String): String? {
|
fun getCategoryForIcon(iconId: String): String? = categories.find { category ->
|
||||||
return categories.find { category ->
|
category.icons.any { it.id == iconId }
|
||||||
category.icons.any { it.id == iconId }
|
}?.name
|
||||||
}?.name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun searchIcons(query: String): List<ProfileIcon> {
|
fun searchIcons(query: String): List<ProfileIcon> {
|
||||||
val lowercaseQuery = query.lowercase()
|
val lowercaseQuery = query.lowercase()
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
|
import io.nekohasekai.sfa.compose.util.icons.MaterialIconsLibrary
|
||||||
|
|
||||||
data class ProfileIcon(
|
data class ProfileIcon(val id: String, val icon: ImageVector, val label: String)
|
||||||
val id: String,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val label: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
object ProfileIcons {
|
object ProfileIcons {
|
||||||
// Use the complete Material Icons library with all available icons
|
// Use the complete Material Icons library with all available icons
|
||||||
@@ -26,13 +22,9 @@ object ProfileIcons {
|
|||||||
return Icons.AutoMirrored.Default.InsertDriveFile
|
return Icons.AutoMirrored.Default.InsertDriveFile
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCategoryForIcon(iconId: String): String? {
|
fun getCategoryForIcon(iconId: String): String? = MaterialIconsLibrary.getCategoryForIcon(iconId)
|
||||||
return MaterialIconsLibrary.getCategoryForIcon(iconId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun searchIcons(query: String): List<ProfileIcon> {
|
fun searchIcons(query: String): List<ProfileIcon> = MaterialIconsLibrary.searchIcons(query)
|
||||||
return MaterialIconsLibrary.searchIcons(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCategories() = MaterialIconsLibrary.categories
|
fun getCategories() = MaterialIconsLibrary.categories
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user