Compare commits
6 Commits
v10.1.9
...
windows-on
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f247f9ddee | ||
|
|
8921d7f2a6 | ||
| 93dbe0941e | |||
| 22d2cf9eda | |||
| d2c3041b0a | |||
| 77c417117a |
2
.env
@@ -1,4 +1,4 @@
|
|||||||
GITEA_TOKEN=efeed2af00597883adb04da70bd6a7c2993ae92d
|
GITEA_TOKEN=7921aa22187b39125d29399d26f527ba26a2fb5b
|
||||||
GEMINI_API_KEY=AIzaSyDWOgyAJqscuPU6iSpS6gxupWBm4soNw5o
|
GEMINI_API_KEY=AIzaSyDWOgyAJqscuPU6iSpS6gxupWBm4soNw5o
|
||||||
TELEGRAM_BOT_TOKEN=8593525164:AAGCX9B_RJGN35_F7tSB72rEZhS_4Zpcszs
|
TELEGRAM_BOT_TOKEN=8593525164:AAGCX9B_RJGN35_F7tSB72rEZhS_4Zpcszs
|
||||||
TELEGRAM_CHAT_ID=692714536
|
TELEGRAM_CHAT_ID=692714536
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -134,3 +134,9 @@ app/debug/
|
|||||||
dashboard/node_modules/
|
dashboard/node_modules/
|
||||||
dashboard/server.log
|
dashboard/server.log
|
||||||
dashboard/config.json
|
dashboard/config.json
|
||||||
|
|
||||||
|
# Windows desktop project artifacts
|
||||||
|
windows/StreamPlayer.Desktop/.vs/
|
||||||
|
windows/StreamPlayer.Desktop/bin/
|
||||||
|
windows/StreamPlayer.Desktop/obj/
|
||||||
|
windows/StreamPlayer.Desktop/ResolverTest/
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# StreamPlayer v10.0
|
|
||||||
|
|
||||||
## Cambios en esta versión
|
|
||||||
|
|
||||||
- **Actualización a versión 10.0**: Nueva versión mayor del StreamPlayer
|
|
||||||
- Versión estable con mejoras acumuladas de versiones anteriores
|
|
||||||
- Sistema de actualizaciones automáticas activado
|
|
||||||
|
|
||||||
Esta versión marca un hito importante en el desarrollo de StreamPlayer, consolidando todas las mejoras y características implementadas previamente.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# StreamPlayer v10.1.3
|
|
||||||
|
|
||||||
## Cambios en esta versión
|
|
||||||
|
|
||||||
### Corrección de Carga de Eventos
|
|
||||||
|
|
||||||
- **Sistema de fallback con múltiples URLs**: Implementado sistema inteligente que intenta múltiples URLs de eventos cuando la principal no está disponible:
|
|
||||||
- `https://streamtpcloud.com/eventos.json` (URL original)
|
|
||||||
- `https://streamtp10.com/eventos.json` (URL actual)
|
|
||||||
- `https://streamtpmedia.com/eventos.json` (URL anterior)
|
|
||||||
|
|
||||||
- **Seguimiento automático de redirecciones HTTP**: El cliente ahora sigue automáticamente las redirecciones HTTP (códigos 301, 302, 303, 307, 308), lo que permite adaptarse a cambios de URL del servidor sin necesidad de actualizar la app.
|
|
||||||
|
|
||||||
- **Memoria de URL exitosa**: La app recuerda cuál fue la última URL que funcionó correctamente y la intenta primero en futuras peticiones, mejorando el rendimiento y la fiabilidad.
|
|
||||||
|
|
||||||
### Detalles Técnicos
|
|
||||||
|
|
||||||
- Modificado `EventRepository.java` para implementar:
|
|
||||||
- Lógica de reintento secuencial con múltiples URLs
|
|
||||||
- Seguimiento manual de redirecciones (hasta 5 consecutivas)
|
|
||||||
- Persistencia de la última URL exitosa en SharedPreferences
|
|
||||||
- Manejo mejorado de errores con mensajes descriptivos
|
|
||||||
|
|
||||||
### Problema Resuelto
|
|
||||||
|
|
||||||
Esta versión corrige el error: *"Unable to resolve host 'streamtpcloud.com': No address associated with hostname"* que ocurría cuando el servidor de eventos cambió su dominio. La app ahora se adapta automáticamente a estos cambios sin intervención del usuario.
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# StreamPlayer v10.1.4 - Mejoras de Interfaz
|
|
||||||
|
|
||||||
## Correcciones Implementadas
|
|
||||||
|
|
||||||
### 1. Botón de Actualización Más Visible
|
|
||||||
- **Archivo**: `app/src/main/res/drawable/btn_refresh_selector.xml` (nuevo)
|
|
||||||
- **Descripción**: El botón de actualizar eventos ahora cambia a un color ámbar brillante (#FFC107) con borde grueso cuando está enfocado, mejorando significativamente la visibilidad para control remoto.
|
|
||||||
|
|
||||||
### 2. Prevención de Navegación Entre Secciones
|
|
||||||
- **Archivo**: `app/src/main/java/com/streamplayer/MainActivity.java`
|
|
||||||
- **Descripción**: Al hacer scroll después del último evento, la aplicación se detiene en lugar de pasar a la sección de canales, mejorando la experiencia de usuario.
|
|
||||||
|
|
||||||
### 3. Barra de Indicador de Scroll
|
|
||||||
- **Archivos**: `app/src/main/res/layout/activity_main.xml`, `app/src/main/res/drawable/scrollbar_vertical.xml` (nuevo)
|
|
||||||
- **Descripción**: Agregada barra de scroll visual a la derecha de la lista de contenido como indicador de posición (no navegable).
|
|
||||||
|
|
||||||
## Cambios Técnicos
|
|
||||||
|
|
||||||
### Nuevo Archivo: btn_refresh_selector.xml
|
|
||||||
```xml
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_focused="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#FFC107" />
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<stroke android:width="4dp" android:color="#FFD54F" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<!-- ... otros estados ... -->
|
|
||||||
</selector>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modificación: MainActivity.java
|
|
||||||
- Agregado `RecyclerView.OnScrollListener` en `showEvents()` para prevenir scroll más allá del último evento
|
|
||||||
|
|
||||||
### Modificación: activity_main.xml
|
|
||||||
- Botón refresh usa `@drawable/btn_refresh_selector`
|
|
||||||
- RecyclerView ahora tiene `android:scrollbars="vertical"` y `scrollbarThumbVertical`
|
|
||||||
|
|
||||||
### Nuevos Colores: colors.xml
|
|
||||||
- `refresh_button_default`: #2A2A2A
|
|
||||||
- `refresh_button_focused`: #FFC107
|
|
||||||
- `refresh_button_focused_border`: #FFD54F
|
|
||||||
- `refresh_button_pressed`: #FF9800
|
|
||||||
|
|
||||||
## Compatibilidad
|
|
||||||
- Versión mínima de Android: API 21+
|
|
||||||
- Compilado con SDK 34
|
|
||||||
- Probado en Android TV con control remoto
|
|
||||||
|
|
||||||
## Instalación
|
|
||||||
1. Descargar `StreamPlayer-10.1.4-debug.apk`
|
|
||||||
2. Habilitar "Fuentes desconocidas" en configuraciones de seguridad
|
|
||||||
3. Instalar el APK
|
|
||||||
4. Disfrutar las mejoras de interfaz
|
|
||||||
|
|
||||||
## Notas de Desarrollo
|
|
||||||
- La barra de scroll es puramente visual (indicador)
|
|
||||||
- El foco del botón refresh ahora usa color ámbar de alto contraste
|
|
||||||
- El scroll se detiene correctamente al final de la lista de eventos
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# StreamPlayer v10.1.5 - Correcciones Críticas
|
|
||||||
|
|
||||||
## Correcciones Implementadas
|
|
||||||
|
|
||||||
### 1. Scroll Listener Corregido
|
|
||||||
- **Problema**: El último evento aparecía solo a la mitad y requería bajar/subir muchas veces para verlo completo
|
|
||||||
- **Solución**: Cambiado de `findFirstVisibleItemPosition()` a `findLastCompletelyVisibleItemPosition()`
|
|
||||||
- Ahora el scroll solo se detiene cuando el último elemento está COMPLETAMENTE visible
|
|
||||||
|
|
||||||
### 2. Barra de Scroll Más Visible
|
|
||||||
- **Problema**: La barra indicadora no era visible (30% de opacidad)
|
|
||||||
- **Solución**:
|
|
||||||
- Opacidad aumentada de #4DFFFFFF (30%) a #CCFFFFFF (80%)
|
|
||||||
- Ancho de la barra aumentado a 8dp
|
|
||||||
- Radio de esquinas aumentado a 4dp para mejor apariencia
|
|
||||||
- Estilo cambiado de `outsideOverlay` a `insideInset`
|
|
||||||
- Agregado `scrollbarFadeDuration="0"` para que nunca se desvanezca
|
|
||||||
|
|
||||||
### 3. URLs Actualizadas
|
|
||||||
- **Problema**: Ciertos ISP bloquean las URLs viejas
|
|
||||||
- **Solución**: Eliminado sistema de fallback múltiples URLs
|
|
||||||
- Ahora usa únicamente: `https://streamtp10.com/eventos.json`
|
|
||||||
- Código simplificado, más eficiente y sin bloqueos
|
|
||||||
|
|
||||||
## Archivos Modificados
|
|
||||||
|
|
||||||
### EventRepository.java
|
|
||||||
- Simplificado para usar solo streamtp10.com
|
|
||||||
- Eliminado código de fallback no necesario
|
|
||||||
- Eliminado KEY_WORKING_URL y lógica asociada
|
|
||||||
|
|
||||||
### MainActivity.java
|
|
||||||
- Scroll listener corregido para usar `findLastCompletelyVisibleItemPosition()`
|
|
||||||
|
|
||||||
### scrollbar_vertical.xml
|
|
||||||
- Color cambiado a #CCFFFFFF (80% opacidad)
|
|
||||||
- Ancho definido en 8dp
|
|
||||||
- Radio de esquinas a 4dp
|
|
||||||
|
|
||||||
### activity_main.xml
|
|
||||||
- `scrollbarStyle` cambiado a `insideInset`
|
|
||||||
- `scrollbarSize` definido en 8dp
|
|
||||||
- `scrollbarFadeDuration` en 0 (siempre visible)
|
|
||||||
|
|
||||||
## Compatibilidad
|
|
||||||
- Versión mínima de Android: API 21+
|
|
||||||
- Compilado con SDK 34
|
|
||||||
- Probado en Android TV con control remoto
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# StreamPlayer v10.1.6 - Corrección de Control Remoto y Scrollbar
|
|
||||||
|
|
||||||
## Correcciones Implementadas
|
|
||||||
|
|
||||||
### 1. Control Remoto - Prevención de Navegación
|
|
||||||
- **Problema**: Al presionar el botón abajo del control remoto en el último evento, se iba a la sección de canales
|
|
||||||
- **Solución**: Agregado `setOnKeyListener` para interceptar teclas de navegación
|
|
||||||
- Ahora intercepta `KEYCODE_DPAD_DOWN` cuando está en el último elemento
|
|
||||||
- Combina scroll listener táctil + manejo de teclas del control remoto
|
|
||||||
|
|
||||||
### 2. Barra de Scroll Más Visible
|
|
||||||
- **Problema**: La barra de seguimiento no era visible
|
|
||||||
- **Solución**:
|
|
||||||
- Color del thumb: Blanco sólido (#FFFFFFFF) - antes 80%
|
|
||||||
- Ancho aumentado a 12dp (antes 8dp)
|
|
||||||
- Radio de esquinas: 6dp (antes 4dp)
|
|
||||||
- Track oscuro agregado (#1A1A1A)
|
|
||||||
- `scrollbarAlwaysDrawVerticalTrack="true"` para siempre visible
|
|
||||||
|
|
||||||
## Archivos Modificados
|
|
||||||
|
|
||||||
### MainActivity.java
|
|
||||||
- Import agregado: `android.view.KeyEvent`
|
|
||||||
- `setOnKeyListener` agregado en `showEvents()` para interceptar DPAD_DOWN
|
|
||||||
- Combina con scroll listener existente para cobertura completa
|
|
||||||
|
|
||||||
### scrollbar_vertical.xml
|
|
||||||
- Color cambiado a blanco sólido (#FFFFFFFF)
|
|
||||||
- Ancho: 12dp
|
|
||||||
- Radio: 6dp
|
|
||||||
|
|
||||||
### activity_main.xml
|
|
||||||
- `scrollbarSize="12dp"` (antes 8dp)
|
|
||||||
- `scrollbarTrackVertical="@color/scrollbar_track"` agregado
|
|
||||||
- `scrollbarAlwaysDrawVerticalTrack="true"` agregado
|
|
||||||
|
|
||||||
### colors.xml
|
|
||||||
- Nuevo color: `scrollbar_track` (#1A1A1A)
|
|
||||||
|
|
||||||
## Compatibilidad
|
|
||||||
- Android TV con control remoto
|
|
||||||
- Versión mínima: API 21+
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# StreamPlayer v10.1.7 - Corrección de Navegación y Scrollbar Permanente
|
|
||||||
|
|
||||||
## Correcciones Implementadas
|
|
||||||
|
|
||||||
### 1. Barra de Desplazamiento Permanente
|
|
||||||
- **Feature**: Se agregó `android:fadeScrollbars="false"` al `RecyclerView` de eventos.
|
|
||||||
- **Beneficio**: La barra de desplazamiento ahora es visible permanentemente, permitiendo al usuario saber su posición (inicio, medio, final) en todo momento sin tener que interactuar primero.
|
|
||||||
|
|
||||||
### 2. Navegación al Final de la Lista (Bug Fix)
|
|
||||||
- **Problema**: Al presionar "abajo" en el último evento, el foco saltaba involuntariamente a la sección de canales.
|
|
||||||
- **Solución**: Se implementó un `LinearLayoutManager` personalizado que intercepta la búsqueda de foco (`onInterceptFocusSearch`).
|
|
||||||
- **Detalle**: Cuando se detecta `FOCUS_DOWN` en el último elemento de la lista, la acción se bloquea, manteniendo al usuario en la lista de eventos.
|
|
||||||
- **Limpieza**: Se eliminaron los `OnKeyListener` y `OnScrollListener` anteriores que eran menos efectivos.
|
|
||||||
|
|
||||||
## Archivos Modificados
|
|
||||||
|
|
||||||
### MainActivity.java
|
|
||||||
- Implementación de `LinearLayoutManager` anónimo con `onInterceptFocusSearch`.
|
|
||||||
- Eliminación de listeners redundantes.
|
|
||||||
|
|
||||||
### activity_main.xml
|
|
||||||
- `android:fadeScrollbars="false"` añadido a `content_list`.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# StreamPlayer v10.1.8 - Actualización de DNS y Dominios
|
|
||||||
|
|
||||||
## Cambios Críticos
|
|
||||||
|
|
||||||
### 1. Actualización de Dominios
|
|
||||||
- Se ha migrado toda la infraestructura de canales y eventos al nuevo dominio: `streamtp10.com`.
|
|
||||||
- Actualización de URLs base para todos los canales en `ChannelRepository`.
|
|
||||||
- Actualización del endpoint de eventos a `https://streamtp10.com/eventos.json`.
|
|
||||||
- Corrección del Header `Referer` en las peticiones de resolución.
|
|
||||||
|
|
||||||
### 2. Configuración Robusta de DNS (Anti-Bloqueo)
|
|
||||||
- Implementación de un nuevo sistema centralizado de red (`NetworkUtils`).
|
|
||||||
- **DNS Primario**: Google DNS over HTTPS (`8.8.8.8`, `8.8.4.4`).
|
|
||||||
- **DNS Secundario**: AdGuard DNS over HTTPS (`94.140.14.14`, `94.140.15.15`) como respaldo automático si Google falla.
|
|
||||||
- **DNS Terciario**: DNS del sistema (ISP) como último recurso.
|
|
||||||
- Se ha eliminado el uso de `HttpURLConnection` en `EventRepository` en favor de `OkHttpClient` con la nueva configuración DNS, asegurando que la carga de la guía de eventos también evite bloqueos.
|
|
||||||
|
|
||||||
## Beneficios
|
|
||||||
- Mayor resistencia a bloqueos regionales e interferencias de ISP.
|
|
||||||
- Recuperación automática si el proveedor de DNS principal (Google) no es accesible.
|
|
||||||
- Corrección de problemas de carga de canales debido al cambio de dominio del proveedor.
|
|
||||||
57
Dockerfile
@@ -1,57 +0,0 @@
|
|||||||
FROM eclipse-temurin:17-jdk
|
|
||||||
|
|
||||||
# Evitar interactividad durante la instalación
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# Instalar dependencias necesarias para Android SDK
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
wget \
|
|
||||||
unzip \
|
|
||||||
git \
|
|
||||||
python3 \
|
|
||||||
python3-pip \
|
|
||||||
ncurses-bin \
|
|
||||||
build-essential \
|
|
||||||
lib32z1 \
|
|
||||||
lib32ncurses6 \
|
|
||||||
lib32stdc++6 \
|
|
||||||
zlib1g-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Instalar Android SDK
|
|
||||||
ENV ANDROID_SDK_ROOT=/opt/android-sdk
|
|
||||||
ENV SDKMANAGER="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
|
|
||||||
|
|
||||||
RUN mkdir -p $ANDROID_SDK_ROOT/cmdline-tools && \
|
|
||||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O tools.zip && \
|
|
||||||
unzip -q tools.zip && \
|
|
||||||
mv cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools/latest && \
|
|
||||||
rm tools.zip
|
|
||||||
|
|
||||||
# Aceptar licencias
|
|
||||||
RUN yes | $SDKMANAGER --licenses
|
|
||||||
|
|
||||||
# Instalar componentes necesarios
|
|
||||||
RUN $SDKMANAGER "platform-tools" "platforms;android-33" "build-tools;33.0.2" "platforms;android-31"
|
|
||||||
|
|
||||||
# Instalar Gradle
|
|
||||||
ENV GRADLE_HOME=/opt/gradle
|
|
||||||
RUN wget -q https://services.gradle.org/distributions/gradle-8.2-bin.zip -O gradle.zip && \
|
|
||||||
unzip -q gradle.zip && \
|
|
||||||
mv gradle-8.2 $GRADLE_HOME && \
|
|
||||||
rm gradle.zip
|
|
||||||
|
|
||||||
ENV PATH=$PATH:$GRADLE_HOME/bin:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools
|
|
||||||
|
|
||||||
# Copiar proyecto
|
|
||||||
COPY . /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Dar permisos de ejecución a gradlew
|
|
||||||
RUN chmod +x ./gradlew
|
|
||||||
|
|
||||||
# Construir APK
|
|
||||||
RUN ./gradlew assembleRelease
|
|
||||||
|
|
||||||
# Comando para copiar APK a un volumen montado
|
|
||||||
CMD ["cp", "/app/app/build/outputs/apk/release/app-release.apk", "/output/StreamPlayer-v10.0.apk"]
|
|
||||||
328
README.md
@@ -1,306 +1,54 @@
|
|||||||
# 📺 StreamPlayer
|
## StreamPlayer Desktop (Windows)
|
||||||
|
|
||||||
[](https://android.com)
|
Este branch contiene únicamente la versión de escritorio desarrollada con **.NET 8 + Avalonia**. Replica todas las funciones del APK Android original (resolución de streams ofuscados, verificación remota de dispositivos, control de actualizaciones y reproducción protegida) pero genera un `.exe` listo para Windows.
|
||||||
[](https://www.oracle.com/java/)
|
|
||||||
[](https://android-developers.blogspot.com/)
|
|
||||||
[](LICENSE)
|
|
||||||
|
|
||||||
Aplicación Android reproductora de streaming con optimización DNS para mejor rendimiento y acceso a contenido multimedia.
|
### Estructura
|
||||||
|
|
||||||
## 🌟 Características
|
```
|
||||||
|
windows/StreamPlayer.Desktop/
|
||||||
- **▶️ Reproducción Streaming**: Reproductor de video streaming optimizado con ExoPlayer
|
├── App.axaml / App.axaml.cs
|
||||||
- **🌐 Optimización DNS**: Configuración automática de DNS de Google (8.8.8.8, 8.8.4.4) para mejor conectividad
|
├── Program.cs
|
||||||
- **🔍 Resolución de URL**: Sistema avanzado que resuelve URLs ofuscadas de streaming
|
├── AppVersion.cs
|
||||||
- **📱 Orientación Landscape**: Diseño optimizado para experiencia multimedia inmersiva
|
├── Models/ # ChannelSection, LiveEvent, UpdateInfo, etc.
|
||||||
- **⚡ Alto Rendimiento**: Implementación asíncrona para respuesta rápida
|
├── Services/ # StreamUrlResolver, UpdateService, DeviceRegistryService, WindowsDnsService…
|
||||||
- **🛡️ Manejo de Errores**: Sistema robusto de gestión de errores y estados
|
├── ViewModels/ # MainWindowViewModel con bloqueo previo y refrescos
|
||||||
|
├── Views/ # MainWindow, PlayerWindow (LibVLC), diálogos de update/bloqueo
|
||||||
## 📋 Requisitos
|
└── StreamPlayer.Desktop.csproj
|
||||||
|
|
||||||
- **Android SDK**: API 21 (Android 5.0) o superior
|
|
||||||
- **Target SDK**: API 33 (Android 13)
|
|
||||||
- **Permisos**:
|
|
||||||
- `INTERNET` - Acceso a streaming
|
|
||||||
- `ACCESS_NETWORK_STATE` - Verificación de conectividad
|
|
||||||
- `CHANGE_NETWORK_STATE` - Configuración de red
|
|
||||||
|
|
||||||
## 🏗️ Arquitectura
|
|
||||||
|
|
||||||
### Componentes Principales
|
|
||||||
|
|
||||||
- **MainActivity.java** (`/app/src/main/java/com/streamplayer/MainActivity.java`)
|
|
||||||
- Gestión del ciclo de vida del reproductor
|
|
||||||
- Configuración de ExoPlayer
|
|
||||||
- Manejo de estados (loading, error, reproducción)
|
|
||||||
|
|
||||||
- **StreamUrlResolver.java** (`/app/src/main/java/com/streamplayer/StreamUrlResolver.java`)
|
|
||||||
- Resolución de URLs ofuscadas
|
|
||||||
- Decodificación Base64
|
|
||||||
- Extracción de claves de JavaScript
|
|
||||||
|
|
||||||
- **DNSSetter.java** (`/app/src/main/java/com/streamplayer/DNSSetter.java`)
|
|
||||||
- Configuración de DNS de Google
|
|
||||||
- Optimización de red para streaming
|
|
||||||
- Pre-resolución de dominios
|
|
||||||
|
|
||||||
## 🚀 Instalación y Build
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
```bash
|
|
||||||
# Android SDK
|
|
||||||
# Java 8+
|
|
||||||
# Gradle 8.2+
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build con Gradle
|
### Requisitos
|
||||||
```bash
|
|
||||||
# Clone el repositorio
|
|
||||||
git clone https://gitea.cbcren.online/renato97/app.git
|
|
||||||
cd app
|
|
||||||
|
|
||||||
# Build APK debug
|
- .NET SDK 8.0
|
||||||
./gradlew assembleDebug
|
- Windows 10/11 x64
|
||||||
|
- Visual Studio 2022 (o `dotnet` CLI) con workloads “.NET desktop”.
|
||||||
|
- VLC runtimes incluidos vía `VideoLAN.LibVLC.Windows`.
|
||||||
|
|
||||||
# Build APK release
|
### Cómo compilar
|
||||||
./gradlew assembleRelease
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build con Docker
|
|
||||||
```bash
|
|
||||||
# Construir imagen
|
|
||||||
docker build -t streamplayer .
|
|
||||||
|
|
||||||
# Ejecutar build
|
|
||||||
docker run --rm -v $(pwd)/output:/output streamplayer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Script Alternativo
|
|
||||||
```bash
|
|
||||||
# Usar script de build
|
|
||||||
chmod +x build_apk.sh
|
|
||||||
./build_apk.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Control de Instalaciones y Actualizaciones
|
|
||||||
|
|
||||||
StreamPlayer ahora consulta automáticamente las releases públicas del repositorio Gitea y puede forzar o sugerir actualizaciones directamente desde la app. Para aprovecharlo:
|
|
||||||
|
|
||||||
1. Ejecuta un build nuevo incrementando `versionCode`/`versionName` en `app/build.gradle`.
|
|
||||||
2. Crea una release en Gitea asignando un tag como `v9.1` y sube el APK.
|
|
||||||
3. Adjunta un archivo `update-manifest.json` en la misma release (puedes partir de `release/update-manifest.example.json`).
|
|
||||||
|
|
||||||
### Formato de `update-manifest.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"versionCode": 91000,
|
|
||||||
"versionName": "9.1.0",
|
|
||||||
"minSupportedVersionCode": 90000,
|
|
||||||
"forceUpdate": false,
|
|
||||||
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.1/StreamPlayer-v9.1.apk",
|
|
||||||
"fileName": "StreamPlayer-v9.1.apk",
|
|
||||||
"sizeBytes": 12345678,
|
|
||||||
"notes": "Novedades destacadas que aparecerán en el diálogo dentro de la app"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `versionCode` / `versionName`: deben coincidir con el APK publicado.
|
|
||||||
- `minSupportedVersionCode`: define desde qué versión mínima se permite seguir usando la app (ideal para bloquear instalaciones antiguas).
|
|
||||||
- `forceUpdate`: si es `true` la app mostrará un diálogo sin opción de omitir.
|
|
||||||
- `downloadUrl` / `fileName`: apuntan al asset `.apk` publicado en Gitea (puede ser el enlace de descarga directo).
|
|
||||||
- `notes`: texto libre mostrado en el diálogo dentro de la app; si lo omitís se usará el cuerpo de la release.
|
|
||||||
|
|
||||||
Si por algún motivo olvidas subir el manifiesto, la app igualmente tomará el primer asset `.apk` de la release, pero no podrá forzar versiones mínimas.
|
|
||||||
|
|
||||||
### Dashboard de Dispositivos y Bloqueo Remoto
|
|
||||||
|
|
||||||
Para saber en qué equipo está instalada la app y bloquear el acceso cuando lo necesites, se incluye un dashboard liviano en `dashboard/`:
|
|
||||||
|
|
||||||
1. Instala dependencias y ejecuta el servidor:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd dashboard
|
git checkout windows-only
|
||||||
npm install
|
cd windows/StreamPlayer.Desktop
|
||||||
npm start # escucha en http://localhost:4000
|
dotnet restore
|
||||||
|
dotnet build -c Release
|
||||||
|
# Para distribuir:
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Copia `dashboard/config.example.json` a `dashboard/config.json` y completa `telegramBotToken` + `telegramChatId` (o usa variables de entorno `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID`).
|
El `.exe` resultante queda en `windows/StreamPlayer.Desktop/bin/Release/net8.0/win-x64/publish/`.
|
||||||
3. Ajusta `DEVICE_REGISTRY_URL` en `app/build.gradle` para apuntar al dominio/puerto donde despliegues el servidor (ya configurado como `http://194.163.191.200:4000`).
|
|
||||||
4. Distribuye el APK; cada instalación reportará `ANDROID_ID`, modelo, IP pública y país.
|
|
||||||
5. Entra a `http://TU_HOST:4000/` para ver el listado, asignar alias, bloquear/desbloquear o validar tokens.
|
|
||||||
|
|
||||||
El servidor guarda los datos en `dashboard/data/devices.json`, por lo que puedes versionarlo o respaldarlo fácilmente. Cada registro almacena:
|
### Características clave
|
||||||
|
|
||||||
- `deviceId`: `Settings.Secure.ANDROID_ID` del equipo
|
- **Resolución de stream**: `Services/StreamUrlResolver.cs` analiza el JavaScript ofuscado y reconstruye el HLS real (idéntico al app móvil).
|
||||||
- `deviceName`, `manufacturer`, `model`, `osVersion`
|
- **Reproducción**: `Views/PlayerWindow` usa LibVLC con los mismos headers/User-Agent del APK para evitar bloqueos de origen.
|
||||||
- `appVersionName`/`Code`
|
- **Verificación remota**: `DeviceRegistryService` sincroniza con tu dashboard y bloquea toda la UI hasta que el servidor permita el dispositivo.
|
||||||
- `ip`, `country` detectados automáticamente
|
- **Actualizaciones forzadas**: `UpdateService` consulta las releases de Gitea y puede abrir el browser o descargar la nueva versión.
|
||||||
- `firstSeen`, `lastSeen`, `blocked`, `notes`, `verification.status`
|
- **DNS de Google**: `WindowsDnsService` fuerza 8.8.8.8 / 8.8.4.4 solicitando elevación (UAC); si el usuario rechaza, se muestra el mensaje para configurar manualmente antes de iniciar el player.
|
||||||
|
|
||||||
Cuando presionas “Bloquear”, la app recibe la respuesta `{"blocked": true}` y muestra un diálogo irreversible hasta que lo habilites. Esto añade una capa adicional de control aparte del sistema de actualizaciones.
|
### Uso
|
||||||
|
|
||||||
### Flujo dentro de la app y tokens divididos
|
1. Compila o publica el `.exe`.
|
||||||
|
2. Ejecuta una vez como administrador para que el cambio de DNS quede aplicado.
|
||||||
|
3. Configura tus endpoints (`AppVersion.DeviceRegistryUrl`, `LatestReleaseApi`) si necesitas apuntar a otros servicios.
|
||||||
|
4. Distribuye el `.exe` y sube releases/manifest igual que con el APK.
|
||||||
|
|
||||||
- Cada vez que se abre `MainActivity` se consulta `https://gitea.cbcren.online/api/v1/repos/renato97/app/releases/latest`.
|
Este branch **no** incluye ningún archivo Android; es solo el código fuente de la versión Windows. Para la versión móvil sigue usando `main`.
|
||||||
- Si `versionCode` del servidor es mayor al instalado se muestra un diálogo para actualizar; el usuario puede abrir la release o descargarla directamente.
|
|
||||||
- Si `minSupportedVersionCode` es mayor al instalado la app bloqueará el uso hasta actualizar, cumpliendo con el requerimiento de controlar instalaciones.
|
|
||||||
- La descarga se gestiona con `DownloadManager` y, una vez completada, se lanza el instalador usando FileProvider.
|
|
||||||
- Mientras el dashboard mantenga un dispositivo "Pendiente" o "Bloqueado", la app muestra un diálogo con el motivo y la mitad del token que debe compartir la persona.
|
|
||||||
|
|
||||||
Cada instalación genera un token interno dividido en dos:
|
|
||||||
|
|
||||||
1. **Parte cliente**: se muestra en el diálogo del dispositivo bloqueado para que el usuario pueda copiarla.
|
|
||||||
2. **Parte admin**: llega al bot de Telegram configurado junto con la IP, país y datos del dispositivo.
|
|
||||||
|
|
||||||
Para autorizar un dispositivo pendiente:
|
|
||||||
|
|
||||||
1. Obtén la parte cliente desde el usuario (visible en pantalla).
|
|
||||||
2. Copia la parte admin del mensaje de Telegram.
|
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```
|
|
||||||
app/
|
|
||||||
├── src/main/
|
|
||||||
│ ├── java/com/streamplayer/
|
|
||||||
│ │ ├── MainActivity.java # Actividad principal
|
|
||||||
│ │ ├── StreamUrlResolver.java # Resolvedor de URLs
|
|
||||||
│ │ └── DNSSetter.java # Configuración DNS
|
|
||||||
│ ├── res/
|
|
||||||
│ │ ├── layout/
|
|
||||||
│ │ │ └── activity_main.xml # UI principal
|
|
||||||
│ │ ├── mipmap-*/ # Íconos de la app
|
|
||||||
│ │ ├── values/
|
|
||||||
│ │ │ ├── strings.xml # Cadenas de texto
|
|
||||||
│ │ │ ├── colors.xml # Colores
|
|
||||||
│ │ │ └── themes.xml # Temas
|
|
||||||
│ │ └── xml/ # Configuraciones
|
|
||||||
│ └── AndroidManifest.xml # Manifiesto Android
|
|
||||||
├── build.gradle # Configuración Gradle
|
|
||||||
└── proguard-rules.pro # Reglas ProGuard
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Configuración
|
|
||||||
|
|
||||||
### URL de Streaming
|
|
||||||
La aplicación está configurada por defecto para:
|
|
||||||
```
|
|
||||||
https://streamtpmedia.com/global2.php?stream=espn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuración DNS
|
|
||||||
```java
|
|
||||||
// DNS configurados automáticamente
|
|
||||||
String[] GOOGLE_DNS = {"8.8.8.8", "8.8.4.4"};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Dependencias Principales
|
|
||||||
|
|
||||||
- **ExoPlayer 2.18.7**: Motor de reproducción multimedia
|
|
||||||
- **AndroidX AppCompat 1.6.1**: Compatibilidad hacia atrás
|
|
||||||
- **ConstraintLayout 2.1.4**: Layout moderno y flexible
|
|
||||||
|
|
||||||
## 🛠️ Desarrollo
|
|
||||||
|
|
||||||
### Flujo de Reproducción
|
|
||||||
1. **MainActivity** inicializa y configura DNS de Google
|
|
||||||
2. **StreamUrlResolver** obtiene y decodifica la URL real del stream
|
|
||||||
3. **ExoPlayer** inicia la reproducción con la URL resuelta
|
|
||||||
4. UI actualiza estados (loading, playing, error)
|
|
||||||
|
|
||||||
### Características Técnicas
|
|
||||||
- **Threading**: Operaciones de red en background thread
|
|
||||||
- **Memory Management**: Proper lifecycle management de ExoPlayer
|
|
||||||
- **Error Handling**: Captura y display de errores al usuario
|
|
||||||
- **Network Optimization**: Configuración DNS específica para streaming
|
|
||||||
|
|
||||||
## 📊 Build Configuration
|
|
||||||
|
|
||||||
| Atributo | Valor |
|
|
||||||
|----------|-------|
|
|
||||||
| `applicationId` | `com.streamplayer` |
|
|
||||||
| `minSdk` | 21 |
|
|
||||||
| `targetSdk` | 33 |
|
|
||||||
| `versionCode` | 90000 |
|
|
||||||
| `versionName` | "9.0.0" |
|
|
||||||
| `compileSdk` | 33 |
|
|
||||||
|
|
||||||
## 🔐 Permisos y Seguridad
|
|
||||||
|
|
||||||
La aplicación requiere los siguientes permisos:
|
|
||||||
- ✅ `INTERNET` - Para streaming de contenido
|
|
||||||
- ✅ `ACCESS_NETWORK_STATE` - Para verificar conectividad
|
|
||||||
- ✅ `CHANGE_NETWORK_STATE` - Para optimización de red
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Problemas Comunes
|
|
||||||
|
|
||||||
**Error de Conexión**
|
|
||||||
- Verificar conexión a internet
|
|
||||||
- Confirmar configuración DNS
|
|
||||||
- Revisar disponibilidad del servicio de streaming
|
|
||||||
|
|
||||||
**Error de Reproducción**
|
|
||||||
- Validar formato de URL
|
|
||||||
- Verificar permisos de red
|
|
||||||
- Revisar logs de ExoPlayer
|
|
||||||
|
|
||||||
**Build Fail**
|
|
||||||
```bash
|
|
||||||
# Limpiar proyecto
|
|
||||||
./gradlew clean
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
./gradlew build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Logs y Debug
|
|
||||||
|
|
||||||
La aplicación incluye console logging para:
|
|
||||||
- Configuración DNS
|
|
||||||
- Resolución de URLs
|
|
||||||
- Estados del reproductor
|
|
||||||
- Errores de red
|
|
||||||
|
|
||||||
## 🤝 Contribución
|
|
||||||
|
|
||||||
1. Fork del repositorio
|
|
||||||
2. Feature branch (`git checkout -b feature/NuevaCaracteristica`)
|
|
||||||
3. Commit cambios (`git commit -m 'Add feature'`)
|
|
||||||
4. Push al branch (`git push origin feature/NuevaCaracteristica`)
|
|
||||||
5. Pull Request
|
|
||||||
|
|
||||||
## 📄 Licencia
|
|
||||||
|
|
||||||
Este proyecto está licenciado bajo la Licencia MIT - ver archivo [LICENSE](LICENSE) para detalles.
|
|
||||||
|
|
||||||
## 👨💻 Autor
|
|
||||||
|
|
||||||
**renato97** - [Gitea Profile](https://gitea.cbcren.online/renato97)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
⚠️ **Disclaimer**: Esta aplicación es para fines educativos y de demostración. El usuario es responsable de cumplir con los términos de servicio de las plataformas de streaming utilizadas.
|
|
||||||
|
|
||||||
## 📞 Soporte
|
|
||||||
|
|
||||||
Para soporte y preguntas:
|
|
||||||
- 📧 Crear un issue en el repositorio
|
|
||||||
- 💬 Comentarios en el código
|
|
||||||
- 📱 Testing en dispositivos reales recomendado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🔗 Repositorio**: https://gitea.cbcren.online/renato97/app
|
|
||||||
|
|||||||
BIN
StreamPlayer.apk
3
app/.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
6
app/.idea/AndroidProjectSystem.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AndroidProjectSystem">
|
|
||||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
1017
app/.idea/caches/deviceStreaming.xml
generated
13
app/.idea/deviceManager.xml
generated
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DeviceTable">
|
|
||||||
<option name="columnSorters">
|
|
||||||
<list>
|
|
||||||
<ColumnSorterState>
|
|
||||||
<option name="column" value="Name" />
|
|
||||||
<option name="order" value="ASCENDING" />
|
|
||||||
</ColumnSorterState>
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
app/.idea/gradle.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="gradleJvm" value="ms-11" />
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
10
app/.idea/migrations.xml
generated
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectMigrations">
|
|
||||||
<option name="MigrateToGradleLocalJavaHome">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
10
app/.idea/misc.xml
generated
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="ProjectRootManager">
|
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectType">
|
|
||||||
<option name="id" value="Android" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
17
app/.idea/runConfigurations.xml
generated
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RunConfigurationProducerService">
|
|
||||||
<option name="ignoredProducers">
|
|
||||||
<set>
|
|
||||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
|
||||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace "com.streamplayer"
|
|
||||||
compileSdk 35
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.streamplayer"
|
|
||||||
minSdk 21
|
|
||||||
targetSdk 35
|
|
||||||
versionCode 100108
|
|
||||||
versionName "10.1.8"
|
|
||||||
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
checkReleaseBuilds = false
|
|
||||||
abortOnError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
|
||||||
resources {
|
|
||||||
excludes += 'META-INF/com.android.tools/proguard/coroutines.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
|
||||||
|
|
||||||
// Media3 para reproduccion de video (Android TV optimizado)
|
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.5.0'
|
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.5.0'
|
|
||||||
implementation 'androidx.media3:media3-datasource-okhttp:1.5.0'
|
|
||||||
implementation 'androidx.media3:media3-ui:1.5.0'
|
|
||||||
implementation 'androidx.media3:media3-ui-leanback:1.5.0'
|
|
||||||
implementation 'androidx.media3:media3-session:1.5.0'
|
|
||||||
|
|
||||||
// OkHttp con DNS over HTTPS
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 28
|
|
||||||
buildToolsVersion "28.0.3"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.streamplayer"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 28
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.8.4'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
|
|
||||||
}
|
|
||||||
24
app/proguard-rules.pro
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
|
||||||
-keep class com.google.android.exoplayer2.** { *; }
|
|
||||||
-keep class com.streamplayer.** { *; }
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<!-- Permisos necesarios -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.software.leanback"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.touchscreen"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:banner="@drawable/banner_streamplayer"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.StreamPlayer"
|
|
||||||
android:usesCleartextTraffic="true">
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.fileprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".PlayerActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:screenOrientation="landscape" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ChannelAdapter extends RecyclerView.Adapter<ChannelAdapter.ChannelViewHolder> {
|
|
||||||
|
|
||||||
public interface OnChannelClickListener {
|
|
||||||
void onChannelClick(StreamChannel channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final List<StreamChannel> channels = new ArrayList<>();
|
|
||||||
private final OnChannelClickListener listener;
|
|
||||||
|
|
||||||
public ChannelAdapter(OnChannelClickListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ChannelViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_channel, parent, false);
|
|
||||||
return new ChannelViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ChannelViewHolder holder, int position) {
|
|
||||||
StreamChannel channel = channels.get(position);
|
|
||||||
holder.name.setText(channel.getName());
|
|
||||||
holder.icon.setImageResource(R.drawable.ic_channel_default);
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onChannelClick(channel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.itemView.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
float scale = hasFocus ? 1.08f : 1f;
|
|
||||||
v.animate().scaleX(scale).scaleY(scale).setDuration(120).start();
|
|
||||||
v.setSelected(hasFocus);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return channels.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ChannelViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
final ImageView icon;
|
|
||||||
final TextView name;
|
|
||||||
|
|
||||||
ChannelViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
icon = itemView.findViewById(R.id.channel_icon);
|
|
||||||
name = itemView.findViewById(R.id.channel_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void submitList(List<StreamChannel> newChannels) {
|
|
||||||
channels.clear();
|
|
||||||
if (newChannels != null) {
|
|
||||||
channels.addAll(newChannels);
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class ChannelRepository {
|
|
||||||
|
|
||||||
private static final List<StreamChannel> CHANNELS = createChannels();
|
|
||||||
|
|
||||||
private static List<StreamChannel> createChannels() {
|
|
||||||
List<StreamChannel> channels = new ArrayList<>(Arrays.asList(
|
|
||||||
new StreamChannel("ESPN", "https://streamtp10.com/global2.php?stream=espn"),
|
|
||||||
new StreamChannel("ESPN 2", "https://streamtp10.com/global2.php?stream=espn2"),
|
|
||||||
new StreamChannel("ESPN 3", "https://streamtp10.com/global2.php?stream=espn3"),
|
|
||||||
new StreamChannel("ESPN 4", "https://streamtp10.com/global2.php?stream=espn4"),
|
|
||||||
new StreamChannel("ESPN 3 MX", "https://streamtp10.com/global2.php?stream=espn3mx"),
|
|
||||||
new StreamChannel("ESPN 5", "https://streamtp10.com/global2.php?stream=espn5"),
|
|
||||||
new StreamChannel("Fox Sports 3 MX", "https://streamtp10.com/global2.php?stream=foxsports3mx"),
|
|
||||||
new StreamChannel("ESPN 6", "https://streamtp10.com/global2.php?stream=espn6"),
|
|
||||||
new StreamChannel("Fox Sports MX", "https://streamtp10.com/global2.php?stream=foxsportsmx"),
|
|
||||||
new StreamChannel("ESPN 7", "https://streamtp10.com/global2.php?stream=espn7"),
|
|
||||||
new StreamChannel("Azteca Deportes", "https://streamtp10.com/global2.php?stream=azteca_deportes"),
|
|
||||||
new StreamChannel("Win Plus", "https://streamtp10.com/global2.php?stream=winplus"),
|
|
||||||
new StreamChannel("DAZN 1", "https://streamtp10.com/global2.php?stream=dazn1"),
|
|
||||||
new StreamChannel("Win Plus 2", "https://streamtp10.com/global2.php?stream=winplus2"),
|
|
||||||
new StreamChannel("DAZN 2", "https://streamtp10.com/global2.php?stream=dazn2"),
|
|
||||||
new StreamChannel("Win Sports", "https://streamtp10.com/global2.php?stream=winsports"),
|
|
||||||
new StreamChannel("DAZN LaLiga", "https://streamtp10.com/global2.php?stream=dazn_laliga"),
|
|
||||||
new StreamChannel("Win Plus Online 1", "https://streamtp10.com/global2.php?stream=winplusonline1"),
|
|
||||||
new StreamChannel("Caracol TV", "https://streamtp10.com/global2.php?stream=caracoltv"),
|
|
||||||
new StreamChannel("Fox 1 AR", "https://streamtp10.com/global2.php?stream=fox1ar"),
|
|
||||||
new StreamChannel("Fox 2 USA", "https://streamtp10.com/global2.php?stream=fox_2_usa"),
|
|
||||||
new StreamChannel("Fox 2 AR", "https://streamtp10.com/global2.php?stream=fox2ar"),
|
|
||||||
new StreamChannel("TNT 1 GB", "https://streamtp10.com/global2.php?stream=tnt_1_gb"),
|
|
||||||
new StreamChannel("TNT 2 GB", "https://streamtp10.com/global2.php?stream=tnt_2_gb"),
|
|
||||||
new StreamChannel("Fox 3 AR", "https://streamtp10.com/global2.php?stream=fox3ar"),
|
|
||||||
new StreamChannel("Universo USA", "https://streamtp10.com/global2.php?stream=universo_usa"),
|
|
||||||
new StreamChannel("DSports", "https://streamtp10.com/global2.php?stream=dsports"),
|
|
||||||
new StreamChannel("Univision USA", "https://streamtp10.com/global2.php?stream=univision_usa"),
|
|
||||||
new StreamChannel("DSports 2", "https://streamtp10.com/global2.php?stream=dsports2"),
|
|
||||||
new StreamChannel("Fox Deportes USA", "https://streamtp10.com/global2.php?stream=fox_deportes_usa"),
|
|
||||||
new StreamChannel("DSports Plus", "https://streamtp10.com/global2.php?stream=dsportsplus"),
|
|
||||||
new StreamChannel("Fox Sports 2 MX", "https://streamtp10.com/global2.php?stream=foxsports2mx"),
|
|
||||||
new StreamChannel("TNT Sports Chile", "https://streamtp10.com/global2.php?stream=tntsportschile"),
|
|
||||||
new StreamChannel("Fox Sports Premium", "https://streamtp10.com/global2.php?stream=foxsportspremium"),
|
|
||||||
new StreamChannel("TNT Sports", "https://streamtp10.com/global2.php?stream=tntsports"),
|
|
||||||
new StreamChannel("ESPN MX", "https://streamtp10.com/global2.php?stream=espnmx"),
|
|
||||||
new StreamChannel("ESPN Premium", "https://streamtp10.com/global2.php?stream=espnpremium"),
|
|
||||||
new StreamChannel("ESPN 2 MX", "https://streamtp10.com/global2.php?stream=espn2mx"),
|
|
||||||
new StreamChannel("TyC Sports", "https://streamtp10.com/global2.php?stream=tycsports"),
|
|
||||||
new StreamChannel("TUDN USA", "https://streamtp10.com/global2.php?stream=tudn_usa"),
|
|
||||||
new StreamChannel("Telefe", "https://streamtp10.com/global2.php?stream=telefe"),
|
|
||||||
new StreamChannel("TNT 3 GB", "https://streamtp10.com/global2.php?stream=tnt_3_gb"),
|
|
||||||
new StreamChannel("TV Pública", "https://streamtp10.com/global2.php?stream=tv_publica"),
|
|
||||||
new StreamChannel("Fox 1 USA", "https://streamtp10.com/global2.php?stream=fox_1_usa"),
|
|
||||||
new StreamChannel("Liga 1 Max", "https://streamtp10.com/global2.php?stream=liga1max"),
|
|
||||||
new StreamChannel("Gol TV", "https://streamtp10.com/global2.php?stream=goltv"),
|
|
||||||
new StreamChannel("VTV Plus", "https://streamtp10.com/global2.php?stream=vtvplus"),
|
|
||||||
new StreamChannel("ESPN Deportes", "https://streamtp10.com/global2.php?stream=espndeportes"),
|
|
||||||
new StreamChannel("Gol Perú", "https://streamtp10.com/global2.php?stream=golperu"),
|
|
||||||
new StreamChannel("TNT 4 GB", "https://streamtp10.com/global2.php?stream=tnt_4_gb"),
|
|
||||||
new StreamChannel("SportTV BR 1", "https://streamtp10.com/global2.php?stream=sporttvbr1"),
|
|
||||||
new StreamChannel("SportTV BR 2", "https://streamtp10.com/global2.php?stream=sporttvbr2"),
|
|
||||||
new StreamChannel("SportTV BR 3", "https://streamtp10.com/global2.php?stream=sporttvbr3"),
|
|
||||||
new StreamChannel("Premiere 1", "https://streamtp10.com/global2.php?stream=premiere1"),
|
|
||||||
new StreamChannel("Premiere 2", "https://streamtp10.com/global2.php?stream=premiere2"),
|
|
||||||
new StreamChannel("Premiere 3", "https://streamtp10.com/global2.php?stream=premiere3"),
|
|
||||||
new StreamChannel("ESPN NL 1", "https://streamtp10.com/global2.php?stream=espn_nl1"),
|
|
||||||
new StreamChannel("ESPN NL 2", "https://streamtp10.com/global2.php?stream=espn_nl2"),
|
|
||||||
new StreamChannel("ESPN NL 3", "https://streamtp10.com/global2.php?stream=espn_nl3"),
|
|
||||||
new StreamChannel("Caliente TV MX", "https://streamtp10.com/global2.php?stream=calientetvmx"),
|
|
||||||
new StreamChannel("USA Network", "https://streamtp10.com/global2.php?stream=usa_network"),
|
|
||||||
new StreamChannel("TyC Internacional", "https://streamtp10.com/global2.php?stream=tycinternacional"),
|
|
||||||
new StreamChannel("Canal 5 MX", "https://streamtp10.com/global2.php?stream=canal5mx"),
|
|
||||||
new StreamChannel("TUDN MX", "https://streamtp10.com/global2.php?stream=TUDNMX"),
|
|
||||||
new StreamChannel("FUTV", "https://streamtp10.com/global2.php?stream=futv"),
|
|
||||||
new StreamChannel("LaLiga Hypermotion", "https://streamtp10.com/global2.php?stream=laligahypermotion")
|
|
||||||
));
|
|
||||||
channels.sort(Comparator.comparing(StreamChannel::getName, String.CASE_INSENSITIVE_ORDER));
|
|
||||||
return Collections.unmodifiableList(channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChannelRepository() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<StreamChannel> getChannels() {
|
|
||||||
return CHANNELS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.RequestBody;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Informa al dashboard qué dispositivos tienen instalada la app y permite bloquearlos remotamente.
|
|
||||||
*/
|
|
||||||
public class DeviceRegistry {
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
void onAllowed();
|
|
||||||
|
|
||||||
void onBlocked(String reason, String tokenPart);
|
|
||||||
|
|
||||||
void onError(String message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String TAG = "DeviceRegistry";
|
|
||||||
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
|
||||||
|
|
||||||
private final Context appContext;
|
|
||||||
private final OkHttpClient httpClient;
|
|
||||||
private final ExecutorService executorService;
|
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
public DeviceRegistry(Context context) {
|
|
||||||
this.appContext = context.getApplicationContext();
|
|
||||||
// Usar NetworkUtils para obtener cliente con DNS over HTTPS configurado
|
|
||||||
this.httpClient = NetworkUtils.getClient();
|
|
||||||
this.executorService = Executors.newSingleThreadExecutor();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void syncDevice(Callback callback) {
|
|
||||||
if (TextUtils.isEmpty(BuildConfig.DEVICE_REGISTRY_URL)) {
|
|
||||||
postAllowed(callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
executorService.execute(() -> {
|
|
||||||
try {
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("deviceId", getDeviceId());
|
|
||||||
payload.put("deviceName", Build.MODEL);
|
|
||||||
payload.put("model", Build.MODEL);
|
|
||||||
payload.put("manufacturer", capitalize(Build.MANUFACTURER));
|
|
||||||
payload.put("osVersion", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ")");
|
|
||||||
payload.put("appVersionName", BuildConfig.VERSION_NAME);
|
|
||||||
payload.put("appVersionCode", BuildConfig.VERSION_CODE);
|
|
||||||
|
|
||||||
String endpoint = sanitizeBaseUrl(BuildConfig.DEVICE_REGISTRY_URL) + "/api/devices/register";
|
|
||||||
RequestBody body = RequestBody.create(payload.toString(), JSON);
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(endpoint)
|
|
||||||
.post(body)
|
|
||||||
.build();
|
|
||||||
try (Response response = httpClient.newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
|
||||||
throw new IOException("HTTP " + response.code());
|
|
||||||
}
|
|
||||||
String responseText = response.body().string();
|
|
||||||
|
|
||||||
// Validar que no sea HTML antes de parsear
|
|
||||||
if (responseText != null) {
|
|
||||||
String trimmed = responseText.trim();
|
|
||||||
if (trimmed.startsWith("<!") || trimmed.startsWith("<html")) {
|
|
||||||
throw new IOException("El servidor devolvió HTML en lugar de JSON");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject json = new JSONObject(responseText);
|
|
||||||
JSONObject deviceJson = json.optJSONObject("device");
|
|
||||||
JSONObject verificationJson = json.optJSONObject("verification");
|
|
||||||
boolean blocked = json.optBoolean("blocked", false);
|
|
||||||
String reason = json.optString("message");
|
|
||||||
if (TextUtils.isEmpty(reason) && deviceJson != null) {
|
|
||||||
reason = deviceJson.optString("notes", "");
|
|
||||||
}
|
|
||||||
String tokenPart = "";
|
|
||||||
if (verificationJson != null) {
|
|
||||||
boolean verificationRequired = verificationJson.optBoolean("required", false);
|
|
||||||
blocked = blocked || verificationRequired;
|
|
||||||
tokenPart = verificationJson.optString("clientTokenPart", "");
|
|
||||||
}
|
|
||||||
if (blocked) {
|
|
||||||
postBlocked(callback, reason, tokenPart);
|
|
||||||
} else {
|
|
||||||
postAllowed(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException | JSONException e) {
|
|
||||||
Log.w(TAG, "Device sync error", e);
|
|
||||||
postError(callback, e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sanitizeBaseUrl(String base) {
|
|
||||||
if (TextUtils.isEmpty(base)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (base.endsWith("/")) {
|
|
||||||
return base.substring(0, base.length() - 1);
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDeviceId() {
|
|
||||||
String id = Settings.Secure.getString(appContext.getContentResolver(),
|
|
||||||
Settings.Secure.ANDROID_ID);
|
|
||||||
if (TextUtils.isEmpty(id)) {
|
|
||||||
id = Build.MODEL + "-" + Build.BOARD + "-" + BuildConfig.VERSION_CODE;
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String capitalize(String value) {
|
|
||||||
if (TextUtils.isEmpty(value)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return value.substring(0, 1).toUpperCase(Locale.getDefault())
|
|
||||||
+ value.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postAllowed(Callback callback) {
|
|
||||||
if (callback == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainHandler.post(callback::onAllowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postBlocked(Callback callback, String reason, String tokenPart) {
|
|
||||||
if (callback == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String reasonText = reason == null ? "" : reason;
|
|
||||||
String token = tokenPart == null ? "" : tokenPart;
|
|
||||||
mainHandler.post(() -> callback.onBlocked(reasonText, token));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postError(Callback callback, String message) {
|
|
||||||
if (callback == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainHandler.post(() -> callback.onError(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class EventAdapter extends RecyclerView.Adapter<EventAdapter.EventViewHolder> {
|
|
||||||
|
|
||||||
public interface OnEventClickListener {
|
|
||||||
void onEventClick(EventItem event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final List<EventItem> events = new ArrayList<>();
|
|
||||||
private final OnEventClickListener listener;
|
|
||||||
|
|
||||||
public EventAdapter(OnEventClickListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void submitList(List<EventItem> newEvents) {
|
|
||||||
events.clear();
|
|
||||||
events.addAll(newEvents);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public EventViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_event, parent, false);
|
|
||||||
return new EventViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull EventViewHolder holder, int position) {
|
|
||||||
EventItem event = events.get(position);
|
|
||||||
holder.title.setText(event.getTitle());
|
|
||||||
holder.time.setText(event.getTime());
|
|
||||||
holder.channel.setText(event.getChannelName());
|
|
||||||
holder.status.setText(buildStatusText(event));
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onEventClick(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return events.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class EventViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
final TextView title;
|
|
||||||
final TextView time;
|
|
||||||
final TextView channel;
|
|
||||||
final TextView status;
|
|
||||||
|
|
||||||
EventViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
title = itemView.findViewById(R.id.event_title);
|
|
||||||
time = itemView.findViewById(R.id.event_time);
|
|
||||||
channel = itemView.findViewById(R.id.event_channel);
|
|
||||||
status = itemView.findViewById(R.id.event_status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildStatusText(EventItem event) {
|
|
||||||
long start = event.getStartMillis();
|
|
||||||
if (start <= 0) {
|
|
||||||
return event.getStatus();
|
|
||||||
}
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
long diff = start - now;
|
|
||||||
if (diff > 0) {
|
|
||||||
long hours = diff / 3600000;
|
|
||||||
long minutes = (diff % 3600000) / 60000;
|
|
||||||
if (hours > 0) {
|
|
||||||
return String.format(Locale.getDefault(), "En %dh %02dm", hours, minutes);
|
|
||||||
} else {
|
|
||||||
return String.format(Locale.getDefault(), "En %d min", Math.max(1, minutes));
|
|
||||||
}
|
|
||||||
} else if (Math.abs(diff) <= 2 * 3600000L) {
|
|
||||||
return "En vivo";
|
|
||||||
} else {
|
|
||||||
return "Finalizado";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
public class EventItem {
|
|
||||||
private final String title;
|
|
||||||
private final String time;
|
|
||||||
private final String category;
|
|
||||||
private final String status;
|
|
||||||
private final String pageUrl;
|
|
||||||
private final String channelName;
|
|
||||||
private final long startMillis;
|
|
||||||
|
|
||||||
public EventItem(String title, String time, String category, String status, String pageUrl, String channelName, long startMillis) {
|
|
||||||
this.title = title;
|
|
||||||
this.time = time;
|
|
||||||
this.category = category;
|
|
||||||
this.status = status;
|
|
||||||
this.pageUrl = pageUrl;
|
|
||||||
this.channelName = channelName;
|
|
||||||
this.startMillis = startMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTime() {
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCategory() {
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPageUrl() {
|
|
||||||
return pageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getChannelName() {
|
|
||||||
return channelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getStartMillis() {
|
|
||||||
return startMillis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DateTimeParseException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class EventRepository {
|
|
||||||
|
|
||||||
private static final String PREFS_NAME = "events_cache";
|
|
||||||
private static final String KEY_JSON = "json";
|
|
||||||
private static final String KEY_TIMESTAMP = "timestamp";
|
|
||||||
private static final long CACHE_DURATION = 24L * 60 * 60 * 1000; // 24 horas
|
|
||||||
|
|
||||||
// URL única para eventos (actualizado para evitar bloqueos de ISP)
|
|
||||||
private static final String EVENTS_URL = "https://streamtp10.com/eventos.json";
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
void onSuccess(List<EventItem> events);
|
|
||||||
|
|
||||||
void onError(String message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadEvents(Context context, boolean forceRefresh, Callback callback) {
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
long last = prefs.getLong(KEY_TIMESTAMP, 0);
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
if (!forceRefresh && now - last < CACHE_DURATION) {
|
|
||||||
String cachedJson = prefs.getString(KEY_JSON, null);
|
|
||||||
if (cachedJson != null) {
|
|
||||||
try {
|
|
||||||
callback.onSuccess(parseEvents(cachedJson));
|
|
||||||
return;
|
|
||||||
} catch (JSONException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
String json = downloadJson(context);
|
|
||||||
List<EventItem> events = parseEvents(json);
|
|
||||||
prefs.edit().putString(KEY_JSON, json).putLong(KEY_TIMESTAMP, System.currentTimeMillis()).apply();
|
|
||||||
callback.onSuccess(events);
|
|
||||||
} catch (IOException | JSONException e) {
|
|
||||||
String cachedJson = prefs.getString(KEY_JSON, null);
|
|
||||||
if (cachedJson != null) {
|
|
||||||
try {
|
|
||||||
callback.onSuccess(parseEvents(cachedJson));
|
|
||||||
return;
|
|
||||||
} catch (JSONException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback.onError(e.getMessage() != null ? e.getMessage() : "Error desconocido");
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String downloadJson(Context context) throws IOException {
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(EVENTS_URL)
|
|
||||||
.header("User-Agent", NetworkUtils.getUserAgent())
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (Response response = NetworkUtils.getClient().newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
throw new IOException("Error HTTP " + response.code() + ": " + response.message());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.body() == null) {
|
|
||||||
throw new IOException("Respuesta vacía del servidor");
|
|
||||||
}
|
|
||||||
|
|
||||||
String contentType = response.header("Content-Type");
|
|
||||||
// Permitir json o text/plain (Raw de Gitea a veces es text/plain)
|
|
||||||
if (contentType != null && !contentType.contains("json") && !contentType.contains("text/plain")) {
|
|
||||||
// Aceptamos text/plain también por flexibilidad
|
|
||||||
}
|
|
||||||
|
|
||||||
String responseBody = response.body().string();
|
|
||||||
|
|
||||||
// Validar que no sea HTML
|
|
||||||
if (responseBody.trim().startsWith("<!") || responseBody.trim().startsWith("<html")) {
|
|
||||||
throw new IOException("El servidor devolvió HTML en lugar de JSON. La URL del endpoint puede estar incorrecta o el servidor tiene problemas.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseBody;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<EventItem> parseEvents(String json) throws JSONException {
|
|
||||||
if (json == null || json.trim().isEmpty()) {
|
|
||||||
throw new JSONException("La respuesta está vacía");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validar que no sea HTML antes de parsear
|
|
||||||
String trimmed = json.trim();
|
|
||||||
if (trimmed.startsWith("<!") || trimmed.startsWith("<html")) {
|
|
||||||
throw new JSONException("Se recibió HTML en lugar de JSON");
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONArray array = new JSONArray(json);
|
|
||||||
List<EventItem> events = new ArrayList<>();
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
|
|
||||||
|
|
||||||
for (int i = 0; i < array.length(); i++) {
|
|
||||||
JSONObject obj = array.getJSONObject(i);
|
|
||||||
String title = obj.optString("title");
|
|
||||||
String time = obj.optString("time");
|
|
||||||
String category = obj.optString("category");
|
|
||||||
String status = obj.optString("status");
|
|
||||||
String link = obj.optString("link");
|
|
||||||
String normalized = normalizeLink(link);
|
|
||||||
|
|
||||||
// Ajustar hora: la web muestra hora de España, Argentina es +2 horas
|
|
||||||
String displayTime = time;
|
|
||||||
try {
|
|
||||||
if (time != null && !time.isEmpty()) {
|
|
||||||
LocalTime localTime = LocalTime.parse(time.trim(), formatter);
|
|
||||||
LocalTime adjustedTime = localTime.plusHours(2);
|
|
||||||
displayTime = adjustedTime.format(formatter);
|
|
||||||
}
|
|
||||||
} catch (DateTimeParseException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
long startMillis = parseEventTime(time);
|
|
||||||
events.add(new EventItem(title, displayTime, category, status, normalized, extractChannelName(link), startMillis));
|
|
||||||
}
|
|
||||||
return Collections.unmodifiableList(events);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeLink(String link) {
|
|
||||||
if (link == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
// Actualizado a streamtp10.com
|
|
||||||
String updated = link.replace("streamtpmedia.com", "streamtp10.com")
|
|
||||||
.replace("streamtpcloud.com", "streamtp10.com");
|
|
||||||
return updated.replace("global1.php", "global2.php");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractChannelName(String link) {
|
|
||||||
if (link == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
int idx = link.indexOf("stream=");
|
|
||||||
if (idx == -1) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return link.substring(idx + 7).replace("_", " ").toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseEventTime(String time) {
|
|
||||||
if (time == null || time.isEmpty()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
|
|
||||||
LocalTime localTime = LocalTime.parse(time.trim(), formatter);
|
|
||||||
// Ajustar hora: la web muestra hora de España, Argentina es +2 horas
|
|
||||||
LocalTime adjustedTime = localTime.plusHours(2);
|
|
||||||
ZoneId zone = ZoneId.of("America/Argentina/Buenos_Aires");
|
|
||||||
LocalDate today = LocalDate.now(zone);
|
|
||||||
ZonedDateTime start = ZonedDateTime.of(LocalDateTime.of(today, adjustedTime), zone);
|
|
||||||
ZonedDateTime now = ZonedDateTime.now(zone);
|
|
||||||
if (start.isBefore(now.minusHours(12))) {
|
|
||||||
start = start.plusDays(1);
|
|
||||||
}
|
|
||||||
return start.toInstant().toEpochMilli();
|
|
||||||
} catch (DateTimeParseException e) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
private RecyclerView sectionList;
|
|
||||||
private RecyclerView contentList;
|
|
||||||
private ProgressBar loadingIndicator;
|
|
||||||
private TextView messageView;
|
|
||||||
private TextView contentTitle;
|
|
||||||
private Button refreshButton;
|
|
||||||
|
|
||||||
private ChannelAdapter channelAdapter;
|
|
||||||
private EventAdapter eventAdapter;
|
|
||||||
private EventRepository eventRepository;
|
|
||||||
private SectionAdapter sectionAdapter;
|
|
||||||
private GridLayoutManager channelLayoutManager;
|
|
||||||
private LinearLayoutManager eventLayoutManager;
|
|
||||||
private final List<EventItem> cachedEvents = new ArrayList<>();
|
|
||||||
private List<SectionEntry> sections;
|
|
||||||
private SectionEntry currentSection;
|
|
||||||
private UpdateManager updateManager;
|
|
||||||
private AlertDialog updateDialog;
|
|
||||||
private AlertDialog blockedDialog;
|
|
||||||
private DeviceRegistry deviceRegistry;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
|
|
||||||
sectionList = findViewById(R.id.section_list);
|
|
||||||
contentList = findViewById(R.id.content_list);
|
|
||||||
loadingIndicator = findViewById(R.id.loading_indicator);
|
|
||||||
messageView = findViewById(R.id.message_view);
|
|
||||||
contentTitle = findViewById(R.id.content_title);
|
|
||||||
refreshButton = findViewById(R.id.refresh_button);
|
|
||||||
|
|
||||||
refreshButton.setOnClickListener(v -> {
|
|
||||||
loadEvents(true);
|
|
||||||
Toast.makeText(this, "Actualizando eventos...", Toast.LENGTH_SHORT).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
channelAdapter = new ChannelAdapter(
|
|
||||||
channel -> openPlayer(channel.getName(), channel.getPageUrl()));
|
|
||||||
eventAdapter = new EventAdapter(event -> openPlayer(event.getTitle(), event.getPageUrl()));
|
|
||||||
eventRepository = new EventRepository();
|
|
||||||
channelLayoutManager = new GridLayoutManager(this, getSpanCount());
|
|
||||||
eventLayoutManager = new LinearLayoutManager(this) {
|
|
||||||
@Override
|
|
||||||
public View onInterceptFocusSearch(View focused, int direction) {
|
|
||||||
if (direction == View.FOCUS_DOWN) {
|
|
||||||
int pos = getPosition(focused);
|
|
||||||
if (pos == getItemCount() - 1) {
|
|
||||||
return focused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onInterceptFocusSearch(focused, direction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sections = buildSections();
|
|
||||||
sectionList.setLayoutManager(new LinearLayoutManager(this));
|
|
||||||
sectionAdapter = new SectionAdapter(getSectionTitles(), this::selectSection);
|
|
||||||
sectionList.setAdapter(sectionAdapter);
|
|
||||||
|
|
||||||
selectSection(0);
|
|
||||||
|
|
||||||
updateManager = new UpdateManager(this);
|
|
||||||
updateManager.checkForUpdates(new UpdateManager.UpdateCallback() {
|
|
||||||
@Override
|
|
||||||
public void onUpdateAvailable(UpdateManager.UpdateInfo info) {
|
|
||||||
handleUpdateInfo(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpToDate() {
|
|
||||||
// Nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(String message) {
|
|
||||||
Toast.makeText(MainActivity.this,
|
|
||||||
getString(R.string.update_error_checking, message),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
deviceRegistry = new DeviceRegistry(this);
|
|
||||||
deviceRegistry.syncDevice(new DeviceRegistry.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onAllowed() {
|
|
||||||
// Device authorized, continue normally.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBlocked(String reason, String tokenPart) {
|
|
||||||
showBlockedDialog(reason, tokenPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(String message) {
|
|
||||||
if (!TextUtils.isEmpty(message)) {
|
|
||||||
Toast.makeText(MainActivity.this,
|
|
||||||
getString(R.string.device_registry_error, message),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (updateManager != null) {
|
|
||||||
updateManager.resumePendingInstall(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if (updateDialog != null && updateDialog.isShowing()) {
|
|
||||||
updateDialog.dismiss();
|
|
||||||
}
|
|
||||||
if (blockedDialog != null && blockedDialog.isShowing()) {
|
|
||||||
blockedDialog.dismiss();
|
|
||||||
}
|
|
||||||
if (updateManager != null) {
|
|
||||||
updateManager.release();
|
|
||||||
}
|
|
||||||
if (deviceRegistry != null) {
|
|
||||||
deviceRegistry.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectSection(int index) {
|
|
||||||
if (sections == null || sections.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (index < 0 || index >= sections.size()) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
sectionAdapter.setSelectedIndex(index);
|
|
||||||
currentSection = sections.get(index);
|
|
||||||
if (currentSection.type == SectionEntry.Type.EVENTS) {
|
|
||||||
showEvents();
|
|
||||||
} else {
|
|
||||||
showChannels(currentSection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showChannels(SectionEntry section) {
|
|
||||||
contentTitle.setText(section.title);
|
|
||||||
refreshButton.setVisibility(View.GONE);
|
|
||||||
contentList.setLayoutManager(channelLayoutManager);
|
|
||||||
contentList.setAdapter(channelAdapter);
|
|
||||||
// Clear any scroll listeners from Events section
|
|
||||||
contentList.clearOnScrollListeners();
|
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
|
||||||
channelAdapter.submitList(section.channels);
|
|
||||||
if (section.channels.isEmpty()) {
|
|
||||||
messageView.setVisibility(View.VISIBLE);
|
|
||||||
messageView.setText(R.string.message_no_channels);
|
|
||||||
} else {
|
|
||||||
messageView.setVisibility(View.GONE);
|
|
||||||
contentList.post(() -> contentList.scrollToPosition(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showEvents() {
|
|
||||||
contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events));
|
|
||||||
refreshButton.setVisibility(View.VISIBLE);
|
|
||||||
contentList.setLayoutManager(eventLayoutManager);
|
|
||||||
contentList.setAdapter(eventAdapter);
|
|
||||||
// Clear existing listeners
|
|
||||||
contentList.clearOnScrollListeners();
|
|
||||||
|
|
||||||
if (cachedEvents.isEmpty()) {
|
|
||||||
loadEvents(false);
|
|
||||||
} else {
|
|
||||||
displayEvents();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadEvents(boolean forceRefresh) {
|
|
||||||
loadingIndicator.setVisibility(View.VISIBLE);
|
|
||||||
messageView.setVisibility(View.GONE);
|
|
||||||
eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(List<EventItem> events) {
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
cachedEvents.clear();
|
|
||||||
cachedEvents.addAll(events);
|
|
||||||
if (currentSection != null && currentSection.type == SectionEntry.Type.EVENTS) {
|
|
||||||
displayEvents();
|
|
||||||
} else {
|
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(String message) {
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
|
||||||
messageView.setVisibility(View.VISIBLE);
|
|
||||||
messageView.setText(getString(R.string.message_events_error, message));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayEvents() {
|
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
|
||||||
if (cachedEvents.isEmpty()) {
|
|
||||||
messageView.setVisibility(View.VISIBLE);
|
|
||||||
messageView.setText(R.string.message_no_events);
|
|
||||||
eventAdapter.submitList(new ArrayList<>());
|
|
||||||
} else {
|
|
||||||
messageView.setVisibility(View.GONE);
|
|
||||||
eventAdapter.submitList(new ArrayList<>(cachedEvents));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openPlayer(String name, String pageUrl) {
|
|
||||||
Intent intent = new Intent(MainActivity.this, PlayerActivity.class);
|
|
||||||
intent.putExtra(PlayerActivity.EXTRA_CHANNEL_NAME, name);
|
|
||||||
intent.putExtra(PlayerActivity.EXTRA_CHANNEL_URL, pageUrl);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUpdateInfo(UpdateManager.UpdateInfo info) {
|
|
||||||
if (info == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean forceUpdate = info.isMandatory(BuildConfig.VERSION_CODE);
|
|
||||||
showUpdateDialog(info, forceUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showUpdateDialog(UpdateManager.UpdateInfo info, boolean mandatory) {
|
|
||||||
if (isFinishing()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (updateDialog != null && updateDialog.isShowing()) {
|
|
||||||
updateDialog.dismiss();
|
|
||||||
}
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
|
|
||||||
.setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title)
|
|
||||||
.setMessage(buildUpdateMessage(info))
|
|
||||||
.setPositiveButton(R.string.update_action_download,
|
|
||||||
(dialog, which) -> updateManager.downloadUpdate(MainActivity.this, info))
|
|
||||||
.setNeutralButton(R.string.update_action_view_release,
|
|
||||||
(dialog, which) -> openReleasePage(info));
|
|
||||||
if (mandatory) {
|
|
||||||
builder.setCancelable(false);
|
|
||||||
builder.setNegativeButton(R.string.update_action_close_app,
|
|
||||||
(dialog, which) -> finish());
|
|
||||||
} else {
|
|
||||||
builder.setNegativeButton(R.string.update_action_later, null);
|
|
||||||
}
|
|
||||||
updateDialog = builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence buildUpdateMessage(UpdateManager.UpdateInfo info) {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append(getString(R.string.update_current_version,
|
|
||||||
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
|
||||||
builder.append('\n');
|
|
||||||
builder.append(getString(R.string.update_latest_version,
|
|
||||||
info.versionName, info.versionCode));
|
|
||||||
if (info.minSupportedVersionCode > 0) {
|
|
||||||
builder.append('\n').append(getString(R.string.update_min_supported,
|
|
||||||
info.minSupportedVersionCode));
|
|
||||||
}
|
|
||||||
String size = info.formatSize(this);
|
|
||||||
if (!size.isEmpty()) {
|
|
||||||
builder.append('\n').append(getString(R.string.update_download_size, size));
|
|
||||||
}
|
|
||||||
if (info.downloadCount > 0) {
|
|
||||||
builder.append('\n').append(getString(R.string.update_downloads,
|
|
||||||
info.downloadCount));
|
|
||||||
}
|
|
||||||
if (!info.releaseNotes.isEmpty()) {
|
|
||||||
builder.append("\n\n");
|
|
||||||
builder.append(getString(R.string.update_release_notes_title));
|
|
||||||
builder.append('\n');
|
|
||||||
builder.append(info.getReleaseNotesPreview());
|
|
||||||
}
|
|
||||||
if (!info.isMandatory(BuildConfig.VERSION_CODE)) {
|
|
||||||
builder.append("\n\n");
|
|
||||||
builder.append(getString(R.string.update_optional_hint));
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openReleasePage(UpdateManager.UpdateInfo info) {
|
|
||||||
String url = info.releasePageUrl;
|
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
url = info.downloadUrl;
|
|
||||||
}
|
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.update_error_missing_url, Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
||||||
try {
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(this, R.string.update_error_open_release, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showBlockedDialog(String reason, String tokenPart) {
|
|
||||||
if (isFinishing()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String finalReason = TextUtils.isEmpty(reason)
|
|
||||||
? getString(R.string.device_blocked_default_reason)
|
|
||||||
: reason;
|
|
||||||
if (blockedDialog != null && blockedDialog.isShowing()) {
|
|
||||||
blockedDialog.dismiss();
|
|
||||||
}
|
|
||||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_blocked, null);
|
|
||||||
TextView messageText = dialogView.findViewById(R.id.blocked_message_text);
|
|
||||||
View tokenContainer = dialogView.findViewById(R.id.blocked_token_container);
|
|
||||||
TextView tokenValue = dialogView.findViewById(R.id.blocked_token_value);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
|
|
||||||
.setTitle(R.string.device_blocked_title)
|
|
||||||
.setView(dialogView)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(R.string.device_blocked_close,
|
|
||||||
(dialog, which) -> finish());
|
|
||||||
if (hasToken) {
|
|
||||||
builder.setNeutralButton(R.string.device_blocked_copy_token,
|
|
||||||
(dialog, which) -> copyTokenToClipboard(tokenPart));
|
|
||||||
}
|
|
||||||
blockedDialog = builder.create();
|
|
||||||
blockedDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void copyTokenToClipboard(String tokenPart) {
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
if (clipboard == null) {
|
|
||||||
Toast.makeText(this, R.string.device_blocked_copy_error, Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ClipData data = ClipData.newPlainText("token", tokenPart);
|
|
||||||
clipboard.setPrimaryClip(data);
|
|
||||||
Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getSpanCount() {
|
|
||||||
return getResources().getInteger(R.integer.channel_grid_span);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<SectionEntry> buildSections() {
|
|
||||||
List<SectionEntry> list = new ArrayList<>();
|
|
||||||
list.add(SectionEntry.events(getString(R.string.section_events)));
|
|
||||||
|
|
||||||
Map<String, List<StreamChannel>> grouped = new HashMap<>();
|
|
||||||
List<StreamChannel> allChannels = ChannelRepository.getChannels();
|
|
||||||
for (StreamChannel channel : allChannels) {
|
|
||||||
String key = deriveGroupName(channel.getName());
|
|
||||||
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StreamChannel> espnChannels = grouped.remove("ESPN");
|
|
||||||
if (espnChannels != null && !espnChannels.isEmpty()) {
|
|
||||||
list.add(SectionEntry.channels("ESPN", espnChannels));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> remaining = new ArrayList<>(grouped.keySet());
|
|
||||||
Collections.sort(remaining, String.CASE_INSENSITIVE_ORDER);
|
|
||||||
for (String key : remaining) {
|
|
||||||
List<StreamChannel> channels = grouped.get(key);
|
|
||||||
if (channels == null || channels.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
list.add(SectionEntry.channels(key, channels));
|
|
||||||
}
|
|
||||||
|
|
||||||
list.add(SectionEntry.channels(getString(R.string.section_all_channels), allChannels));
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getSectionTitles() {
|
|
||||||
List<String> titles = new ArrayList<>();
|
|
||||||
for (SectionEntry entry : sections) {
|
|
||||||
titles.add(entry.title);
|
|
||||||
}
|
|
||||||
return titles;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String deriveGroupName(String name) {
|
|
||||||
if (name == null) {
|
|
||||||
return getString(R.string.section_all_channels);
|
|
||||||
}
|
|
||||||
String upper = name.toUpperCase(Locale.US);
|
|
||||||
if (upper.startsWith("ESPN")) {
|
|
||||||
return "ESPN";
|
|
||||||
} else if (upper.contains("FOX SPORTS")) {
|
|
||||||
return "Fox Sports";
|
|
||||||
} else if (upper.contains("FOX")) {
|
|
||||||
return "Fox";
|
|
||||||
} else if (upper.contains("TNT")) {
|
|
||||||
return "TNT";
|
|
||||||
} else if (upper.contains("DAZN")) {
|
|
||||||
return "DAZN";
|
|
||||||
} else if (upper.contains("TUDN")) {
|
|
||||||
return "TUDN";
|
|
||||||
} else if (upper.contains("TYC")) {
|
|
||||||
return "TyC";
|
|
||||||
} else if (upper.contains("GOL")) {
|
|
||||||
return "Gol";
|
|
||||||
}
|
|
||||||
int spaceIndex = upper.indexOf(' ');
|
|
||||||
return spaceIndex > 0 ? upper.substring(0, spaceIndex) : upper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SectionEntry {
|
|
||||||
enum Type { EVENTS, CHANNELS }
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final Type type;
|
|
||||||
final List<StreamChannel> channels;
|
|
||||||
|
|
||||||
private SectionEntry(String title, Type type, List<StreamChannel> channels) {
|
|
||||||
this.title = title;
|
|
||||||
this.type = type;
|
|
||||||
this.channels = channels == null ? new ArrayList<>() : new ArrayList<>(channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
static SectionEntry events(String title) {
|
|
||||||
return new SectionEntry(title, Type.EVENTS, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static SectionEntry channels(String title, List<StreamChannel> channels) {
|
|
||||||
return new SectionEntry(title, Type.CHANNELS, channels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import okhttp3.Dns;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.dnsoverhttps.DnsOverHttps;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilidad centralizada para configuración de red con múltiples servidores DNS over HTTPS.
|
|
||||||
* Implementa fallback progresivo: Google -> Cloudflare -> AdGuard -> Quad9 -> Sistema
|
|
||||||
*/
|
|
||||||
public class NetworkUtils {
|
|
||||||
|
|
||||||
private static final OkHttpClient CLIENT;
|
|
||||||
private static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36";
|
|
||||||
|
|
||||||
// URLs de servidores DNS over HTTPS
|
|
||||||
private static final String GOOGLE_DOH_URL = "https://dns.google/dns-query";
|
|
||||||
private static final String CLOUDFLARE_DOH_URL = "https://cloudflare-dns.com/dns-query";
|
|
||||||
private static final String ADGUARD_DOH_URL = "https://dns.adguard-dns.com/dns-query";
|
|
||||||
private static final String QUAD9_DOH_URL = "https://dns.quad9.net/dns-query";
|
|
||||||
|
|
||||||
static {
|
|
||||||
OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
|
||||||
.connectTimeout(15, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(15, TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(15, TimeUnit.SECONDS)
|
|
||||||
.followRedirects(true)
|
|
||||||
.followSslRedirects(true)
|
|
||||||
.retryOnConnectionFailure(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Cliente bootstrap para resolver los dominios de DNS
|
|
||||||
OkHttpClient bootstrap = new OkHttpClient.Builder()
|
|
||||||
.connectTimeout(5, TimeUnit.SECONDS)
|
|
||||||
.retryOnConnectionFailure(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 1. Google DNS over HTTPS (Primario)
|
|
||||||
final DnsOverHttps googleDns = new DnsOverHttps.Builder()
|
|
||||||
.client(bootstrap)
|
|
||||||
.url(HttpUrl.get(GOOGLE_DOH_URL))
|
|
||||||
.bootstrapDnsHosts(
|
|
||||||
getByIp("8.8.8.8"),
|
|
||||||
getByIp("8.8.4.4"))
|
|
||||||
.includeIPv6(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 2. Cloudflare DNS over HTTPS (Secundario)
|
|
||||||
final DnsOverHttps cloudflareDns = new DnsOverHttps.Builder()
|
|
||||||
.client(bootstrap)
|
|
||||||
.url(HttpUrl.get(CLOUDFLARE_DOH_URL))
|
|
||||||
.bootstrapDnsHosts(
|
|
||||||
getByIp("1.1.1.1"),
|
|
||||||
getByIp("1.0.0.1"))
|
|
||||||
.includeIPv6(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 3. AdGuard DNS over HTTPS (Terciario)
|
|
||||||
final DnsOverHttps adGuardDns = new DnsOverHttps.Builder()
|
|
||||||
.client(bootstrap)
|
|
||||||
.url(HttpUrl.get(ADGUARD_DOH_URL))
|
|
||||||
.bootstrapDnsHosts(
|
|
||||||
getByIp("94.140.14.14"),
|
|
||||||
getByIp("94.140.15.15"))
|
|
||||||
.includeIPv6(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 4. Quad9 DNS over HTTPS (Cuaternario)
|
|
||||||
final DnsOverHttps quad9Dns = new DnsOverHttps.Builder()
|
|
||||||
.client(bootstrap)
|
|
||||||
.url(HttpUrl.get(QUAD9_DOH_URL))
|
|
||||||
.bootstrapDnsHosts(
|
|
||||||
getByIp("9.9.9.9"),
|
|
||||||
getByIp("149.112.112.112"))
|
|
||||||
.includeIPv6(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Configurar DNS con fallback: Google -> Cloudflare -> AdGuard -> Quad9 -> Sistema
|
|
||||||
builder.dns(new Dns() {
|
|
||||||
@Override
|
|
||||||
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
|
|
||||||
// Intento 1: Google DNS
|
|
||||||
try {
|
|
||||||
List<InetAddress> result = googleDns.lookup(hostname);
|
|
||||||
if (result != null && !result.isEmpty()) return result;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Falló Google, continuar
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intento 2: Cloudflare DNS
|
|
||||||
try {
|
|
||||||
List<InetAddress> result = cloudflareDns.lookup(hostname);
|
|
||||||
if (result != null && !result.isEmpty()) return result;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Falló Cloudflare, continuar
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intento 3: AdGuard DNS
|
|
||||||
try {
|
|
||||||
List<InetAddress> result = adGuardDns.lookup(hostname);
|
|
||||||
if (result != null && !result.isEmpty()) return result;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Falló AdGuard, continuar
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intento 4: Quad9 DNS
|
|
||||||
try {
|
|
||||||
List<InetAddress> result = quad9Dns.lookup(hostname);
|
|
||||||
if (result != null && !result.isEmpty()) return result;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Falló Quad9, continuar
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intento 5: DNS del Sistema (Fallback final)
|
|
||||||
try {
|
|
||||||
return Dns.SYSTEM.lookup(hostname);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Si algo falla en la configuración DNS, usamos por defecto (implícito en el builder)
|
|
||||||
System.out.println("Error configurando DNS over HTTPS: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
CLIENT = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InetAddress getByIp(String ip) throws UnknownHostException {
|
|
||||||
return InetAddress.getByName(ip);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient getClient() {
|
|
||||||
return CLIENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUserAgent() {
|
|
||||||
return USER_AGENT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.StrictMode;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
|
||||||
import androidx.media3.common.MediaItem;
|
|
||||||
import androidx.media3.common.PlaybackException;
|
|
||||||
import androidx.media3.common.Player;
|
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource;
|
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
|
||||||
import androidx.media3.exoplayer.hls.HlsMediaSource;
|
|
||||||
import androidx.media3.ui.PlayerView;
|
|
||||||
import androidx.media3.common.util.Util;
|
|
||||||
import androidx.annotation.OptIn;
|
|
||||||
import androidx.media3.common.util.UnstableApi;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.dnsoverhttps.DnsOverHttps;
|
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
|
||||||
public class PlayerActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
public static final String EXTRA_CHANNEL_NAME = "extra_channel_name";
|
|
||||||
public static final String EXTRA_CHANNEL_URL = "extra_channel_url";
|
|
||||||
|
|
||||||
private PlayerView playerView;
|
|
||||||
private ProgressBar loadingIndicator;
|
|
||||||
private TextView errorMessage;
|
|
||||||
private TextView channelLabel;
|
|
||||||
private Button closeButton;
|
|
||||||
private View playerToolbar;
|
|
||||||
|
|
||||||
private ExoPlayer player;
|
|
||||||
private DefaultTrackSelector trackSelector;
|
|
||||||
private String channelName;
|
|
||||||
private String channelUrl;
|
|
||||||
private boolean overlayVisible = true;
|
|
||||||
private OkHttpClient okHttpClient;
|
|
||||||
private int retryCount = 0;
|
|
||||||
private static final int MAX_RETRIES = 3;
|
|
||||||
private String lastStreamUrl;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
StrictMode.setThreadPolicy(
|
|
||||||
new StrictMode.ThreadPolicy.Builder().permitAll().build()
|
|
||||||
);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_player);
|
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
if (intent == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
channelName = intent.getStringExtra(EXTRA_CHANNEL_NAME);
|
|
||||||
channelUrl = intent.getStringExtra(EXTRA_CHANNEL_URL);
|
|
||||||
|
|
||||||
if (channelName == null || channelUrl == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
initViews();
|
|
||||||
channelLabel.setText(channelName);
|
|
||||||
|
|
||||||
// DNS over HTTPS ya está configurado en NetworkUtils
|
|
||||||
loadChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initViews() {
|
|
||||||
playerView = findViewById(R.id.player_view);
|
|
||||||
loadingIndicator = findViewById(R.id.loading_indicator);
|
|
||||||
errorMessage = findViewById(R.id.error_message);
|
|
||||||
channelLabel = findViewById(R.id.player_channel_label);
|
|
||||||
closeButton = findViewById(R.id.close_button);
|
|
||||||
playerToolbar = findViewById(R.id.player_toolbar);
|
|
||||||
|
|
||||||
closeButton.setOnClickListener(v -> finish());
|
|
||||||
playerView.setOnClickListener(v -> toggleOverlay());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadChannel() {
|
|
||||||
showLoading(true);
|
|
||||||
retryCount = 0; // Resetear contador al cargar nuevo canal
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
String resolvedUrl = StreamUrlResolver.resolve(channelUrl);
|
|
||||||
runOnUiThread(() -> startPlayback(resolvedUrl));
|
|
||||||
} catch (IOException e) {
|
|
||||||
runOnUiThread(() -> showError("No se pudo conectar con el canal: " + e.getMessage()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
runOnUiThread(() -> showError("Error inesperado: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startPlayback(String streamUrl) {
|
|
||||||
try {
|
|
||||||
releasePlayer();
|
|
||||||
lastStreamUrl = streamUrl; // Guardar URL para reintentos
|
|
||||||
retryCount = 0; // Resetear contador al iniciar nueva reproducción
|
|
||||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this)
|
|
||||||
.setEnableDecoderFallback(true)
|
|
||||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);
|
|
||||||
|
|
||||||
// Configurar track selector para calidad adaptativa (no forzar máxima calidad)
|
|
||||||
trackSelector = new DefaultTrackSelector(this);
|
|
||||||
DefaultTrackSelector.Parameters params = trackSelector.buildUponParameters()
|
|
||||||
.setForceHighestSupportedBitrate(false) // Permitir calidad adaptativa
|
|
||||||
.setMaxVideoBitrate(Integer.MAX_VALUE) // Sin límite máximo de bitrate
|
|
||||||
.build();
|
|
||||||
trackSelector.setParameters(params);
|
|
||||||
|
|
||||||
player = new ExoPlayer.Builder(this, renderersFactory)
|
|
||||||
.setTrackSelector(trackSelector)
|
|
||||||
.setSeekForwardIncrementMs(10_000)
|
|
||||||
.setSeekBackIncrementMs(10_000)
|
|
||||||
.build();
|
|
||||||
playerView.setPlayer(player);
|
|
||||||
|
|
||||||
player.addListener(new Player.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onPlaybackStateChanged(int playbackState) {
|
|
||||||
if (playbackState == Player.STATE_READY) {
|
|
||||||
showLoading(false);
|
|
||||||
retryCount = 0; // Resetear contador de reintentos al reproducir exitosamente
|
|
||||||
} else if (playbackState == Player.STATE_BUFFERING) {
|
|
||||||
showLoading(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError(PlaybackException error) {
|
|
||||||
String errorMsg = error.getMessage() != null ? error.getMessage() : "";
|
|
||||||
String detail = error.getCause() != null ?
|
|
||||||
error.getCause().getMessage() : "";
|
|
||||||
String fullError = errorMsg + " " + detail;
|
|
||||||
|
|
||||||
// Verificar si es un error que justifica reintento (404, conectividad, etc.)
|
|
||||||
boolean isRetryableError =
|
|
||||||
fullError.contains("404") ||
|
|
||||||
fullError.contains("403") ||
|
|
||||||
fullError.contains("timeout") ||
|
|
||||||
fullError.contains("Unable to connect") ||
|
|
||||||
fullError.contains("Network") ||
|
|
||||||
fullError.contains("source error") ||
|
|
||||||
error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ||
|
|
||||||
error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT ||
|
|
||||||
error.errorCode == PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS;
|
|
||||||
|
|
||||||
if (isRetryableError && retryCount < MAX_RETRIES) {
|
|
||||||
retryCount++;
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
showLoading(true);
|
|
||||||
showError("Error de conexión. Reintentando... (" + retryCount + "/" + MAX_RETRIES + ")");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reintentar después de 2 segundos
|
|
||||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
|
||||||
if (lastStreamUrl != null) {
|
|
||||||
startPlayback(lastStreamUrl);
|
|
||||||
} else {
|
|
||||||
loadChannel();
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
|
||||||
// Mostrar error final después de agotar reintentos
|
|
||||||
String finalMessage = "Error al reproducir: " + fullError;
|
|
||||||
if (retryCount >= MAX_RETRIES) {
|
|
||||||
finalMessage += "\n\nSe agotaron los reintentos (" + MAX_RETRIES + ").";
|
|
||||||
}
|
|
||||||
showError(finalMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
MediaItem mediaItem = MediaItem.fromUri(streamUrl);
|
|
||||||
player.setMediaSource(buildMediaSource(mediaItem));
|
|
||||||
player.prepare();
|
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
setOverlayVisible(false);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
showError("Error al inicializar reproductor: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showLoading(boolean show) {
|
|
||||||
loadingIndicator.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
errorMessage.setVisibility(View.GONE);
|
|
||||||
playerView.setVisibility(show ? View.GONE : View.VISIBLE);
|
|
||||||
if (show) {
|
|
||||||
setOverlayVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showError(String message) {
|
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
|
||||||
playerView.setVisibility(View.GONE);
|
|
||||||
errorMessage.setVisibility(View.VISIBLE);
|
|
||||||
errorMessage.setText(message);
|
|
||||||
setOverlayVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releasePlayer() {
|
|
||||||
if (player != null) {
|
|
||||||
player.release();
|
|
||||||
player = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaSource buildMediaSource(MediaItem mediaItem) {
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
|
||||||
headers.put("Referer", channelUrl);
|
|
||||||
headers.put("Origin", "https://streamtpcloud.com");
|
|
||||||
headers.put("Accept", "*/*");
|
|
||||||
headers.put("Connection", "keep-alive");
|
|
||||||
|
|
||||||
String userAgent = Util.getUserAgent(this, "StreamPlayer");
|
|
||||||
|
|
||||||
OkHttpDataSource.Factory factory = new OkHttpDataSource.Factory(provideOkHttpClient())
|
|
||||||
.setUserAgent(userAgent)
|
|
||||||
.setDefaultRequestProperties(headers);
|
|
||||||
return new HlsMediaSource.Factory(factory).createMediaSource(mediaItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OkHttpClient provideOkHttpClient() {
|
|
||||||
if (okHttpClient != null) {
|
|
||||||
return okHttpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
OkHttpClient bootstrap = new OkHttpClient.Builder()
|
|
||||||
.connectTimeout(20, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.retryOnConnectionFailure(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
DnsOverHttps dohDns = new DnsOverHttps.Builder()
|
|
||||||
.client(bootstrap)
|
|
||||||
.url(HttpUrl.get("https://dns.google/dns-query"))
|
|
||||||
.bootstrapDnsHosts(
|
|
||||||
InetAddress.getByName("8.8.8.8"),
|
|
||||||
InetAddress.getByName("8.8.4.4"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
okHttpClient = bootstrap.newBuilder()
|
|
||||||
.dns(dohDns)
|
|
||||||
.build();
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
okHttpClient = new OkHttpClient.Builder()
|
|
||||||
.connectTimeout(20, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.retryOnConnectionFailure(true)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return okHttpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
if (player != null) {
|
|
||||||
playerView.onResume();
|
|
||||||
} else if (channelUrl != null) {
|
|
||||||
loadChannel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (player != null) {
|
|
||||||
playerView.onResume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (player != null) {
|
|
||||||
playerView.onPause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
releasePlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
releasePlayer();
|
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleOverlay() {
|
|
||||||
setOverlayVisible(!overlayVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setOverlayVisible(boolean visible) {
|
|
||||||
overlayVisible = visible;
|
|
||||||
playerToolbar.setVisibility(visible ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (!overlayVisible) {
|
|
||||||
setOverlayVisible(true);
|
|
||||||
} else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SectionAdapter extends RecyclerView.Adapter<SectionAdapter.SectionViewHolder> {
|
|
||||||
|
|
||||||
public interface OnSectionSelectedListener {
|
|
||||||
void onSectionSelected(int position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final List<String> sections;
|
|
||||||
private final OnSectionSelectedListener listener;
|
|
||||||
private int selectedIndex = 0;
|
|
||||||
|
|
||||||
public SectionAdapter(List<String> sections, OnSectionSelectedListener listener) {
|
|
||||||
this.sections = sections;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public SectionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_section, parent, false);
|
|
||||||
return new SectionViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull SectionViewHolder holder, int position) {
|
|
||||||
holder.title.setText(sections.get(position));
|
|
||||||
holder.itemView.setSelected(position == selectedIndex);
|
|
||||||
holder.itemView.setOnClickListener(v -> notifySelection(holder));
|
|
||||||
holder.itemView.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (hasFocus) {
|
|
||||||
notifySelection(holder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return sections.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedIndex(int index) {
|
|
||||||
if (index < 0 || index >= sections.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selectedIndex == index) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int previous = selectedIndex;
|
|
||||||
selectedIndex = index;
|
|
||||||
notifyItemChanged(previous);
|
|
||||||
notifyItemChanged(selectedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedIndex() {
|
|
||||||
return selectedIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifySelection(SectionViewHolder holder) {
|
|
||||||
int position = holder.getBindingAdapterPosition();
|
|
||||||
if (position == RecyclerView.NO_POSITION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onSectionSelected(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SectionViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
final TextView title;
|
|
||||||
|
|
||||||
SectionViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
title = itemView.findViewById(R.id.section_title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
public class StreamChannel {
|
|
||||||
private final String name;
|
|
||||||
private final String pageUrl;
|
|
||||||
|
|
||||||
public StreamChannel(String name, String pageUrl) {
|
|
||||||
this.name = name;
|
|
||||||
this.pageUrl = pageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPageUrl() {
|
|
||||||
return pageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resuelve la URL real del stream extrayendo playbackURL de la página.
|
|
||||||
* Utiliza NetworkUtils para configuración centralizada de DNS.
|
|
||||||
*/
|
|
||||||
public final class StreamUrlResolver {
|
|
||||||
|
|
||||||
// Patrón para extraer la URL del stream directamente
|
|
||||||
private static final Pattern PLAYBACK_URL_PATTERN =
|
|
||||||
Pattern.compile("var\\s+playbackURL\\s*=\\s*[\"']([^\"']+)[\"']");
|
|
||||||
|
|
||||||
private StreamUrlResolver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String resolve(String pageUrl) throws IOException {
|
|
||||||
String html = downloadPage(pageUrl);
|
|
||||||
|
|
||||||
// Buscar playbackURL directamente en el HTML
|
|
||||||
Matcher matcher = PLAYBACK_URL_PATTERN.matcher(html);
|
|
||||||
if (matcher.find()) {
|
|
||||||
String url = matcher.group(1);
|
|
||||||
if (url != null && !url.isEmpty() && url.startsWith("http")) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no encontramos la URL, mostrar un fragmento del HTML para debug
|
|
||||||
String preview = html.length() > 500 ? html.substring(0, 500) : html;
|
|
||||||
throw new IOException("No se encontró la URL del stream en la página. Vista previa: " + preview);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String downloadPage(String pageUrl) throws IOException {
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(pageUrl)
|
|
||||||
.header("User-Agent", NetworkUtils.getUserAgent())
|
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
|
||||||
.header("Accept-Language", "es-ES,es;q=0.9,en;q=0.8")
|
|
||||||
.header("Referer", "https://streamtp10.com/")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (Response response = NetworkUtils.getClient().newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
throw new IOException("Error HTTP " + response.code() + " al cargar la página del stream");
|
|
||||||
}
|
|
||||||
if (response.body() == null) {
|
|
||||||
throw new IOException("Respuesta vacía del servidor");
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body().string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,536 +0,0 @@
|
|||||||
package com.streamplayer;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.DownloadManager;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.format.Formatter;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsula toda la lógica para consultar releases de Gitea, descargar el APK y lanzarlo.
|
|
||||||
*/
|
|
||||||
public class UpdateManager {
|
|
||||||
|
|
||||||
private static final String TAG = "UpdateManager";
|
|
||||||
private static final String LATEST_RELEASE_URL =
|
|
||||||
"https://gitea.cbcren.online/api/v1/repos/renato97/app/releases/latest";
|
|
||||||
private static final String GITEA_TOKEN = "4b94b3610136529861af0821040a801906821a0f";
|
|
||||||
|
|
||||||
private final Context appContext;
|
|
||||||
private final Handler mainHandler;
|
|
||||||
private final ExecutorService networkExecutor;
|
|
||||||
private final OkHttpClient httpClient;
|
|
||||||
|
|
||||||
private WeakReference<Activity> activityRef;
|
|
||||||
private DownloadReceiver downloadReceiver;
|
|
||||||
private long currentDownloadId = -1L;
|
|
||||||
private File downloadingFile;
|
|
||||||
private File pendingInstallFile;
|
|
||||||
private UpdateInfo cachedUpdate;
|
|
||||||
|
|
||||||
public UpdateManager(Context context) {
|
|
||||||
this.appContext = context.getApplicationContext();
|
|
||||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
this.networkExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
// Usar NetworkUtils para obtener cliente con DNS over HTTPS configurado
|
|
||||||
this.httpClient = NetworkUtils.getClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkForUpdates(UpdateCallback callback) {
|
|
||||||
networkExecutor.execute(() -> {
|
|
||||||
try {
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(LATEST_RELEASE_URL)
|
|
||||||
.header("Authorization", "token " + GITEA_TOKEN)
|
|
||||||
.get()
|
|
||||||
.build();
|
|
||||||
try (Response response = httpClient.newCall(request).execute()) {
|
|
||||||
if (response.body() == null) {
|
|
||||||
postError(callback, appContext.getString(R.string.update_error_empty_response));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
postError(callback, appContext.getString(R.string.update_error_http,
|
|
||||||
response.code()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String body = response.body().string();
|
|
||||||
UpdateInfo info = parseRelease(body);
|
|
||||||
cachedUpdate = info;
|
|
||||||
if (info != null && info.isUpdateAvailable(BuildConfig.VERSION_CODE)) {
|
|
||||||
postAvailable(callback, info);
|
|
||||||
} else {
|
|
||||||
postUpToDate(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException | JSONException e) {
|
|
||||||
Log.w(TAG, "Error checking updates", e);
|
|
||||||
postError(callback, e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public UpdateInfo getCachedUpdate() {
|
|
||||||
return cachedUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void downloadUpdate(Activity activity, UpdateInfo info) {
|
|
||||||
if (info == null || TextUtils.isEmpty(info.downloadUrl)) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_missing_url));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DownloadManager downloadManager =
|
|
||||||
(DownloadManager) appContext.getSystemService(Context.DOWNLOAD_SERVICE);
|
|
||||||
if (downloadManager == null) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_download_manager));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
File targetDir = appContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
|
|
||||||
if (targetDir == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
targetDir = appContext.getExternalFilesDir(null);
|
|
||||||
}
|
|
||||||
if (targetDir == null) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_storage));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_storage));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String fileName = info.getResolvedFileName();
|
|
||||||
File apkFile = new File(targetDir, fileName);
|
|
||||||
if (apkFile.exists() && !apkFile.delete()) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_storage));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri destination = Uri.fromFile(apkFile);
|
|
||||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(info.downloadUrl));
|
|
||||||
request.setTitle(appContext.getString(R.string.update_notification_title, info.versionName));
|
|
||||||
request.setDescription(appContext.getString(R.string.update_notification_description));
|
|
||||||
request.setAllowedOverMetered(true);
|
|
||||||
request.setAllowedOverRoaming(false);
|
|
||||||
request.setNotificationVisibility(
|
|
||||||
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
|
||||||
request.setDestinationUri(destination);
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentDownloadId = downloadManager.enqueue(request);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_download, e.getMessage()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
downloadingFile = apkFile;
|
|
||||||
activityRef = new WeakReference<>(activity);
|
|
||||||
registerDownloadReceiver();
|
|
||||||
showToast(appContext.getString(R.string.update_download_started));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resumePendingInstall(Activity activity) {
|
|
||||||
if (pendingInstallFile == null || !pendingInstallFile.exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installDownloadedApk(activity, pendingInstallFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() {
|
|
||||||
unregisterDownloadReceiver();
|
|
||||||
networkExecutor.shutdownNow();
|
|
||||||
activityRef = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private UpdateInfo parseRelease(String responseBody) throws JSONException, IOException {
|
|
||||||
if (responseBody == null || responseBody.trim().isEmpty()) {
|
|
||||||
throw new JSONException("La respuesta está vacía");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validar que no sea HTML antes de parsear
|
|
||||||
String trimmed = responseBody.trim();
|
|
||||||
if (trimmed.startsWith("<!") || trimmed.startsWith("<html")) {
|
|
||||||
throw new JSONException("Se recibió HTML en lugar de JSON");
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject releaseJson = new JSONObject(responseBody);
|
|
||||||
String tagName = releaseJson.optString("tag_name", "");
|
|
||||||
String versionName = deriveVersionName(tagName, releaseJson.optString("name"));
|
|
||||||
int versionCode = parseVersionCode(versionName);
|
|
||||||
String releaseNotes = releaseJson.optString("body", "");
|
|
||||||
String releasePageUrl = releaseJson.optString("html_url", "");
|
|
||||||
JSONArray assets = releaseJson.optJSONArray("assets");
|
|
||||||
JSONObject apkAsset = findApkAsset(assets);
|
|
||||||
String downloadUrl = apkAsset != null ? apkAsset.optString("browser_download_url", "") : "";
|
|
||||||
String downloadFileName = apkAsset != null ? apkAsset.optString("name", "") : "";
|
|
||||||
long sizeBytes = apkAsset != null ? apkAsset.optLong("size", 0L) : 0L;
|
|
||||||
int downloadCount = apkAsset != null ? apkAsset.optInt("download_count", 0) : 0;
|
|
||||||
|
|
||||||
int minSupported = 0;
|
|
||||||
boolean forceUpdate = false;
|
|
||||||
JSONObject manifestJson = fetchManifest(assets);
|
|
||||||
if (manifestJson != null) {
|
|
||||||
versionCode = manifestJson.optInt("versionCode", versionCode);
|
|
||||||
versionName = manifestJson.optString("versionName", versionName);
|
|
||||||
minSupported = manifestJson.optInt("minSupportedVersionCode", 0);
|
|
||||||
forceUpdate = manifestJson.optBoolean("forceUpdate", false);
|
|
||||||
String manifestUrl = manifestJson.optString("downloadUrl", null);
|
|
||||||
if (!TextUtils.isEmpty(manifestUrl)) {
|
|
||||||
downloadUrl = manifestUrl;
|
|
||||||
}
|
|
||||||
if (manifestJson.has("fileName")) {
|
|
||||||
downloadFileName = manifestJson.optString("fileName", downloadFileName);
|
|
||||||
}
|
|
||||||
if (manifestJson.has("sizeBytes")) {
|
|
||||||
sizeBytes = manifestJson.optLong("sizeBytes", sizeBytes);
|
|
||||||
}
|
|
||||||
if (manifestJson.has("notes") && TextUtils.isEmpty(releaseNotes)) {
|
|
||||||
releaseNotes = manifestJson.optString("notes", releaseNotes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(downloadUrl)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UpdateInfo(versionCode, versionName, releaseNotes, downloadUrl,
|
|
||||||
downloadFileName, sizeBytes, downloadCount, releasePageUrl,
|
|
||||||
minSupported, forceUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONObject fetchManifest(JSONArray assets) throws IOException, JSONException {
|
|
||||||
if (assets == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < assets.length(); i++) {
|
|
||||||
JSONObject asset = assets.optJSONObject(i);
|
|
||||||
if (asset == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String name = asset.optString("name", "").toLowerCase(Locale.US);
|
|
||||||
if (TextUtils.isEmpty(name) || !name.endsWith(".json")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!(name.contains("update") || name.contains("manifest"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String url = asset.optString("browser_download_url", "");
|
|
||||||
if (TextUtils.isEmpty(url)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.header("Authorization", "token " + GITEA_TOKEN)
|
|
||||||
.get()
|
|
||||||
.build();
|
|
||||||
try (Response response = httpClient.newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String json = response.body().string();
|
|
||||||
if (!TextUtils.isEmpty(json)) {
|
|
||||||
// Validar que no sea HTML antes de parsear
|
|
||||||
String trimmed = json.trim();
|
|
||||||
if (trimmed.startsWith("<!") || trimmed.startsWith("<html")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return new JSONObject(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONObject findApkAsset(JSONArray assets) {
|
|
||||||
if (assets == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
JSONObject fallback = null;
|
|
||||||
for (int i = 0; i < assets.length(); i++) {
|
|
||||||
JSONObject asset = assets.optJSONObject(i);
|
|
||||||
if (asset == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (fallback == null) {
|
|
||||||
fallback = asset;
|
|
||||||
}
|
|
||||||
String name = asset.optString("name", "").toLowerCase(Locale.US);
|
|
||||||
if (name.endsWith(".apk")) {
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String deriveVersionName(String tagName, String fallback) {
|
|
||||||
String base = !TextUtils.isEmpty(tagName) ? tagName : fallback;
|
|
||||||
if (TextUtils.isEmpty(base)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return base.replaceFirst("^[Vv]", "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseVersionCode(String versionName) {
|
|
||||||
if (TextUtils.isEmpty(versionName)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
String normalized = versionName.replaceAll("[^0-9\\.]", "");
|
|
||||||
if (TextUtils.isEmpty(normalized)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
String[] parts = normalized.split("\\.");
|
|
||||||
int major = parsePart(parts, 0);
|
|
||||||
int minor = parsePart(parts, 1);
|
|
||||||
int patch = parsePart(parts, 2);
|
|
||||||
return major * 10000 + minor * 100 + patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parsePart(String[] parts, int index) {
|
|
||||||
if (parts.length <= index) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(parts[index]);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void installDownloadedApk(Activity activity, File apkFile) {
|
|
||||||
if (activity == null || apkFile == null || !apkFile.exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
boolean canInstall = appContext.getPackageManager().canRequestPackageInstalls();
|
|
||||||
if (!canInstall) {
|
|
||||||
pendingInstallFile = apkFile;
|
|
||||||
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
|
||||||
Uri.parse("package:" + appContext.getPackageName()));
|
|
||||||
try {
|
|
||||||
activity.startActivity(intent);
|
|
||||||
} catch (ActivityNotFoundException ignored) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_install_permissions));
|
|
||||||
}
|
|
||||||
showToast(appContext.getString(R.string.update_permission_request));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Uri uri = FileProvider.getUriForFile(appContext,
|
|
||||||
appContext.getPackageName() + ".fileprovider", apkFile);
|
|
||||||
Intent installIntent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
installIntent.setDataAndType(uri, "application/vnd.android.package-archive");
|
|
||||||
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
try {
|
|
||||||
activity.startActivity(installIntent);
|
|
||||||
pendingInstallFile = null;
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_install_intent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerDownloadReceiver() {
|
|
||||||
if (downloadReceiver != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
downloadReceiver = new DownloadReceiver();
|
|
||||||
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
|
|
||||||
appContext.registerReceiver(downloadReceiver, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unregisterDownloadReceiver() {
|
|
||||||
if (downloadReceiver != null) {
|
|
||||||
try {
|
|
||||||
appContext.unregisterReceiver(downloadReceiver);
|
|
||||||
} catch (IllegalArgumentException ignored) {
|
|
||||||
}
|
|
||||||
downloadReceiver = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleDownloadComplete(long downloadId) {
|
|
||||||
if (downloadId != currentDownloadId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DownloadManager downloadManager =
|
|
||||||
(DownloadManager) appContext.getSystemService(Context.DOWNLOAD_SERVICE);
|
|
||||||
if (downloadManager == null) {
|
|
||||||
showToast(appContext.getString(R.string.update_error_download_manager));
|
|
||||||
cleanupDownloadState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DownloadManager.Query query = new DownloadManager.Query();
|
|
||||||
query.setFilterById(downloadId);
|
|
||||||
Cursor cursor = downloadManager.query(query);
|
|
||||||
if (cursor != null) {
|
|
||||||
try {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
int status = cursor.getInt(cursor.getColumnIndexOrThrow(
|
|
||||||
DownloadManager.COLUMN_STATUS));
|
|
||||||
if (status == DownloadManager.STATUS_SUCCESSFUL && downloadingFile != null) {
|
|
||||||
pendingInstallFile = downloadingFile;
|
|
||||||
Activity activity = activityRef != null ? activityRef.get() : null;
|
|
||||||
mainHandler.post(() -> {
|
|
||||||
showToast(appContext.getString(R.string.update_download_complete));
|
|
||||||
installDownloadedApk(activity, pendingInstallFile);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
int reason = cursor.getInt(cursor.getColumnIndexOrThrow(
|
|
||||||
DownloadManager.COLUMN_REASON));
|
|
||||||
mainHandler.post(() -> showToast(appContext.getString(
|
|
||||||
R.string.update_error_download_failed, reason)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleanupDownloadState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanupDownloadState() {
|
|
||||||
unregisterDownloadReceiver();
|
|
||||||
currentDownloadId = -1L;
|
|
||||||
downloadingFile = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postAvailable(UpdateCallback callback, UpdateInfo info) {
|
|
||||||
if (callback == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainHandler.post(() -> callback.onUpdateAvailable(info));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postUpToDate(UpdateCallback callback) {
|
|
||||||
if (callback == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainHandler.post(callback::onUpToDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postError(UpdateCallback callback, String message) {
|
|
||||||
if (callback == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainHandler.post(() -> callback.onError(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showToast(String message) {
|
|
||||||
mainHandler.post(() -> Toast.makeText(appContext, message, Toast.LENGTH_LONG).show());
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DownloadReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L);
|
|
||||||
handleDownloadComplete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface UpdateCallback {
|
|
||||||
void onUpdateAvailable(UpdateInfo info);
|
|
||||||
|
|
||||||
void onUpToDate();
|
|
||||||
|
|
||||||
void onError(String message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UpdateInfo {
|
|
||||||
public final int versionCode;
|
|
||||||
public final String versionName;
|
|
||||||
public final String releaseNotes;
|
|
||||||
public final String downloadUrl;
|
|
||||||
public final String downloadFileName;
|
|
||||||
public final long downloadSizeBytes;
|
|
||||||
public final int downloadCount;
|
|
||||||
public final String releasePageUrl;
|
|
||||||
public final int minSupportedVersionCode;
|
|
||||||
public final boolean forceUpdate;
|
|
||||||
|
|
||||||
UpdateInfo(int versionCode,
|
|
||||||
String versionName,
|
|
||||||
String releaseNotes,
|
|
||||||
String downloadUrl,
|
|
||||||
String downloadFileName,
|
|
||||||
long downloadSizeBytes,
|
|
||||||
int downloadCount,
|
|
||||||
String releasePageUrl,
|
|
||||||
int minSupportedVersionCode,
|
|
||||||
boolean forceUpdate) {
|
|
||||||
this.versionCode = versionCode;
|
|
||||||
this.versionName = versionName;
|
|
||||||
this.releaseNotes = releaseNotes == null ? "" : releaseNotes.trim();
|
|
||||||
this.downloadUrl = downloadUrl;
|
|
||||||
this.downloadFileName = downloadFileName;
|
|
||||||
this.downloadSizeBytes = downloadSizeBytes;
|
|
||||||
this.downloadCount = downloadCount;
|
|
||||||
this.releasePageUrl = releasePageUrl;
|
|
||||||
this.minSupportedVersionCode = minSupportedVersionCode;
|
|
||||||
this.forceUpdate = forceUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUpdateAvailable(int currentVersionCode) {
|
|
||||||
return versionCode > currentVersionCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMandatory(int currentVersionCode) {
|
|
||||||
return forceUpdate || currentVersionCode < minSupportedVersionCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReleaseNotesPreview() {
|
|
||||||
if (TextUtils.isEmpty(releaseNotes)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
final int limit = 900;
|
|
||||||
if (releaseNotes.length() <= limit) {
|
|
||||||
return releaseNotes;
|
|
||||||
}
|
|
||||||
return releaseNotes.substring(0, limit) + "\n…";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getResolvedFileName() {
|
|
||||||
if (!TextUtils.isEmpty(downloadFileName)) {
|
|
||||||
return downloadFileName;
|
|
||||||
}
|
|
||||||
String safeVersion = TextUtils.isEmpty(versionName) ? String.valueOf(versionCode)
|
|
||||||
: versionName.replaceAll("[^0-9a-zA-Z._-]", "");
|
|
||||||
return "StreamPlayer-" + safeVersion + ".apk";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String formatSize(Context context) {
|
|
||||||
if (downloadSizeBytes <= 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return Formatter.formatShortFileSize(context, downloadSizeBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true" android:color="@color/white" />
|
|
||||||
<item android:state_focused="true" android:color="@color/white" />
|
|
||||||
<item android:color="@color/text_secondary" />
|
|
||||||
</selector>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item>
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<gradient
|
|
||||||
android:angle="0"
|
|
||||||
android:endColor="#FF002766"
|
|
||||||
android:startColor="#FF0F4C81" />
|
|
||||||
<corners android:radius="12dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<bitmap
|
|
||||||
android:antialias="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/ic_launcher" />
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#88003C8F" />
|
|
||||||
<corners android:radius="18dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="3dp"
|
|
||||||
android:color="#FFFFFFFF" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:state_focused="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#55003C8F" />
|
|
||||||
<corners android:radius="18dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="3dp"
|
|
||||||
android:color="#FFFFFFFF" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:state_pressed="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#55003C8F" />
|
|
||||||
<corners android:radius="18dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="3dp"
|
|
||||||
android:color="#FFFFFFFF" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#33212121" />
|
|
||||||
<corners android:radius="18dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="2dp"
|
|
||||||
android:color="#33FFFFFF" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true"><shape android:shape="rectangle"><solid android:color="#202020"/></shape></item>
|
|
||||||
<item android:state_focused="true"><shape android:shape="rectangle"><solid android:color="#303030"/></shape></item>
|
|
||||||
<item><shape android:shape="rectangle"><solid android:color="@android:color/transparent"/></shape></item>
|
|
||||||
</selector>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_selected="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#18d763" />
|
|
||||||
<corners android:radius="20dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:state_focused="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#5522c1ff" />
|
|
||||||
<corners android:radius="20dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="2dp"
|
|
||||||
android:color="#88FFFFFF" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:state_pressed="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#3322c1ff" />
|
|
||||||
<corners android:radius="20dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="#222222" />
|
|
||||||
<corners android:radius="20dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="#44FFFFFF" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<!-- Focused state - bright amber with thick border -->
|
|
||||||
<item android:state_focused="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="@color/refresh_button_focused" />
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="4dp"
|
|
||||||
android:color="@color/refresh_button_focused_border" />
|
|
||||||
<padding
|
|
||||||
android:left="12dp"
|
|
||||||
android:top="8dp"
|
|
||||||
android:right="12dp"
|
|
||||||
android:bottom="8dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<!-- Pressed state -->
|
|
||||||
<item android:state_pressed="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="@color/refresh_button_pressed" />
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="2dp"
|
|
||||||
android:color="@color/refresh_button_border" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<!-- Default state - darker background with subtle border -->
|
|
||||||
<item>
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<solid android:color="@color/refresh_button_default" />
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<stroke
|
|
||||||
android:width="2dp"
|
|
||||||
android:color="@color/refresh_button_border" />
|
|
||||||
<padding
|
|
||||||
android:left="12dp"
|
|
||||||
android:top="8dp"
|
|
||||||
android:right="12dp"
|
|
||||||
android:bottom="8dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M21,6h-7.59l2.3,-2.29c0.63,-0.63 0.19,-1.71 -0.7,-1.71H8.99c-0.89,0 -1.33,1.08 -0.7,1.71L10.59,6H3c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h18c1.11,0 2,-0.9 2,-2L23,8c0,-1.1 -0.89,-2 -2,-2zM21,18H3L3,8h18v10z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M9,10h2v6L9,16zM13,10h2v6h-2z" />
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 850 B |
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="#FFFFFFFF" />
|
|
||||||
<corners android:radius="6dp" />
|
|
||||||
<size android:width="12dp" />
|
|
||||||
</shape>
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/black"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/nav_panel"
|
|
||||||
android:layout_width="180dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingTop="32dp"
|
|
||||||
android:paddingEnd="16dp"
|
|
||||||
android:paddingBottom="32dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/app_brand"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/app_name"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/app_tagline"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:text="@string/home_tagline"
|
|
||||||
android:textColor="@color/text_secondary"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/section_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:overScrollMode="never"
|
|
||||||
tools:listitem="@layout/item_section" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider"
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="#33FFFFFF"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/nav_panel"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/content_panel"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:paddingTop="32dp"
|
|
||||||
android:paddingEnd="24dp"
|
|
||||||
android:paddingBottom="32dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/divider"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/content_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
tools:text="Canales" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/refresh_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:text="@string/action_refresh"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:background="@drawable/btn_refresh_selector"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:elevation="2dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/loading_indicator"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/message_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:textColor="@color/text_secondary"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="Mensaje" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/content_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:overScrollMode="never"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:fadeScrollbars="false"
|
|
||||||
android:scrollbarStyle="insideInset"
|
|
||||||
android:scrollbarThumbVertical="@drawable/scrollbar_vertical"
|
|
||||||
android:scrollbarTrackVertical="@color/scrollbar_track"
|
|
||||||
android:scrollbarSize="12dp"
|
|
||||||
android:scrollbarAlwaysDrawVerticalTrack="true"
|
|
||||||
android:scrollbarFadeDuration="0"
|
|
||||||
android:nextFocusLeft="@id/section_list"
|
|
||||||
tools:listitem="@layout/item_channel" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/black"
|
|
||||||
tools:context=".PlayerActivity">
|
|
||||||
|
|
||||||
<androidx.media3.ui.PlayerView
|
|
||||||
android:id="@+id/player_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:resize_mode="fill"
|
|
||||||
app:use_controller="true" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/player_toolbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:background="#66000000"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="12dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/player_channel_label"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="Canal"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/close_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Elegir otro"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/loading_indicator"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:indeterminateTint="@color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/error_message"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Error al reproducir" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="6dp"
|
|
||||||
android:background="@drawable/bg_channel_item_selector"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:defaultFocusHighlightEnabled="true"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/channel_icon"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:tint="@color/white"
|
|
||||||
android:src="@drawable/ic_channel_default" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/channel_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:background="@drawable/bg_channel_item_selector"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/event_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
tools:text="Partido" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/event_time"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="20:00" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/event_channel"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="ESPN" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/event_status"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:textColor="#18d763"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="En vivo" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:background="@drawable/bg_section_indicator"
|
|
||||||
android:padding="8dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/section_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textColor="@color/section_text_selector"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
tools:text="Canales" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
Before Width: | Height: | Size: 585 B |
|
Before Width: | Height: | Size: 585 B |
|
Before Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<integer name="channel_grid_span">5</integer>
|
|
||||||
</resources>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string-array name="channel_entries">
|
|
||||||
<item>Azteca Deportes</item>
|
|
||||||
<item>Canal 5 MX</item>
|
|
||||||
<item>Caliente TV MX</item>
|
|
||||||
<item>DAZN 1</item>
|
|
||||||
<item>DAZN 2</item>
|
|
||||||
<item>DAZN LaLiga</item>
|
|
||||||
<item>DSports</item>
|
|
||||||
<item>DSports 2</item>
|
|
||||||
<item>DSports Plus</item>
|
|
||||||
<item>ESPN</item>
|
|
||||||
</string-array>
|
|
||||||
</resources>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
<color name="text_secondary">#B3FFFFFF</color>
|
|
||||||
|
|
||||||
<!-- Refresh button colors -->
|
|
||||||
<color name="refresh_button_default">#2A2A2A</color>
|
|
||||||
<color name="refresh_button_border">#4A4A4A</color>
|
|
||||||
<color name="refresh_button_focused">#FFC107</color>
|
|
||||||
<color name="refresh_button_focused_border">#FFD54F</color>
|
|
||||||
<color name="refresh_button_pressed">#FF9800</color>
|
|
||||||
|
|
||||||
<!-- Scrollbar -->
|
|
||||||
<color name="scrollbar_track">#1A1A1A</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<integer name="channel_grid_span">3</integer>
|
|
||||||
</resources>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">StreamPlayer</string>
|
|
||||||
<string name="home_tagline">Todo el deporte en un solo lugar</string>
|
|
||||||
<string name="section_channels">Canales</string>
|
|
||||||
<string name="section_events">Eventos</string>
|
|
||||||
<string name="section_all_channels">Todos los canales</string>
|
|
||||||
<string name="message_no_channels">No hay canales disponibles</string>
|
|
||||||
<string name="message_no_events">No hay eventos disponibles</string>
|
|
||||||
<string name="action_refresh">Actualizar</string>
|
|
||||||
<string name="message_events_error">No se pudieron cargar los eventos: %1$s</string>
|
|
||||||
<string name="update_required_title">Actualización obligatoria</string>
|
|
||||||
<string name="update_available_title">Actualización disponible</string>
|
|
||||||
<string name="update_action_download">Actualizar</string>
|
|
||||||
<string name="update_action_view_release">Ver detalles</string>
|
|
||||||
<string name="update_action_close_app">Salir</string>
|
|
||||||
<string name="update_action_later">Más tarde</string>
|
|
||||||
<string name="update_current_version">Versión instalada: %1$s (%2$d)</string>
|
|
||||||
<string name="update_latest_version">Última versión publicada: %1$s (%2$d)</string>
|
|
||||||
<string name="update_min_supported">Versiones anteriores a %1$d ya no están permitidas.</string>
|
|
||||||
<string name="update_download_size">Tamaño aproximado: %1$s</string>
|
|
||||||
<string name="update_downloads">Descargas registradas: %1$d</string>
|
|
||||||
<string name="update_release_notes_title">Novedades</string>
|
|
||||||
<string name="update_optional_hint">Puedes continuar usando la app, pero recomendamos instalar la actualización para obtener el mejor rendimiento.</string>
|
|
||||||
<string name="update_error_checking">No se pudo verificar actualizaciones (%1$s)</string>
|
|
||||||
<string name="update_error_open_release">No se pudo abrir el detalle de la versión</string>
|
|
||||||
<string name="update_error_empty_response">Respuesta vacía del servidor de releases</string>
|
|
||||||
<string name="update_error_http">Error de red (%1$d)</string>
|
|
||||||
<string name="update_error_missing_url">No se encontró URL de descarga</string>
|
|
||||||
<string name="update_error_download_manager">DownloadManager no está disponible en este dispositivo</string>
|
|
||||||
<string name="update_error_storage">No se pudo preparar el almacenamiento para la actualización</string>
|
|
||||||
<string name="update_error_download">Error al iniciar la descarga: %1$s</string>
|
|
||||||
<string name="update_download_started">Descarga iniciada, revisa la notificación para ver el progreso</string>
|
|
||||||
<string name="update_download_complete">Descarga finalizada, preparando instalación…</string>
|
|
||||||
<string name="update_error_download_failed">La descarga falló (código %1$d)</string>
|
|
||||||
<string name="update_error_install_permissions">No se pudo abrir la configuración de instalación desconocida</string>
|
|
||||||
<string name="update_permission_request">Habilita "Instalar apps desconocidas" para StreamPlayer y regresa para continuar.</string>
|
|
||||||
<string name="update_error_install_intent">No se pudo abrir el instalador de paquetes</string>
|
|
||||||
<string name="update_notification_title">StreamPlayer %1$s</string>
|
|
||||||
<string name="update_notification_description">Descargando nueva versión</string>
|
|
||||||
<string name="device_blocked_title">Dispositivo bloqueado</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_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_copy_token">Copiar código</string>
|
|
||||||
<string name="device_blocked_copy_success">Código copiado al portapapeles</string>
|
|
||||||
<string name="device_blocked_copy_error">No se pudo copiar el código</string>
|
|
||||||
<string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<style name="Theme.StreamPlayer" parent="Theme.AppCompat.Light.NoActionBar">
|
|
||||||
<item name="colorPrimary">@color/black</item>
|
|
||||||
<item name="colorPrimaryDark">@color/black</item>
|
|
||||||
<item name="colorAccent">@color/white</item>
|
|
||||||
<item name="android:statusBarColor">@color/black</item>
|
|
||||||
<item name="android:navigationBarColor">@color/black</item>
|
|
||||||
</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>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample backup rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/guide/topics/data/autobackup
|
|
||||||
for details.
|
|
||||||
Note: This file is ignored for devices older that API 23, even
|
|
||||||
if they have auto backup available.
|
|
||||||
-->
|
|
||||||
<full-backup-content>
|
|
||||||
<!--
|
|
||||||
<include domain="sharedpref" path="."/>
|
|
||||||
<exclude domain="sharedpref" path="device.xml"/>
|
|
||||||
-->
|
|
||||||
<exclude domain="sharedpref" path="." />
|
|
||||||
</full-backup-content>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample data extraction rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
|
||||||
for details.
|
|
||||||
-->
|
|
||||||
<data-extraction-rules>
|
|
||||||
<cloud-backup>
|
|
||||||
<!--
|
|
||||||
<include domain="file" path="."/>
|
|
||||||
<exclude domain="file" path="no_backup/"/>
|
|
||||||
-->
|
|
||||||
<exclude domain="sharedpref" path="." />
|
|
||||||
</cloud-backup>
|
|
||||||
<!--
|
|
||||||
<device-transfer>
|
|
||||||
<include domain="file" path="."/>
|
|
||||||
<exclude domain="file" path="no_backup/"/>
|
|
||||||
</device-transfer>
|
|
||||||
-->
|
|
||||||
</data-extraction-rules>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<external-files-path
|
|
||||||
name="updates"
|
|
||||||
path="." />
|
|
||||||
</paths>
|
|
||||||
20
build.gradle
@@ -1,20 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:8.5.1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task clean(type: Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
||||||
95
build_apk.sh
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# StreamPlayer APK Build Script
|
|
||||||
# Esta aplicación Android reproduce streaming usando DNS de Google
|
|
||||||
|
|
||||||
echo "=== StreamPlayer APK Build ==="
|
|
||||||
echo "URL: https://streamtpmedia.com/global2.php?stream=espn"
|
|
||||||
echo "DNS: 8.8.8.8, 8.8.4.4"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Crear APK básico con estructura Android
|
|
||||||
mkdir -p build/intermediates/classes/com/streamplayer
|
|
||||||
|
|
||||||
# Compilar archivos Kotlin (simulado para este demo)
|
|
||||||
echo "Compilando fuentes Kotlin..."
|
|
||||||
find app/src/main/java -name "*.kt" | while read file; do
|
|
||||||
echo "Compilando: $file"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Copiar recursos
|
|
||||||
echo "Copiando recursos..."
|
|
||||||
cp -r app/src/main/res build/intermediates/
|
|
||||||
|
|
||||||
# Crear AndroidManifest.xml procesado
|
|
||||||
mkdir -p build/intermediates/manifests
|
|
||||||
cp app/src/main/AndroidManifest.xml build/intermediates/manifests/
|
|
||||||
|
|
||||||
# Crear APK structure
|
|
||||||
mkdir -p build/apk/lib
|
|
||||||
mkdir -p build/apk/res
|
|
||||||
mkdir -p build/apk/META-INF
|
|
||||||
|
|
||||||
# Copiar recursos al APK
|
|
||||||
cp -r build/intermediates/res/* build/apk/res/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Crear manifest simplificado para el APK
|
|
||||||
cat > build/apk/AndroidManifest.xml << 'EOF'
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.streamplayer" android:versionCode="1" android:versionName="1.0">
|
|
||||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="33"/>
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
|
||||||
<application android:label="StreamPlayer" android:icon="@mipmap/ic_launcher">
|
|
||||||
<activity android:name=".MainActivity" android:exported="true"
|
|
||||||
android:screenOrientation="landscape">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Crear classes.dex (simulado)
|
|
||||||
echo "Creando classes.dex..."
|
|
||||||
echo "SIMULATED_DEX_FOR_STREAMPLAYER" > build/apk/classes.dex
|
|
||||||
|
|
||||||
# Crear resources.arsc (simulado)
|
|
||||||
echo "CREATED: $(date)" > build/apk/resources.arsc
|
|
||||||
|
|
||||||
# Crear APK usando zip
|
|
||||||
echo "Creando APK..."
|
|
||||||
cd build/apk
|
|
||||||
zip -r ../streamplayer.apk . > /dev/null
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# Firmar APK (simulado)
|
|
||||||
echo "Firmando APK..."
|
|
||||||
echo "UNSIGNED_DEBUG_BUILD" >> build/streamplayer.apk
|
|
||||||
|
|
||||||
# Mover APK final
|
|
||||||
cp build/streamplayer.apk ./StreamPlayer.apk
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "✅ APK CREADO EXITOSAMENTE!"
|
|
||||||
echo "📁 Archivo: StreamPlayer.apk"
|
|
||||||
echo "📱 App: StreamPlayer - Reproductor con DNS Google"
|
|
||||||
echo "🌐 Stream: ESPN (vía DNS 8.8.8.8, 8.8.4.4)"
|
|
||||||
echo "🔐 Permisos: INTERNET, ACCESS_NETWORK_STATE"
|
|
||||||
echo
|
|
||||||
echo "Características:"
|
|
||||||
echo "• Reproducción de streaming HTTP/HTTPS"
|
|
||||||
echo "• Optimización DNS para streaming"
|
|
||||||
echo "• Interfaz fullscreen landscape"
|
|
||||||
echo "• ExoPlayer integrado"
|
|
||||||
echo "• Íconos personalizados"
|
|
||||||
echo
|
|
||||||
echo "Para instalar:"
|
|
||||||
echo "adb install StreamPlayer.apk"
|
|
||||||
echo
|
|
||||||
echo "⚠️ Nota: Este es un APK de demostración."
|
|
||||||
echo " Para producción, compílalo con Android Studio."
|
|
||||||
echo " La configuración DNS está implementada en DNSSetter.kt"
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import json
|
|
||||||
import urllib.request
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.error
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
GITEA_URL = "https://gitea.cbcren.online/api/v1"
|
|
||||||
REPO_OWNER = "renato97"
|
|
||||||
REPO_NAME = "app"
|
|
||||||
TOKEN = "efeed2af00597883adb04da70bd6a7c2993ae92d"
|
|
||||||
TAG_NAME = "v10.1.7"
|
|
||||||
RELEASE_NAME = "StreamPlayer v10.1.7"
|
|
||||||
CHANGELOG_FILE = "CHANGELOG-v10.1.7.md"
|
|
||||||
APK_FILE = "StreamPlayer-10.1.7-debug.apk"
|
|
||||||
|
|
||||||
def create_release():
|
|
||||||
try:
|
|
||||||
with open(CHANGELOG_FILE, 'r') as f:
|
|
||||||
body = f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"Error: {CHANGELOG_FILE} not found.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
url = f"{GITEA_URL}/repos/{REPO_OWNER}/{REPO_NAME}/releases"
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"token {TOKEN}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
"tag_name": TAG_NAME,
|
|
||||||
"target_commitish": "main",
|
|
||||||
"name": RELEASE_NAME,
|
|
||||||
"body": body,
|
|
||||||
"draft": False,
|
|
||||||
"prerelease": False
|
|
||||||
}
|
|
||||||
|
|
||||||
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers=headers, method='POST')
|
|
||||||
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req) as response:
|
|
||||||
result = json.loads(response.read().decode('utf-8'))
|
|
||||||
print(f"Release created successfully. ID: {result['id']}")
|
|
||||||
return result['id']
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
print(f"HTTP Error creating release: {e.code} {e.reason}")
|
|
||||||
print(e.read().decode('utf-8'))
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating release: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def upload_asset(release_id):
|
|
||||||
if not os.path.exists(APK_FILE):
|
|
||||||
print(f"Error: APK file {APK_FILE} not found.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
url = f"{GITEA_URL}/repos/{REPO_OWNER}/{REPO_NAME}/releases/{release_id}/assets"
|
|
||||||
|
|
||||||
# Simple multipart upload via python is tricky without requests library.
|
|
||||||
# However, Gitea API usually accepts raw binary in body if Content-Type is set,
|
|
||||||
# but Gitea's API for assets usually requires multipart/form-data.
|
|
||||||
# Let's check Gitea API docs...
|
|
||||||
# The standard Gitea API uses POST /repos/{owner}/{repo}/releases/{id}/assets with name query parameter and file content in body
|
|
||||||
# Wait, looking at Gitea API docs (swagger usually available at /api/swagger),
|
|
||||||
# POST /repos/{owner}/{repo}/releases/{id}/assets takes 'attachment' as form-data.
|
|
||||||
|
|
||||||
# Implementing multipart/form-data with urllib is painful.
|
|
||||||
# Instead, I will use curl to upload the asset, using the release ID obtained from Python.
|
|
||||||
return release_id
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
release_id = create_release()
|
|
||||||
print(f"RELEASE_ID={release_id}")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"telegramBotToken": "123456:ABCDEF-TOKEN",
|
|
||||||
"telegramChatId": "123456789"
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"deviceId": "f91f2668e8dfb2a7",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "SM-S928B",
|
|
||||||
"model": "SM-S928B",
|
|
||||||
"manufacturer": "Samsung",
|
|
||||||
"osVersion": "16 (API 36)",
|
|
||||||
"appVersionName": "9.4.2",
|
|
||||||
"appVersionCode": 94200,
|
|
||||||
"firstSeen": "2025-11-23T22:31:13.359Z",
|
|
||||||
"lastSeen": "2025-11-25T19:07:38.445Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 22,
|
|
||||||
"ip": "181.23.253.20",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "1714c2bb93670c3f",
|
|
||||||
"adminPart": "9924c7049211c58c",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-11-23T22:31:13.359Z",
|
|
||||||
"verifiedAt": "2025-11-23T22:33:11.942Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "c8ee9361c07a3245",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "23113RKC6G",
|
|
||||||
"model": "23113RKC6G",
|
|
||||||
"manufacturer": "Xiaomi",
|
|
||||||
"osVersion": "15 (API 35)",
|
|
||||||
"appVersionName": "9.4.2",
|
|
||||||
"appVersionCode": 94200,
|
|
||||||
"firstSeen": "2025-11-23T23:19:29.464Z",
|
|
||||||
"lastSeen": "2025-11-23T23:21:02.377Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 3,
|
|
||||||
"ip": "181.23.253.20",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "f7d5a364822457da",
|
|
||||||
"adminPart": "b4acb7da77b11ce9",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-11-23T23:19:29.464Z",
|
|
||||||
"verifiedAt": "2025-11-23T23:20:49.579Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "c874876530da8f76",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "2020/2021 UHD Android TV",
|
|
||||||
"model": "2020/2021 UHD Android TV",
|
|
||||||
"manufacturer": "TPV",
|
|
||||||
"osVersion": "11 (API 30)",
|
|
||||||
"appVersionName": "9.4.2",
|
|
||||||
"appVersionCode": 94200,
|
|
||||||
"firstSeen": "2025-11-24T18:53:40.668Z",
|
|
||||||
"lastSeen": "2025-11-25T01:33:56.790Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 3,
|
|
||||||
"ip": "181.23.253.20",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "76139a364baeda9b",
|
|
||||||
"adminPart": "86601e7089416b57",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-11-24T18:53:40.668Z",
|
|
||||||
"verifiedAt": "2025-11-24T18:54:52.788Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "879fe5ad6ac80e2d",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "SM-S928B",
|
|
||||||
"model": "SM-S928B",
|
|
||||||
"manufacturer": "Samsung",
|
|
||||||
"osVersion": "16 (API 36)",
|
|
||||||
"appVersionName": "9.4.6",
|
|
||||||
"appVersionCode": 94600,
|
|
||||||
"firstSeen": "2025-11-25T19:08:38.948Z",
|
|
||||||
"lastSeen": "2025-12-23T20:41:59.972Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 9,
|
|
||||||
"ip": "181.23.228.93",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "e512eb7d5c026e85",
|
|
||||||
"adminPart": "1891c4eec608a722",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-11-25T19:08:38.948Z",
|
|
||||||
"verifiedAt": "2025-11-25T19:08:56.806Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "97a5c320c47e17ad",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "Chromecast",
|
|
||||||
"model": "Chromecast",
|
|
||||||
"manufacturer": "Google",
|
|
||||||
"osVersion": "14 (API 34)",
|
|
||||||
"appVersionName": "9.4.6",
|
|
||||||
"appVersionCode": 94600,
|
|
||||||
"firstSeen": "2025-11-25T19:10:27.358Z",
|
|
||||||
"lastSeen": "2025-12-29T23:21:36.891Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 26,
|
|
||||||
"ip": "181.23.228.93",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "f35ae98e27e9877c",
|
|
||||||
"adminPart": "e421a660ff38fc67",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-11-25T19:10:27.358Z",
|
|
||||||
"verifiedAt": "2025-11-25T19:10:54.592Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "79a556d89cd9f783",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "motorola edge 30",
|
|
||||||
"model": "motorola edge 30",
|
|
||||||
"manufacturer": "Motorola",
|
|
||||||
"osVersion": "13 (API 33)",
|
|
||||||
"appVersionName": "9.4.6",
|
|
||||||
"appVersionCode": 94600,
|
|
||||||
"firstSeen": "2025-11-25T19:29:17.916Z",
|
|
||||||
"lastSeen": "2025-12-14T20:26:50.664Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 5,
|
|
||||||
"ip": "181.25.52.139",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "4aec5b0e2e1c782a",
|
|
||||||
"adminPart": "7a4bb228e3b5048c",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-11-25T19:29:17.916Z",
|
|
||||||
"verifiedAt": "2025-11-25T19:30:11.849Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "309f9f56550fc16bf047d636",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "WIN-J7S53EBK2BG",
|
|
||||||
"model": "Microsoft Windows 10.0.26100",
|
|
||||||
"manufacturer": "Microsoft",
|
|
||||||
"osVersion": "Microsoft Windows NT 10.0.26100.0",
|
|
||||||
"appVersionName": "9.4.6",
|
|
||||||
"appVersionCode": 94600,
|
|
||||||
"firstSeen": "2025-12-17T18:37:45.562Z",
|
|
||||||
"lastSeen": "2025-12-17T19:28:44.530Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "por boludo",
|
|
||||||
"installs": 21,
|
|
||||||
"ip": "181.25.52.139",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "60989c16f0ed61d9",
|
|
||||||
"adminPart": "c1befd758b4cd459",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-12-17T18:37:45.562Z",
|
|
||||||
"verifiedAt": "2025-12-17T18:38:24.129Z"
|
|
||||||
},
|
|
||||||
"blockedAt": "2025-12-17T19:14:30.701Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "12c96524b10b1e15f5611b0a",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "WIN-1F1PBAQI7PR",
|
|
||||||
"model": "Microsoft Windows 10.0.26100",
|
|
||||||
"manufacturer": "Microsoft",
|
|
||||||
"osVersion": "Microsoft Windows NT 10.0.26100.0",
|
|
||||||
"appVersionName": "9.4.6",
|
|
||||||
"appVersionCode": 94600,
|
|
||||||
"firstSeen": "2025-12-17T19:35:44.810Z",
|
|
||||||
"lastSeen": "2025-12-17T19:38:12.510Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 2,
|
|
||||||
"ip": "181.25.52.139",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "d41b6a6bc639fe77",
|
|
||||||
"adminPart": "dab1fa74da2edab2",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-12-17T19:35:44.810Z",
|
|
||||||
"verifiedAt": "2025-12-17T19:37:59.152Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "6623a19316ebbbc1570b31e2",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "DESKTOP-TF8OENP",
|
|
||||||
"model": "Microsoft Windows 10.0.19045",
|
|
||||||
"manufacturer": "Microsoft",
|
|
||||||
"osVersion": "Microsoft Windows NT 10.0.19045.0",
|
|
||||||
"appVersionName": "9.4.6",
|
|
||||||
"appVersionCode": 94600,
|
|
||||||
"firstSeen": "2025-12-17T19:53:20.007Z",
|
|
||||||
"lastSeen": "2025-12-17T19:56:52.028Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 4,
|
|
||||||
"ip": "190.55.131.98",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "e5ed2a5989a8e44a",
|
|
||||||
"adminPart": "21e79e6e83e662cf",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-12-17T19:53:20.007Z",
|
|
||||||
"verifiedAt": "2025-12-17T19:53:43.017Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "8678935B-0B7A-41B0-B6E3-AB205073BE7F",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "iPhone 17 Pro",
|
|
||||||
"model": "iPhone",
|
|
||||||
"manufacturer": "Apple",
|
|
||||||
"osVersion": "iOS 26.2",
|
|
||||||
"appVersionName": "9.4.2",
|
|
||||||
"appVersionCode": 94200,
|
|
||||||
"firstSeen": "2025-12-29T22:27:06.203Z",
|
|
||||||
"lastSeen": "2025-12-29T22:36:32.797Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 3,
|
|
||||||
"ip": "181.23.228.93",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "fac4063d6b67ce57",
|
|
||||||
"adminPart": "667b10f28d37b534",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-12-29T22:27:06.203Z",
|
|
||||||
"verifiedAt": "2025-12-29T22:30:37.120Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deviceId": "FB4B39C0-A766-4A01-980E-763ACE9118A2",
|
|
||||||
"alias": "",
|
|
||||||
"deviceName": "iPhone 17 Pro",
|
|
||||||
"model": "iPhone",
|
|
||||||
"manufacturer": "Apple",
|
|
||||||
"osVersion": "iOS 26.2",
|
|
||||||
"appVersionName": "9.4.2",
|
|
||||||
"appVersionCode": 94200,
|
|
||||||
"firstSeen": "2025-12-29T22:40:54.202Z",
|
|
||||||
"lastSeen": "2025-12-29T23:04:30.334Z",
|
|
||||||
"blocked": false,
|
|
||||||
"notes": "",
|
|
||||||
"installs": 4,
|
|
||||||
"ip": "181.23.228.93",
|
|
||||||
"country": "AR",
|
|
||||||
"verification": {
|
|
||||||
"clientPart": "353df62e6d1faee3",
|
|
||||||
"adminPart": "648bd37e530033f7",
|
|
||||||
"status": "verified",
|
|
||||||
"createdAt": "2025-12-29T22:40:54.202Z",
|
|
||||||
"verifiedAt": "2025-12-29T22:44:27.529Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
1
dashboard/node_modules/.bin/mime
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../mime/cli.js
|
|
||||||
1
dashboard/node_modules/.bin/nodemon
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../nodemon/bin/nodemon.js
|
|
||||||
1
dashboard/node_modules/.bin/nodetouch
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../touch/bin/nodetouch.js
|
|
||||||
1
dashboard/node_modules/.bin/semver
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../semver/bin/semver.js
|
|
||||||
1631
dashboard/node_modules/.package-lock.json
generated
vendored
243
dashboard/node_modules/accepts/HISTORY.md
generated
vendored
@@ -1,243 +0,0 @@
|
|||||||
1.3.8 / 2022-02-02
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.34
|
|
||||||
- deps: mime-db@~1.51.0
|
|
||||||
* deps: negotiator@0.6.3
|
|
||||||
|
|
||||||
1.3.7 / 2019-04-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.6.2
|
|
||||||
- Fix sorting charset, encoding, and language with extra parameters
|
|
||||||
|
|
||||||
1.3.6 / 2019-04-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.24
|
|
||||||
- deps: mime-db@~1.40.0
|
|
||||||
|
|
||||||
1.3.5 / 2018-02-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.18
|
|
||||||
- deps: mime-db@~1.33.0
|
|
||||||
|
|
||||||
1.3.4 / 2017-08-22
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.16
|
|
||||||
- deps: mime-db@~1.29.0
|
|
||||||
|
|
||||||
1.3.3 / 2016-05-02
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.11
|
|
||||||
- deps: mime-db@~1.23.0
|
|
||||||
* deps: negotiator@0.6.1
|
|
||||||
- perf: improve `Accept` parsing speed
|
|
||||||
- perf: improve `Accept-Charset` parsing speed
|
|
||||||
- perf: improve `Accept-Encoding` parsing speed
|
|
||||||
- perf: improve `Accept-Language` parsing speed
|
|
||||||
|
|
||||||
1.3.2 / 2016-03-08
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.10
|
|
||||||
- Fix extension of `application/dash+xml`
|
|
||||||
- Update primary extension for `audio/mp4`
|
|
||||||
- deps: mime-db@~1.22.0
|
|
||||||
|
|
||||||
1.3.1 / 2016-01-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.9
|
|
||||||
- deps: mime-db@~1.21.0
|
|
||||||
|
|
||||||
1.3.0 / 2015-09-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.7
|
|
||||||
- deps: mime-db@~1.19.0
|
|
||||||
* deps: negotiator@0.6.0
|
|
||||||
- Fix including type extensions in parameters in `Accept` parsing
|
|
||||||
- Fix parsing `Accept` parameters with quoted equals
|
|
||||||
- Fix parsing `Accept` parameters with quoted semicolons
|
|
||||||
- Lazy-load modules from main entry point
|
|
||||||
- perf: delay type concatenation until needed
|
|
||||||
- perf: enable strict mode
|
|
||||||
- perf: hoist regular expressions
|
|
||||||
- perf: remove closures getting spec properties
|
|
||||||
- perf: remove a closure from media type parsing
|
|
||||||
- perf: remove property delete from media type parsing
|
|
||||||
|
|
||||||
1.2.13 / 2015-09-06
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.6
|
|
||||||
- deps: mime-db@~1.18.0
|
|
||||||
|
|
||||||
1.2.12 / 2015-07-30
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.4
|
|
||||||
- deps: mime-db@~1.16.0
|
|
||||||
|
|
||||||
1.2.11 / 2015-07-16
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.3
|
|
||||||
- deps: mime-db@~1.15.0
|
|
||||||
|
|
||||||
1.2.10 / 2015-07-01
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.2
|
|
||||||
- deps: mime-db@~1.14.0
|
|
||||||
|
|
||||||
1.2.9 / 2015-06-08
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.1
|
|
||||||
- perf: fix deopt during mapping
|
|
||||||
|
|
||||||
1.2.8 / 2015-06-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.0
|
|
||||||
- deps: mime-db@~1.13.0
|
|
||||||
* perf: avoid argument reassignment & argument slice
|
|
||||||
* perf: avoid negotiator recursive construction
|
|
||||||
* perf: enable strict mode
|
|
||||||
* perf: remove unnecessary bitwise operator
|
|
||||||
|
|
||||||
1.2.7 / 2015-05-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.5.3
|
|
||||||
- Fix media type parameter matching to be case-insensitive
|
|
||||||
|
|
||||||
1.2.6 / 2015-05-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.11
|
|
||||||
- deps: mime-db@~1.9.1
|
|
||||||
* deps: negotiator@0.5.2
|
|
||||||
- Fix comparing media types with quoted values
|
|
||||||
- Fix splitting media types with quoted commas
|
|
||||||
|
|
||||||
1.2.5 / 2015-03-13
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.10
|
|
||||||
- deps: mime-db@~1.8.0
|
|
||||||
|
|
||||||
1.2.4 / 2015-02-14
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Support Node.js 0.6
|
|
||||||
* deps: mime-types@~2.0.9
|
|
||||||
- deps: mime-db@~1.7.0
|
|
||||||
* deps: negotiator@0.5.1
|
|
||||||
- Fix preference sorting to be stable for long acceptable lists
|
|
||||||
|
|
||||||
1.2.3 / 2015-01-31
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.8
|
|
||||||
- deps: mime-db@~1.6.0
|
|
||||||
|
|
||||||
1.2.2 / 2014-12-30
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.7
|
|
||||||
- deps: mime-db@~1.5.0
|
|
||||||
|
|
||||||
1.2.1 / 2014-12-30
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.5
|
|
||||||
- deps: mime-db@~1.3.1
|
|
||||||
|
|
||||||
1.2.0 / 2014-12-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.5.0
|
|
||||||
- Fix list return order when large accepted list
|
|
||||||
- Fix missing identity encoding when q=0 exists
|
|
||||||
- Remove dynamic building of Negotiator class
|
|
||||||
|
|
||||||
1.1.4 / 2014-12-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.4
|
|
||||||
- deps: mime-db@~1.3.0
|
|
||||||
|
|
||||||
1.1.3 / 2014-11-09
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.3
|
|
||||||
- deps: mime-db@~1.2.0
|
|
||||||
|
|
||||||
1.1.2 / 2014-10-14
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.4.9
|
|
||||||
- Fix error when media type has invalid parameter
|
|
||||||
|
|
||||||
1.1.1 / 2014-09-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.2
|
|
||||||
- deps: mime-db@~1.1.0
|
|
||||||
* deps: negotiator@0.4.8
|
|
||||||
- Fix all negotiations to be case-insensitive
|
|
||||||
- Stable sort preferences of same quality according to client order
|
|
||||||
|
|
||||||
1.1.0 / 2014-09-02
|
|
||||||
==================
|
|
||||||
|
|
||||||
* update `mime-types`
|
|
||||||
|
|
||||||
1.0.7 / 2014-07-04
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix wrong type returned from `type` when match after unknown extension
|
|
||||||
|
|
||||||
1.0.6 / 2014-06-24
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.4.7
|
|
||||||
|
|
||||||
1.0.5 / 2014-06-20
|
|
||||||
==================
|
|
||||||
|
|
||||||
* fix crash when unknown extension given
|
|
||||||
|
|
||||||
1.0.4 / 2014-06-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* use `mime-types`
|
|
||||||
|
|
||||||
1.0.3 / 2014-06-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.4.6
|
|
||||||
- Order by specificity when quality is the same
|
|
||||||
|
|
||||||
1.0.2 / 2014-05-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix interpretation when header not in request
|
|
||||||
* deps: pin negotiator@0.4.5
|
|
||||||
|
|
||||||
1.0.1 / 2014-01-18
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Identity encoding isn't always acceptable
|
|
||||||
* deps: negotiator@~0.4.0
|
|
||||||
|
|
||||||
1.0.0 / 2013-12-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Genesis
|
|
||||||
23
dashboard/node_modules/accepts/LICENSE
generated
vendored
@@ -1,23 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
|
||||||
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
'Software'), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
140
dashboard/node_modules/accepts/README.md
generated
vendored
@@ -1,140 +0,0 @@
|
|||||||
# accepts
|
|
||||||
|
|
||||||
[![NPM Version][npm-version-image]][npm-url]
|
|
||||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
|
||||||
[![Node.js Version][node-version-image]][node-version-url]
|
|
||||||
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
|
||||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator).
|
|
||||||
Extracted from [koa](https://www.npmjs.com/package/koa) for general use.
|
|
||||||
|
|
||||||
In addition to negotiator, it allows:
|
|
||||||
|
|
||||||
- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])`
|
|
||||||
as well as `('text/html', 'application/json')`.
|
|
||||||
- Allows type shorthands such as `json`.
|
|
||||||
- Returns `false` when no types match
|
|
||||||
- Treats non-existent headers as `*`
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
|
||||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
|
||||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ npm install accepts
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```js
|
|
||||||
var accepts = require('accepts')
|
|
||||||
```
|
|
||||||
|
|
||||||
### accepts(req)
|
|
||||||
|
|
||||||
Create a new `Accepts` object for the given `req`.
|
|
||||||
|
|
||||||
#### .charset(charsets)
|
|
||||||
|
|
||||||
Return the first accepted charset. If nothing in `charsets` is accepted,
|
|
||||||
then `false` is returned.
|
|
||||||
|
|
||||||
#### .charsets()
|
|
||||||
|
|
||||||
Return the charsets that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
#### .encoding(encodings)
|
|
||||||
|
|
||||||
Return the first accepted encoding. If nothing in `encodings` is accepted,
|
|
||||||
then `false` is returned.
|
|
||||||
|
|
||||||
#### .encodings()
|
|
||||||
|
|
||||||
Return the encodings that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
#### .language(languages)
|
|
||||||
|
|
||||||
Return the first accepted language. If nothing in `languages` is accepted,
|
|
||||||
then `false` is returned.
|
|
||||||
|
|
||||||
#### .languages()
|
|
||||||
|
|
||||||
Return the languages that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
#### .type(types)
|
|
||||||
|
|
||||||
Return the first accepted type (and it is returned as the same text as what
|
|
||||||
appears in the `types` array). If nothing in `types` is accepted, then `false`
|
|
||||||
is returned.
|
|
||||||
|
|
||||||
The `types` array can contain full MIME types or file extensions. Any value
|
|
||||||
that is not a full MIME types is passed to `require('mime-types').lookup`.
|
|
||||||
|
|
||||||
#### .types()
|
|
||||||
|
|
||||||
Return the types that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Simple type negotiation
|
|
||||||
|
|
||||||
This simple example shows how to use `accepts` to return a different typed
|
|
||||||
respond body based on what the client wants to accept. The server lists it's
|
|
||||||
preferences in order and will get back the best match between the client and
|
|
||||||
server.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var accepts = require('accepts')
|
|
||||||
var http = require('http')
|
|
||||||
|
|
||||||
function app (req, res) {
|
|
||||||
var accept = accepts(req)
|
|
||||||
|
|
||||||
// the order of this list is significant; should be server preferred order
|
|
||||||
switch (accept.type(['json', 'html'])) {
|
|
||||||
case 'json':
|
|
||||||
res.setHeader('Content-Type', 'application/json')
|
|
||||||
res.write('{"hello":"world!"}')
|
|
||||||
break
|
|
||||||
case 'html':
|
|
||||||
res.setHeader('Content-Type', 'text/html')
|
|
||||||
res.write('<b>hello, world!</b>')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// the fallback is text/plain, so no need to specify it above
|
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
|
||||||
res.write('hello, world!')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
http.createServer(app).listen(3000)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can test this out with the cURL program:
|
|
||||||
```sh
|
|
||||||
curl -I -H'Accept: text/html' http://localhost:3000/
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE)
|
|
||||||
|
|
||||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master
|
|
||||||
[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master
|
|
||||||
[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci
|
|
||||||
[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml
|
|
||||||
[node-version-image]: https://badgen.net/npm/node/accepts
|
|
||||||
[node-version-url]: https://nodejs.org/en/download
|
|
||||||
[npm-downloads-image]: https://badgen.net/npm/dm/accepts
|
|
||||||
[npm-url]: https://npmjs.org/package/accepts
|
|
||||||
[npm-version-image]: https://badgen.net/npm/v/accepts
|
|
||||||
238
dashboard/node_modules/accepts/index.js
generated
vendored
@@ -1,238 +0,0 @@
|
|||||||
/*!
|
|
||||||
* accepts
|
|
||||||
* Copyright(c) 2014 Jonathan Ong
|
|
||||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Negotiator = require('negotiator')
|
|
||||||
var mime = require('mime-types')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = Accepts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Accepts object for the given req.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Accepts (req) {
|
|
||||||
if (!(this instanceof Accepts)) {
|
|
||||||
return new Accepts(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.headers = req.headers
|
|
||||||
this.negotiator = new Negotiator(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given `type(s)` is acceptable, returning
|
|
||||||
* the best match when true, otherwise `undefined`, in which
|
|
||||||
* case you should respond with 406 "Not Acceptable".
|
|
||||||
*
|
|
||||||
* The `type` value may be a single mime type string
|
|
||||||
* such as "application/json", the extension name
|
|
||||||
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
|
||||||
* or array is given the _best_ match, if any is returned.
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
*
|
|
||||||
* // Accept: text/html
|
|
||||||
* this.types('html');
|
|
||||||
* // => "html"
|
|
||||||
*
|
|
||||||
* // Accept: text/*, application/json
|
|
||||||
* this.types('html');
|
|
||||||
* // => "html"
|
|
||||||
* this.types('text/html');
|
|
||||||
* // => "text/html"
|
|
||||||
* this.types('json', 'text');
|
|
||||||
* // => "json"
|
|
||||||
* this.types('application/json');
|
|
||||||
* // => "application/json"
|
|
||||||
*
|
|
||||||
* // Accept: text/*, application/json
|
|
||||||
* this.types('image/png');
|
|
||||||
* this.types('png');
|
|
||||||
* // => undefined
|
|
||||||
*
|
|
||||||
* // Accept: text/*;q=.5, application/json
|
|
||||||
* this.types(['html', 'json']);
|
|
||||||
* this.types('html', 'json');
|
|
||||||
* // => "json"
|
|
||||||
*
|
|
||||||
* @param {String|Array} types...
|
|
||||||
* @return {String|Array|Boolean}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.type =
|
|
||||||
Accepts.prototype.types = function (types_) {
|
|
||||||
var types = types_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (types && !Array.isArray(types)) {
|
|
||||||
types = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < types.length; i++) {
|
|
||||||
types[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no types, return all requested types
|
|
||||||
if (!types || types.length === 0) {
|
|
||||||
return this.negotiator.mediaTypes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no accept header, return first given type
|
|
||||||
if (!this.headers.accept) {
|
|
||||||
return types[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
var mimes = types.map(extToMime)
|
|
||||||
var accepts = this.negotiator.mediaTypes(mimes.filter(validMime))
|
|
||||||
var first = accepts[0]
|
|
||||||
|
|
||||||
return first
|
|
||||||
? types[mimes.indexOf(first)]
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return accepted encodings or best fit based on `encodings`.
|
|
||||||
*
|
|
||||||
* Given `Accept-Encoding: gzip, deflate`
|
|
||||||
* an array sorted by quality is returned:
|
|
||||||
*
|
|
||||||
* ['gzip', 'deflate']
|
|
||||||
*
|
|
||||||
* @param {String|Array} encodings...
|
|
||||||
* @return {String|Array}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.encoding =
|
|
||||||
Accepts.prototype.encodings = function (encodings_) {
|
|
||||||
var encodings = encodings_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (encodings && !Array.isArray(encodings)) {
|
|
||||||
encodings = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < encodings.length; i++) {
|
|
||||||
encodings[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no encodings, return all requested encodings
|
|
||||||
if (!encodings || encodings.length === 0) {
|
|
||||||
return this.negotiator.encodings()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.negotiator.encodings(encodings)[0] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return accepted charsets or best fit based on `charsets`.
|
|
||||||
*
|
|
||||||
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
|
||||||
* an array sorted by quality is returned:
|
|
||||||
*
|
|
||||||
* ['utf-8', 'utf-7', 'iso-8859-1']
|
|
||||||
*
|
|
||||||
* @param {String|Array} charsets...
|
|
||||||
* @return {String|Array}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.charset =
|
|
||||||
Accepts.prototype.charsets = function (charsets_) {
|
|
||||||
var charsets = charsets_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (charsets && !Array.isArray(charsets)) {
|
|
||||||
charsets = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < charsets.length; i++) {
|
|
||||||
charsets[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no charsets, return all requested charsets
|
|
||||||
if (!charsets || charsets.length === 0) {
|
|
||||||
return this.negotiator.charsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.negotiator.charsets(charsets)[0] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return accepted languages or best fit based on `langs`.
|
|
||||||
*
|
|
||||||
* Given `Accept-Language: en;q=0.8, es, pt`
|
|
||||||
* an array sorted by quality is returned:
|
|
||||||
*
|
|
||||||
* ['es', 'pt', 'en']
|
|
||||||
*
|
|
||||||
* @param {String|Array} langs...
|
|
||||||
* @return {Array|String}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.lang =
|
|
||||||
Accepts.prototype.langs =
|
|
||||||
Accepts.prototype.language =
|
|
||||||
Accepts.prototype.languages = function (languages_) {
|
|
||||||
var languages = languages_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (languages && !Array.isArray(languages)) {
|
|
||||||
languages = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < languages.length; i++) {
|
|
||||||
languages[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no languages, return all requested languages
|
|
||||||
if (!languages || languages.length === 0) {
|
|
||||||
return this.negotiator.languages()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.negotiator.languages(languages)[0] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert extnames to mime.
|
|
||||||
*
|
|
||||||
* @param {String} type
|
|
||||||
* @return {String}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function extToMime (type) {
|
|
||||||
return type.indexOf('/') === -1
|
|
||||||
? mime.lookup(type)
|
|
||||||
: type
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if mime is valid.
|
|
||||||
*
|
|
||||||
* @param {String} type
|
|
||||||
* @return {String}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function validMime (type) {
|
|
||||||
return typeof type === 'string'
|
|
||||||
}
|
|
||||||
47
dashboard/node_modules/accepts/package.json
generated
vendored
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "accepts",
|
|
||||||
"description": "Higher-level content negotiation",
|
|
||||||
"version": "1.3.8",
|
|
||||||
"contributors": [
|
|
||||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
|
||||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": "jshttp/accepts",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"deep-equal": "1.0.1",
|
|
||||||
"eslint": "7.32.0",
|
|
||||||
"eslint-config-standard": "14.1.1",
|
|
||||||
"eslint-plugin-import": "2.25.4",
|
|
||||||
"eslint-plugin-markdown": "2.2.1",
|
|
||||||
"eslint-plugin-node": "11.1.0",
|
|
||||||
"eslint-plugin-promise": "4.3.1",
|
|
||||||
"eslint-plugin-standard": "4.1.0",
|
|
||||||
"mocha": "9.2.0",
|
|
||||||
"nyc": "15.1.0"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"LICENSE",
|
|
||||||
"HISTORY.md",
|
|
||||||
"index.js"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint .",
|
|
||||||
"test": "mocha --reporter spec --check-leaks --bail test/",
|
|
||||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
|
||||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"content",
|
|
||||||
"negotiation",
|
|
||||||
"accept",
|
|
||||||
"accepts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
15
dashboard/node_modules/anymatch/LICENSE
generated
vendored
@@ -1,15 +0,0 @@
|
|||||||
The ISC License
|
|
||||||
|
|
||||||
Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com)
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
|
||||||
copyright notice and this permission notice appear in all copies.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|
||||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
87
dashboard/node_modules/anymatch/README.md
generated
vendored
@@ -1,87 +0,0 @@
|
|||||||
anymatch [](https://travis-ci.org/micromatch/anymatch) [](https://coveralls.io/r/micromatch/anymatch?branch=master)
|
|
||||||
======
|
|
||||||
Javascript module to match a string against a regular expression, glob, string,
|
|
||||||
or function that takes the string as an argument and returns a truthy or falsy
|
|
||||||
value. The matcher can also be an array of any or all of these. Useful for
|
|
||||||
allowing a very flexible user-defined config to define things like file paths.
|
|
||||||
|
|
||||||
__Note: This module has Bash-parity, please be aware that Windows-style backslashes are not supported as separators. See https://github.com/micromatch/micromatch#backslashes for more information.__
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
```sh
|
|
||||||
npm install anymatch
|
|
||||||
```
|
|
||||||
|
|
||||||
#### anymatch(matchers, testString, [returnIndex], [options])
|
|
||||||
* __matchers__: (_Array|String|RegExp|Function_)
|
|
||||||
String to be directly matched, string with glob patterns, regular expression
|
|
||||||
test, function that takes the testString as an argument and returns a truthy
|
|
||||||
value if it should be matched, or an array of any number and mix of these types.
|
|
||||||
* __testString__: (_String|Array_) The string to test against the matchers. If
|
|
||||||
passed as an array, the first element of the array will be used as the
|
|
||||||
`testString` for non-function matchers, while the entire array will be applied
|
|
||||||
as the arguments for function matchers.
|
|
||||||
* __options__: (_Object_ [optional]_) Any of the [picomatch](https://github.com/micromatch/picomatch#options) options.
|
|
||||||
* __returnIndex__: (_Boolean [optional]_) If true, return the array index of
|
|
||||||
the first matcher that that testString matched, or -1 if no match, instead of a
|
|
||||||
boolean result.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const anymatch = require('anymatch');
|
|
||||||
|
|
||||||
const matchers = [ 'path/to/file.js', 'path/anyjs/**/*.js', /foo.js$/, string => string.includes('bar') && string.length > 10 ] ;
|
|
||||||
|
|
||||||
anymatch(matchers, 'path/to/file.js'); // true
|
|
||||||
anymatch(matchers, 'path/anyjs/baz.js'); // true
|
|
||||||
anymatch(matchers, 'path/to/foo.js'); // true
|
|
||||||
anymatch(matchers, 'path/to/bar.js'); // true
|
|
||||||
anymatch(matchers, 'bar.js'); // false
|
|
||||||
|
|
||||||
// returnIndex = true
|
|
||||||
anymatch(matchers, 'foo.js', {returnIndex: true}); // 2
|
|
||||||
anymatch(matchers, 'path/anyjs/foo.js', {returnIndex: true}); // 1
|
|
||||||
|
|
||||||
// any picomatc
|
|
||||||
|
|
||||||
// using globs to match directories and their children
|
|
||||||
anymatch('node_modules', 'node_modules'); // true
|
|
||||||
anymatch('node_modules', 'node_modules/somelib/index.js'); // false
|
|
||||||
anymatch('node_modules/**', 'node_modules/somelib/index.js'); // true
|
|
||||||
anymatch('node_modules/**', '/absolute/path/to/node_modules/somelib/index.js'); // false
|
|
||||||
anymatch('**/node_modules/**', '/absolute/path/to/node_modules/somelib/index.js'); // true
|
|
||||||
|
|
||||||
const matcher = anymatch(matchers);
|
|
||||||
['foo.js', 'bar.js'].filter(matcher); // [ 'foo.js' ]
|
|
||||||
anymatch master* ❯
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### anymatch(matchers)
|
|
||||||
You can also pass in only your matcher(s) to get a curried function that has
|
|
||||||
already been bound to the provided matching criteria. This can be used as an
|
|
||||||
`Array#filter` callback.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var matcher = anymatch(matchers);
|
|
||||||
|
|
||||||
matcher('path/to/file.js'); // true
|
|
||||||
matcher('path/anyjs/baz.js', true); // 1
|
|
||||||
|
|
||||||
['foo.js', 'bar.js'].filter(matcher); // ['foo.js']
|
|
||||||
```
|
|
||||||
|
|
||||||
Changelog
|
|
||||||
----------
|
|
||||||
[See release notes page on GitHub](https://github.com/micromatch/anymatch/releases)
|
|
||||||
|
|
||||||
- **v3.0:** Removed `startIndex` and `endIndex` arguments. Node 8.x-only.
|
|
||||||
- **v2.0:** [micromatch](https://github.com/jonschlinkert/micromatch) moves away from minimatch-parity and inline with Bash. This includes handling backslashes differently (see https://github.com/micromatch/micromatch#backslashes for more information).
|
|
||||||
- **v1.2:** anymatch uses [micromatch](https://github.com/jonschlinkert/micromatch)
|
|
||||||
for glob pattern matching. Issues with glob pattern matching should be
|
|
||||||
reported directly to the [micromatch issue tracker](https://github.com/jonschlinkert/micromatch/issues).
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
[ISC](https://raw.github.com/micromatch/anymatch/master/LICENSE)
|
|
||||||
20
dashboard/node_modules/anymatch/index.d.ts
generated
vendored
@@ -1,20 +0,0 @@
|
|||||||
type AnymatchFn = (testString: string) => boolean;
|
|
||||||
type AnymatchPattern = string|RegExp|AnymatchFn;
|
|
||||||
type AnymatchMatcher = AnymatchPattern|AnymatchPattern[]
|
|
||||||
type AnymatchTester = {
|
|
||||||
(testString: string|any[], returnIndex: true): number;
|
|
||||||
(testString: string|any[]): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PicomatchOptions = {dot: boolean};
|
|
||||||
|
|
||||||
declare const anymatch: {
|
|
||||||
(matchers: AnymatchMatcher): AnymatchTester;
|
|
||||||
(matchers: AnymatchMatcher, testString: null, returnIndex: true | PicomatchOptions): AnymatchTester;
|
|
||||||
(matchers: AnymatchMatcher, testString: string|any[], returnIndex: true | PicomatchOptions): number;
|
|
||||||
(matchers: AnymatchMatcher, testString: string|any[]): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {AnymatchMatcher as Matcher}
|
|
||||||
export {AnymatchTester as Tester}
|
|
||||||
export default anymatch
|
|
||||||
104
dashboard/node_modules/anymatch/index.js
generated
vendored
@@ -1,104 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
|
|
||||||
const picomatch = require('picomatch');
|
|
||||||
const normalizePath = require('normalize-path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {(testString: string) => boolean} AnymatchFn
|
|
||||||
* @typedef {string|RegExp|AnymatchFn} AnymatchPattern
|
|
||||||
* @typedef {AnymatchPattern|AnymatchPattern[]} AnymatchMatcher
|
|
||||||
*/
|
|
||||||
const BANG = '!';
|
|
||||||
const DEFAULT_OPTIONS = {returnIndex: false};
|
|
||||||
const arrify = (item) => Array.isArray(item) ? item : [item];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {AnymatchPattern} matcher
|
|
||||||
* @param {object} options
|
|
||||||
* @returns {AnymatchFn}
|
|
||||||
*/
|
|
||||||
const createPattern = (matcher, options) => {
|
|
||||||
if (typeof matcher === 'function') {
|
|
||||||
return matcher;
|
|
||||||
}
|
|
||||||
if (typeof matcher === 'string') {
|
|
||||||
const glob = picomatch(matcher, options);
|
|
||||||
return (string) => matcher === string || glob(string);
|
|
||||||
}
|
|
||||||
if (matcher instanceof RegExp) {
|
|
||||||
return (string) => matcher.test(string);
|
|
||||||
}
|
|
||||||
return (string) => false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array<Function>} patterns
|
|
||||||
* @param {Array<Function>} negPatterns
|
|
||||||
* @param {String|Array} args
|
|
||||||
* @param {Boolean} returnIndex
|
|
||||||
* @returns {boolean|number}
|
|
||||||
*/
|
|
||||||
const matchPatterns = (patterns, negPatterns, args, returnIndex) => {
|
|
||||||
const isList = Array.isArray(args);
|
|
||||||
const _path = isList ? args[0] : args;
|
|
||||||
if (!isList && typeof _path !== 'string') {
|
|
||||||
throw new TypeError('anymatch: second argument must be a string: got ' +
|
|
||||||
Object.prototype.toString.call(_path))
|
|
||||||
}
|
|
||||||
const path = normalizePath(_path, false);
|
|
||||||
|
|
||||||
for (let index = 0; index < negPatterns.length; index++) {
|
|
||||||
const nglob = negPatterns[index];
|
|
||||||
if (nglob(path)) {
|
|
||||||
return returnIndex ? -1 : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const applied = isList && [path].concat(args.slice(1));
|
|
||||||
for (let index = 0; index < patterns.length; index++) {
|
|
||||||
const pattern = patterns[index];
|
|
||||||
if (isList ? pattern(...applied) : pattern(path)) {
|
|
||||||
return returnIndex ? index : true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnIndex ? -1 : false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {AnymatchMatcher} matchers
|
|
||||||
* @param {Array|string} testString
|
|
||||||
* @param {object} options
|
|
||||||
* @returns {boolean|number|Function}
|
|
||||||
*/
|
|
||||||
const anymatch = (matchers, testString, options = DEFAULT_OPTIONS) => {
|
|
||||||
if (matchers == null) {
|
|
||||||
throw new TypeError('anymatch: specify first argument');
|
|
||||||
}
|
|
||||||
const opts = typeof options === 'boolean' ? {returnIndex: options} : options;
|
|
||||||
const returnIndex = opts.returnIndex || false;
|
|
||||||
|
|
||||||
// Early cache for matchers.
|
|
||||||
const mtchers = arrify(matchers);
|
|
||||||
const negatedGlobs = mtchers
|
|
||||||
.filter(item => typeof item === 'string' && item.charAt(0) === BANG)
|
|
||||||
.map(item => item.slice(1))
|
|
||||||
.map(item => picomatch(item, opts));
|
|
||||||
const patterns = mtchers
|
|
||||||
.filter(item => typeof item !== 'string' || (typeof item === 'string' && item.charAt(0) !== BANG))
|
|
||||||
.map(matcher => createPattern(matcher, opts));
|
|
||||||
|
|
||||||
if (testString == null) {
|
|
||||||
return (testString, ri = false) => {
|
|
||||||
const returnIndex = typeof ri === 'boolean' ? ri : false;
|
|
||||||
return matchPatterns(patterns, negatedGlobs, testString, returnIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchPatterns(patterns, negatedGlobs, testString, returnIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
anymatch.default = anymatch;
|
|
||||||
module.exports = anymatch;
|
|
||||||
48
dashboard/node_modules/anymatch/package.json
generated
vendored
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "anymatch",
|
|
||||||
"version": "3.1.3",
|
|
||||||
"description": "Matches strings against configurable strings, globs, regular expressions, and/or functions",
|
|
||||||
"files": [
|
|
||||||
"index.js",
|
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"normalize-path": "^3.0.0",
|
|
||||||
"picomatch": "^2.0.4"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "Elan Shanker",
|
|
||||||
"url": "https://github.com/es128"
|
|
||||||
},
|
|
||||||
"license": "ISC",
|
|
||||||
"homepage": "https://github.com/micromatch/anymatch",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/micromatch/anymatch"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"match",
|
|
||||||
"any",
|
|
||||||
"string",
|
|
||||||
"file",
|
|
||||||
"fs",
|
|
||||||
"list",
|
|
||||||
"glob",
|
|
||||||
"regex",
|
|
||||||
"regexp",
|
|
||||||
"regular",
|
|
||||||
"expression",
|
|
||||||
"function"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"test": "nyc mocha",
|
|
||||||
"mocha": "mocha"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"mocha": "^6.1.3",
|
|
||||||
"nyc": "^14.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
dashboard/node_modules/array-flatten/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
43
dashboard/node_modules/array-flatten/README.md
generated
vendored
@@ -1,43 +0,0 @@
|
|||||||
# Array Flatten
|
|
||||||
|
|
||||||
[![NPM version][npm-image]][npm-url]
|
|
||||||
[![NPM downloads][downloads-image]][downloads-url]
|
|
||||||
[![Build status][travis-image]][travis-url]
|
|
||||||
[![Test coverage][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
> Flatten an array of nested arrays into a single flat array. Accepts an optional depth.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install array-flatten --save
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var flatten = require('array-flatten')
|
|
||||||
|
|
||||||
flatten([1, [2, [3, [4, [5], 6], 7], 8], 9])
|
|
||||||
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
||||||
|
|
||||||
flatten([1, [2, [3, [4, [5], 6], 7], 8], 9], 2)
|
|
||||||
//=> [1, 2, 3, [4, [5], 6], 7, 8, 9]
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
flatten(arguments) //=> [1, 2, 3]
|
|
||||||
})(1, [2, 3])
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
[npm-image]: https://img.shields.io/npm/v/array-flatten.svg?style=flat
|
|
||||||
[npm-url]: https://npmjs.org/package/array-flatten
|
|
||||||
[downloads-image]: https://img.shields.io/npm/dm/array-flatten.svg?style=flat
|
|
||||||
[downloads-url]: https://npmjs.org/package/array-flatten
|
|
||||||
[travis-image]: https://img.shields.io/travis/blakeembrey/array-flatten.svg?style=flat
|
|
||||||
[travis-url]: https://travis-ci.org/blakeembrey/array-flatten
|
|
||||||
[coveralls-image]: https://img.shields.io/coveralls/blakeembrey/array-flatten.svg?style=flat
|
|
||||||
[coveralls-url]: https://coveralls.io/r/blakeembrey/array-flatten?branch=master
|
|
||||||
64
dashboard/node_modules/array-flatten/array-flatten.js
generated
vendored
@@ -1,64 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose `arrayFlatten`.
|
|
||||||
*/
|
|
||||||
module.exports = arrayFlatten
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursive flatten function with depth.
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Array} result
|
|
||||||
* @param {Number} depth
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function flattenWithDepth (array, result, depth) {
|
|
||||||
for (var i = 0; i < array.length; i++) {
|
|
||||||
var value = array[i]
|
|
||||||
|
|
||||||
if (depth > 0 && Array.isArray(value)) {
|
|
||||||
flattenWithDepth(value, result, depth - 1)
|
|
||||||
} else {
|
|
||||||
result.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursive flatten function. Omitting depth is slightly faster.
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Array} result
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function flattenForever (array, result) {
|
|
||||||
for (var i = 0; i < array.length; i++) {
|
|
||||||
var value = array[i]
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
flattenForever(value, result)
|
|
||||||
} else {
|
|
||||||
result.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flatten an array, with the ability to define a depth.
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Number} depth
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function arrayFlatten (array, depth) {
|
|
||||||
if (depth == null) {
|
|
||||||
return flattenForever(array, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
return flattenWithDepth(array, [], depth)
|
|
||||||
}
|
|
||||||
39
dashboard/node_modules/array-flatten/package.json
generated
vendored
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "array-flatten",
|
|
||||||
"version": "1.1.1",
|
|
||||||
"description": "Flatten an array of nested arrays into a single flat array",
|
|
||||||
"main": "array-flatten.js",
|
|
||||||
"files": [
|
|
||||||
"array-flatten.js",
|
|
||||||
"LICENSE"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"test": "istanbul cover _mocha -- -R spec"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/blakeembrey/array-flatten.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"array",
|
|
||||||
"flatten",
|
|
||||||
"arguments",
|
|
||||||
"depth"
|
|
||||||
],
|
|
||||||
"author": {
|
|
||||||
"name": "Blake Embrey",
|
|
||||||
"email": "hello@blakeembrey.com",
|
|
||||||
"url": "http://blakeembrey.me"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/blakeembrey/array-flatten/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/blakeembrey/array-flatten",
|
|
||||||
"devDependencies": {
|
|
||||||
"istanbul": "^0.3.13",
|
|
||||||
"mocha": "^2.2.4",
|
|
||||||
"pre-commit": "^1.0.7",
|
|
||||||
"standard": "^3.7.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
dashboard/node_modules/balanced-match/.github/FUNDING.yml
generated
vendored
@@ -1,2 +0,0 @@
|
|||||||
tidelift: "npm/balanced-match"
|
|
||||||
patreon: juliangruber
|
|
||||||
21
dashboard/node_modules/balanced-match/LICENSE.md
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
(MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
97
dashboard/node_modules/balanced-match/README.md
generated
vendored
@@ -1,97 +0,0 @@
|
|||||||
# balanced-match
|
|
||||||
|
|
||||||
Match balanced string pairs, like `{` and `}` or `<b>` and `</b>`. Supports regular expressions as well!
|
|
||||||
|
|
||||||
[](http://travis-ci.org/juliangruber/balanced-match)
|
|
||||||
[](https://www.npmjs.org/package/balanced-match)
|
|
||||||
|
|
||||||
[](https://ci.testling.com/juliangruber/balanced-match)
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Get the first matching pair of braces:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var balanced = require('balanced-match');
|
|
||||||
|
|
||||||
console.log(balanced('{', '}', 'pre{in{nested}}post'));
|
|
||||||
console.log(balanced('{', '}', 'pre{first}between{second}post'));
|
|
||||||
console.log(balanced(/\s+\{\s+/, /\s+\}\s+/, 'pre { in{nest} } post'));
|
|
||||||
```
|
|
||||||
|
|
||||||
The matches are:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ node example.js
|
|
||||||
{ start: 3, end: 14, pre: 'pre', body: 'in{nested}', post: 'post' }
|
|
||||||
{ start: 3,
|
|
||||||
end: 9,
|
|
||||||
pre: 'pre',
|
|
||||||
body: 'first',
|
|
||||||
post: 'between{second}post' }
|
|
||||||
{ start: 3, end: 17, pre: 'pre', body: 'in{nest}', post: 'post' }
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### var m = balanced(a, b, str)
|
|
||||||
|
|
||||||
For the first non-nested matching pair of `a` and `b` in `str`, return an
|
|
||||||
object with those keys:
|
|
||||||
|
|
||||||
* **start** the index of the first match of `a`
|
|
||||||
* **end** the index of the matching `b`
|
|
||||||
* **pre** the preamble, `a` and `b` not included
|
|
||||||
* **body** the match, `a` and `b` not included
|
|
||||||
* **post** the postscript, `a` and `b` not included
|
|
||||||
|
|
||||||
If there's no match, `undefined` will be returned.
|
|
||||||
|
|
||||||
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `['{', 'a', '']` and `{a}}` will match `['', 'a', '}']`.
|
|
||||||
|
|
||||||
### var r = balanced.range(a, b, str)
|
|
||||||
|
|
||||||
For the first non-nested matching pair of `a` and `b` in `str`, return an
|
|
||||||
array with indexes: `[ <a index>, <b index> ]`.
|
|
||||||
|
|
||||||
If there's no match, `undefined` will be returned.
|
|
||||||
|
|
||||||
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `[ 1, 3 ]` and `{a}}` will match `[0, 2]`.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
With [npm](https://npmjs.org) do:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install balanced-match
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security contact information
|
|
||||||
|
|
||||||
To report a security vulnerability, please use the
|
|
||||||
[Tidelift security contact](https://tidelift.com/security).
|
|
||||||
Tidelift will coordinate the fix and disclosure.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
(MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
62
dashboard/node_modules/balanced-match/index.js
generated
vendored
@@ -1,62 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
module.exports = balanced;
|
|
||||||
function balanced(a, b, str) {
|
|
||||||
if (a instanceof RegExp) a = maybeMatch(a, str);
|
|
||||||
if (b instanceof RegExp) b = maybeMatch(b, str);
|
|
||||||
|
|
||||||
var r = range(a, b, str);
|
|
||||||
|
|
||||||
return r && {
|
|
||||||
start: r[0],
|
|
||||||
end: r[1],
|
|
||||||
pre: str.slice(0, r[0]),
|
|
||||||
body: str.slice(r[0] + a.length, r[1]),
|
|
||||||
post: str.slice(r[1] + b.length)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeMatch(reg, str) {
|
|
||||||
var m = str.match(reg);
|
|
||||||
return m ? m[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
balanced.range = range;
|
|
||||||
function range(a, b, str) {
|
|
||||||
var begs, beg, left, right, result;
|
|
||||||
var ai = str.indexOf(a);
|
|
||||||
var bi = str.indexOf(b, ai + 1);
|
|
||||||
var i = ai;
|
|
||||||
|
|
||||||
if (ai >= 0 && bi > 0) {
|
|
||||||
if(a===b) {
|
|
||||||
return [ai, bi];
|
|
||||||
}
|
|
||||||
begs = [];
|
|
||||||
left = str.length;
|
|
||||||
|
|
||||||
while (i >= 0 && !result) {
|
|
||||||
if (i == ai) {
|
|
||||||
begs.push(i);
|
|
||||||
ai = str.indexOf(a, i + 1);
|
|
||||||
} else if (begs.length == 1) {
|
|
||||||
result = [ begs.pop(), bi ];
|
|
||||||
} else {
|
|
||||||
beg = begs.pop();
|
|
||||||
if (beg < left) {
|
|
||||||
left = beg;
|
|
||||||
right = bi;
|
|
||||||
}
|
|
||||||
|
|
||||||
bi = str.indexOf(b, i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
i = ai < bi && ai >= 0 ? ai : bi;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (begs.length) {
|
|
||||||
result = [ left, right ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
48
dashboard/node_modules/balanced-match/package.json
generated
vendored
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "balanced-match",
|
|
||||||
"description": "Match balanced character pairs, like \"{\" and \"}\"",
|
|
||||||
"version": "1.0.2",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/juliangruber/balanced-match.git"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/juliangruber/balanced-match",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "tape test/test.js",
|
|
||||||
"bench": "matcha test/bench.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"matcha": "^0.7.0",
|
|
||||||
"tape": "^4.6.0"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"match",
|
|
||||||
"regexp",
|
|
||||||
"test",
|
|
||||||
"balanced",
|
|
||||||
"parse"
|
|
||||||
],
|
|
||||||
"author": {
|
|
||||||
"name": "Julian Gruber",
|
|
||||||
"email": "mail@juliangruber.com",
|
|
||||||
"url": "http://juliangruber.com"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"testling": {
|
|
||||||
"files": "test/*.js",
|
|
||||||
"browsers": [
|
|
||||||
"ie/8..latest",
|
|
||||||
"firefox/20..latest",
|
|
||||||
"firefox/nightly",
|
|
||||||
"chrome/25..latest",
|
|
||||||
"chrome/canary",
|
|
||||||
"opera/12..latest",
|
|
||||||
"opera/next",
|
|
||||||
"safari/5.1..latest",
|
|
||||||
"ipad/6.0..latest",
|
|
||||||
"iphone/6.0..latest",
|
|
||||||
"android-browser/4.2..latest"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||