From 4504ea230a6f9de625f130ace27103e95a448e0f Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:04:10 -0500 Subject: [PATCH] google_photos: the connect page says what to do and that it moves on by itself; -m stays up on micropython.exe Connect page: three plain steps (scan, pick, tap Done - this screen moves on by itself), a live 'Waiting for your picks... 0:07' line, and buttons that name what they do: Open on this PC (desktop only), New link, Back. Back after PICK now puts the old list back (engine.snapshot/put_back) instead of landing on an empty one. Portrait stacks; landscape puts the QR beside the text. The link is printed once per new session. A leftover session restored from prefs is no longer drawn or printed while the new one is being created - that was the 'second link' in #143; create_session runs once per start. #142: micropython.exe can't read its command line on pydevices <= 0.5.0, so the launcher also recognises -m from sys.argv on MicroPython. The launcher no longer clears PYDEVICES_WIDTH/HEIGHT when its own panel size is None, so screenshot.py --resolution works on this example. --- docs/screenshots/google_photos_connect.png | Bin 10300 -> 11931 bytes lib/examples/google_photos/README.md | 5 +- lib/examples/google_photos/google_photos.py | 34 ++- lib/examples/google_photos/gphotos_engine.py | 33 ++- lib/examples/google_photos/gphotos_lvgl.py | 253 +++++++++++++++---- lib/examples/google_photos/gphotos_sim.py | 5 +- tests/test_gphotos_engine.py | 21 ++ 7 files changed, 286 insertions(+), 65 deletions(-) diff --git a/docs/screenshots/google_photos_connect.png b/docs/screenshots/google_photos_connect.png index e56f58abbeb777dffc74d023ffb444d9203cf241..7df0b99dfcce7ea9990ee8f8f5b060e621c066ee 100644 GIT binary patch literal 11931 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 G`o91WtvA8| literal 10300 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 H + H // 8 + if landscape: + qr_side = min(H - 2 * gap, (W * 45) // 100, 320) + col_x = qr_side + 3 * pad + col_w = W - col_x - pad + else: + col_x = pad + col_w = W - 2 * pad + + # Buttons: one row when every label gets a fair width, else the first + # button on its own row and the rest sharing the row below it. n = len(labels) - w = W - 2 * pad - bw = (w - (n - 1) * gap) // n - y_btn = H - btn_h - for i, (text, role, cb) in enumerate(labels): - self._button(parent, text, pad + i * (bw + gap), y_btn, bw, btn_h, role, cb) + need = max(self._text_w(text, self.font_sm) for text, _r, _c in labels) + 4 * pad + rows = [labels] if n == 1 or (col_w - (n - 1) * gap) // n >= need else [labels[:1], labels[1:]] + y = H - btn_h + for row in reversed(rows): + bw = (col_w - (len(row) - 1) * gap) // len(row) + for i, (text, role, cb) in enumerate(row): + self._button(parent, text, col_x + i * (bw + gap), y, bw, btn_h, role, cb, self.font_sm) + y -= btn_h + gap + buttons_top = y + btn_h + gap + + tight = (H if landscape else W) < 260 + self._wait_short = col_w < 200 + steps = self._text(parent, col_w, _COL["text"], self.font_sm) + if tight: + steps.set_text("Scan with your phone, pick photos, tap Done. This screen moves on by itself.") + else: + steps.set_text( + "1. Scan this code with your phone.\n" + "2. Pick photos in Google Photos.\n" + "3. Tap Done. This screen moves on by itself." + ) + steps.set_pos(col_x, 0 if landscape else gap) - caption = lv.label(parent) - caption.set_width(w) - self._long_mode(caption, "WRAP") - caption.set_style_text_color(_hex(_COL["text"]), 0) - _apply_font(caption, self.font_sm) - ta = getattr(lv, "TEXT_ALIGN", None) - center = getattr(ta, "CENTER", None) if ta is not None else None - if center is not None: + wait = self._text(parent, col_w, _COL["accent"], self.font_sm) + self._long_mode(wait, "CLIP") + self._wait_lbl = wait + self._update_wait() + wait.set_pos(col_x, buttons_top - gap - self._line_h(self.font_sm)) + + if not landscape: try: - caption.set_style_text_align(center, 0) + steps.update_layout() + steps_h = int(steps.get_height()) except Exception: - pass - caption_h = max(2 * 16, 2 * (self.unit // 16)) - caption.align(lv.ALIGN.BOTTOM_MID, 0, -(btn_h + gap)) + steps_h = 4 * self._line_h(self.font_sm) + top = gap + steps_h + gap + bottom = buttons_top - 2 * gap - self._line_h(self.font_sm) + qr_side = min(W - 4 * pad, bottom - top, 320) + qr_x = (W - qr_side) // 2 + qr_y = top + max(0, (bottom - top - qr_side) // 2) + else: + qr_x = pad + qr_y = (H - qr_side) // 2 - qr_side = min(W - 4 * pad, y_btn - caption_h - 3 * gap, 320) if uri: - if self.engine.sim: - caption.set_text("Simulator: a real session shows a Google Photos link here.\nPicks arrive by themselves in a moment.") - else: - caption.set_text("Scan with your phone, pick photos in Google Photos, tap Done.") qr_cls = getattr(lv, "qrcode", None) if qr_cls is not None and qr_side >= 48: try: @@ -622,25 +672,82 @@ def _build_connect(self, parent): qr.set_quiet_zone(True) raw = uri.encode("utf-8") # bytes: buffer protocol on every binding qr.update(raw, len(raw)) - qr.align(lv.ALIGN.TOP_MID, 0, gap) + qr.set_pos(int(qr_x), int(qr_y)) except Exception as err: - self._link_label(parent, uri, w, "QR unavailable (%s)" % err) - else: - self._link_label(parent, uri, w, "") - if not self.engine.sim: - print("google_photos: open this link in Google Photos:\n " + uri) - if self._poll_timed_out: - self._set_status("picker timed out - tap RETRY") + self._link_label(parent, uri, col_w, "QR unavailable (%s)" % err) else: - self._set_status("waiting for your picks...") - else: - caption.set_text("Tap RETRY to start a picker session.") - if self.engine.last_error: - self._set_status(self.engine.last_error) - elif not self.engine.has_credentials(): - self._set_status("no tokens file - see README") - else: - self._set_status("starting picker session...") + self._link_label(parent, uri, col_w, "") + self._set_status( + "simulator: picks arrive by themselves" if self.engine.sim else "pick photos on your phone" + ) + elif self._creating: + self._set_status("pick photos on your phone") + elif self.engine.last_error: + self._set_status(self.engine.last_error) + elif not self.engine.has_credentials(): + self._set_status("no tokens file - see README") + + def _text(self, parent, w, color, font): + lbl = lv.label(parent) + lbl.set_width(int(w)) + self._long_mode(lbl, "WRAP") + lbl.set_style_text_color(_hex(color), 0) + _apply_font(lbl, font) + return lbl + + @staticmethod + def _text_w(text, font): + """Pixel width of one line of ``text`` (a rough estimate if LVGL can't say).""" + try: + lbl = lv.label(lv.screen_active()) + _apply_font(lbl, font) + lbl.set_text(text) + lbl.update_layout() + w = int(lbl.get_width()) + lbl.delete() + return w + except Exception: + return len(text) * 8 + + @staticmethod + def _line_h(font): + try: + return int(font.get_line_height()) + except Exception: + try: + return int(font.line_height) + except Exception: + return 16 + + def _wait_text(self): + """The live line under the QR: what the screen is doing right now.""" + if self._creating: + return "Getting a link from Google..." + if self._picked: + return "Got your picks - loading them..." + session = self.engine.session + if not session: + return "Tap New link to get a code." + if self._poll_timed_out: + return "This code timed out. Tap New link." + started = self._session_started + secs = ticks_diff(ticks_ms(), started) // 1000 if started is not None else 0 + dots = "." * (1 + secs % 3) + if self._wait_short: + return "Waiting%s %d:%02d" % (dots + " " * (3 - len(dots)), secs // 60, secs % 60) + return "Waiting for your picks%s %d:%02d" % (dots + " " * (3 - len(dots)), secs // 60, secs % 60) + + def _update_wait(self): + lbl = self._wait_lbl + if lbl is None: + return + text = self._wait_text() + if text != self._last_wait_text: + self._last_wait_text = text + try: + lbl.set_text(text) + except Exception: + pass def _link_label(self, parent, uri, w, note): lbl = lv.label(parent) @@ -657,24 +764,37 @@ def _open_browser(self): return try: _webbrowser.open(uri) - self._set_status("opened in your browser - pick, then tap Done") + self._set_status("opened in your browser - pick, then tap Done there") except Exception as err: self._set_status("browser: %s" % err) def _start_pick(self, rebuild=True): - """Create a fresh picker session in the background, then show its QR.""" + """Create a fresh picker session in the background, then show its QR. + + Until the new session arrives the page says so instead of showing a + leftover session's code (a restored one from the last run, or the + one New link is replacing). The link is printed once, here, when + Google hands out a new session -- not on every redraw of the page. + """ self._poll_timed_out = False self._session_started = None self._poll_at = None self._poll_busy = False + self._picked = False + self._creating = True self.slideshow = False self.list_offset = 0 engine = self.engine + if engine.items: + self._before_pick = engine.snapshot() def _work(): s = engine.create_session() + self._creating = False if s: self._session_started = ticks_ms() + if not engine.sim: + print("google_photos: new picker session; open this link in Google Photos:\n " + s.get("pickerUri", "")) else: self._pending_status = engine.last_error or "could not start a session" self._pending_connect = True @@ -683,16 +803,40 @@ def _work(): if rebuild: self._show_page("connect") + def _cancel_pick(self): + """Back from a PICK: put the old list back and drop the new session.""" + snap = self._before_pick + self._before_pick = None + if snap is None: + self._goto_list() + return + self._creating = True # no polling of the session we are abandoning + engine = self.engine + + def _work(): + abandoned = engine.put_back(snap) + self._creating = False + if abandoned is not None and abandoned is not snap[0]: + engine.delete_session(abandoned) + self._pending_back = True + + self._run_bg(_work) + def _poll_pick(self): engine = self.engine self._poll_busy = True def _work(): + if self._pending_back or self._creating: # Back was tapped meanwhile + self._poll_busy = False + return try: done = engine.poll_session() if done: + self._picked = True items = engine.list_items() if items is None: + self._picked = False self._pending_status = engine.last_error self._pending_connect = True else: @@ -1115,8 +1259,15 @@ def _pump(self, _timer=None): if text: self._set_status(text) + if self._pending_back: + self._pending_back = False + self._pending_connect = False + self._goto_list() + return + if self._pending_list: self._pending_list = False + self._before_pick = None self.list_offset = 0 self.index = 0 self._show_page("list") @@ -1149,16 +1300,16 @@ def _pump(self, _timer=None): if self.page == "connect": session = self.engine.session - if session and not self._poll_busy and not self._poll_timed_out: + if session and not self._creating and not self._poll_busy and not self._poll_timed_out: started = self._session_started timeout_ms = int(float(session.get("timeoutIn") or 1800.0) * 1000) if started is not None and ticks_diff(ticks_ms(), started) > timeout_ms: self._poll_timed_out = True - self._set_status("picker timed out - tap RETRY") else: interval = int(self.engine.poll_interval_s() * 1000) if self._poll_at is None or ticks_diff(ticks_ms(), self._poll_at) >= interval: self._poll_pick() + self._update_wait() if ( self.page == "view" diff --git a/lib/examples/google_photos/gphotos_sim.py b/lib/examples/google_photos/gphotos_sim.py index 7a25182f..993ee5a9 100644 --- a/lib/examples/google_photos/gphotos_sim.py +++ b/lib/examples/google_photos/gphotos_sim.py @@ -203,8 +203,9 @@ def poll_session(self): self.session["mediaItemsSet"] = done return done - def delete_session(self): - self.session = None + def delete_session(self, session=None): + if session is None: + self.session = None return True def list_items(self): diff --git a/tests/test_gphotos_engine.py b/tests/test_gphotos_engine.py index d90c87ee..08515bc5 100644 --- a/tests/test_gphotos_engine.py +++ b/tests/test_gphotos_engine.py @@ -362,6 +362,27 @@ def test_delete_session(self): self.assertIsNone(eng.session) self.assertEqual(self.transport.calls[-1][0], "DELETE") + def test_snapshot_put_back_after_abandoned_pick(self): + # PICK then Back: the old list comes back and the new session is dropped. + self.transport.route("DELETE", ge.PICKER_BASE + "/sessions/", lambda u, h, b: (200, {})) + eng = self.make() + eng.create_session() + eng.list_items() + old_session = eng.session + snap = eng.snapshot() + new_session = eng.create_session() + self.assertEqual(eng.items, []) + replaced = eng.put_back(snap) + self.assertIs(replaced, new_session) + self.assertIs(eng.session, old_session) + self.assertEqual(len(eng.items), 3) + self.assertTrue(eng.delete_session(replaced)) + self.assertIs(eng.session, old_session) # only the abandoned one went + self.assertEqual(self.transport.calls[-1][0], "DELETE") + other = self.make() + self.assertTrue(other.restore()) + self.assertEqual(len(other.items), 3) + class FakeSocketModule: """Enough of ``socket`` for ``_socket_request`` (plain HTTP only)."""