-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapps.sh
More file actions
643 lines (549 loc) · 18.9 KB
/
apps.sh
File metadata and controls
643 lines (549 loc) · 18.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
#!/usr/bin/env bash
#
# apps.sh — Privacy-friendly app installer for Google Pixel
#
# Uses fdroidcl for F-Droid / IzzyOnDroid apps.
# Uses GitHub Releases for apps not in any F-Droid repo.
# Only shows apps not already installed on your device.
#
# Usage:
# ./apps.sh — interactive installer
set -euo pipefail
# Colors
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[38;5;39m'
ICE='\033[38;5;195m'
# Cleanup
_cleanup() { printf "\033[?25h" 2>/dev/null; stty echo 2>/dev/null || true; }
trap _cleanup EXIT
trap 'exit 130' INT TERM
# UI primitives
_read_key() {
local key=""
IFS= read -rsn1 key < /dev/tty || true
if [[ "$key" == $'\x1b' ]]; then
local seq=""
read -rsn2 -t 1 seq < /dev/tty || true
case "$seq" in
'[A') echo "up" ;;
'[B') echo "down" ;;
'[C') echo "right" ;;
'[D') echo "left" ;;
*) echo "" ;;
esac
elif [[ "$key" == '' ]]; then
echo "enter"
elif [[ "$key" == ' ' ]]; then
echo "space"
else
echo "$key"
fi
}
_ui_start() {
stty -echo 2>/dev/null || true
printf "\033[?25l" >&2
}
_ui_end() {
stty echo 2>/dev/null || true
printf "\033[?25h" >&2
}
log_ok() { printf ' %b✓%b %s\n' "$GREEN" "$RESET" "$1"; }
log_info() { printf ' %b›%b %s\n' "$CYAN" "$RESET" "$1"; }
log_err() { printf ' %b✗%b %s\n' "$RED" "$RESET" "$1"; }
wait_enter() {
printf '\n %bpress enter to continue%b ' "$DIM" "$RESET"
while true; do
local _k=""
IFS= read -rsn1 _k < /dev/tty || true
if [[ "$_k" == $'\x1b' ]]; then
read -rsn2 -t 1 _ < /dev/tty || true
continue
fi
[[ "$_k" == "" ]] && break
done
printf '\n'
}
confirm() {
local msg="${1:-Continue?}"
printf ' %b%s%b %b[y/n]%b ' "$YELLOW" "$msg" "$RESET" "$DIM" "$RESET"
while true; do
local key=""
IFS= read -rsn1 key < /dev/tty || true
if [[ "$key" == $'\x1b' ]]; then
read -rsn2 -t 1 _ < /dev/tty || true
continue
fi
case "$key" in
[Yy]) printf '%s\n' "$key"; return 0 ;;
[Nn]) printf '%s\n' "$key"; return 1 ;;
*) ;;
esac
done
}
# Dependency checks
if ! command -v adb &>/dev/null; then
printf '%badb not found.%b\n' "$RED" "$RESET"
if command -v brew &>/dev/null; then
printf ' Run: %bbrew install android-platform-tools%b\n' "$BOLD" "$RESET"
elif command -v apt-get &>/dev/null; then
printf ' Run: %bsudo apt install adb%b\n' "$BOLD" "$RESET"
elif command -v pacman &>/dev/null; then
printf ' Run: %bsudo pacman -S android-tools%b\n' "$BOLD" "$RESET"
elif command -v dnf &>/dev/null; then
printf ' Run: %bsudo dnf install android-tools%b\n' "$BOLD" "$RESET"
else
printf ' Install Android Platform Tools: %bhttps://developer.android.com/tools/releases/platform-tools%b\n' "$BOLD" "$RESET"
fi
exit 1
fi
if ! command -v fdroidcl &>/dev/null; then
printf '%bfdroidcl not found.%b\n' "$RED" "$RESET"
if command -v brew &>/dev/null; then
printf ' %bInstall via Homebrew?%b %b[y/n]%b ' "$YELLOW" "$RESET" "$DIM" "$RESET"
while true; do
_k=""
IFS= read -rsn1 _k < /dev/tty || true
case "$_k" in
[Yy]) printf '%s\n' "$_k"; break ;;
[Nn]) printf '%s\n' "$_k"; exit 0 ;;
*) ;;
esac
done
if ! brew install fdroidcl; then
printf ' %bInstallation failed%b\n' "$RED" "$RESET"
printf ' Install manually: %bhttps://github.com/mvdan/fdroidcl/releases%b\n' "$BOLD" "$RESET"
exit 1
fi
else
printf ' Install from: %bhttps://github.com/mvdan/fdroidcl/releases%b\n' "$BOLD" "$RESET"
exit 1
fi
fi
STATE=$(adb get-state 2>&1) || true
if [[ "$STATE" == *"unauthorized"* ]]; then
printf '%bDevice unauthorized. Accept the USB debugging prompt on your phone.%b\n' "$RED" "$RESET"
exit 1
elif [[ "$STATE" != "device" ]]; then
printf '%bNo device connected. Enable USB debugging and plug in your Pixel.%b\n' "$RED" "$RESET"
exit 1
fi
DEVICE=$(adb shell getprop ro.product.model 2>/dev/null | tr -d '\r')
ANDROID=$(adb shell getprop ro.build.version.release 2>/dev/null | tr -d '\r')
# App definitions
# CAT:Name = category header
# pkg|name|description|source
# source: fdroid | izzy | github:owner/repo:arm64-pattern
ALL_DEFINITIONS=(
"CAT:Privacy"
"ch.protonmail.android|Proton Mail|Gmail replacement|fdroid"
"proton.android.pass|Proton Pass|Password manager|fdroid"
"io.ente.photos.independent|Ente Photos|Google Photos replacement|fdroid"
"CAT:Browser"
"org.ironfoxoss.ironfox|IronFox|Hardened Firefox fork|izzy"
"org.cromite.cromite|Cromite|Hardened Chromium fork|github:uazo/cromite:arm64.*universal"
"CAT:Tools"
"com.machiav3lli.fdroid|Neo Store|F-Droid client|fdroid"
"com.deniscerri.ytdl|YTDL-ng|YouTube / media downloader|fdroid"
)
IZZY_REPO="https://apt.izzysoft.de/fdroid/repo"
# Check installed packages
declare -A INSTALLED_MAP=()
while IFS= read -r pkg; do
[[ -n "$pkg" ]] && INSTALLED_MAP["$pkg"]=1
done < <(adb shell pm list packages --user 0 2>/dev/null | tr -d '\r' | sed 's/package://')
is_installed() { [[ -v INSTALLED_MAP["$1"] ]]; }
# Ensure IzzyOnDroid repo is added if needed
ensure_izzy_repo() {
if ! fdroidcl repo list 2>/dev/null | grep -q "izzysoft"; then
log_info "Adding IzzyOnDroid repo..."
fdroidcl repo add "$IZZY_REPO" 2>/dev/null || true
fi
}
# Build display list — only non-installed apps, with category headers
DISPLAY_TYPE=()
DISPLAY_PKG=()
DISPLAY_NAME=()
DISPLAY_DESC=()
DISPLAY_SRC=()
DISPLAY_CAT=()
build_display() {
local pending_cat=""
local cat_has_items=false
local needs_izzy=false
for entry in "${ALL_DEFINITIONS[@]}"; do
if [[ "$entry" == CAT:* ]]; then
pending_cat="${entry#CAT:}"
cat_has_items=false
continue
fi
IFS='|' read -r pkg name desc src <<< "$entry"
if ! is_installed "$pkg"; then
if [[ -n "$pending_cat" ]] && ! $cat_has_items; then
if [[ ${#DISPLAY_TYPE[@]} -gt 0 ]]; then
DISPLAY_TYPE+=("blank")
DISPLAY_PKG+=("")
DISPLAY_NAME+=("")
DISPLAY_DESC+=("")
DISPLAY_SRC+=("")
DISPLAY_CAT+=("")
fi
DISPLAY_TYPE+=("cat")
DISPLAY_PKG+=("")
DISPLAY_NAME+=("")
DISPLAY_DESC+=("")
DISPLAY_SRC+=("")
DISPLAY_CAT+=("$pending_cat")
cat_has_items=true
fi
DISPLAY_TYPE+=("pkg")
DISPLAY_PKG+=("$pkg")
DISPLAY_NAME+=("$name")
DISPLAY_DESC+=("$desc")
DISPLAY_SRC+=("$src")
DISPLAY_CAT+=("")
[[ "$src" == "izzy" ]] && needs_izzy=true
fi
done
if $needs_izzy; then ensure_izzy_repo; fi
}
# Scrollable selector
SELECTION=()
_apps_selector() {
local item_count=${#DISPLAY_TYPE[@]}
if [[ $item_count -eq 0 ]]; then
log_info "All apps already installed" >&2
return 1
fi
local pkg_count=0
local selected=()
for ((i=0; i<item_count; i++)); do
if [[ "${DISPLAY_TYPE[$i]}" == "pkg" ]]; then
selected[i]="0"
pkg_count=$((pkg_count + 1))
else
selected[i]="-"
fi
done
local cursor=0
while [[ $cursor -lt $item_count && "${DISPLAY_TYPE[$cursor]}" != "pkg" ]]; do
cursor=$((cursor + 1))
done
local term_h
term_h=$(tput lines 2>/dev/null || echo 24)
local chrome=6
local need_scroll=false
local visible_count=$item_count
if [[ $item_count -gt $((term_h - chrome)) ]]; then
need_scroll=true
visible_count=$((term_h - chrome - 2))
[[ $visible_count -lt 5 ]] && visible_count=5
fi
local vp_top=0
local total_lines=$((visible_count + 4))
$need_scroll && total_lines=$((total_lines + 2))
local R="$RESET"
local first_draw=true
_ui_start
while true; do
if $need_scroll; then
if [[ $cursor -lt $vp_top ]]; then
vp_top=$cursor
while [[ $vp_top -gt 0 && "${DISPLAY_TYPE[$((vp_top-1))]}" == "cat" ]]; do
vp_top=$((vp_top - 1))
done
while [[ $vp_top -gt 0 && "${DISPLAY_TYPE[$((vp_top-1))]}" == "blank" ]]; do
vp_top=$((vp_top - 1))
done
fi
local vp_bottom=$((vp_top + visible_count))
if [[ $cursor -ge $vp_bottom ]]; then
vp_top=$((cursor - visible_count + 1))
[[ $vp_top -lt 0 ]] && vp_top=0
fi
fi
if $first_draw; then
first_draw=false
clear
printf "\033[K\n" >&2
else
printf "\033[?2026h" >&2
printf "\033[%dA\r" "$total_lines" >&2
printf "\033[K\n" >&2
fi
local sel_count=0
for ((i=0; i<item_count; i++)); do
[[ "${selected[$i]}" == "1" ]] && sel_count=$((sel_count + 1))
done
printf ' %b%s%b / Android %s %b%d apps available%b\033[K\n' \
"$BOLD" "$DEVICE" "$R" "$ANDROID" "$DIM" "$pkg_count" "$R" >&2
printf '\033[K\n' >&2
if $need_scroll; then
if [[ $vp_top -gt 0 ]]; then
printf ' %b▲ ···%b\033[K\n' "$DIM" "$R" >&2
else
printf '\033[K\n' >&2
fi
fi
local rendered=0
for ((i=0; i<item_count; i++)); do
if $need_scroll && [[ $i -lt $vp_top || $rendered -ge $visible_count ]]; then
continue
fi
local type="${DISPLAY_TYPE[$i]}"
if [[ "$type" == "blank" ]]; then
printf '\033[K\n' >&2
rendered=$((rendered + 1))
continue
fi
if [[ "$type" == "cat" ]]; then
printf ' %b%b%s%b\033[K\n' "$BOLD" "$ICE" "${DISPLAY_CAT[$i]}" "$R" >&2
rendered=$((rendered + 1))
continue
fi
local name="${DISPLAY_NAME[$i]}"
local desc="${DISPLAY_DESC[$i]}"
local src="${DISPLAY_SRC[$i]}"
local sel="${selected[$i]}"
local src_label=""
case "$src" in
fdroid) src_label="F-Droid" ;;
izzy) src_label="IzzyOnDroid" ;;
github:*) src_label="GitHub" ;;
esac
local display="" cursor_char="" icon_color=""
if [[ "$sel" == "1" ]]; then
display=$(printf '%s %b%s · %s%b' "$name" "$DIM" "$desc" "$src_label" "$R")
cursor_char="[*]"; icon_color="$GREEN"
else
display=$(printf '%b%s %s · %s%b' "$DIM" "$name" "$desc" "$src_label" "$R")
cursor_char="[ ]"; icon_color="$DIM"
fi
if [[ $i -eq $cursor ]]; then
printf ' %b›%b %b%s%b %s\033[K\n' "$CYAN" "$R" "$icon_color" "$cursor_char" "$R" "$display" >&2
else
printf ' %b%s%b %s\033[K\n' "$icon_color" "$cursor_char" "$R" "$display" >&2
fi
rendered=$((rendered + 1))
done
if $need_scroll; then
if [[ $((vp_top + visible_count)) -lt $item_count ]]; then
printf ' %b▼ ···%b\033[K\n' "$DIM" "$R" >&2
else
printf '\033[K\n' >&2
fi
fi
local hint="↑↓ move ␣ toggle a all n invert ↵ install q quit"
local count_str=""
[[ $sel_count -gt 0 ]] && count_str="${sel_count} selected"
if [[ -n "$count_str" ]]; then
printf ' %b%s%b %b· %s%b\033[K\n' "$DIM" "$hint" "$R" "$CYAN" "$count_str" "$R" >&2
else
printf ' %b%s%b\033[K\n' "$DIM" "$hint" "$R" >&2
fi
printf "\033[?2026l" >&2
local key
key=$(_read_key)
case "$key" in
up)
local prev=$cursor
if [[ $cursor -gt 0 ]]; then
cursor=$((cursor - 1))
while [[ $cursor -gt 0 && "${DISPLAY_TYPE[$cursor]}" != "pkg" ]]; do
cursor=$((cursor - 1))
done
[[ "${DISPLAY_TYPE[$cursor]}" != "pkg" ]] && cursor=$prev
fi ;;
down)
if [[ $cursor -lt $((item_count - 1)) ]]; then
cursor=$((cursor + 1))
while [[ $cursor -lt $((item_count - 1)) && "${DISPLAY_TYPE[$cursor]}" != "pkg" ]]; do
cursor=$((cursor + 1))
done
if [[ "${DISPLAY_TYPE[$cursor]}" != "pkg" ]]; then
cursor=$((cursor - 1))
while [[ "${DISPLAY_TYPE[$cursor]}" != "pkg" ]]; do
cursor=$((cursor - 1))
done
fi
fi ;;
space)
if [[ "${DISPLAY_TYPE[$cursor]}" == "pkg" ]]; then
if [[ "${selected[cursor]}" == "1" ]]; then
selected[cursor]="0"
else
selected[cursor]="1"
fi
fi ;;
a|A)
local all_on=true
for ((i=0; i<item_count; i++)); do
[[ "${DISPLAY_TYPE[$i]}" != "pkg" ]] && continue
[[ "${selected[$i]}" == "0" ]] && { all_on=false; break; }
done
local val="1"; $all_on && val="0"
for ((i=0; i<item_count; i++)); do
[[ "${DISPLAY_TYPE[$i]}" != "pkg" ]] && continue
selected[i]="$val"
done ;;
n|N)
for ((i=0; i<item_count; i++)); do
[[ "${DISPLAY_TYPE[$i]}" != "pkg" ]] && continue
if [[ "${selected[$i]}" == "1" ]]; then
selected[i]="0"
else
selected[i]="1"
fi
done ;;
enter|right)
break ;;
q|Q)
_ui_end
SELECTION=()
return 1 ;;
esac
done
_ui_end
clear
printf "\n"
local cat_names_list=() cat_sel_counts=()
local ci=-1
for ((i=0; i<item_count; i++)); do
if [[ "${DISPLAY_TYPE[$i]}" == "cat" ]]; then
cat_names_list+=("${DISPLAY_CAT[$i]}")
cat_sel_counts+=(0)
ci=$(( ${#cat_names_list[@]} - 1 ))
elif [[ "${DISPLAY_TYPE[$i]}" == "pkg" && "${selected[$i]}" == "1" ]]; then
cat_sel_counts[ci]=$(( cat_sel_counts[ci] + 1 ))
fi
done
local sum_sel=0 cats_active=0
for ((i=0; i<${#cat_names_list[@]}; i++)); do
local sc="${cat_sel_counts[$i]}"
if [[ $sc -gt 0 ]]; then
printf ' %b✓%b %s — %d selected\n' "$GREEN" "$RESET" "${cat_names_list[$i]}" "$sc"
sum_sel=$((sum_sel + sc))
cats_active=$((cats_active + 1))
else
printf ' %b-%b %s\n' "$DIM" "$RESET" "${cat_names_list[$i]}"
fi
done
printf "\n"
if [[ $sum_sel -eq 0 ]]; then
log_info "Nothing selected"
wait_enter
SELECTION=()
return 0
fi
printf ' %b%d install from %d categories%b\n\n' "$BOLD" "$sum_sel" "$cats_active" "$RESET"
if ! confirm "Install?"; then
SELECTION=()
return 0
fi
SELECTION=()
for ((i=0; i<item_count; i++)); do
[[ "${DISPLAY_TYPE[$i]}" != "pkg" ]] && continue
[[ "${selected[$i]}" == "1" ]] && SELECTION+=("${DISPLAY_PKG[$i]}|${DISPLAY_SRC[$i]}")
done
return 0
}
# Install via fdroidcl or GitHub
install_github() {
local pkg="$1" repo="$2" pattern="$3"
local tmpfile
tmpfile=$(mktemp "${TMPDIR:-/tmp}/apps_XXXXXX.apk")
log_info "Fetching latest release from $repo..."
local url
url=$(curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null \
| grep "browser_download_url" \
| grep -i "$pattern" \
| head -1 \
| sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')
if [[ -z "$url" ]]; then
log_err "$pkg — could not find APK in latest release"
rm -f "$tmpfile"
return 1
fi
log_info "Downloading $(basename "$url")..."
if ! curl -fsSL "$url" -o "$tmpfile" 2>/dev/null; then
log_err "$pkg — download failed"
rm -f "$tmpfile"
return 1
fi
local result
result=$(adb install "$tmpfile" 2>&1 | tr -d '\r')
rm -f "$tmpfile"
if [[ "$result" == *"Success"* ]]; then
return 0
else
log_err "$pkg — install failed: $result"
return 1
fi
}
apply_selection() {
local installed=0 failed=0
local needs_fdroid=false
for entry in "${SELECTION[@]}"; do
local src="${entry#*|}"
[[ "$src" == "fdroid" || "$src" == "izzy" ]] && { needs_fdroid=true; break; }
done
if $needs_fdroid; then
printf "\n"
log_info "Updating F-Droid index..."
fdroidcl update 2>/dev/null
fi
printf "\n"
for entry in "${SELECTION[@]}"; do
local pkg="${entry%%|*}"
local src="${entry#*|}"
case "$src" in
fdroid|izzy)
if fdroidcl install "$pkg" 2>/dev/null; then
log_ok "$pkg"
installed=$((installed + 1))
else
log_err "$pkg — install failed"
failed=$((failed + 1))
fi
;;
github:*)
local gh="${src#github:}"
local repo="${gh%%:*}"
local pattern="${gh#*:}"
if install_github "$pkg" "$repo" "$pattern"; then
log_ok "$pkg"
installed=$((installed + 1))
else
failed=$((failed + 1))
fi
;;
esac
done
local summary="${installed} installed"
[[ $failed -gt 0 ]] && summary+=", ${failed} failed"
printf '\n'
log_info "$summary"
}
# Main
printf '%b%s%b / Android %s\n\n' "$BOLD" "$DEVICE" "$RESET" "$ANDROID" >&2
log_info "Checking installed apps..." >&2
build_display
pkg_total=0
for ((i=0; i<${#DISPLAY_TYPE[@]}; i++)); do
[[ "${DISPLAY_TYPE[$i]}" == "pkg" ]] && pkg_total=$((pkg_total + 1))
done
if [[ $pkg_total -eq 0 ]]; then
log_info "All apps already installed" >&2
exit 0
fi
log_info "Found ${pkg_total} apps available" >&2
_apps_selector || exit 0
if [[ ${#SELECTION[@]} -gt 0 ]]; then
clear
apply_selection
wait_enter
fi