3 Commits

Author SHA1 Message Date
renato97
2c65578bdd Update v9.4.2: Enhanced UI Theme & Visual Consistency
- Incremented version to 9.4.2 (versionCode: 94200)
- Added custom AlertDialog theme with white text styling
- Enhanced visual consistency for all dialog components
- Improved theme overlay for better readability
- Applied custom styling to update and blocked dialogs
- Better contrast and visual hierarchy in dialogs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:13:29 +01:00
renato97
6aef195f30 Update v9.4.1: Enhanced Playback & Device Management
- Incremented version to 9.4.1 (versionCode: 94100)
- Added keep screen on functionality during video playback
- Implemented device deletion in dashboard with confirmation
- Enhanced device management with delete capability
- Improved user experience during media playback
- Better device lifecycle management in dashboard
- Added confirmation dialog for device deletion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:07:04 +01:00
renato97
cc4696dec2 Update v9.4.0: Advanced Telegram Integration & Custom UI
- Incremented version to 9.4.0 (versionCode: 94000)
- Added custom blocked dialog layout with improved UX
- Implemented Telegram commands: /allow, /deny, /pending
- Enhanced Telegram polling for real-time device management
- Custom token display with click-to-copy functionality
- Improved device verification workflow via Telegram
- Better error handling and user feedback
- Enhanced documentation for Telegram integration
- Improved token validation and management system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 23:15:37 +01:00
11 changed files with 270 additions and 41 deletions

View File

@@ -160,7 +160,13 @@ Para autorizar un dispositivo pendiente:
3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente. 3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente.
4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras. 4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras.
Cada nuevo registro también dispara una notificación de Telegram para que puedas reaccionar en tiempo real. También puedes gestionar todo desde Telegram:
- `/allow <deviceId> <token_cliente>` autoriza el dispositivo (verifica el token y lo desbloquea).
- `/deny <deviceId> <token_cliente> [motivo]` lo bloquea con un motivo opcional.
- `/pending` lista los registros que aún esperan un token válido.
Cada nuevo registro dispara una notificación de Telegram con la parte admin del token y recordatorios de esos comandos.
## 📱 Estructura del Proyecto ## 📱 Estructura del Proyecto

View File

@@ -8,8 +8,8 @@ android {
applicationId "com.streamplayer" applicationId "com.streamplayer"
minSdk 21 minSdk 21
targetSdk 33 targetSdk 33
versionCode 93100 versionCode 94200
versionName "9.3.1" versionName "9.4.2"
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"' buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
} }

View File

