/* * mygram-droid: Android TV client for Foldergram. * Copyright (C) 2026 Danilo M. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ package mygram.droid; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; /** One folder as a grid, paged in lazily; the header toggle picks random, date or name order. */ public final class FolderActivity extends Activity { private static final String[] ORDERS = {"Random", "By date", "By name"}; private static final int[] ICONS = {R.drawable.ic_shuffle, R.drawable.ic_schedule, R.drawable.ic_sort_by_alpha}; private FrameLayout content; private RecyclerView grid; /** Replaced, not cleared, on a reorder: an open viewer may still hold the old list. */ private List posts = new ArrayList<>(); private List tiles = new ArrayList<>(); private Callable> more; private boolean loading; static void open(Context c, Api.Folder f) { c.startActivity(new Intent(c, FolderActivity.class).putExtra("slug", f.slug).putExtra("name", f.name) .putExtra("count", f.count).putExtra("avatar", f.avatarUrl)); } @Override protected void onCreate(Bundle state) { super.onCreate(state); LinearLayout column = new LinearLayout(this); column.setOrientation(LinearLayout.VERTICAL); LinearLayout header = Ui.header(this, getIntent().getStringExtra("name"), getIntent().getStringExtra("avatar")); header.setClipToPadding(false); // the toggle's focus zoom draws into the padding header.addView(new View(this), new LinearLayout.LayoutParams(0, 0, 1)); TextView order = Ui.text(this, "", 18); int p = Ui.dp(this, 12); order.setPadding(p * 2, p, p * 2, p); order.setCompoundDrawablePadding(p); order.setGravity(Gravity.CENTER_VERTICAL); showOrder(order); Ui.focusable(order); order.setOnClickListener(v -> { Prefs.setFolderOrder((Prefs.folderOrder() + 1) % ORDERS.length); showOrder(order); load(false); }); header.addView(order); column.addView(header); content = new FrameLayout(this); column.addView(content, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1)); setContentView(column); load(true); } private static void showOrder(TextView order) { order.setText(ORDERS[Prefs.folderOrder()]); order.setCompoundDrawablesRelativeWithIntrinsicBounds(ICONS[Prefs.folderOrder()], 0, 0, 0); } /** (Re)loads the grid in the saved order; focus moves to the grid only on the first load. */ private void load(boolean focusGrid) { posts = new ArrayList<>(); tiles = new ArrayList<>(); grid = null; show(Ui.text(this, "Loading…", 18)); Callable> pager = Api.folderPages(getIntent().getStringExtra("slug"), getIntent().getIntExtra("count", 0), Prefs.folderOrder()); more = pager; loading = true; Api.async(pager, page -> { if (isDestroyed() || more != pager) return; // a newer order replaced this one loading = false; append(page); if (page.isEmpty()) { show(Ui.text(this, "Nothing to show", 18)); return; } grid = Ui.grid(this, tiles, Ui.POST_W, Ui.POST_H, 6); grid.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView rv, int dx, int dy) { loadMore(); } }); show(grid); if (focusGrid) grid.post(grid::requestFocus); // after layout }, e -> { if (more != pager) return; loading = false; show(Ui.text(this, "Cannot load folder: " + e.getMessage(), 18)); }); } /** Loads the next page when the last visible tile is within 12 of the end. */ private void loadMore() { if (more == null || loading || grid == null) return; if (((GridLayoutManager) grid.getLayoutManager()).findLastVisibleItemPosition() < tiles.size() - 12) return; loading = true; Callable> pager = more; Api.async(pager, page -> { if (isDestroyed() || more != pager) return; loading = false; if (page.isEmpty()) more = null; append(page); }, e -> { if (more != pager) return; loading = false; if (!isDestroyed()) Toast.makeText(this, "Cannot load more: " + e.getMessage(), Toast.LENGTH_SHORT).show(); }); } /** Pages from one pager never overlap, so no id check is needed here. */ private void append(List page) { posts.addAll(page); addTiles(); } private void addTiles() { int old = tiles.size(); List list = posts; for (int i = old; i < list.size(); i++) { int start = i; tiles.add(TileAdapter.post(list.get(i), () -> Playlist.open(this, list, start, more))); } if (grid != null && tiles.size() > old) grid.getAdapter().notifyItemRangeInserted(old, tiles.size() - old); } @Override protected void onResume() { super.onResume(); if (grid != null && grid.getAdapter() != null) { addTiles(); // the viewer may have appended to the shared list grid.getAdapter().notifyItemRangeChanged(0, grid.getAdapter().getItemCount()); } } private void show(View v) { content.removeAllViews(); content.addView(v); } }