6 Commits

Author SHA1 Message Date
93dbe0941e Update v9.4.6: Custom Update Dialog & Focus States 2025-11-25 19:23:17 +00:00
22d2cf9eda Update v9.4.5: Forced Update Flow & Focus Highlight 2025-11-25 19:16:12 +00:00
d2c3041b0a Update v9.4.4: Dialog Contrast & Event Refresh Button 2025-11-25 19:10:08 +00:00
77c417117a Update v9.4.3: Manual Events Refresh & Background Sync 2025-11-25 19:01:21 +00:00
renato97
2c65578bdd Update v9.4.2: Enhanced UI Theme & Visual Consistency
- Incremented version to 9.4.2 (versionCode: 94200)
- Added custom AlertDialog theme with white text styling
- Enhanced visual consistency for all dialog components
- Improved theme overlay for better readability
- Applied custom styling to update and blocked dialogs
- Better contrast and visual hierarchy in dialogs

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:07:04 +01:00
17 changed files with 535 additions and 76 deletions

View File

@@ -8,8 +8,8 @@ android {
applicationId "com.streamplayer" applicationId "com.streamplayer"
minSdk 21 minSdk 21
targetSdk 33 targetSdk 33
versionCode 94000 versionCode 94600
versionName "9.4.0" versionName "9.4.6"
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"' buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
} }

View File

