Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2d66a7906 | |||
| 5ade6350eb | |||
| 96c4c360ee |
@@ -9,6 +9,7 @@ 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> {
|
||||
@@ -17,11 +18,10 @@ public class ChannelAdapter extends RecyclerView.Adapter<ChannelAdapter.ChannelV
|
||||
void onChannelClick(StreamChannel channel);
|
||||
}
|
||||
|
||||
private final List<StreamChannel> channels;
|
||||
private final List<StreamChannel> channels = new ArrayList<>();
|
||||
private final OnChannelClickListener listener;
|
||||
|
||||
public ChannelAdapter(List<StreamChannel> channels, OnChannelClickListener listener) {
|
||||
this.channels = channels;
|
||||
public ChannelAdapter(OnChannelClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@@ -65,4 +65,12 @@ public class ChannelAdapter extends RecyclerView.Adapter<ChannelAdapter.ChannelV
|
||||
name = itemView.findViewById(R.id.channel_name);
|
||||
}
|
||||
}
|
||||
|
||||
public void submitList(List<StreamChannel> newChannels) {
|
||||
channels.clear();
|
||||
if (newChannels != null) {
|
||||
channels.addAll(newChannels);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
97
app/src/main/java/com/streamplayer/EventAdapter.java
Normal file
97
app/src/main/java/com/streamplayer/EventAdapter.java
Normal file
@@ -0,0 +1,97 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/com/streamplayer/EventItem.java
Normal file
49
app/src/main/java/com/streamplayer/EventItem.java
Normal file
@@ -0,0 +1,49 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
148
app/src/main/java/com/streamplayer/EventRepository.java
Normal file
148
app/src/main/java/com/streamplayer/EventRepository.java
Normal file
@@ -0,0 +1,148 @@
|
||||
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.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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;
|
||||
|
||||
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
|
||||
private static final String EVENTS_URL = "https://streamtpmedia.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();
|
||||
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() throws IOException {
|
||||
URL url = new URL(EVENTS_URL);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(15000);
|
||||
connection.setReadTimeout(15000);
|
||||
connection.setRequestMethod("GET");
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
return builder.toString();
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private List<EventItem> parseEvents(String json) throws JSONException {
|
||||
JSONArray array = new JSONArray(json);
|
||||
List<EventItem> events = new ArrayList<>();
|
||||
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);
|
||||
long startMillis = parseEventTime(time);
|
||||
events.add(new EventItem(title, time, category, status, normalized, extractChannelName(link), startMillis));
|
||||
}
|
||||
return Collections.unmodifiableList(events);
|
||||
}
|
||||
|
||||
private String normalizeLink(String link) {
|
||||
if (link == null) {
|
||||
return "";
|
||||
}
|
||||
return link.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);
|
||||
ZoneId zone = ZoneId.of("America/Argentina/Buenos_Aires");
|
||||
LocalDate today = LocalDate.now(zone);
|
||||
ZonedDateTime start = ZonedDateTime.of(LocalDateTime.of(today, localTime), 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,242 @@ package com.streamplayer;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
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 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;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.channel_grid);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this, getSpanCount()));
|
||||
recyclerView.setHasFixedSize(true);
|
||||
ChannelAdapter adapter = new ChannelAdapter(
|
||||
ChannelRepository.getChannels(),
|
||||
channel -> {
|
||||
Intent intent = new Intent(MainActivity.this, PlayerActivity.class);
|
||||
intent.putExtra(PlayerActivity.EXTRA_CHANNEL_NAME, channel.getName());
|
||||
intent.putExtra(PlayerActivity.EXTRA_CHANNEL_URL, channel.getPageUrl());
|
||||
startActivity(intent);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
sections = buildSections();
|
||||
sectionList.setLayoutManager(new LinearLayoutManager(this));
|
||||
sectionAdapter = new SectionAdapter(getSectionTitles(), this::selectSection);
|
||||
sectionList.setAdapter(sectionAdapter);
|
||||
|
||||
selectSection(0);
|
||||
}
|
||||
|
||||
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);
|
||||
contentList.setLayoutManager(channelLayoutManager);
|
||||
contentList.setAdapter(channelAdapter);
|
||||
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));
|
||||
contentList.setLayoutManager(eventLayoutManager);
|
||||
contentList.setAdapter(eventAdapter);
|
||||
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);
|
||||
}
|
||||
});
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.post(recyclerView::requestFocus);
|
||||
}
|
||||
|
||||
@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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +223,8 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
super.onStart();
|
||||
if (player != null) {
|
||||
playerView.onResume();
|
||||
} else if (channelUrl != null) {
|
||||
loadChannel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,9 +247,7 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (player != null) {
|
||||
playerView.onPause();
|
||||
}
|
||||
releasePlayer();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
88
app/src/main/java/com/streamplayer/SectionAdapter.java
Normal file
88
app/src/main/java/com/streamplayer/SectionAdapter.java
Normal file
@@ -0,0 +1,88 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
app/src/main/res/color/section_text_selector.xml
Normal file
6
app/src/main/res/color/section_text_selector.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
6
app/src/main/res/drawable/bg_section_indicator.xml
Normal file
6
app/src/main/res/drawable/bg_section_indicator.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
33
app/src/main/res/drawable/bg_tab_selector.xml
Normal file
33
app/src/main/res/drawable/bg_tab_selector.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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>
|
||||
@@ -7,32 +7,106 @@
|
||||
android:background="@color/black"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="Selecciona un canal"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
<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" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/channel_grid"
|
||||
<LinearLayout
|
||||
android:id="@+id/content_panel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="12dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="12dp"
|
||||
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_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
tools:listitem="@layout/item_channel" />
|
||||
app:layout_constraintStart_toEndOf="@id/divider"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/content_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Canales" />
|
||||
|
||||
<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:nextFocusLeft="@id/section_list"
|
||||
tools:listitem="@layout/item_channel" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
56
app/src/main/res/layout/item_event.xml
Normal file
56
app/src/main/res/layout/item_event.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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>
|
||||
23
app/src/main/res/layout/item_section.xml
Normal file
23
app/src/main/res/layout/item_section.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="text_secondary">#B3FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,3 +1,10 @@
|
||||
<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="message_events_error">No se pudieron cargar los eventos: %1$s</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user