[controller] agregado código qr para compartir contenido. y corregir arranque de proxy
This commit is contained in:
parent
af7655b572
commit
6ff9fc48c0
|
|
@ -79,4 +79,6 @@ dependencies {
|
||||||
implementation 'androidx.biometric:biometric:1.1.0'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.webkit:webkit:1.12.0'
|
implementation 'androidx.webkit:webkit:1.12.0'
|
||||||
|
// ZXing for QR Code generation
|
||||||
|
implementation 'com.google.zxing:core:3.5.2'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,17 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- VPN Recovery Receiver (The Boomerang) -->
|
<!-- VPN Recovery Receiver -->
|
||||||
<receiver android:name=".VpnRecoveryReceiver" android:exported="false">
|
<receiver android:name=".VpnRecoveryReceiver" android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="org.iiab.controller.RECOVER_VPN" />
|
<action android:name="org.iiab.controller.RECOVER_VPN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<activity android:name=".QrActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.TransparentQR" />
|
||||||
|
|
||||||
<!-- Termux Result Callback Receiver -->
|
<!-- Termux Result Callback Receiver -->
|
||||||
<receiver android:name=".TermuxCallbackReceiver" android:exported="false">
|
<receiver android:name=".TermuxCallbackReceiver" android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
private String serverTransitionText = "";
|
private String serverTransitionText = "";
|
||||||
private final Handler timeoutHandler = new Handler(android.os.Looper.getMainLooper());
|
private final Handler timeoutHandler = new Handler(android.os.Looper.getMainLooper());
|
||||||
private Runnable timeoutRunnable;
|
private Runnable timeoutRunnable;
|
||||||
|
private boolean isWifiActive = false;
|
||||||
|
private boolean isHotspotActive = false;
|
||||||
private String currentTargetUrl = null;
|
private String currentTargetUrl = null;
|
||||||
private long pulseStartTime = 0;
|
private long pulseStartTime = 0;
|
||||||
|
|
||||||
|
|
@ -267,6 +269,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
|
|
||||||
deckContainer = findViewById(R.id.deck_container);
|
deckContainer = findViewById(R.id.deck_container);
|
||||||
btnServerControl = findViewById(R.id.btn_server_control);
|
btnServerControl = findViewById(R.id.btn_server_control);
|
||||||
|
ImageButton btnShareQr = findViewById(R.id.btn_share_qr);
|
||||||
|
|
||||||
dashboardManager = new DashboardManager(this, findViewById(android.R.id.content), () -> {
|
dashboardManager = new DashboardManager(this, findViewById(android.R.id.content), () -> {
|
||||||
handleControlClick();
|
handleControlClick();
|
||||||
|
|
@ -335,6 +338,23 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- QR Share Button Logic ---
|
||||||
|
btnShareQr.setOnClickListener(v -> {
|
||||||
|
if (!isServerAlive) {
|
||||||
|
// Rule 1: Server must be running
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), R.string.qr_error_no_server, Snackbar.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isWifiActive && !isHotspotActive) {
|
||||||
|
// Rule 2: At least one network must be active
|
||||||
|
Snackbar.make(findViewById(android.R.id.content), R.string.qr_error_no_network, Snackbar.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the new QrActivity
|
||||||
|
startActivity(new Intent(MainActivity.this, QrActivity.class));
|
||||||
|
});
|
||||||
|
|
||||||
connectionLog.setMovementMethod(new ScrollingMovementMethod());
|
connectionLog.setMovementMethod(new ScrollingMovementMethod());
|
||||||
connectionLog.setTextIsSelectable(true);
|
connectionLog.setTextIsSelectable(true);
|
||||||
connectionLog.setOnTouchListener((v, event) -> {
|
connectionLog.setOnTouchListener((v, event) -> {
|
||||||
|
|
@ -1003,8 +1023,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/iiab-termux");
|
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/iiab-termux");
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{actionFlag});
|
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{actionFlag});
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
|
intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
|
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/env");
|
||||||
intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
|
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{
|
||||||
|
"INTENT_MODE=headless",
|
||||||
|
"/data/data/com.termux/files/usr/bin/bash",
|
||||||
|
"/data/data/com.termux/files/usr/bin/iiab-termux",
|
||||||
|
actionFlag
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
@ -1043,6 +1068,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
} catch (Exception ex) {}
|
} catch (Exception ex) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store states for the QR button logic
|
||||||
|
this.isWifiActive = isWifiOn;
|
||||||
|
this.isHotspotActive = isHotspotOn;
|
||||||
|
|
||||||
// Let the Dashboard handle the LEDs!
|
// Let the Dashboard handle the LEDs!
|
||||||
if (dashboardManager != null) dashboardManager.updateConnectivityLeds(isWifiOn, isHotspotOn);
|
if (dashboardManager != null) dashboardManager.updateConnectivityLeds(isWifiOn, isHotspotOn);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
package org.iiab.controller;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.WriterException;
|
||||||
|
import com.google.zxing.common.BitMatrix;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class QrActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private TextView titleText;
|
||||||
|
private TextView ipText;
|
||||||
|
private ImageView qrImageView;
|
||||||
|
private ImageButton btnFlip;
|
||||||
|
private View cardContainer;
|
||||||
|
|
||||||
|
private String wifiIp = null;
|
||||||
|
private String hotspotIp = null;
|
||||||
|
private boolean showingWifi = true; // Tracks which network is currently displayed
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_qr); // Rename your dialog_qr.xml to this
|
||||||
|
|
||||||
|
titleText = findViewById(R.id.qr_network_title);
|
||||||
|
ipText = findViewById(R.id.qr_ip_text);
|
||||||
|
qrImageView = findViewById(R.id.qr_image_view);
|
||||||
|
btnFlip = findViewById(R.id.btn_flip_qr);
|
||||||
|
cardContainer = findViewById(R.id.qr_card_container);
|
||||||
|
Button btnClose = findViewById(R.id.btn_close_qr);
|
||||||
|
|
||||||
|
// Improve 3D perspective to avoid visual clipping during rotation
|
||||||
|
float distance = 8000 * getResources().getDisplayMetrics().density;
|
||||||
|
cardContainer.setCameraDistance(distance);
|
||||||
|
|
||||||
|
btnClose.setOnClickListener(v -> finish());
|
||||||
|
|
||||||
|
btnFlip.setOnClickListener(v -> {
|
||||||
|
// Disable button during animation to prevent spam
|
||||||
|
btnFlip.setEnabled(false);
|
||||||
|
animateCardFlip();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Fetch real physical IPs with strict interface naming
|
||||||
|
fetchNetworkInterfaces();
|
||||||
|
|
||||||
|
// 2. Determine initial state and button visibility
|
||||||
|
if (wifiIp != null && hotspotIp != null) {
|
||||||
|
btnFlip.setVisibility(View.VISIBLE); // Both active, enable flipping
|
||||||
|
showingWifi = true;
|
||||||
|
} else if (wifiIp != null) {
|
||||||
|
btnFlip.setVisibility(View.GONE);
|
||||||
|
showingWifi = true;
|
||||||
|
} else if (hotspotIp != null) {
|
||||||
|
btnFlip.setVisibility(View.GONE);
|
||||||
|
showingWifi = false;
|
||||||
|
} else {
|
||||||
|
// Fallback just in case they died between the MainActivity click and this onCreate
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQrDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a 3D flip animation. Swaps the data halfway through when the card is invisible.
|
||||||
|
*/
|
||||||
|
private void animateCardFlip() {
|
||||||
|
// Phase 1: Rotate out (0 to 90 degrees)
|
||||||
|
ObjectAnimator flipOut = ObjectAnimator.ofFloat(cardContainer, "rotationY", 0f, 90f);
|
||||||
|
flipOut.setDuration(200); // 200ms
|
||||||
|
flipOut.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
// Card is edge-on (invisible). Swap the data!
|
||||||
|
showingWifi = !showingWifi;
|
||||||
|
updateQrDisplay();
|
||||||
|
|
||||||
|
// Phase 2: Rotate in from the other side (-90 to 0 degrees)
|
||||||
|
cardContainer.setRotationY(-90f);
|
||||||
|
ObjectAnimator flipIn = ObjectAnimator.ofFloat(cardContainer, "rotationY", -90f, 0f);
|
||||||
|
flipIn.setDuration(200);
|
||||||
|
flipIn.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
btnFlip.setEnabled(true); // Unlock button
|
||||||
|
}
|
||||||
|
});
|
||||||
|
flipIn.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
flipOut.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the UI text and generates the new QR Code
|
||||||
|
*/
|
||||||
|
private void updateQrDisplay() {
|
||||||
|
String currentIp = showingWifi ? wifiIp : hotspotIp;
|
||||||
|
String title = showingWifi ? getString(R.string.qr_title_wifi) : getString(R.string.qr_title_hotspot);
|
||||||
|
|
||||||
|
// 8085 is the default port for the IIAB interface
|
||||||
|
String url = "http://" + currentIp + ":8085";
|
||||||
|
|
||||||
|
titleText.setText(title);
|
||||||
|
ipText.setText(url);
|
||||||
|
|
||||||
|
Bitmap qrBitmap = generateQrCode(url);
|
||||||
|
if (qrBitmap != null) {
|
||||||
|
qrImageView.setImageBitmap(qrBitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly categorizes network interfaces to avoid Hotspot being labeled as Wi-Fi.
|
||||||
|
*/
|
||||||
|
private void fetchNetworkInterfaces() {
|
||||||
|
try {
|
||||||
|
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||||
|
for (NetworkInterface intf : interfaces) {
|
||||||
|
String name = intf.getName();
|
||||||
|
if (!intf.isUp()) continue;
|
||||||
|
|
||||||
|
// Strict categorizations
|
||||||
|
boolean isStrictWifi = name.equals("wlan0");
|
||||||
|
boolean isHotspot = name.startsWith("ap") || name.startsWith("swlan") || name.equals("wlan1") || name.equals("wlan2");
|
||||||
|
|
||||||
|
if (isStrictWifi || isHotspot) {
|
||||||
|
List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
|
||||||
|
for (InetAddress addr : addrs) {
|
||||||
|
if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
|
||||||
|
if (isStrictWifi) wifiIp = addr.getHostAddress();
|
||||||
|
if (isHotspot) hotspotIp = addr.getHostAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a pure Black & White Bitmap using ZXing.
|
||||||
|
*/
|
||||||
|
private Bitmap generateQrCode(String text) {
|
||||||
|
QRCodeWriter writer = new QRCodeWriter();
|
||||||
|
try {
|
||||||
|
// 800x800 guarantees high resolution on any screen
|
||||||
|
BitMatrix bitMatrix = writer.encode(text, BarcodeFormat.QR_CODE, 800, 800);
|
||||||
|
int width = bitMatrix.getWidth();
|
||||||
|
int height = bitMatrix.getHeight();
|
||||||
|
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
|
||||||
|
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
bmp.setPixel(x, y, bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
} catch (WriterException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="#DD000000"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/qr_card_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/rounded_button"
|
||||||
|
android:backgroundTint="#1A1A1A"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/qr_network_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toStartOf="@+id/btn_flip_qr"
|
||||||
|
android:text="Wi-Fi Network"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_flip_qr"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_popup_sync"
|
||||||
|
android:contentDescription="@string/qr_flip_network"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/qr_ip_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="http://---"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/qr_image_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="#FFFFFF"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_close_qr"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="Close"
|
||||||
|
android:backgroundTint="@color/btn_danger"
|
||||||
|
android:textColor="#FFFFFF" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -31,6 +31,19 @@
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_share_qr"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_toStartOf="@id/btn_settings"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_menu_share"
|
||||||
|
android:contentDescription="Share via QR"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:tint="#FFFFFF" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_settings"
|
android:id="@+id/btn_settings"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
|
|
|
||||||
|
|
@ -90,4 +90,10 @@
|
||||||
<string name="battery_opt_xiaomi_extra">\n\nXiaomi detected: Please set battery saver to \'No restrictions\' in settings.</string>
|
<string name="battery_opt_xiaomi_extra">\n\nXiaomi detected: Please set battery saver to \'No restrictions\' in settings.</string>
|
||||||
<string name="battery_opt_denied">For the app to work 100%, please disable battery optimization.</string>
|
<string name="battery_opt_denied">For the app to work 100%, please disable battery optimization.</string>
|
||||||
<string name="fix_action">FIX</string>
|
<string name="fix_action">FIX</string>
|
||||||
|
|
||||||
|
<string name="qr_error_no_server">Please start the server to share over the network.</string>
|
||||||
|
<string name="qr_error_no_network">Please enable Wi-Fi or Hotspot to share over the network.</string>
|
||||||
|
<string name="qr_title_wifi">Wi-Fi Network</string>
|
||||||
|
<string name="qr_title_hotspot">Hotspot Network</string>
|
||||||
|
<string name="qr_flip_network">Switch Network</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -11,4 +11,10 @@
|
||||||
<item name="sectionBackground">@color/section_body_bg_light</item>
|
<item name="sectionBackground">@color/section_body_bg_light</item>
|
||||||
<item name="sectionHeaderBackground">@color/section_header_bg</item>
|
<item name="sectionHeaderBackground">@color/section_header_bg</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="Theme.TransparentQR" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="android:windowIsTranslucent">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:backgroundDimEnabled">false</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue