aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-22 17:52:23 +0200
committerDanilo M. <danix@danix.xyz>2026-10-22 17:52:23 +0200
commitb35ce9ed6eb66cef1578bf141cd382f17a541566 (patch)
treea5c539a13f587ecbe55ead4a74d8100745366279
parent9fba091bb394e8e4c7e8ccd0702081bd1a939047 (diff)
downloadmygram-droid-b35ce9ed6eb66cef1578bf141cd382f17a541566.tar.gz
mygram-droid-b35ce9ed6eb66cef1578bf141cd382f17a541566.zip
Folder grid order toggle: random, by date, by nameHEADmaster
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 <noreply@anthropic.com>
-rw-r--r--app/src/main/java/mygram/droid/Api.java33
-rw-r--r--app/src/main/java/mygram/droid/FolderActivity.java74
-rw-r--r--app/src/main/java/mygram/droid/Post.java3
-rw-r--r--app/src/main/java/mygram/droid/Prefs.java9
-rw-r--r--app/src/main/res/drawable/ic_schedule.xml6
-rw-r--r--app/src/main/res/drawable/ic_shuffle.xml6
-rw-r--r--app/src/main/res/drawable/ic_sort_by_alpha.xml6
7 files changed, 110 insertions, 27 deletions
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<List<Post>> 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<List<Post>> folderPages(String slug, int count, int order) {
List<Integer> 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<Post> posts = Post.list(call("GET", path + pages.get(next[0]++)), Prefs.server());
- if (!posts.isEmpty()) {
- Collections.shuffle(posts);
- return posts;
- }
- }
- return new ArrayList<>();
+ List<Post> 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<Post> posts = new ArrayList<>();
- private final List<TileAdapter.Tile> tiles = new ArrayList<>();
+ /** Replaced, not cleared, on a reorder: an open viewer may still hold the old list. */
+ private List<Post> posts = new ArrayList<>();
+ private List<TileAdapter.Tile> tiles = new ArrayList<>();
private Callable<List<Post>> 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<List<Post>> 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<List<Post>> 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<Post> 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> 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 @@
+<!-- Material Icons "schedule", Apache License 2.0 -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp" android:height="24dp"
+ android:viewportWidth="24" android:viewportHeight="24">
+ <path android:fillColor="#FFFFFFFF" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
+</vector>
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 @@
+<!-- Material Icons "shuffle", Apache License 2.0 -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp" android:height="24dp"
+ android:viewportWidth="24" android:viewportHeight="24">
+ <path android:fillColor="#FFFFFFFF" android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5V4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20H20v-5.5l-2.04,2.04 -3.13,-3.13z" />
+</vector>
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 @@
+<!-- Material Icons "sort_by_alpha", Apache License 2.0 -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp" android:height="24dp"
+ android:viewportWidth="24" android:viewportHeight="24">
+ <path android:fillColor="#FFFFFFFF" android:pathData="M14.94,4.66h-4.72l2.36,-2.36zM10.25,19.37h4.66l-2.33,2.33zM6.1,6.27L1.6,17.73h1.84l0.92,-2.45h5.11l0.92,2.45h1.84L7.74,6.27H6.1zM4.97,13.64l1.94,-5.18 1.94,5.18H4.97zM15.73,16.14h6.12v1.59h-8.53v-1.29l5.92,-8.56h-5.88v-1.6h8.3v1.26l-5.93,8.6z" />
+</vector>