Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;

import androidx.annotation.Nullable;

import com.liskovsoft.mediaserviceinterfaces.oauth.Account;
import com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;
import com.liskovsoft.mediaserviceinterfaces.data.MediaItem;
import com.liskovsoft.sharedutils.helpers.Helpers;
import com.liskovsoft.sharedutils.locale.LocaleUtility;
import com.liskovsoft.sharedutils.mylogger.Log;
Expand Down Expand Up @@ -42,6 +45,7 @@
import com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;
import com.liskovsoft.smartyoutubetv2.common.prefs.BlockedChannelData;
import com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;
import com.liskovsoft.smartyoutubetv2.common.prefs.NewVideoCounterData;
import com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;
import com.liskovsoft.smartyoutubetv2.common.utils.Utils;

Expand Down Expand Up @@ -76,6 +80,15 @@ public class BrowsePresenter extends BasePresenter<BrowseView> implements Sectio
private long mLastUpdateTimeMs = -1;
private int mBootSectionIndex;
private int mBootstrapSectionId = -1;
// New video counter fields
private final Handler mCounterResetHandler = new Handler(Looper.getMainLooper());
private Runnable mCounterResetRunnable;
private Runnable mCounterRefreshRunnable;
// Map: sectionId -> newest known video data (String: timestamp_videoId) from RSS feed
private final Map<Integer, String> mCurrentNewestVideoData = new HashMap<>();
private final Map<Integer, String> mPinnedChannelIds = new HashMap<>(); // sectionId -> channelId
private final Map<Integer, String> mOriginalTitles = new HashMap<>(); // sectionId -> original title (without counter)
private final List<Disposable> mCounterActions = new ArrayList<>();

