From b35ce9ed6eb66cef1578bf141cd382f17a541566 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 22 Oct 2026 17:52:23 +0200 Subject: Folder grid order toggle: random, by date, by name A button at the top right of the folder header cycles the grid order and reloads it; the choice is saved for every folder. Foldergram has no per-request sort, so "by date" is the server's order (the instance's default folder order) and "by name" fetches every page and sorts by file name on the device. Co-Authored-By: Claude Opus 5.5 --- app/src/main/java/mygram/droid/Api.java | 33 ++++++---- app/src/main/java/mygram/droid/FolderActivity.java | 74 +++++++++++++++++----- app/src/main/java/mygram/droid/Post.java | 3 +- app/src/main/java/mygram/droid/Prefs.java | 9 +++ app/src/main/res/drawable/ic_schedule.xml | 6 ++ app/src/main/res/drawable/ic_shuffle.xml | 6 ++ app/src/main/res/drawable/ic_sort_by_alpha.xml | 6 ++ 7 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 app/src/main/res/drawable/ic_schedule.xml create mode 100644 app/src/main/res/drawable/ic_shuffle.xml create mode 100644 app/src/main/res/drawable/ic_sort_by_alpha.xml (limited to 'app') diff --git a/app/src/main/java/mygram/droid/Api.java b/app/src/main/java/mygram/droid/Api.java index c7d08eb..6e4ce74 100644 --- a/app/src/main/java/mygram/droid/Api.java +++ b/app/src/main/java/mygram/droid/Api.java @@ -172,25 +172,36 @@ final class Api { return out; } - /** 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. + /** Folder orders: RANDOM shuffles, DATE keeps the server's order, NAME sorts by file name. */ + static final int RANDOM = 0, DATE = 1, NAME = 2; + static Callable> folderPages(String slug, int count) { + return folderPages(slug, count, RANDOM); + } + + /** Pager over a folder: each call returns the next page; 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. + // ponytail: NAME fetches every page in its first call, since the server cannot sort by name; slow on folders of thousands. + // DATE is the server's order: newest first unless the instance's default folder order says oldest. + static Callable> folderPages(String slug, int count, int order) { List pages = new ArrayList<>(); for (int p = 1; p <= (count + PAGE - 1) / PAGE; p++) pages.add(p); - Collections.shuffle(pages); + if (order == RANDOM) + 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 posts = Post.list(call("GET", path + pages.get(next[0]++)), Prefs.server()); - if (!posts.isEmpty()) { - Collections.shuffle(posts); - return posts; - } - } - return new ArrayList<>(); + List out = new ArrayList<>(); + // imageCount counts carousel slides, so later pages may be empty + while (next[0] < pages.size() && (order == NAME || out.isEmpty())) + out.addAll(Post.list(call("GET", path + pages.get(next[0]++)), Prefs.server())); + if (order == RANDOM) + Collections.shuffle(out); + else if (order == NAME) + out.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.filename, b.filename)); + return out; } }; } diff --git a/app/src/main/java/mygram/droid/FolderActivity.java b/app/src/main/java/mygram/droid/FolderActivity.java index 84e7125..7e71d82 100644 --- a/app/src/main/java/mygram/droid/FolderActivity.java +++ b/app/src/main/java/mygram/droid/FolderActivity.java @@ -19,9 +19,12 @@ 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; @@ -31,12 +34,15 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; -/** One folder as a grid, paged in lazily in random page order. */ +/** 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; - private final List posts = new ArrayList<>(); - private final List tiles = new ArrayList<>(); + /** 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; @@ -50,17 +56,48 @@ public final class FolderActivity extends Activity { super.onCreate(state); LinearLayout column = new LinearLayout(this); column.setOrientation(LinearLayout.VERTICAL); - column.addView(Ui.header(this, getIntent().getStringExtra("name"), getIntent().getStringExtra("avatar"))); + 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)); - more = Api.folderPages(getIntent().getStringExtra("slug"), getIntent().getIntExtra("count", 0)); + Callable> pager = Api.folderPages(getIntent().getStringExtra("slug"), + getIntent().getIntExtra("count", 0), Prefs.folderOrder()); + more = pager; loading = true; - Api.async(more, page -> { + Api.async(pager, page -> { + if (isDestroyed() || more != pager) + return; // a newer order replaced this one loading = false; - if (isDestroyed()) - return; append(page); if (page.isEmpty()) { show(Ui.text(this, "Nothing to show", 18)); @@ -74,8 +111,11 @@ public final class FolderActivity extends Activity { } }); show(grid); - grid.post(grid::requestFocus); // after layout + 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)); }); @@ -88,14 +128,17 @@ public final class FolderActivity extends Activity { if (((GridLayoutManager) grid.getLayoutManager()).findLastVisibleItemPosition() < tiles.size() - 12) return; loading = true; - Api.async(more, page -> { - loading = false; - if (isDestroyed()) + 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(); @@ -110,9 +153,10 @@ public final class FolderActivity extends Activity { private void addTiles() { int old = tiles.size(); - for (int i = old; i < posts.size(); i++) { + List list = posts; + for (int i = old; i < list.size(); i++) { int start = i; - tiles.add(TileAdapter.post(posts.get(i), () -> Playlist.open(this, posts, start, more))); + 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); @@ -127,7 +171,7 @@ public final class FolderActivity extends Activity { } } - private void show(android.view.View v) { + private void show(View v) { content.removeAllViews(); content.addView(v); } diff --git a/app/src/main/java/mygram/droid/Post.java b/app/src/main/java/mygram/droid/Post.java index b4211cf..8578a04 100644 --- a/app/src/main/java/mygram/droid/Post.java +++ b/app/src/main/java/mygram/droid/Post.java @@ -41,13 +41,14 @@ final class Post { } final int id; - final String folderSlug, folderName; + final String folderSlug, folderName, filename; final List media = new ArrayList<>(); Post(JSONObject o, String base) { id = o.optInt("id"); folderSlug = o.optString("folderSlug"); folderName = o.optString("folderName"); + filename = o.optString("filename"); JSONArray items = o.optJSONArray("mediaItems"); if (items != null) for (int i = 0; i < items.length(); i++) diff --git a/app/src/main/java/mygram/droid/Prefs.java b/app/src/main/java/mygram/droid/Prefs.java index 0ff295d..e46672c 100644 --- a/app/src/main/java/mygram/droid/Prefs.java +++ b/app/src/main/java/mygram/droid/Prefs.java @@ -51,6 +51,15 @@ final class Prefs { return prefs.getInt("background", 15); } + /** Api.RANDOM, DATE or NAME, for folder grids. */ + static int folderOrder() { + return prefs.getInt("order", Api.RANDOM); + } + + static void setFolderOrder(int order) { + prefs.edit().putInt("order", order).apply(); + } + static void setServer(String base, String password) { prefs.edit().putString("server", base).putString("password", password).apply(); } diff --git a/app/src/main/res/drawable/ic_schedule.xml b/app/src/main/res/drawable/ic_schedule.xml new file mode 100644 index 0000000..5fd2139 --- /dev/null +++ b/app/src/main/res/drawable/ic_schedule.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_shuffle.xml b/app/src/main/res/drawable/ic_shuffle.xml new file mode 100644 index 0000000..9d03c0f --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_sort_by_alpha.xml b/app/src/main/res/drawable/ic_sort_by_alpha.xml new file mode 100644 index 0000000..bf6b31f --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_by_alpha.xml @@ -0,0 +1,6 @@ + + + + -- cgit v1.2.3