diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/src/main/java/mygram/droid/Api.java | 49 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/FolderActivity.java | 82 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/HomeRows.java | 11 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/Playlist.java | 33 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/Ui.java | 2 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/ViewerActivity.java | 22 |
6 files changed, 145 insertions, 54 deletions
diff --git a/app/src/main/java/mygram/droid/Api.java b/app/src/main/java/mygram/droid/Api.java index 7cce5b6..f603b1b 100644 --- a/app/src/main/java/mygram/droid/Api.java +++ b/app/src/main/java/mygram/droid/Api.java @@ -165,15 +165,42 @@ final class Api { return out; } - /** Every post of a folder, in server order. */ - static List<Post> folderPosts(String slug) throws IOException, JSONException { - List<Post> all = new ArrayList<>(); - for (int page = 1;; page++) { - JSONObject o = call("GET", "/folders/" + Uri.encode(slug) + "/images?limit=" + PAGE + "&page=" + page); - all.addAll(Post.list(o, Prefs.server())); - if (!o.optBoolean("hasMore")) - return all; - } + /** Pager over a folder: each call returns the next page, shuffled; empty once all pages are used. */ + // ponytail: random page order plus in-page shuffle, not a full shuffle; neighbours within a page stay together; upgrade: fetch all ids once if that ever matters. + static Callable<List<Post>> folderPages(String slug, int count) { + List<Integer> pages = new ArrayList<>(); + for (int p = 1; p <= (count + PAGE - 1) / PAGE; p++) + pages.add(p); + Collections.shuffle(pages); + String path = "/folders/" + Uri.encode(slug) + "/images?limit=" + PAGE + "&page="; + int[] next = {0}; + return () -> { + synchronized (next) { + while (next[0] < pages.size()) { // imageCount counts carousel slides, so later pages may be empty + List<Post> posts = Post.list(call("GET", path + pages.get(next[0]++)), Prefs.server()); + if (!posts.isEmpty()) { + Collections.shuffle(posts); + return posts; + } + } + return new ArrayList<>(); + } + }; + } + + /** Pager over random Reels with a fixed seed; empty once the server reports no more. */ + static Callable<List<Post>> reelsPages(int seed) { + int[] state = {0, 1}; // page, hasMore + return () -> { + synchronized (state) { + if (state[1] == 0) + return new ArrayList<>(); + JSONObject o = call("GET", "/reels?mode=random&limit=12&seed=" + seed + "&page=" + ++state[0]); + if (!o.optBoolean("hasMore")) + state[1] = 0; + return Post.list(o, Prefs.server()); + } + }; } /** Up to n random posts of a folder. */ @@ -193,10 +220,6 @@ final class Api { return Post.list(call("GET", "/feed?mode=random&limit=" + PAGE), Prefs.server()); } - static JSONObject reels(int seed, int page) throws IOException, JSONException { - return call("GET", "/reels?mode=random&limit=12&seed=" + seed + "&page=" + page); - } - /** Liked posts, newest like first. */ static List<Post> likes() throws IOException, JSONException { return Post.list(call("GET", "/likes"), Prefs.server()); diff --git a/app/src/main/java/mygram/droid/FolderActivity.java b/app/src/main/java/mygram/droid/FolderActivity.java index 10a9940..c7002c5 100644 --- a/app/src/main/java/mygram/droid/FolderActivity.java +++ b/app/src/main/java/mygram/droid/FolderActivity.java @@ -23,18 +23,27 @@ 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.Collections; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; -/** One folder as a grid, shuffled once per open. */ +/** One folder as a grid, paged in lazily in random page order. */ public final class FolderActivity extends Activity { private FrameLayout content; private RecyclerView grid; + private final List<Post> posts = new ArrayList<>(); + private final List<TileAdapter.Tile> tiles = new ArrayList<>(); + private Callable<List<Post>> 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)); + c.startActivity(new Intent(c, FolderActivity.class).putExtra("slug", f.slug).putExtra("name", f.name) + .putExtra("count", f.count)); } @Override @@ -49,19 +58,76 @@ public final class FolderActivity extends Activity { column.addView(content, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1)); setContentView(column); show(Ui.text(this, "Loading…", 18)); - Api.async(() -> Api.folderPosts(getIntent().getStringExtra("slug")), posts -> { - Collections.shuffle(posts); - grid = Ui.grid(this, Ui.postTiles(this, posts), Ui.POST_W, Ui.POST_H, 6); + more = Api.folderPages(getIntent().getStringExtra("slug"), getIntent().getIntExtra("count", 0)); + loading = true; + Api.async(more, page -> { + loading = false; + if (isDestroyed()) + return; + 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); grid.post(grid::requestFocus); // after layout - }, e -> show(Ui.text(this, "Cannot load folder: " + e.getMessage(), 18))); + }, e -> { + 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; + Api.async(more, page -> { + loading = false; + if (isDestroyed()) + return; + if (page.isEmpty()) + more = null; + append(page); + }, e -> { + 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<Post> page) { + posts.addAll(page); + addTiles(); + } + + private void addTiles() { + int old = tiles.size(); + for (int i = old; i < posts.size(); i++) { + int start = i; + tiles.add(TileAdapter.post(posts.get(i), () -> Playlist.open(this, posts, 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) + 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(android.view.View v) { diff --git a/app/src/main/java/mygram/droid/HomeRows.java b/app/src/main/java/mygram/droid/HomeRows.java index e4abeb5..6105c1c 100644 --- a/app/src/main/java/mygram/droid/HomeRows.java +++ b/app/src/main/java/mygram/droid/HomeRows.java @@ -20,12 +20,10 @@ import android.content.Context; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; -import android.widget.Toast; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -102,14 +100,9 @@ final class HomeRows extends RecyclerView.Adapter<HomeRows.Row> { return folders.size(); } - /** Viewer at p, then the rest of the folder, shuffled. */ + /** Viewer at p at once; the rest of the folder pages in lazily, in random page order. */ private void open(Api.Folder f, Post p) { - Api.async(() -> Api.folderPosts(f.slug), all -> { - Collections.shuffle(all); - all.removeIf(x -> x.id == p.id); - all.add(0, p); - Playlist.open(activity, all, 0); - }, e -> Toast.makeText(activity, "Cannot load folder: " + e.getMessage(), Toast.LENGTH_LONG).show()); + Playlist.open(activity, new ArrayList<>(List.of(p)), 0, Api.folderPages(f.slug, f.count)); } /** Rebinds visible tiles so hearts follow likes changed in the viewer. */ diff --git a/app/src/main/java/mygram/droid/Playlist.java b/app/src/main/java/mygram/droid/Playlist.java index 98379fb..b885397 100644 --- a/app/src/main/java/mygram/droid/Playlist.java +++ b/app/src/main/java/mygram/droid/Playlist.java @@ -18,32 +18,43 @@ import android.content.Context; import android.content.Intent; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; import java.util.concurrent.ThreadLocalRandom; /** Hands a post list to ViewerActivity; intents cannot carry hundreds of posts. */ final class Playlist { + /** Shared with the caller (e.g. a folder grid), not copied, so both see appended pages. */ static List<Post> posts = new ArrayList<>(); static int start; - /** Non-zero while the viewer plays endless Reels and pages for more. */ - static int reelsSeed; - static int reelsPage; + /** Fetches the next page of posts; null when there is nothing more. */ + static Callable<List<Post>> more; private Playlist() { } - static void open(Context c, List<Post> list, int startIndex) { - posts = new ArrayList<>(list); + static void open(Context c, List<Post> list, int startIndex, Callable<List<Post>> pager) { + posts = list; start = startIndex; - reelsSeed = 0; + more = pager; c.startActivity(new Intent(c, ViewerActivity.class)); } static void openReels(Context c) { - posts = new ArrayList<>(); - start = 0; - reelsSeed = 1 + ThreadLocalRandom.current().nextInt(1_000_000); - reelsPage = 0; - c.startActivity(new Intent(c, ViewerActivity.class)); + open(c, new ArrayList<>(), 0, Api.reelsPages(1 + ThreadLocalRandom.current().nextInt(1_000_000))); + } + + /** Appends the posts not already in the list; returns how many were added. */ + static int append(List<Post> page) { + Set<Integer> ids = new HashSet<>(); + for (Post p : posts) + ids.add(p.id); + int old = posts.size(); + for (Post p : page) + if (ids.add(p.id)) + posts.add(p); + return posts.size() - old; } } diff --git a/app/src/main/java/mygram/droid/Ui.java b/app/src/main/java/mygram/droid/Ui.java index 757b7fe..65de7f8 100644 --- a/app/src/main/java/mygram/droid/Ui.java +++ b/app/src/main/java/mygram/droid/Ui.java @@ -150,7 +150,7 @@ final class Ui { List<TileAdapter.Tile> tiles = new ArrayList<>(); for (int i = 0; i < posts.size(); i++) { int start = i; - tiles.add(TileAdapter.post(posts.get(i), () -> Playlist.open(c, posts, start))); + tiles.add(TileAdapter.post(posts.get(i), () -> Playlist.open(c, posts, start, null))); } return tiles; } diff --git a/app/src/main/java/mygram/droid/ViewerActivity.java b/app/src/main/java/mygram/droid/ViewerActivity.java index 59e6948..4845cbd 100644 --- a/app/src/main/java/mygram/droid/ViewerActivity.java +++ b/app/src/main/java/mygram/droid/ViewerActivity.java @@ -131,7 +131,7 @@ public final class ViewerActivity extends Activity { if (!Playlist.posts.isEmpty()) { show(Playlist.start); - } else if (Playlist.reelsSeed != 0) { + } else if (Playlist.more != null) { message.setText("Loading…"); maybeLoadMore(); } else { @@ -330,35 +330,33 @@ public final class ViewerActivity extends Activity { }); } - /** Reels only: fetches the next page when three posts remain. */ + /** Fetches the next page when three posts remain. */ private void maybeLoadMore() { - if (Playlist.reelsSeed == 0 || loadingMore || index < Playlist.posts.size() - 3) + if (Playlist.more == null || loadingMore || index < Playlist.posts.size() - 3) return; loadingMore = true; - int page = Playlist.reelsPage + 1; List<Post> list = Playlist.posts; - Api.async(() -> Api.reels(Playlist.reelsSeed, page), o -> { + Api.async(Playlist.more, page -> { if (isDestroyed() || Playlist.posts != list) return; loadingMore = false; - Playlist.reelsPage = page; boolean first = list.isEmpty(); int old = list.size(); - list.addAll(Post.list(o, Prefs.server())); - if (!o.optBoolean("hasMore")) - Playlist.reelsSeed = 0; + if (page.isEmpty()) + Playlist.more = null; + Playlist.append(page); if (first && !list.isEmpty()) show(0); else if (parkedAtEnd && index == old - 1 && list.size() > old) show(index + 1); - else if (list.isEmpty() && Playlist.reelsSeed == 0) - message.setText("No reels"); + else if (list.isEmpty() && Playlist.more == null) + message.setText("Nothing to show"); }, e -> { if (isDestroyed()) return; loadingMore = false; if (Playlist.posts.isEmpty()) - message.setText("Cannot load reels. Press Down to retry."); + message.setText("Cannot load posts. Press Down to retry."); else Toast.makeText(this, "Cannot load more: " + e.getMessage(), Toast.LENGTH_SHORT).show(); }); |