@@ -73,6 +73,21 @@ public class EventRepository {
}).start(); }).start();
} }
public void prefetchEvents(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
new Thread(() -> {
try {
String json = downloadJson();
parseEvents(json);
prefs.edit()
.putString(KEY_JSON, json)
.putLong(KEY_TIMESTAMP, System.currentTimeMillis())
.apply();
} catch (IOException | JSONException ignored) {
}
}).start();
}
private String downloadJson() throws IOException { private String downloadJson() throws IOException {
URL url = new URL(EVENTS_URL); URL url = new URL(EVENTS_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();

View File

@@ -5,13 +5,16 @@ import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.graphics.drawable.ColorDrawable;
import android.view.View; import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.text.TextUtils;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
@@ -24,14 +27,18 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final long EVENT_PREFETCH_INTERVAL_MS = TimeUnit.HOURS.toMillis(1);
private RecyclerView sectionList; private RecyclerView sectionList;
private RecyclerView contentList; private RecyclerView contentList;
private ProgressBar loadingIndicator; private ProgressBar loadingIndicator;
private TextView messageView; private TextView messageView;
private TextView contentTitle; private TextView contentTitle;
private Button eventsRefreshButton;
private ChannelAdapter channelAdapter; private ChannelAdapter channelAdapter;
private EventAdapter eventAdapter; private EventAdapter eventAdapter;
@@ -46,6 +53,9 @@ public class MainActivity extends AppCompatActivity {
private AlertDialog updateDialog; private AlertDialog updateDialog;
private AlertDialog blockedDialog; private AlertDialog blockedDialog;
private DeviceRegistry deviceRegistry; private DeviceRegistry deviceRegistry;
private Handler eventPrefetchHandler;
private Runnable eventPrefetchRunnable;
private boolean isEventsRefreshing;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -57,6 +67,9 @@ public class MainActivity extends AppCompatActivity {
loadingIndicator = findViewById(R.id.loading_indicator); loadingIndicator = findViewById(R.id.loading_indicator);
messageView = findViewById(R.id.message_view); messageView = findViewById(R.id.message_view);
contentTitle = findViewById(R.id.content_title); contentTitle = findViewById(R.id.content_title);
eventsRefreshButton = findViewById(R.id.events_refresh_button);
eventsRefreshButton.setOnClickListener(v -> manualRefreshEvents());
applyFocusHighlight(eventsRefreshButton);
channelAdapter = new ChannelAdapter( channelAdapter = new ChannelAdapter(
channel -> openPlayer(channel.getName(), channel.getPageUrl())); channel -> openPlayer(channel.getName(), channel.getPageUrl()));
@@ -113,6 +126,8 @@ public class MainActivity extends AppCompatActivity {
} }
} }
}); });
startEventPrefetchScheduler();
} }
@Override @Override
@@ -138,6 +153,12 @@ public class MainActivity extends AppCompatActivity {
if (deviceRegistry != null) { if (deviceRegistry != null) {
deviceRegistry.release(); deviceRegistry.release();
} }
stopEventPrefetchScheduler();
}
@Override
public void onBackPressed() {
closeAppCompletely();
} }
private void selectSection(int index) { private void selectSection(int index) {
@@ -157,6 +178,7 @@ public class MainActivity extends AppCompatActivity {
} }
private void showChannels(SectionEntry section) { private void showChannels(SectionEntry section) {
updateEventsRefreshVisibility(false);
contentTitle.setText(section.title); contentTitle.setText(section.title);
contentList.setLayoutManager(channelLayoutManager); contentList.setLayoutManager(channelLayoutManager);
contentList.setAdapter(channelAdapter); contentList.setAdapter(channelAdapter);
@@ -172,6 +194,7 @@ public class MainActivity extends AppCompatActivity {
} }
private void showEvents() { private void showEvents() {
updateEventsRefreshVisibility(true);
contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events)); contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events));
contentList.setLayoutManager(eventLayoutManager); contentList.setLayoutManager(eventLayoutManager);
contentList.setAdapter(eventAdapter); contentList.setAdapter(eventAdapter);
@@ -183,6 +206,9 @@ public class MainActivity extends AppCompatActivity {
} }
private void loadEvents(boolean forceRefresh) { private void loadEvents(boolean forceRefresh) {
if (currentSection != null && currentSection.type == SectionEntry.Type.EVENTS) {
setEventsRefreshing(true);
}
loadingIndicator.setVisibility(View.VISIBLE); loadingIndicator.setVisibility(View.VISIBLE);
messageView.setVisibility(View.GONE); messageView.setVisibility(View.GONE);
eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() { eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() {
@@ -196,6 +222,7 @@ public class MainActivity extends AppCompatActivity {
} else { } else {
loadingIndicator.setVisibility(View.GONE); loadingIndicator.setVisibility(View.GONE);
} }
setEventsRefreshing(false);
}); });
} }
@@ -205,11 +232,48 @@ public class MainActivity extends AppCompatActivity {
loadingIndicator.setVisibility(View.GONE); loadingIndicator.setVisibility(View.GONE);
messageView.setVisibility(View.VISIBLE); messageView.setVisibility(View.VISIBLE);
messageView.setText(getString(R.string.message_events_error, message)); messageView.setText(getString(R.string.message_events_error, message));
setEventsRefreshing(false);
}); });
} }
}); });
} }
private void manualRefreshEvents() {
if (isEventsRefreshing) {
return;
}
if (currentSection == null || currentSection.type != SectionEntry.Type.EVENTS) {
return;
}
loadEvents(true);
}
private void updateEventsRefreshVisibility(boolean visible) {
if (eventsRefreshButton == null) {
return;
}
eventsRefreshButton.setVisibility(visible ? View.VISIBLE : View.GONE);
if (visible) {
eventsRefreshButton.setEnabled(!isEventsRefreshing);
eventsRefreshButton.setText(isEventsRefreshing
? getString(R.string.events_refreshing)
: getString(R.string.events_refresh_action));
}
}
private void setEventsRefreshing(boolean refreshing) {
isEventsRefreshing = refreshing;
if (eventsRefreshButton == null) {
return;
}
if (eventsRefreshButton.getVisibility() == View.VISIBLE) {
eventsRefreshButton.setEnabled(!refreshing);
eventsRefreshButton.setText(refreshing
? getString(R.string.events_refreshing)
: getString(R.string.events_refresh_action));
}
}
private void displayEvents() { private void displayEvents() {
loadingIndicator.setVisibility(View.GONE); loadingIndicator.setVisibility(View.GONE);
if (cachedEvents.isEmpty()) { if (cachedEvents.isEmpty()) {
@@ -233,32 +297,41 @@ public class MainActivity extends AppCompatActivity {
if (info == null) { if (info == null) {
return; return;
} }
boolean forceUpdate = info.isMandatory(BuildConfig.VERSION_CODE); showUpdateDialog(info);
showUpdateDialog(info, forceUpdate);
} }
private void showUpdateDialog(UpdateManager.UpdateInfo info, boolean mandatory) { private void showUpdateDialog(UpdateManager.UpdateInfo info) {
if (isFinishing()) { if (isFinishing()) {
return; return;
} }
if (updateDialog != null && updateDialog.isShowing()) { if (updateDialog != null && updateDialog.isShowing()) {
updateDialog.dismiss(); updateDialog.dismiss();
} }
AlertDialog.Builder builder = new AlertDialog.Builder(this) View dialogView = getLayoutInflater().inflate(R.layout.dialog_update, null);
.setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title) TextView titleView = dialogView.findViewById(R.id.update_title);
.setMessage(buildUpdateMessage(info)) TextView messageView = dialogView.findViewById(R.id.update_message);
.setPositiveButton(R.string.update_action_download, Button positiveButton = dialogView.findViewById(R.id.update_positive_button);
(dialog, which) -> updateManager.downloadUpdate(MainActivity.this, info)) Button negativeButton = dialogView.findViewById(R.id.update_negative_button);
.setNeutralButton(R.string.update_action_view_release, titleView.setText(R.string.update_required_title);
(dialog, which) -> openReleasePage(info)); messageView.setText(buildUpdateMessage(info));
if (mandatory) { AlertDialog dialog = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
builder.setCancelable(false); .setView(dialogView)
builder.setNegativeButton(R.string.update_action_close_app, .setCancelable(false)
(dialog, which) -> finish()); .create();
} else { dialog.setCanceledOnTouchOutside(false);
builder.setNegativeButton(R.string.update_action_later, null); dialog.show();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
} }
updateDialog = builder.show(); positiveButton.setOnClickListener(v -> {
dialog.dismiss();
updateManager.downloadUpdate(MainActivity.this, info);
});
negativeButton.setOnClickListener(v -> closeAppCompletely());
applyFocusHighlight(positiveButton);
applyFocusHighlight(negativeButton);
positiveButton.requestFocus();
updateDialog = dialog;
} }
private CharSequence buildUpdateMessage(UpdateManager.UpdateInfo info) { private CharSequence buildUpdateMessage(UpdateManager.UpdateInfo info) {
@@ -286,30 +359,9 @@ public class MainActivity extends AppCompatActivity {
builder.append('\n'); builder.append('\n');
builder.append(info.getReleaseNotesPreview()); 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(); 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) { private void showBlockedDialog(String reason, String tokenPart) {
if (isFinishing()) { if (isFinishing()) {
return; return;
@@ -333,12 +385,12 @@ public class MainActivity extends AppCompatActivity {
} else { } else {
tokenContainer.setVisibility(View.GONE); tokenContainer.setVisibility(View.GONE);
} }
AlertDialog.Builder builder = new AlertDialog.Builder(this) AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
.setTitle(R.string.device_blocked_title) .setTitle(R.string.device_blocked_title)
.setView(dialogView) .setView(dialogView)
.setCancelable(false) .setCancelable(false)
.setPositiveButton(R.string.device_blocked_close, .setPositiveButton(R.string.device_blocked_close,
(dialog, which) -> finish()); (dialog, which) -> closeAppCompletely());
if (hasToken) { if (hasToken) {
builder.setNeutralButton(R.string.device_blocked_copy_token, builder.setNeutralButton(R.string.device_blocked_copy_token,
(dialog, which) -> copyTokenToClipboard(tokenPart)); (dialog, which) -> copyTokenToClipboard(tokenPart));
@@ -358,6 +410,50 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show();
} }
private void applyFocusHighlight(View view) {
if (view == null) {
return;
}
view.setOnFocusChangeListener((v, hasFocus) -> {
float scale = hasFocus ? 1.05f : 1f;
v.animate().scaleX(scale).scaleY(scale).setDuration(120).start();
});
}
private void startEventPrefetchScheduler() {
if (eventPrefetchHandler == null) {
eventPrefetchHandler = new Handler(Looper.getMainLooper());
}
if (eventPrefetchRunnable == null) {
eventPrefetchRunnable = new Runnable() {
@Override
public void run() {
if (eventRepository != null) {
eventRepository.prefetchEvents(getApplicationContext());
}
if (eventPrefetchHandler != null) {
eventPrefetchHandler.postDelayed(this, EVENT_PREFETCH_INTERVAL_MS);
}
}
};
} else {
eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable);
}
eventPrefetchHandler.post(eventPrefetchRunnable);
}
private void stopEventPrefetchScheduler() {
if (eventPrefetchHandler != null && eventPrefetchRunnable != null) {
eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable);
}
}
private void closeAppCompletely() {
finishAffinity();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private int getSpanCount() { private int getSpanCount() {
return getResources().getInteger(R.integer.channel_grid_span); return getResources().getInteger(R.integer.channel_grid_span);
} }

View File

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

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape>
<solid android:color="@color/accent_blue_disabled" />
<corners android:radius="28dp" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item android:state_pressed="true">
<shape>
<solid android:color="@color/accent_blue_light" />
<corners android:radius="28dp" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="@color/accent_blue_light" />
<corners android:radius="28dp" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item android:state_hovered="true">
<shape>
<solid android:color="@color/accent_blue_light" />
<corners android:radius="28dp" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="@color/accent_blue" />
<corners android:radius="28dp" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape>
<solid android:color="#00000000" />
<corners android:radius="28dp" />
<stroke
android:width="2dp"
android:color="@color/accent_blue_disabled" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item android:state_pressed="true">
<shape>
<solid android:color="#26FFFFFF" />
<corners android:radius="28dp" />
<stroke
android:width="2dp"
android:color="@color/white" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="#26FFFFFF" />
<corners android:radius="28dp" />
<stroke
android:width="2dp"
android:color="@color/white" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item android:state_hovered="true">
<shape>
<solid android:color="#26FFFFFF" />
<corners android:radius="28dp" />
<stroke
android:width="2dp"
android:color="@color/white" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="#00000000" />
<corners android:radius="28dp" />
<stroke
android:width="2dp"
android:color="@color/white" />
<padding
android:bottom="8dp"
android:left="16dp"
android:right="16dp"
android:top="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/dialog_background" />
<corners android:radius="18dp" />
<padding
android:bottom="16dp"
android:left="16dp"
android:right="16dp"
android:top="16dp" />
</shape>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape>
<solid android:color="@color/accent_blue_disabled" />
<corners android:radius="20dp" />
<padding
android:bottom="8dp"
android:left="20dp"
android:right="20dp"
android:top="8dp" />
</shape>
</item>
<item android:state_pressed="true">
<shape>
<solid android:color="@color/accent_blue_light" />
<corners android:radius="20dp" />
<padding
android:bottom="8dp"
android:left="20dp"
android:right="20dp"
android:top="8dp" />
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="@color/accent_blue_light" />
<corners android:radius="20dp" />
<padding
android:bottom="8dp"
android:left="20dp"
android:right="20dp"
android:top="8dp" />
</shape>
</item>
<item android:state_hovered="true">
<shape>
<solid android:color="@color/accent_blue_light" />
<corners android:radius="20dp" />
<padding
android:bottom="8dp"
android:left="20dp"
android:right="20dp"
android:top="8dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="@color/accent_blue" />
<corners android:radius="20dp" />
<padding
android:bottom="8dp"
android:left="20dp"
android:right="20dp"
android:top="8dp" />
</shape>
</item>
</selector>

View File

@@ -72,14 +72,37 @@
app:layout_constraintStart_toEndOf="@id/divider" app:layout_constraintStart_toEndOf="@id/divider"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView <LinearLayout
android:id="@+id/content_title" android:id="@+id/content_header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/white" android:gravity="center_vertical"
android:textSize="18sp" android:orientation="horizontal">
android:textStyle="bold"
tools:text="Canales" /> <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/events_refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/bg_events_refresh_button"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:textColor="@color/white"
android:text="@string/events_refresh_action"
android:textAllCaps="false"
android:visibility="gone" />
</LinearLayout>
<ProgressBar <ProgressBar
android:id="@+id/loading_indicator" android:id="@+id/loading_indicator"

View File

@@ -13,7 +13,7 @@
android:id="@+id/blocked_message_text" android:id="@+id/blocked_message_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@android:color/black" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
<LinearLayout <LinearLayout
@@ -29,7 +29,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/device_blocked_token_label" android:text="@string/device_blocked_token_label"
android:textColor="@android:color/black" android:textColor="@color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -40,7 +40,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:padding="8dp" android:padding="8dp"
android:textColor="@android:color/black" android:textColor="@color/white"
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,79 @@
<?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:background="@drawable/bg_dialog_dark"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/update_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:text="@string/update_required_title" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:maxHeight="320dp"
android:overScrollMode="never">
<TextView
android:id="@+id/update_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="4dp"
android:textColor="@color/white"
android:textSize="14sp" />
</ScrollView>
<LinearLayout
android:id="@+id/update_button_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/update_positive_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_dialog_button_primary"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/update_action_download"
android:textAllCaps="false"
android:textColor="@color/white"
android:textStyle="bold" />
<Space
android:layout_width="16dp"
android:layout_height="wrap_content" />
<Button
android:id="@+id/update_negative_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_dialog_button_secondary"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/update_action_close_app"
android:textAllCaps="false"
android:textColor="@color/white"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>

View File

@@ -3,4 +3,8 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="text_secondary">#B3FFFFFF</color> <color name="text_secondary">#B3FFFFFF</color>
<color name="accent_blue">#FF1E88E5</color>
<color name="accent_blue_light">#FF42A5F5</color>
<color name="accent_blue_disabled">#661E88E5</color>
<color name="dialog_background">#FF121212</color>
</resources> </resources>

View File

@@ -10,16 +10,13 @@
<string name="update_required_title">Actualización obligatoria</string> <string name="update_required_title">Actualización obligatoria</string>
<string name="update_available_title">Actualización disponible</string> <string name="update_available_title">Actualización disponible</string>
<string name="update_action_download">Actualizar</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_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_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_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_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_download_size">Tamaño aproximado: %1$s</string>
<string name="update_downloads">Descargas registradas: %1$d</string> <string name="update_downloads">Descargas registradas: %1$d</string>
<string name="update_release_notes_title">Novedades</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_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_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_empty_response">Respuesta vacía del servidor de releases</string>
@@ -46,4 +43,6 @@
<string name="device_blocked_copy_success">Código copiado al portapapeles</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_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> <string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
<string name="events_refresh_action">Actualizar ahora</string>
<string name="events_refreshing">Actualizando...</string>
</resources> </resources>

View File

@@ -6,4 +6,18 @@
<item name="android:statusBarColor">@color/black</item> <item name="android:statusBarColor">@color/black</item>
<item name="android:navigationBarColor">@color/black</item> <item name="android:navigationBarColor">@color/black</item>
</style> </style>
</resources>
<style name="ThemeOverlay.StreamPlayer.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowBackground">@drawable/bg_dialog_dark</item>
<item name="android:colorBackground">@color/dialog_background</item>
<item name="colorBackgroundFloating">@color/dialog_background</item>
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
<item name="colorAccent">@color/accent_blue</item>
<item name="android:buttonStyle">@style/StreamPlayer.DialogButton</item>
</style>
<style name="StreamPlayer.DialogButton" parent="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog">
<item name="android:textColor">@color/accent_blue</item>
</style>
</resources>

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
{ {
"versionCode": 93100, "versionCode": 94600,
"versionName": "9.3.1", "versionName": "9.4.6",
"minSupportedVersionCode": 91000, "minSupportedVersionCode": 91000,
"forceUpdate": false, "forceUpdate": true,
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.3.1/StreamPlayer-v9.3.1.apk", "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.4.6/StreamPlayer-v9.4.6.apk",
"fileName": "StreamPlayer-v9.3.1.apk", "fileName": "StreamPlayer-v9.4.6.apk",
"sizeBytes": 5943075, "sizeBytes": 5951142,
"notes": "StreamPlayer v9.3.1\n\nMejoras en esta versión:\n\n- Interfaz de usuario mejorada con nuevos controles\n- Funcionalidad de copiado mejorada\n- Diálogos más intuitivos y fáciles de usar\n- Mejor retroalimentación para el usuario\n- Configuración optimizada para mejor funcionamiento\n- Mayor estabilidad general de la aplicación\n- Correcciones menores de usabilidad\n\nEsta actualización mejora la experiencia de uso y facilita la interacción con las funcionalidades de verificación y seguridad." "notes": "StreamPlayer v9.4.6\n\nMejoras clave:\n\n- Diálogo de actualización totalmente personalizado con fondo oscuro, compatible con OneUI 8/Android 16 y sin cuadros blancos.\n- Botones \"Actualizar\" y \"Salir\" ahora tienen indicadores de foco/hover y animaciones para controles de TV.\n- El botón \"Actualizar ahora\" en Eventos también hereda resaltado al navegar con control remoto.\n- Se mantiene el flujo obligatorio de actualización y la descarga directa desde la app.\n\nRecomendamos instalar esta versión para garantizar la mejor experiencia en televisores y dispositivos Samsung recientes."
} }