private BrowsePresenter(Context context) {
super(context);
Expand Down Expand Up @@ -125,6 +138,18 @@ public void onViewInitialized() {
int selectedSectionIndex = findSectionIndex(mCurrentSection != null ? mCurrentSection.getId() : mBootstrapSectionId);
mBootstrapSectionId = -1;
getView().selectSection(selectedSectionIndex != -1 ? selectedSectionIndex : mBootSectionIndex, true);

// Start recurring 20-minute counter fetch
if (mCounterRefreshRunnable == null) {
mCounterRefreshRunnable = new Runnable() {
@Override
public void run() {
fetchNewVideoCounts();
mCounterResetHandler.postDelayed(this, 20 * 60 * 1_000);
}
};
mCounterResetHandler.postDelayed(mCounterRefreshRunnable, 20 * 60 * 1_000);
}
}

@Override
Expand Down Expand Up @@ -292,6 +317,8 @@ public void updateSections() {
initPinnedData();

refreshSections();

fetchNewVideoCounts();
}

private void refreshSections() {
Expand Down Expand Up @@ -407,6 +434,11 @@ private void updateCategoryType(int categoryId, int categoryType) {
@Override
public void onViewDestroyed() {
super.onViewDestroyed();
RxHelper.disposeActions(mCounterActions);
if (mCounterRefreshRunnable != null) {
mCounterResetHandler.removeCallbacks(mCounterRefreshRunnable);
mCounterRefreshRunnable = null;
}
disposeActions();
saveSelectedItems();
}
Expand Down Expand Up @@ -496,6 +528,24 @@ public void onSectionFocused(int sectionId) {
mCurrentVideo = null; // fast scroll through the sections (fix empty selected item)
updateCurrentSection();
restoreSelectedItems(); // Don't place anywhere else

// Cancel any pending counter reset
if (mCounterResetRunnable != null) {
mCounterResetHandler.removeCallbacks(mCounterResetRunnable);
mCounterResetRunnable = null;
}

// Start counter reset timer for pinned channel sections
if (getMainUIData().isNewVideoCounterEnabled() && mCurrentSection != null && isPinnedId(mCurrentSection.getId())) {
final int focusedSectionId = sectionId;
mCounterResetRunnable = () -> {
// Only reset if still on the same section
if (mCurrentSection != null && mCurrentSection.getId() == focusedSectionId) {
resetNewVideoCounter(focusedSectionId);
}
};
mCounterResetHandler.postDelayed(mCounterResetRunnable, 5_000);
}
}

@Override
Expand Down Expand Up @@ -575,6 +625,9 @@ public void pinItem(Video item) {
mSections.add(newSection);
}
getView().addSection(-1, newSection);

// Immediately fetch baseline or counter for the newly pinned channel
fetchNewVideoCounts();
}

public void pinItem(String title, int resId, ErrorFragmentData data) {
Expand Down Expand Up @@ -1224,4 +1277,172 @@ private void appendLocalHistory(VideoGroup videoGroup) {
videoGroup.add(0, state.video);
}
}

// ==================== New Video Counter ====================

private void fetchNewVideoCounts() {
if (getContext() == null) {
return;
}

boolean isEnabled = getMainUIData().isNewVideoCounterEnabled();

if (!isEnabled) {
return;
}

// Stop any currently running background counters so we don't duplicate
RxHelper.disposeActions(mCounterActions);

NewVideoCounterData counterData = NewVideoCounterData.instance(getContext());

// Collect pinned channel sections and their channel IDs
mPinnedChannelIds.clear();
mOriginalTitles.clear();
mCurrentNewestVideoData.clear();

for (BrowseSection section : mSections) {
boolean isPinned = isPinnedId(section.getId());
boolean isVideo = section.getData() instanceof Video;
if (isPinned && isVideo) {
Video item = (Video) section.getData();
boolean hasChan = item.hasChannel();

String cleanTitle = section.getTitle();
if (cleanTitle != null) {
cleanTitle = cleanTitle.replaceFirst("^\\(\\d+\\)\\s+", "");
}

if (hasChan) {
mPinnedChannelIds.put(section.getId(), item.channelId);
mOriginalTitles.put(section.getId(), cleanTitle);
} else if (item.channelId != null && !item.channelId.isEmpty()) {
// Fallback just in case hasChannel() requires something else
mPinnedChannelIds.put(section.getId(), item.channelId);
mOriginalTitles.put(section.getId(), cleanTitle);
}
}
}

if (mPinnedChannelIds.isEmpty()) {
return;
}

// Fetch RSS for each pinned channel sequentially to avoid rate limiting
for (Map.Entry<Integer, String> entry : mPinnedChannelIds.entrySet()) {
int sectionId = entry.getKey();
String channelId = entry.getValue();

Disposable action = getContentService().getRssFeedObserve(channelId)
.subscribeOn(io.reactivex.schedulers.Schedulers.io())
.observeOn(io.reactivex.android.schedulers.AndroidSchedulers.mainThread())
.subscribe(
mediaGroup -> {
if (mediaGroup == null || getView() == null) {
return;
}

List<MediaItem> items = mediaGroup.getMediaItems();
if (items == null || items.isEmpty()) {
return;
}

String newestFetchedData = items.get(0).getPublishedDate() + "_" + items.get(0).getVideoId();
mCurrentNewestVideoData.put(sectionId, newestFetchedData);

String lastKnownData = counterData.getLastKnownVideoData(sectionId);

if (lastKnownData == null) {
// First time: store baseline, no counter shown
counterData.setLastKnownVideoData(sectionId, newestFetchedData);
} else {
int underscoreIdx = lastKnownData.indexOf('_');
long lastDate = 0;
String lastId = "";

if (underscoreIdx > 0) {
try { lastDate = Long.parseLong(lastKnownData.substring(0, underscoreIdx)); } catch (Exception ignored) {}
lastId = lastKnownData.substring(underscoreIdx + 1);
} else {
lastId = lastKnownData; // Fallback for backward compatibility
}

int newVideoCount = 0;
boolean found = false;
for (int i = 0; i < items.size(); i++) {
if (lastId.equals(items.get(i).getVideoId())) {
newVideoCount = i;
found = true;
break;
}
}

// If previous newest video is not in the feed at all (e.g., deleted or 16+ new videos),
// fallback to counting videos with a newer timestamp.
if (!found) {
newVideoCount = 0;
for (MediaItem item : items) {
if (item.getPublishedDate() > lastDate) {
newVideoCount++;
}
}

// If timestamp is missing/broken, default to max items
if (newVideoCount == 0 && lastDate > 0) {
// It wasn't found, but timestamps are older? Probably a deleted video that was
// the only new one. So it should be 0.
} else if (newVideoCount == 0) {
newVideoCount = items.size();
}
}

updateSectionTitleWithCounter(sectionId, newVideoCount);
}
},
error -> Log.e(TAG, "onError fetching RSS for counter " + channelId, error)
);

mCounterActions.add(action);
}
}

private void updateSectionTitleWithCounter(int sectionId, int newCount) {
if (getView() == null) {
return;
}

BrowseSection section = findSectionById(sectionId);

if (section == null) {
return;
}

String originalTitle = mOriginalTitles.get(sectionId);
if (originalTitle == null) {
originalTitle = section.getTitle();
}

if (newCount > 0) {
section.setTitle("(" + newCount + ") " + originalTitle);
} else {
section.setTitle(originalTitle);
}

getView().updateSectionTitle(section);
}

private void resetNewVideoCounter(int sectionId) {
if (getContext() == null) {
return;
}

NewVideoCounterData counterData = NewVideoCounterData.instance(getContext());
String currentNewestData = mCurrentNewestVideoData.get(sectionId);

if (currentNewestData != null) {
counterData.setLastKnownVideoData(sectionId, currentNewestData);
}

updateSectionTitleWithCounter(sectionId, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ private void appendMiscCategory(AppDialogPresenter settingsPresenter) {
},
mMainUIData.isPinnedChannelRowsEnabled()));

options.add(UiOptionItem.from(getContext().getString(R.string.new_video_counter),
optionItem -> {
mMainUIData.setNewVideoCounterEnabled(optionItem.isSelected());
mRestartApp = true;
},
mMainUIData.isNewVideoCounterEnabled()));

options.add(UiOptionItem.from(getContext().getString(R.string.playlists_rows),
optionItem -> {
mMainUIData.setPlaylistsStyle(optionItem.isSelected() ? MainUIData.PLAYLISTS_STYLE_ROWS : MainUIData.PLAYLISTS_STYLE_GRID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public interface BrowseView {
void updateSection(VideoGroup group);
void updateSection(SettingsGroup group);
void clearSection(BrowseSection section);
void updateSectionTitle(BrowseSection section);
void selectSectionItem(int index);
void selectSectionItem(Video item);
void showError(ErrorFragmentData data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public class MainUIData extends DataChangeBase implements ProfileChangeListener
private int mCardPreviewType;
private final Runnable mPersistStateInt = this::persistStateInt;
private boolean mIsUnlocalizedTitlesEnabled;
private boolean mIsNewVideoCounterEnabled;

private MainUIData(Context context) {
mContext = context;
Expand Down Expand Up @@ -354,6 +355,15 @@ public void setUnlocalizedTitlesEnabled(boolean enabled) {
persistState();
}

public boolean isNewVideoCounterEnabled() {
return mIsNewVideoCounterEnabled;
}

public void setNewVideoCounterEnabled(boolean enabled) {
mIsNewVideoCounterEnabled = enabled;
persistState();
}

private void initColorSchemes() {
mColorSchemes.add(new ColorScheme(
R.string.color_scheme_teal,
Expand Down Expand Up @@ -427,6 +437,7 @@ private void restoreState() {
mIsPinnedChannelRowsEnabled = Helpers.parseBoolean(split, 20, true);
mCardPreviewType = Helpers.parseInt(split, 21, CARD_PREVIEW_DISABLED);
mIsUnlocalizedTitlesEnabled = Helpers.parseBoolean(split, 22, false);
mIsNewVideoCounterEnabled = Helpers.parseBoolean(split, 23, false);

int idx = -1;
for (Long menuItem : MENU_ITEM_DEFAULT_ORDER) {
Expand Down Expand Up @@ -470,7 +481,7 @@ private void persistStateInt() {
mIsUploadsOldLookEnabled, mIsUploadsAutoLoadEnabled, mCardTextScrollSpeed, mMenuItems, mTopButtons,
null, mThumbQuality, mIsCardMultilineSubtitleEnabled, Helpers.mergeList(mMenuItemsOrdered),
mIsChannelsFilterEnabled, mIsChannelSearchBarEnabled, mIsPinnedChannelRowsEnabled, mCardPreviewType,
mIsUnlocalizedTitlesEnabled));
mIsUnlocalizedTitlesEnabled, mIsNewVideoCounterEnabled));
}

public static class ColorScheme {
Expand Down
Loading