@@ -244,7 +244,7 @@ public class MainActivity extends AppCompatActivity {
if (updateDialog != null && updateDialog.isShowing()) { if (updateDialog != null && updateDialog.isShowing()) {
updateDialog.dismiss(); updateDialog.dismiss();
} }
AlertDialog.Builder builder = new AlertDialog.Builder(this) AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
.setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title) .setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title)
.setMessage(buildUpdateMessage(info)) .setMessage(buildUpdateMessage(info))
.setPositiveButton(R.string.update_action_download, .setPositiveButton(R.string.update_action_download,
@@ -320,24 +320,30 @@ public class MainActivity extends AppCompatActivity {
if (blockedDialog != null && blockedDialog.isShowing()) { if (blockedDialog != null && blockedDialog.isShowing()) {
blockedDialog.dismiss(); blockedDialog.dismiss();
} }
StringBuilder messageBuilder = new StringBuilder(); View dialogView = getLayoutInflater().inflate(R.layout.dialog_blocked, null);
messageBuilder.append(getString(R.string.device_blocked_message, finalReason)); TextView messageText = dialogView.findViewById(R.id.blocked_message_text);
if (!TextUtils.isEmpty(tokenPart)) { View tokenContainer = dialogView.findViewById(R.id.blocked_token_container);
messageBuilder.append("\n\n") TextView tokenValue = dialogView.findViewById(R.id.blocked_token_value);
.append(getString(R.string.device_blocked_token_hint, tokenPart)); messageText.setText(getString(R.string.device_blocked_message, finalReason));
boolean hasToken = !TextUtils.isEmpty(tokenPart);
if (hasToken) {
tokenContainer.setVisibility(View.VISIBLE);
tokenValue.setText(tokenPart);
tokenValue.setOnClickListener(v -> copyTokenToClipboard(tokenPart));
} else {
tokenContainer.setVisibility(View.GONE);
} }
blockedDialog = new AlertDialog.Builder(this) AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
.setTitle(R.string.device_blocked_title) .setTitle(R.string.device_blocked_title)
.setMessage(messageBuilder.toString()) .setView(dialogView)
.setCancelable(false) .setCancelable(false)
.setPositiveButton(R.string.device_blocked_close, .setPositiveButton(R.string.device_blocked_close,
(dialog, which) -> finish()) (dialog, which) -> finish());
.create(); if (hasToken) {
if (!TextUtils.isEmpty(tokenPart)) { builder.setNeutralButton(R.string.device_blocked_copy_token,
blockedDialog.setButton(AlertDialog.BUTTON_NEUTRAL,
getString(R.string.device_blocked_copy_token),
(dialog, which) -> copyTokenToClipboard(tokenPart)); (dialog, which) -> copyTokenToClipboard(tokenPart));
} }
blockedDialog = builder.create();
blockedDialog.show(); blockedDialog.show();
} }

View File

@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.StrictMode; import android.os.StrictMode;
import android.view.View; import android.view.View;
import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
@@ -58,6 +59,7 @@ public class PlayerActivity extends AppCompatActivity {
); );
setContentView(R.layout.activity_player); setContentView(R.layout.activity_player);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Intent intent = getIntent(); Intent intent = getIntent();
if (intent == null) { if (intent == null) {
@@ -254,6 +256,7 @@ public class PlayerActivity extends AppCompatActivity {
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
releasePlayer(); releasePlayer();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} }
private void toggleOverlay() { private void toggleOverlay() {

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/blocked_message_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/blocked_token_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/blocked_token_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/device_blocked_token_label"
android:textColor="@android:color/black"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/blocked_token_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@android:color/transparent"
android:padding="8dp"
android:textColor="@android:color/black"
android:textIsSelectable="true"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -40,6 +40,7 @@
<string name="device_blocked_message">Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s</string> <string name="device_blocked_message">Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s</string>
<string name="device_blocked_default_reason">Sin motivo especificado.</string> <string name="device_blocked_default_reason">Sin motivo especificado.</string>
<string name="device_blocked_token_hint">Comparte este código con el administrador para solicitar acceso: %1$s</string> <string name="device_blocked_token_hint">Comparte este código con el administrador para solicitar acceso: %1$s</string>
<string name="device_blocked_token_label">Código de verificación</string>
<string name="device_blocked_close">Salir</string> <string name="device_blocked_close">Salir</string>
<string name="device_blocked_copy_token">Copiar código</string> <string name="device_blocked_copy_token">Copiar código</string>
<string name="device_blocked_copy_success">Código copiado al portapapeles</string> <string name="device_blocked_copy_success">Código copiado al portapapeles</string>

View File

@@ -6,4 +6,10 @@
<item name="android:statusBarColor">@color/black</item> <item name="android:statusBarColor">@color/black</item>
<item name="android:navigationBarColor">@color/black</item> <item name="android:navigationBarColor">@color/black</item>
</style> </style>
<style name="ThemeOverlay.StreamPlayer.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">@color/white</item>
<item name="colorAccent">@color/white</item>
</style>
</resources> </resources>

View File

@@ -6,21 +6,21 @@
"model": "SM-S928B", "model": "SM-S928B",
"manufacturer": "Samsung", "manufacturer": "Samsung",
"osVersion": "16 (API 36)", "osVersion": "16 (API 36)",
"appVersionName": "9.3.0", "appVersionName": "9.4.1",
"appVersionCode": 93000, "appVersionCode": 94100,
"firstSeen": "2025-11-23T20:53:43.615Z", "firstSeen": "2025-11-23T22:31:13.359Z",
"lastSeen": "2025-11-23T21:12:30.345Z", "lastSeen": "2025-11-23T23:11:07.215Z",
"blocked": false, "blocked": false,
"notes": "no pagó", "notes": "",
"installs": 9, "installs": 7,
"blockedAt": "2025-11-23T20:54:05.413Z",
"ip": "181.23.253.20", "ip": "181.23.253.20",
"country": "AR", "country": "AR",
"verification": { "verification": {
"clientPart": "6e05a220abe0ed05", "clientPart": "1714c2bb93670c3f",
"adminPart": "19d6ee4c992ee1a0", "adminPart": "9924c7049211c58c",
"status": "pending", "status": "verified",
"createdAt": "2025-11-23T21:09:04.607Z" "createdAt": "2025-11-23T22:31:13.359Z",
"verifiedAt": "2025-11-23T22:33:11.942Z"
} }
} }
] ]

View File

