aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/mygram/droid/Ui.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/mygram/droid/Ui.java')
-rw-r--r--app/src/main/java/mygram/droid/Ui.java155
1 files changed, 155 insertions, 0 deletions
diff --git a/app/src/main/java/mygram/droid/Ui.java b/app/src/main/java/mygram/droid/Ui.java
new file mode 100644
index 0000000..852fe54
--- /dev/null
+++ b/app/src/main/java/mygram/droid/Ui.java
@@ -0,0 +1,155 @@
+/*
+ * mygram-droid: Android TV client for Foldergram.
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * 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.content.Context;
+import android.graphics.Color;
+import android.graphics.RenderEffect;
+import android.graphics.Shader;
+import android.text.InputType;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** View helpers. The UI is built in code, there are no layout files. */
+final class Ui {
+ static final int RED = 0xFFFF3B5C;
+ /** Post tiles: portrait, most media is 9:16. */
+ static final int POST_W = 128, POST_H = 228;
+
+ private Ui() {
+ }
+
+ static int dp(Context c, float v) {
+ return Math.round(v * c.getResources().getDisplayMetrics().density);
+ }
+
+ static TextView text(Context c, String s, int sp) {
+ TextView t = new TextView(c);
+ t.setText(s);
+ t.setTextSize(sp);
+ t.setTextColor(Color.WHITE);
+ return t;
+ }
+
+ static LinearLayout column(Context c) {
+ LinearLayout box = new LinearLayout(c);
+ box.setOrientation(LinearLayout.VERTICAL);
+ box.setGravity(Gravity.CENTER);
+ int p = dp(c, 32);
+ box.setPadding(p, p, p, p);
+ return box;
+ }
+
+ static LinearLayout row(Context c) {
+ LinearLayout row = new LinearLayout(c);
+ row.setGravity(Gravity.CENTER);
+ return row;
+ }
+
+ static EditText field(Context c, String hint, int inputType) {
+ EditText e = new EditText(c);
+ e.setHint(hint);
+ e.setInputType(inputType);
+ e.setTextColor(Color.WHITE);
+ e.setHintTextColor(Color.GRAY);
+ e.setMinWidth(dp(c, 400));
+ e.setSingleLine(true);
+ return e;
+ }
+
+ static int password() {
+ return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+
+ /** Focus ring and a small zoom: the only focus feedback on a TV. */
+ static void focusable(View v) {
+ v.setFocusable(true);
+ v.setForeground(v.getContext().getDrawable(R.drawable.focus_ring));
+ v.setOnFocusChangeListener((x, has) -> zoom(x, has));
+ }
+
+ private static void zoom(View v, boolean in) {
+ v.animate().scaleX(in ? 1.08f : 1f).scaleY(in ? 1.08f : 1f).setDuration(120).start();
+ }
+
+ /** Icon-only button; its label shows under it while it has focus. Child 0 is the ImageView. */
+ static LinearLayout iconButton(Context c, int icon, String label, View.OnClickListener click) {
+ LinearLayout box = new LinearLayout(c);
+ box.setOrientation(LinearLayout.VERTICAL);
+ box.setGravity(Gravity.CENTER_HORIZONTAL);
+ int p = dp(c, 10);
+ box.setPadding(p, p, p, p / 2);
+ ImageView image = new ImageView(c);
+ image.setImageResource(icon);
+ image.setContentDescription(label);
+ int s = dp(c, 36);
+ box.addView(image, new LinearLayout.LayoutParams(s, s));
+ TextView t = text(c, label, 12);
+ t.setVisibility(View.INVISIBLE);
+ box.addView(t);
+ box.setFocusable(true);
+ box.setForeground(c.getDrawable(R.drawable.focus_ring));
+ box.setOnFocusChangeListener((v, has) -> {
+ t.setVisibility(has ? View.VISIBLE : View.INVISIBLE);
+ zoom(v, has);
+ });
+ box.setOnClickListener(click);
+ return box;
+ }
+
+ static void blur(ImageView v, float radius) {
+ v.setRenderEffect(RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP));
+ }
+
+ static RecyclerView list(Context c, int orientation) {
+ RecyclerView rv = new RecyclerView(c);
+ rv.setLayoutManager(new LinearLayoutManager(c, orientation, false));
+ int p = dp(c, 24);
+ rv.setPadding(p, 0, p, 0);
+ rv.setClipToPadding(false);
+ return rv;
+ }
+
+ static RecyclerView grid(Context c, List<TileAdapter.Tile> tiles, int w, int h, int span) {
+ RecyclerView rv = new RecyclerView(c);
+ rv.setLayoutManager(new GridLayoutManager(c, span));
+ rv.setAdapter(new TileAdapter(tiles, w, h));
+ int p = dp(c, 24);
+ rv.setPadding(p, p, p, p);
+ rv.setClipToPadding(false);
+ return rv;
+ }
+
+ /** Tiles that open the viewer on posts, starting at the chosen one. */
+ static List<TileAdapter.Tile> postTiles(Context c, List<Post> posts) {
+ 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)));
+ }
+ return tiles;
+ }
+}