soul-browser/scripts/compile-closed-tabs.sh
KaKi87 3e6278f29c
Add "Recently closed" tab to the tab list (#146)
* Add "Recently closed" tab to the tab list

A history icon between the smiley and spy icons opens a page listing recently closed normal tabs. Tapping an entry reopens it as a new tab. The page follows the Thumbnail/List/Simple mode. Closed tabs are recorded by a hook in DbUtil.a before their rows are deleted; private tabs are never recorded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Show "Recently closed" tab in Mini mode too

The Mini mode tab list is a separate bottom-sheet dialog (DialogTabMini). Give it the same three-page layout, history icon, reopen path and Delete all handling as DialogTabMain.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Use the tab list components on the Recently closed page

Items now come from the same WebTabAdapter, list view and layout managers as the normal and private pages, so they follow the Thumbnail/List/Simple modes in both the full and Mini dialogs. Thumbnails are copied to DbTabThumb rows when a tab closes. Delete all asks for confirmation with DialogDeleteItem. The header icons are centred under the indicator and the history icon stroke is thinner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Restore tab history and fix empty background on Recently closed

The closed tab's saved page stack is copied next to its thumbnail and restored into the reopened tab through the app's own saved-tab loader. The empty page now uses the same background as the other pages.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Save the current tab's history when the tab list opens

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Save tab history for tabs closed without opening the tab list

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 12:13:10 +02:00

344 lines
12 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC="${ROOT_DIR}/sources/java/com/mycompany/app/dialog/ClosedTabsHelper.java"
OUT_SMALI="${ROOT_DIR}/app/smali_classes3/com/mycompany/app/dialog"
TOOLS="${ROOT_DIR}/tools"
ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-${HOME}/android-sdk}}"
ANDROID_JAR="$(ls -1 "${ANDROID_HOME}/platforms"/android-*/android.jar 2>/dev/null | sort -V | tail -1)"
D8="$(find "${ANDROID_HOME}/build-tools" -name d8 -type f 2>/dev/null | sort -V | tail -1)"
DEX_TOOLS_ZIP="${TOOLS}/dex-tools-v2.4.zip"
DEX_TOOLS_DIR="${TOOLS}/dex-tools-v2.4"
BAKSMALI_JAR="${TOOLS}/baksmali.jar"
mkdir -p "${TOOLS}" "${OUT_SMALI}"
if [[ ! -f "${ANDROID_JAR}" ]]; then
echo "error: android.jar not found" >&2
exit 1
fi
if [[ -z "${D8}" ]]; then
echo "error: d8 not found" >&2
exit 1
fi
if [[ ! -f "${DEX_TOOLS_DIR}/d2j-dex2jar.sh" ]]; then
if [[ ! -f "${DEX_TOOLS_ZIP}" ]]; then
echo "Downloading dex-tools v2.4..."
curl -fsSL -o "${DEX_TOOLS_ZIP}" \
"https://github.com/pxb1988/dex2jar/releases/download/v2.4/dex-tools-v2.4.zip"
fi
unzip -qo "${DEX_TOOLS_ZIP}" -d "${TOOLS}"
fi
chmod +x "${DEX_TOOLS_DIR}/"*.sh "${DEX_TOOLS_DIR}/d2j_invoke.sh" 2>/dev/null || true
if [[ ! -f "${BAKSMALI_JAR}" ]]; then
curl -fsSL -o "${BAKSMALI_JAR}" \
"https://repo1.maven.org/maven2/org/smali/baksmali/2.5.2/baksmali-2.5.2.jar"
fi
STAGE="$(mktemp -d)"
cleanup() { rm -rf "${STAGE}"; }
trap cleanup EXIT
STUBS="${STAGE}/stubs"
mkdir -p \
"${STUBS}/com/mycompany/app/main" \
"${STUBS}/com/mycompany/app/pref" \
"${STUBS}/com/mycompany/app/db/book" \
"${STUBS}/com/mycompany/app/dialog" \
"${STUBS}/com/mycompany/app/view" \
"${STUBS}/com/mycompany/app/web" \
"${STUBS}/androidx/viewpager2/widget"
cat > "${STUBS}/com/mycompany/app/main/MainApp.java" <<'EOF'
package com.mycompany.app.main;
public final class MainApp {
public static boolean K1;
}
EOF
cat > "${STUBS}/com/mycompany/app/pref/PrefSync.java" <<'EOF'
package com.mycompany.app.pref;
public final class PrefSync {
public static boolean k;
}
EOF
cat > "${STUBS}/com/mycompany/app/pref/PrefZone.java" <<'EOF'
package com.mycompany.app.pref;
public final class PrefZone {
public static int C;
}
EOF
cat > "${STUBS}/com/mycompany/app/db/book/DbTabThumb.java" <<'EOF'
package com.mycompany.app.db.book;
import android.content.Context;
import android.graphics.Bitmap;
public class DbTabThumb {
public static Bitmap d(Context context, long uid) { return null; }
}
EOF
cat > "${STUBS}/com/mycompany/app/dialog/DialogTabMain.java" <<'EOF'
package com.mycompany.app.dialog;
import android.app.Dialog;
import android.content.Context;
public class DialogTabMain extends Dialog {
public DialogTabMain(Context c) { super(c); }
public static void u(DialogTabMain dialog, boolean secret) { }
}
EOF
cat > "${STUBS}/com/mycompany/app/dialog/DialogTabMini.java" <<'EOF'
package com.mycompany.app.dialog;
import android.app.Dialog;
import android.content.Context;
public class DialogTabMini extends Dialog {
public DialogTabMini(Context c) { super(c); }
public static void D(DialogTabMini dialog, boolean secret) { }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyButtonRelative.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.RelativeLayout;
public class MyButtonRelative extends RelativeLayout {
public MyButtonRelative(Context c) { super(c); }
public void setBgPreColor(int color) { }
}
EOF
cat > "${STUBS}/com/mycompany/app/web/WebNestView.java" <<'EOF'
package com.mycompany.app.web;
import android.content.Context;
import android.webkit.WebView;
public class WebNestView extends WebView {
public WebNestView(Context c) { super(c); }
public final void A(String url, com.mycompany.app.db.book.DbTabState.StateItem state) { }
}
EOF
cat > "${STUBS}/com/mycompany/app/web/WebNestFrame.java" <<'EOF'
package com.mycompany.app.web;
public class WebNestFrame extends android.view.View {
public WebNestFrame(android.content.Context c) { super(c); }
public long i;
public String getListToStr() { return null; }
}
EOF
cat > "${STUBS}/com/mycompany/app/web/WebViewActivity.java" <<'EOF'
package com.mycompany.app.web;
import android.app.Activity;
public class WebViewActivity extends Activity {
public WebNestView I2;
public com.mycompany.app.web.WebNestFrame H2;
public String A8;
public static String o3() { return null; }
}
EOF
cat > "${STUBS}/com/mycompany/app/db/book/DbTabState.java" <<'EOF'
package com.mycompany.app.db.book;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DbTabState extends SQLiteOpenHelper {
public static class StateItem { }
public static StateItem d(Context context, long uid) { return null; }
DbTabState(Context c) { super(c, "x", null, 1); }
public void onCreate(SQLiteDatabase db) { }
public void onUpgrade(SQLiteDatabase db, int a, int b) { }
public static DbTabState b(Context context) { return null; }
}
EOF
cat > "${STUBS}/androidx/viewpager2/widget/ViewPager2.java" <<'EOF'
package androidx.viewpager2.widget;
import android.content.Context;
import android.view.ViewGroup;
public class ViewPager2 extends ViewGroup {
public ViewPager2(Context c) { super(c); }
protected void onLayout(boolean changed, int l, int t, int r, int b) { }
public void setCurrentItem(int item) { }
}
EOF
mkdir -p \
"${STUBS}/androidx/recyclerview/widget" \
"${STUBS}/androidx/appcompat/widget"
cat > "${STUBS}/androidx/recyclerview/widget/RecyclerView.java" <<'EOF'
package androidx.recyclerview.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class RecyclerView extends ViewGroup {
public RecyclerView(Context c, AttributeSet a) { super(c, a); }
protected void onLayout(boolean changed, int l, int t, int r, int b) { }
public void setLayoutManager(LayoutManager manager) { }
public void setAdapter(Adapter adapter) { }
public abstract static class LayoutManager { }
public abstract static class Adapter<VH extends ViewHolder> { }
public abstract static class ViewHolder { }
}
EOF
cat > "${STUBS}/androidx/recyclerview/widget/LinearLayoutManager.java" <<'EOF'
package androidx.recyclerview.widget;
public class LinearLayoutManager extends RecyclerView.LayoutManager {
public LinearLayoutManager(int orientation) { }
public void x0(int position) { }
}
EOF
cat > "${STUBS}/androidx/recyclerview/widget/GridLayoutManager.java" <<'EOF'
package androidx.recyclerview.widget;
public class GridLayoutManager extends LinearLayoutManager {
public int F;
public SpanSizeLookup K;
public GridLayoutManager(int spanCount) { super(1); F = spanCount; }
public void x1(int spanCount) { F = spanCount; }
public abstract static class SpanSizeLookup {
public abstract int c(int position);
}
}
EOF
cat > "${STUBS}/androidx/appcompat/widget/AppCompatTextView.java" <<'EOF'
package androidx.appcompat.widget;
import android.content.Context;
import android.widget.TextView;
public class AppCompatTextView extends TextView {
public AppCompatTextView(Context c) { super(c); }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyRecyclerView.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import androidx.recyclerview.widget.RecyclerView;
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(Context c) { super(c, null); }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyRoundImage.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.ImageView;
public class MyRoundImage extends ImageView {
public MyRoundImage(Context c) { super(c); }
public void o(int color, int resId) { }
}
EOF
cat > "${STUBS}/com/mycompany/app/dialog/DialogDeleteItem.java" <<'EOF'
package com.mycompany.app.dialog;
import android.app.Activity;
import android.app.Dialog;
import androidx.appcompat.widget.AppCompatTextView;
import com.mycompany.app.view.MyRoundImage;
public class DialogDeleteItem extends Dialog {
public DialogDeleteItem(Activity activity, DelItemListener listener) { super(activity); }
public interface DelItemListener {
void a();
void b(MyRoundImage image, AppCompatTextView text);
}
}
EOF
cat > "${STUBS}/com/mycompany/app/pref/PrefZtwo.java" <<'EOF'
package com.mycompany.app.pref;
public final class PrefZtwo {
public static boolean z;
}
EOF
cat > "${STUBS}/com/mycompany/app/main/MainApp.java" <<'EOF'
package com.mycompany.app.main;
public final class MainApp {
public static boolean K1;
public static int q1;
public static int F1;
public static int G1;
}
EOF
cat > "${STUBS}/com/mycompany/app/web/WebTabAdapter.java" <<'EOF'
package com.mycompany.app.web;
import android.content.Context;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mycompany.app.view.MyRecyclerView;
import java.util.List;
public class WebTabAdapter extends RecyclerView.Adapter<WebTabAdapter.WebTabHolder> {
public WebTabListener x;
public WebTabAdapter(Context context, MyRecyclerView list, boolean z, List a, List b, long j, int i, int i2,
int i3, int i4, boolean z2, LinearLayoutManager manager) { }
public final WebTabItem H(int position) { return null; }
public final void Z(int height, int columns, int itemHeight, boolean z) { }
public final void b0(List a, List b, long j, int i, int i2) { }
public final void P() { }
public static class WebTabHolder extends RecyclerView.ViewHolder { }
public static class WebTabItem {
public int a;
public long c;
public int h;
public int i;
public String j;
public String k;
}
public interface WebTabListener {
void a();
void b(View view, int position);
void c(WebTabHolder holder, int position);
void d(int position);
void e(WebTabHolder holder, int position);
}
}
EOF
cat > "${STUBS}/com/mycompany/app/db/book/DbTabThumb.java" <<'EOF'
package com.mycompany.app.db.book;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DbTabThumb extends SQLiteOpenHelper {
DbTabThumb(Context c) { super(c, "x", null, 1); }
public void onCreate(SQLiteDatabase db) { }
public void onUpgrade(SQLiteDatabase db, int a, int b) { }
public static DbTabThumb b(Context context) { return null; }
}
EOF
echo "Compiling ClosedTabsHelper..."
javac --release 8 -classpath "${ANDROID_JAR}:${STUBS}" -d "${STAGE}/classes" "${SRC}"
mkdir -p "${STAGE}/dex"
mapfile -t CLASS_FILES < <(find "${STAGE}/classes" -name '*.class' -type f)
"${D8}" --min-api 23 --lib "${ANDROID_JAR}" --output "${STAGE}/dex" "${CLASS_FILES[@]}"
if [[ -x "${DEX_TOOLS_DIR}/d2j-baksmali.sh" ]]; then
bash "${DEX_TOOLS_DIR}/d2j-baksmali.sh" -f -o "${STAGE}/out" "${STAGE}/dex/classes.dex"
else
java -cp "${BAKSMALI_JAR}" org.jf.baksmali.Main d "${STAGE}/dex/classes.dex" -o "${STAGE}/out"
fi
# d2j-baksmali prints const/high16 operands as the upper 16 bits in decimal; smali wants the full hex literal.
python3 -I - "${STAGE}/out" <<'PY'
import pathlib, re, sys
for f in pathlib.Path(sys.argv[1]).rglob("*.smali"):
text = f.read_text()
fixed = re.sub(r"const/high16 (v\d+), (-?\d+)\b",
lambda m: "const/high16 %s, 0x%08x" % (m.group(1), (int(m.group(2)) << 16) & 0xFFFFFFFF),
text)
if fixed != text:
f.write_text(fixed)
PY
rm -f "${OUT_SMALI}"/ClosedTabsHelper*.smali
cp -a "${STAGE}/out/com/mycompany/app/dialog/ClosedTabsHelper"* "${OUT_SMALI}/"
echo "Wrote ${OUT_SMALI}/ClosedTabsHelper*.smali"