@@ -45,6 +45,7 @@ function renderTable(devices) {
actions.push('<button data-action="verify" class="primary">Verificar token</button>'); actions.push('<button data-action="verify" class="primary">Verificar token</button>');
} }
actions.push(device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>'); actions.push(device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>');
actions.push('<button data-action="delete" class="danger ghost">Borrar</button>');
tr.innerHTML = ` tr.innerHTML = `
<td> <td>
@@ -97,6 +98,19 @@ async function unblockDevice(deviceId) {
await fetchDevices(); await fetchDevices();
} }
async function deleteDevice(deviceId) {
const confirmation = confirm('¿Seguro que quieres borrar este dispositivo? Generará un nuevo token cuando se registre de nuevo.');
if (!confirmation) {
return;
}
const response = await fetch(`/api/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' });
if (!response.ok) {
alert('No se pudo borrar el dispositivo');
return;
}
await fetchDevices();
}
async function verifyDevice(deviceId) { async function verifyDevice(deviceId) {
const clientTokenPart = prompt('Introduce el token que aparece en el dispositivo:'); const clientTokenPart = prompt('Introduce el token que aparece en el dispositivo:');
if (clientTokenPart === null) { if (clientTokenPart === null) {
@@ -154,6 +168,8 @@ tableBody.addEventListener('click', async (event) => {
await updateAlias(deviceId); await updateAlias(deviceId);
} else if (action === 'verify') { } else if (action === 'verify') {
await verifyDevice(deviceId); await verifyDevice(deviceId);
} else if (action === 'delete') {
await deleteDevice(deviceId);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -23,7 +23,9 @@ const CONFIG_PATH = path.join(__dirname, 'config.json');
const config = loadConfig(); const config = loadConfig();
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || ''; const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || '';
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID || config.telegramChatId || ''; const TELEGRAM_CHAT_ID = (process.env.TELEGRAM_CHAT_ID || config.telegramChatId || '').toString();
let telegramOffset = 0;
let telegramPollingStarted = false;
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
@@ -109,22 +111,30 @@ function generateTokenParts() {
}; };
} }
async function sendTelegramNotification(message) { async function sendTelegramMessage(message, options = {}) {
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
return; return;
} }
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`; const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
try { try {
await axios.post(url, { const payload = {
chat_id: TELEGRAM_CHAT_ID, chat_id: TELEGRAM_CHAT_ID,
text: message, text: message,
parse_mode: 'Markdown' disable_web_page_preview: true
}); };
if (options.parseMode) {
payload.parse_mode = options.parseMode;
}
await axios.post(url, payload);
} catch (error) { } catch (error) {
console.warn('Failed to send Telegram notification', error.response ? error.response.data : error.message); console.warn('Failed to send Telegram message', error.response ? error.response.data : error.message);
} }
} }
async function sendTelegramNotification(message) {
await sendTelegramMessage(message, { parseMode: 'Markdown' });
}
function formatTelegramMessage(device, verificationRequired) { function formatTelegramMessage(device, verificationRequired) {
const lines = [ const lines = [
'*Nuevo registro de dispositivo*', '*Nuevo registro de dispositivo*',
@@ -137,11 +147,143 @@ function formatTelegramMessage(device, verificationRequired) {
]; ];
if (verificationRequired && device.verification) { if (verificationRequired && device.verification) {
lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`'); lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`');
lines.push('Comparte el token del cliente y este admin para autorizar.'); lines.push('Autorizar: `/allow ' + device.deviceId + ' TOKEN_CLIENTE`');
lines.push('Rechazar: `/deny ' + device.deviceId + ' TOKEN_CLIENTE [motivo]`');
} }
return lines.join('\n'); return lines.join('\n');
} }
async function initializeTelegramOffset() {
if (!TELEGRAM_BOT_TOKEN) {
return;
}
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`;
try {
const { data } = await axios.get(url, { params: { timeout: 1 } });
if (data.ok && Array.isArray(data.result) && data.result.length > 0) {
telegramOffset = data.result[data.result.length - 1].update_id + 1;
}
} catch (error) {
console.warn('Failed to initialize Telegram offset', error.message);
}
}
async function pollTelegramUpdates() {
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
return;
}
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`;
const params = {
timeout: 25
};
if (telegramOffset) {
params.offset = telegramOffset;
}
try {
const { data } = await axios.get(url, { params });
if (data.ok && Array.isArray(data.result)) {
for (const update of data.result) {
telegramOffset = update.update_id + 1;
if (update.message) {
processTelegramMessage(update.message);
}
}
}
} catch (error) {
console.warn('Telegram polling error', error.message);
} finally {
setTimeout(pollTelegramUpdates, 3000);
}
}
async function startTelegramPolling() {
if (telegramPollingStarted || !TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
return;
}
telegramPollingStarted = true;
await initializeTelegramOffset();
pollTelegramUpdates();
}
function processTelegramMessage(message) {
const chatId = message.chat && message.chat.id ? message.chat.id.toString() : '';
if (chatId && TELEGRAM_CHAT_ID && chatId !== TELEGRAM_CHAT_ID.toString()) {
return;
}
const text = (message.text || '').trim();
if (!text.startsWith('/')) {
return;
}
const parts = text.split(/\s+/);
const command = parts[0].toLowerCase();
if (command === '/allow' && parts.length >= 3) {
handleTelegramAllow(parts[1], parts[2]);
} else if (command === '/deny' && parts.length >= 3) {
const reason = parts.slice(3).join(' ') || 'Bloqueado desde Telegram';
handleTelegramDeny(parts[1], parts[2], reason);
} else if (command === '/pending') {
handleTelegramPending();
} else {
sendTelegramMessage('Comandos disponibles:\n/allow <deviceId> <token_cliente>\n/deny <deviceId> <token_cliente> [motivo]\n/pending');
}
}
async function handleTelegramAllow(deviceId, clientToken) {
const devices = readDevices();
const device = devices.find(d => d.deviceId === deviceId);
if (!device || !device.verification) {
await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`);
return;
}
if (device.verification.status === 'verified') {
await sendTelegramMessage(` El dispositivo ${deviceId} ya está verificado.`);
return;
}
if (device.verification.clientPart !== clientToken.trim()) {
await sendTelegramMessage('❌ Token del cliente inválido.');
return;
}
device.verification.status = 'verified';
device.verification.verifiedAt = new Date().toISOString();
device.blocked = false;
device.notes = '';
writeDevices(devices);
await sendTelegramMessage(`✅ Dispositivo ${deviceId} autorizado correctamente.`);
}
async function handleTelegramDeny(deviceId, clientToken, reason) {
const devices = readDevices();
const device = devices.find(d => d.deviceId === deviceId);
if (!device || !device.verification) {
await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`);
return;
}
if (device.verification.clientPart !== clientToken.trim()) {
await sendTelegramMessage('❌ Token del cliente inválido.');
return;
}
device.verification.status = 'denied';
device.verification.deniedAt = new Date().toISOString();
device.blocked = true;
device.notes = reason;
writeDevices(devices);
await sendTelegramMessage(`🚫 Dispositivo ${deviceId} bloqueado. Motivo: ${reason}`);
}
async function handleTelegramPending() {
const devices = readDevices();
const pending = devices.filter(d => !d.verification || d.verification.status !== 'verified');
if (!pending.length) {
await sendTelegramMessage('No hay dispositivos pendientes.');
return;
}
const lines = pending.slice(0, 10).map(device => {
const token = device.verification ? device.verification.clientPart : 'N/A';
return `${device.deviceId} - Token cliente: ${token}`;
});
await sendTelegramMessage('Pendientes:\n' + lines.join('\n'));
}
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() }); res.json({ status: 'ok', timestamp: new Date().toISOString() });
}); });
@@ -318,4 +460,5 @@ app.delete('/api/devices/:deviceId', (req, res) => {
app.listen(PORT, () => { app.listen(PORT, () => {
ensureDataFile(); ensureDataFile();
console.log(`StreamPlayer dashboard server listening on port ${PORT}`); console.log(`StreamPlayer dashboard server listening on port ${PORT}`);
startTelegramPolling();
}); });

View File

@@ -1,10 +1,10 @@
{ {
"versionCode": 93000, "versionCode": 94100,
"versionName": "9.3.0", "versionName": "9.4.1",
"minSupportedVersionCode": 91000, "minSupportedVersionCode": 91000,
"forceUpdate": false, "forceUpdate": false,
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.3.0/StreamPlayer-v9.3.0.apk", "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.4.1/StreamPlayer-v9.4.1.apk",
"fileName": "StreamPlayer-v9.3.0.apk", "fileName": "StreamPlayer-v9.4.1.apk",
"sizeBytes": 5941443, "sizeBytes": 5944680,
"notes": "StreamPlayer v9.3.0\n\nMejoras importantes en esta versión:\n\n- Sistema de seguridad mejorado con verificación avanzada\n- Nuevas herramientas de administración y control\n- Mejoras en la gestión de dispositivos\n- Interfaz de usuario optimizada\n- Mayor rendimiento y estabilidad\n- Correcciones de seguridad mejoradas\n- Sistema de notificaciones más eficiente\n\nEsta actualización fortalece la seguridad y mejora la experiencia general de uso manteniendo todas las funcionalidades existentes." "notes": "StreamPlayer v9.4.1\n\nMejoras en esta versión:\n\n- Experiencia de reproducción optimizada e ininterrumpida\n- Mejores controles de administración y gestión de dispositivos\n- Funcionalidad de eliminación de registros con confirmación segura\n- Optimización de energía durante el uso de la aplicación\n- Interfaz administrativa mejorada con más opciones\n- Flujo de trabajo más eficiente para la gestión\n- Mejor respuesta y estabilidad general\n- Correcciones de usabilidad menores\n\nEsta actualización mejora tanto la experiencia de visualización como las herramientas de administración para un mejor control y uso de la aplicación."
} }