mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-21 01:01:46 +02:00
Games installed from an install link get a row on the game page (where it came from, the installed version) with Check for updates, and the same entry in the page's menu: it asks the link now instead of waiting for the 6-hourly check. Manifests that named no version (e.g. an app's first release) were never checked; the installed APK's own versionName (from the analysis) is now the version to compare with, in the automatic check, the button and frameport link-updates.
221 lines
11 KiB
Python
221 lines
11 KiB
Python
"""Menu structure: game menus (Library right-click, game page "…"), the Monitor process menu and menu_items."""
|
|
import flet as ft
|
|
|
|
from frameport.ui import components as C
|
|
from frameport.ui import theme as T
|
|
from frameport.ui.menus import Header, MenuState, menu_sections, process_menu
|
|
|
|
DESTRUCTIVE = {"Uninstall from Frame…", "Uninstall from this PC…", "Remove from library…"}
|
|
QUEST = {"package": "com.q", "title": "Q"}
|
|
RIFT = {"package": "rift.r", "kind": "rift", "title": "R"}
|
|
LINUX = {"package": "linux.l", "kind": "linux", "title": "L"}
|
|
|
|
|
|
def _key(k):
|
|
return k
|
|
|
|
|
|
def _play(where="Frame"):
|
|
return (f"Play on {where}", ft.Icons.PLAY_ARROW_ROUNDED, f"play_{where}")
|
|
|
|
|
|
def _install(label="Reinstall on Frame"):
|
|
return (label, "fp:frame", "install")
|
|
|
|
|
|
def _states():
|
|
"""(name, game, state) for the cases the menus have to handle."""
|
|
return [
|
|
("quest on Frame", QUEST, MenuState(connected=True, on_frame=True, plays=[_play()], installs=[_install()],
|
|
settings=True, selectable=True, android=True, saves=True)),
|
|
("quest not installed", QUEST, MenuState(connected=True, installs=[_install("Install on Frame")],
|
|
settings=True, selectable=True, android=True)),
|
|
("rift on PC", RIFT, MenuState(on_pc=True, plays=[_play("this PC")], programs=True, selectable=True,
|
|
installs=[_install("Reinstall on this PC")], new_version=True)),
|
|
("linux on Frame", LINUX, MenuState(connected=True, on_frame=True, plays=[_play()], installs=[_install()],
|
|
selectable=True, new_version=True)),
|
|
("linux job running", LINUX, MenuState(connected=True, on_frame=True, job=True, selectable=True,
|
|
new_version=True)),
|
|
("job running", QUEST, MenuState(connected=True, on_frame=True, job=True, settings=True, selectable=True,
|
|
android=True)),
|
|
]
|
|
|
|
|
|
def _items(entries):
|
|
return [e for e in entries if e is not None and not isinstance(e, Header)]
|
|
|
|
|
|
def _section_of(entries, label):
|
|
"""The header the item sits under (None = top section)."""
|
|
head = None
|
|
for e in entries:
|
|
if isinstance(e, Header):
|
|
head = e.label
|
|
elif e is not None and e[0] == label:
|
|
return head
|
|
raise AssertionError(f"{label} not in menu")
|
|
|
|
|
|
def test_library_menu_is_short():
|
|
for name, g, st in _states():
|
|
entries = C.menu_entries(menu_sections(g, st, _key))
|
|
labels = [e[0] for e in _items(entries)]
|
|
assert not any(isinstance(e, Header) for e in entries), name # no section headers
|
|
assert len(labels) <= 10, (name, labels) # 10: a Quest game on the Frame with "Update with a newer APK…"
|
|
assert all(e[1] for e in _items(entries)), name
|
|
assert "Open game page" in labels
|
|
if st.job:
|
|
assert labels == ["Show progress", "Cancel", "Open game page"]
|
|
continue
|
|
assert "Select" in labels[:3], (name, labels)
|
|
assert "More actions…" in labels
|
|
for gone in ("Find artwork…", "Analyze again", "Collect logs", "Screenshots", "Run launch test on Frame",
|
|
"Share working recipe…", "Type on Frame"):
|
|
assert gone not in labels, (name, gone)
|
|
# destructive items close the menu, after a divider
|
|
n = sum(1 for lbl in labels if lbl in DESTRUCTIVE)
|
|
assert labels[-n:] == [lbl for lbl in labels if lbl in DESTRUCTIVE] and labels[-1] == "Remove from library…"
|
|
first = next(i for i, e in enumerate(entries) if e is not None and e[0] in DESTRUCTIVE)
|
|
assert entries[first - 1] is None, name
|
|
assert entries[first - 2][0] == "More actions…", name
|
|
if st.plays:
|
|
assert labels[0].startswith("Play on")
|
|
if st.installs:
|
|
assert st.installs[0][0] in labels
|
|
assert ("Game settings…" in labels) == st.settings
|
|
assert "Rename…" in labels and labels.index("Rename…") < labels.index("More actions…")
|
|
assert ("Uninstall from Frame…" in labels) == st.on_frame
|
|
assert ("Uninstall from this PC…" in labels) == st.on_pc
|
|
|
|
|
|
def test_page_menu_is_the_full_one_without_its_buttons():
|
|
for name, g, st in _states():
|
|
st.quick = False
|
|
entries = menu_sections(g, st, _key)
|
|
labels = [e[0] for e in _items(entries)]
|
|
for gone in ("Select", "Open game page", "Show progress", "Cancel", "Play on Frame", "Reinstall on Frame",
|
|
"Run launch test on Frame", "Uninstall from Frame…", "More actions…"):
|
|
assert gone not in labels, (name, gone)
|
|
assert labels[-1] == "Remove from library…"
|
|
assert entries[-2] is None
|
|
for i, e in enumerate(entries): # every header has items under it
|
|
if isinstance(e, Header):
|
|
assert i + 1 < len(entries) and entries[i + 1] is not None and not isinstance(entries[i + 1], Header)
|
|
assert all(e[1] for e in _items(entries)), name
|
|
assert "Rename…" in labels, name # every kind of game (Quest, PC VR, Linux), also while a job runs
|
|
|
|
|
|
def test_sections_and_conditions():
|
|
_, g, st = _states()[0]
|
|
st.quick = False
|
|
entries = menu_sections(g, st, _key)
|
|
heads = [e.label for e in entries if isinstance(e, Header)]
|
|
assert heads == ["Saves", "Name and artwork", "Recipe", "Troubleshoot"]
|
|
assert _section_of(entries, "Analyze again") == "Troubleshoot"
|
|
assert _section_of(entries, "Update art on Frame") == "Name and artwork"
|
|
assert _section_of(entries, "Rename…") == "Name and artwork"
|
|
assert _section_of(entries, "Share working recipe…") == "Recipe"
|
|
linux_st = _states()[3][2]
|
|
linux_st.quick = False
|
|
linux = menu_sections(LINUX, linux_st, _key)
|
|
assert "Recipe" not in [e.label for e in linux if isinstance(e, Header)]
|
|
rift_st = _states()[2][2]
|
|
rift_st.quick = False
|
|
rift = [e[0] for e in _items(menu_sections(RIFT, rift_st, _key))]
|
|
assert {"Check game files", "Change program…"} <= set(rift)
|
|
assert "Analyze again" not in rift
|
|
# the reset icon isn't the Frame's Restart icon
|
|
reset = next(e for e in _items(entries) if e[0] == "Reset to suggested recipe…")
|
|
assert reset[1] != ft.Icons.RESTART_ALT_ROUNDED
|
|
|
|
|
|
def test_update_app_only_for_quest_games_on_the_frame():
|
|
"""Discussion #180: the new build without replacing game data changed on the Frame (e.g. a modded OBB)."""
|
|
def has(g, st):
|
|
st.quick = False
|
|
return "Update app only…" in [e[0] for e in _items(menu_sections(g, st, _key))]
|
|
states = {name: (g, st) for name, g, st in _states()}
|
|
assert has(*states["quest on Frame"])
|
|
assert _section_of(menu_sections(QUEST, states["quest on Frame"][1], _key), "Update app only…") is None
|
|
for name in ("quest not installed", "rift on PC", "linux on Frame", "job running"):
|
|
assert not has(*states[name]), name
|
|
assert "Update app only…" not in [e[0] for e in _items(menu_sections(QUEST, _states()[0][2], _key))] # quick
|
|
|
|
|
|
def test_menu_entries_trim_headers_and_dividers():
|
|
act = ("A", ft.Icons.ADD_ROUNDED, None)
|
|
raw = [None, Header("Empty"), None, Header("Kept"), act, None, None, Header("Last")]
|
|
assert C.menu_entries(raw) == [Header("Kept"), act]
|
|
items = C.menu_items([act, None, Header("H"), act])
|
|
assert items[0].height == items[3].height == T.px(32) and items[2].height == T.px(24)
|
|
assert items[1].content is None # divider
|
|
assert items[2].disabled and items[0].disabled # no handler: shown disabled
|
|
|
|
|
|
def test_process_menu():
|
|
proc = {"name": "x", "pid": 5, "game": "com.q"}
|
|
labels = [e[0] if e else "—" for e in process_menu(proc, _key)]
|
|
assert labels == ["Copy name", "Copy PID 5", "—", "End…", "Force quit…", "—", "End game…"]
|
|
locked = process_menu({**proc, "locked": True}, _key)
|
|
prot = next(e for e in locked if e and e[0] == "Protected by SteamOS")
|
|
assert prot[2] is None and "End…" not in [e[0] for e in locked if e]
|
|
group = [e[0] if e else "—" for e in process_menu({"kind": "group", "count": 3, "name": "g"}, _key)]
|
|
assert group == ["Show its processes", "Copy name", "—", "End all 3…", "Force quit all 3…"]
|
|
end = next(e for e in process_menu(proc, _key) if e and e[0] == "End…")
|
|
assert end[1] != ft.Icons.CLOSE_ROUNDED
|
|
|
|
|
|
def test_newer_apk_for_quest_games_in_both_menus():
|
|
"""GitHub #161: update a game with a newer APK from its menus (right-click and the page's "…")."""
|
|
for name, g, st in _states():
|
|
quick = [e[0] for e in _items(menu_sections(g, st, _key))]
|
|
st.quick = False
|
|
full = [e[0] for e in _items(menu_sections(g, st, _key))]
|
|
want = g is QUEST and not st.job
|
|
assert ("Update with a newer APK…" in quick) == want, name
|
|
assert ("Update with a newer APK…" in full) == want, name
|
|
|
|
|
|
def test_newer_version_for_linux_apps_and_pc_vr_games():
|
|
"""GitHub #161: Linux apps and PC VR games get "Update with a newer version…" (a file or folder), not the APK
|
|
entry, in both menus; never while a job runs."""
|
|
for name, g, st in _states():
|
|
quick = [e[0] for e in _items(menu_sections(g, st, _key))]
|
|
st.quick = False
|
|
full = [e[0] for e in _items(menu_sections(g, st, _key))]
|
|
want = g is not QUEST and not st.job
|
|
assert ("Update with a newer version…" in quick) == want, name
|
|
assert ("Update with a newer version…" in full) == want, name
|
|
if g is not QUEST:
|
|
assert "Update with a newer APK…" not in quick + full, name
|
|
|
|
|
|
def test_save_backups_in_the_full_menu_of_quest_games():
|
|
"""Discussion #352: back up / restore a Quest game's saves (installed on the Frame, no job running); the backups
|
|
folder for every Quest game; nothing for PC VR games and Linux apps; never in the short right-click menu."""
|
|
for name, g, st in _states():
|
|
quick = [e[0] for e in _items(menu_sections(g, st, _key))]
|
|
assert not {"Back up saves", "Restore saves…", "Open backups folder"} & set(quick), name
|
|
st.quick = False
|
|
entries = menu_sections(g, st, _key)
|
|
full = [e[0] for e in _items(entries)]
|
|
live = g is QUEST and st.saves and not st.job
|
|
assert ("Back up saves" in full) == live, name
|
|
assert ("Restore saves…" in full) == live, name
|
|
assert ("Open backups folder" in full) == (g is QUEST), name
|
|
if g is QUEST:
|
|
assert _section_of(entries, "Open backups folder") == "Saves"
|
|
|
|
|
|
def test_check_for_updates_for_games_from_install_links():
|
|
"""Games installed from an install link get "Check for updates" in the page's full menu (asks the link now);
|
|
never while a job runs, never for other games, never in the short right-click menu."""
|
|
for name, g, st in _states():
|
|
st.quick = False
|
|
assert "Check for updates" not in [e[0] for e in _items(menu_sections(g, st, _key))], name
|
|
st.link = True
|
|
full = menu_sections(g, st, _key)
|
|
assert ("Check for updates" in [e[0] for e in _items(full)]) == (not st.job), name
|
|
st.quick = True
|
|
assert "Check for updates" not in [e[0] for e in _items(menu_sections(g, st, _key))], name
|