Skip to content

feat: add DevicePower and WindowFullscreen - #1

Merged
anilcancakir merged 5 commits into
mainfrom
feat/initial
Oct 29, 2026
Merged

anilcancakir merged 5 commits into
mainfrom
feat/initial

Conversation

@anilcancakir

Copy link
Copy Markdown
Member

What

The first release of magic_device, 0.0.1.

  • DevicePower: whether the device is on mains power (charging) and whether the viewer is away (idle). macOS and Windows read the system-wide last-input clock through flutter_inactive_timer; every other platform follows the app lifecycle. A television predicate (default () => false) answers charging true on a batteryless box that reports unknown. Every platform read is a constructor parameter.
  • WindowFullscreen: the macOS window's fullscreen state (active, target, transitioning) and toggle(), through one shared instance. Listeners hear every change, including the green button, Esc and the Window menu. available is true on macOS only.
  • macOS plugin MagicDevicePlugin on the magic_device/window channel. It finds the window holding the Flutter view on each call, so the host's Runner needs no edit. CocoaPods and Swift Package Manager are both supported.
  • BatteryState is exported from the barrel, so a host that supplies its own battery reader does not need battery_plus as a direct dependency.
  • example/: a macOS app that toggles the window and logs each notification in order.

Why

Watchools kept both of these as app code: the power gate for background work and the fullscreen bridge in its own MainFlutterWindow.swift. Neither is IPTV specific. Any Magic app that runs heavy work on a battery device, or that has a macOS window, needs the same two signals, so they move into a package.

Testing

  • flutter test: 27 passed across test/power and test/window
  • dart analyze: no issues
  • dart format --set-exit-if-changed .: 0 changed
  • flutter pub publish --dry-run: 0 warnings
  • cd example && flutter build macos --debug: built

@codecov

codecov Bot commented Oct 29, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@kodizm

kodizm Bot commented Oct 29, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

The package is well structured and well tested. One real bug: a toggle that AppKit silently ignores locks WindowFullscreen.toggle() for the rest of the session.

Major

lib/src/window/window_fullscreen.dart:120: correctness. toggle() sets _pressed = true, and only _apply (a native will/did notification) or a PlatformException clears it. The _pressed doc comment says AppKit ignores toggleFullScreen for an app that is not frontmost and sends nothing. In that case _pressed stays true, and every later toggle() on the shared instance returns early until the user changes fullscreen another way (green button, Esc or the menu). You could release the press after a timeout, or when the window reports no change (for example, re-read isFullScreen after a short delay). There is also no test for the ignored-call path.

Minor

lib/src/power/device_power.dart:113: correctness. On mobile, start() registers the lifecycle observer but leaves _idle false. If start() runs while the app is already backgrounded, idle reads false until the next lifecycle change. You could seed it from WidgetsBinding.instance.lifecycleState.

Tests

test/window covers press, animation, refusal, reset and dispose, and test/power covers the mains mapping, the television predicate, the desktop poll and the mobile lifecycle. Nothing covers a toggle the window silently ignores, or start() while already backgrounded.

CI

Lint & Test: success
codecov/patch: success (no base report to compare against)

I did not review the generated example scaffolding (Xcode project, xib, icons) beyond the manifest.

…tart

WindowFullscreen.toggle() held the press until a will/did notification, which never comes when AppKit ignores the call for an app that is not frontmost, so every later toggle on the shared reader returned early. An accepted call now ends the press after pressTimeout (1 second) by re-reading isFullScreen; a notification that lands first still ends it and cancels the timer.

DevicePower.start() on mobile only heard later lifecycle changes, so an app started in the background read idle as false. It now seeds idle from WidgetsBinding.instance.lifecycleState through the observer's own mapping.
@anilcancakir

Copy link
Copy Markdown
Member Author

Round 1 addressed in 6c01b08. Both findings reproduced with a failing test before the fix.

MAJOR lib/src/window/window_fullscreen.dart:120, press held forever when AppKit ignores the call: confirmed. Fixed in 6c01b08.

  • After an accepted toggleFullScreen call that produced no notification, toggle() now starts a timer of WindowFullscreen.pressTimeout (1 second, documented on the constant, lib/src/window/window_fullscreen.dart:56-63). When it fires, isFullScreen is re-read while the press still holds, so a press in that gap cannot start a second change, and the settled reading ends the press (_expirePress, lib/src/window/window_fullscreen.dart:139-148).
  • A will/did notification still ends the press first and cancels the timer (_apply), so the double-press guard during the animation is unchanged; dispose() cancels the timer too.
  • Tests in test/window/window_fullscreen_test.dart: an ignored toggle releases the press after pressTimeout (a second press at pressTimeout minus 1 ms is still ignored, one at pressTimeout goes through), a toggle that sent no notification re-reads the window, a notification releases the press before the timeout (no stray re-read after the timeout).

MINOR lib/src/power/device_power.dart:113, mobile idle not seeded at start: confirmed. Fixed in 6c01b08.

  • start() on mobile now reads WidgetsBinding.instance.lifecycleState and passes it through didChangeAppLifecycleState, so the mapping stays in one place (lib/src/power/device_power.dart:100-106).
  • Tests in test/power/device_power_test.dart: starting while backgrounded reads idle at once, starting in front reads not idle.

README and CHANGELOG describe both behaviours.

Gates on 6c01b08:

  • flutter test: 32 passed, 0 failed (red run before the fix: the two re-read and release tests and starting while backgrounded reads idle at once failed on the missing behaviour)
  • dart analyze: No issues found
  • flutter analyze --no-fatal-infos: No issues found
  • dart format --set-exit-if-changed .: 0 changed
  • flutter pub publish --dry-run: 0 warnings

@kodizm please re-review.

@kodizm

kodizm Bot commented Oct 29, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Both round-1 findings are fixed in 6c01b08, and I found nothing new; the change looks correct.

What changed since the last review: only 6c01b08. I read its changes to lib/, the tests and the docs.

  • lib/src/window/window_fullscreen.dart:134-148: an accepted toggle() now arms a pressTimeout timer. _apply cancels it on any will/did notification, and so does dispose(). _expirePress re-reads isFullScreen while _pressed still holds, so a second press during that gap is still ignored. The native isFullScreen handler (MagicDevicePlugin.swift:64-65) always answers a bool and never a FlutterError, so the unawaited re-read cannot fail and leave the press held. A re-read that finishes after dispose() exits early in _apply because _disposed is set.
  • lib/src/power/device_power.dart:100-106: start() on mobile now seeds idle from binding.lifecycleState through didChangeAppLifecycleState, so the mapping stays in one place. A null state, before the first frame, is skipped correctly.
  • The CHANGELOG and README describe both behaviours, as the CLAUDE.md post-change checklist asks.

Tests

test/window/window_fullscreen_test.dart covers the release at pressTimeout (including a press at the timeout minus 1 ms), the re-read, and a notification that arrives before the timeout. test/power/device_power_test.dart covers starting while backgrounded and starting in front.

CI

Lint & Test: success
codecov/patch: success (no base report to compare against)

I did not review the generated example scaffolding (Xcode project, xib, icons) beyond the manifest.

@anilcancakir
anilcancakir merged commit 721b690 into main Oct 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant