16 Commits

Author SHA1 Message Date
agra
27cfc87def notifications + window: add Android native plugins
`ux/notifications/events` and `ux/window/events` only had macOS stream
handlers, so on Android/iOS the unconditional Dart subscription threw
MissingPluginException at startup (EventChannel reports activation
failures straight to FlutterError.onError, bypassing the `onError:`
callback).

- Gate each Dart event-channel subscription to platforms that register a
  native handler (`defaultTargetPlatform`), silencing iOS.
- `WindowPlugin`: report app foreground/background as host focus via
  `ProcessLifecycleOwner` ON_START/ON_STOP, so a backgrounded-but-alive
  process reports `focused = false`.
- `NotificationsPlugin`: local notifications (show/cancel by thread/all),
  POST_NOTIFICATIONS request on 13+, and tap routing back over the event
  channel — a tap that cold-starts the process is buffered until Dart
  subscribes.
- Regression tests for the subscription gate plus contract tests for the
  method/event payloads.
2026-05-30 13:39:49 +03:00
agra
2398c8ad35 insets: lazy _start capture matches frame-callback ts domain
`_start` was assigned from `currentSystemFrameTimeStamp` (raw,
since-boot) while the frame callback's `ts` parameter is
epoch-adjusted (since the binding's first observed frame). The two
live in different time domains; `dt = (ts - _start) / animDuration`
came out massively negative, `clamp(0, 1)` pinned `t = 0`, and
`_current` never moved past `_from` no matter how many frames fired.

The bug was masked on most flows because `_current` happened to
coincide with the post-animation target value already, but it
surfaced on Android EMUI 12 after dismissing a gallery from a
keyboard-open chat: the bottom inset stayed at 0 instead of
animating back to the nav-bar height, leaving the composer flush
with the screen edge and the system nav bar painted on top of it.
Opening the keyboard again forced an unrelated metric pump that
finally drove `_tick` to completion.

Fix: capture `_start` lazily from the first `_tick`'s `ts`, so both
sides of the subtraction live in the same epoch-adjusted domain.

Regression test asserts viewPadding advances past `_from` across
pumped frames; pre-fix it stayed at 0.
2026-05-29 08:40:19 +03:00
agra
de4925adf9 video_player + insets: native playback backend + animated viewPadding
- video_player: ExoPlayer (Android) / AVPlayer (iOS/macOS) backend with
  PixelBufferSink, method-channel adapter, Dart-side XVideoPlayer +
  testing fake.
- insets: XInsets singleton + XAnimatedInsets widget lerp the system
  viewPadding over 220ms so OS bar visibility toggles
  (immersiveSticky <-> edgeToEdge) slide bottom-/top-anchored UI into
  place instead of snapping by the nav-bar / status-bar height.
2026-05-23 15:57:15 +03:00
agra
d68a2978eb ux: bulk WIP — UxPlugin→XPlugin rename + new anim/core/navi/reactive packages
Catch-all commit for outstanding pre-existing local changes. Mixes
several themes that would normally be split:

- Rename: UxPlugin → XPlugin across iOS, macOS, Android registrants.
- New top-level packages under lib/src/: anim/ (animated values,
  panes, sheets, dock, measured), core/ (Emitter, ReactiveBuilder
  scaffolding, presenter/widget/value/dispose primitives), navi/
  (Screen/ScreenStack/Router/hero/transitions), reactive/.
- Edits across existing plugins (clipboard, crash, file, gallery,
  keyboard, scanner, sensor, url) to align with the new core.
- Test updates and CHANGELOG/README touches accompanying the above.
2026-05-21 08:58:07 +03:00
agra
a508aca2bb camera: requestAudioPermission — in-app prompt first, settings on permanent denial
Banner-tap entry point that shows the system mic prompt when the OS
will still surface one, and deep-links to Settings only on permanent
denial. Fixes the fresh-install trap where the mic entry isn't in the
Privacy pane until requestAccess has fired at least once.

Android tracks the first-asked state in SharedPreferences because
shouldShowRequestPermissionRationale returns false in two
observationally identical states (never asked vs permanently denied).
The existing initialize() request path writes the flag too, so a
banner tap after a record-then-deny correctly routes to Settings.

Refactored Android pendingPermission into PendingPermission(primary,
kind, cb) so audio-only requests check RECORD_AUDIO results instead
of always checking CAMERA.
2026-05-21 08:50:39 +03:00
agra
16f986ab37 camera tests: cover async previewSize updates via PreviewSizeChanged
New controller test exercises the Android path where create returns
with the iOS-style synchronous previewSize and the event then
revises it once CameraX's SurfaceRequest fires with the negotiated
resolution. Asserts that previewRotationQuarterTurns stays untouched
(events don't carry rotation; rotation is fixed per camera).

33/33 tests in test/camera/ now green.
2026-05-13 18:24:59 +03:00
agra
cc243b7b0a camera: previewRotationQuarterTurns + async previewSize event
Black-screen + extra-90°-rotation on Android both came from
AVFoundation vs CameraX behaving differently at the preview output:

  - AVFoundation: data-output connection's `videoOrientation`
    pre-rotates sample buffers. The Flutter Texture displays them
    upright; `device.activeFormat` reports the sensor-native size
    synchronously.
  - CameraX: the SurfaceProvider hands back a Surface; CameraX
    writes raw sensor frames into it. Rotation is a *transform hint*
    via Preview.setTargetRotation that consumers must apply
    themselves. And the final negotiated resolution isn't known
    until the first SurfaceRequest fires — which happens AFTER
    bindToLifecycle, AFTER lifecycle.start, async on the camera
    executor. So `create` was returning Size(0,0).

Surface extension to bridge the gap:

  - UxCameraValue.previewRotationQuarterTurns (int 0/1/2/3).
    iOS native always emits 0; Android native emits
    `(sensorRotationDegrees / 90) % 4` for the active camera.
    [UxCameraPreview] wraps the Texture in a RotatedBox by that many
    quarter-turns (applied *before* the front-cam mirror so the
    flip lives in screen space, not sensor space).

  - UxCameraPreviewSizeChanged event. Android emits this from
    PreviewSink.onResize whenever a SurfaceRequest carries a new
    resolution; the controller copies it into value.previewSize.
    First emission is what unblocks the camera_thumb's SizedBox
    from its initial 0x0 = "render nothing" state.

  - UxCameraBackend.setDescription's return changed from `Size` to
    `({Size previewSize, int previewRotationQuarterTurns})` so
    a lens swap can both update the rotation and signal that a new
    previewSizeChanged event is incoming.

iOS continues to send previewSize in the create result (the active
format is known synchronously); no previewSizeChanged emission is
needed there. The new field is set to 0 in both create and
setDescription results on iOS.
2026-05-13 17:44:45 +03:00
agra
6d6a871c53 camera: iOS implementation (Phase 2+3)
Native plugin owning AVCaptureSession + AVAssetWriter, mirroring
telegram-iOS's Camera module decomposition. Photo + video capture with
the writer-track transform set from a per-call orientation snapshot
(the three-way preview/capture/device split that camera_avfoundation
can't give us).

Modules:
  CameraPlugin           channels + per-handle instance map
  CameraInstance         session + texture + outputs + recorder
  CameraSession          AVCaptureSession + runtime-error/interrupt obs
  CaptureDevice          front/back discovery, per-device config
  PhotoOutput            AVCapturePhotoOutput, per-shot orientation
  VideoRecorder          AVAssetWriter, lazy inputs, pending-audio queue,
                         stop()/cancel() pair (matches telegram)
  PreviewSink            CVPixelBuffer → FlutterTexture
  AudioSession           setCategory + setActive(true) (only-widen)
  DeviceOrientationBridge

Recorder details:
  - Lazy videoInput/audioInput on first sample, sourceFormatHint:.
  - Audio settings derived from CMAudioFormatDescriptionGet*
    + recommendedAudioSettingsForAssetWriter, gated startWriting.
  - Stop sets stopSampleTime; next sample crossing it triggers
    maybeFinish → finishWriting. No watchdog — telegram pattern.
  - cancel() drops pending audio + cancelWriting + deletes file,
    used by CameraInstance.dispose when teardown finds in-flight
    recording.
  - Diagnostic stream → ux/camera/events {event: "diagnostic"}.

Dart surface extensions over Phase 1:
  - UxCameraValue.audioPermissionGranted
  - UxCameraController.refreshAudioPermission()
  - Static UxCameraController.audioPermissionGranted() /
    openSystemSettings()
  - UxCameraDiagnostic event variant
  - FakeUxCameraBackend.{emitDiagnostic, audioPermission,
    openSettingsCalls}

Tests: 32/32 in test/camera (controller + channel) green.
2026-05-13 16:56:49 +03:00
agra
45aac312a8 camera: Dart facade + backend + channel + preview + tests
Phase 1c+1d of the ux.camera plan (see ~/banlu/plans/ux_camera.md).

  lib/src/camera/camera.dart            — UxCameraController (ValueNotifier),
                                          UxCameraValue, UxCameraDescription,
                                          enums, UxCameraException,
                                          uxAvailableCameras().
  lib/src/camera/camera_backend.dart    — abstract UxCameraBackend +
                                          UxCameraCreateResult + sealed
                                          UxCameraEvent variants.
  lib/src/camera/camera_channel.dart    — MethodChannelUxCameraBackend over
                                          ux/camera + ux/camera/events. Per-
                                          handle event demux. Maps
                                          PlatformException → UxCameraException.
  lib/src/camera/camera_preview.dart    — UxCameraPreview: Texture-backed,
                                          Hero-flightable preview widget.

  lib/src/testing/fake_camera.dart      — FakeUxCameraBackend with per-method
                                          call lists + emitXxx event injection.
                                          Exported from package:ux/testing.dart.

  test/camera/camera_controller_test    — 16 tests covering init/dispose,
                                          orientation events, takePicture
                                          (explicit + UxSensor fallback),
                                          startVideoRecording / stop,
                                          flip, flash, lock/unlock,
                                          multi-instance, error propagation.
  test/camera/camera_channel_test       — 10 tests pinning the wire format
                                          for every method + PlatformException
                                          mapping.

Orientation snapshot for capture is computed Dart-side and passed in as an
explicit arg to takePicture / startVideoRecording (default falls back to
UxSensor.orientation at call time). Native never queries UIDevice itself
for the snapshot — Dart-side fakes drive orientation deterministically.

Native plugin code lands in Phase 2+; today every channel call throws
MissingPluginException at runtime, which is fine — the controller is only
mounted from the camera page once Phase 5 cuts over. The test backend
already exercises the full controller surface.
2026-05-13 14:27:52 +03:00
agra
1d00f16122 log: HttpSink + native crash capture (iOS/Android)
Three new pieces, all composable through the existing Log API
(`Log.configure(sink: ConsoleSink() + HttpSink(...))`) — no new
facade, no install side-effects.

  HttpSink (lib/src/log_http.dart)
    - Extends LogSink. Batches records and POSTs them as a JSON array
      to a configurable endpoint with bearer auth.
    - Defaults: batchSize=25, flushInterval=2s, queueCapacity=2000,
      initialBackoff=1s capped at maxBackoff=30s.
    - Drops oldest on queue overflow (single console warning).
    - Retries 5xx and network errors with exponential backoff; drops
      on 4xx with a single console warning.
    - Pluggable `HttpSender` typedef for tests; default uses
      dart:io.HttpClient.

  CrashPlugin (ios/Classes/CrashPlugin.swift,
               android/src/main/kotlin/.../CrashPlugin.kt)
    - Installs uncaught-exception handlers
      (NSSetUncaughtExceptionHandler / Thread.UncaughtExceptionHandler),
      chains to the prior handler so the platform's default kill path
      still runs.
    - Writes one JSON file per crash to <cacheDir>/ux_crashes/<uuid>.json.
      iOS captures NSException.name/reason/userInfo + call-stack symbols
      and return addresses. Android captures thread name, exception
      class, message, full stack (including cause chain).
    - Caps the directory at 50 files; drops oldest by mtime on overflow.
    - Exposes method channel `ux/crash` with drainPending / ackCrash /
      triggerTestCrash. Registered in UxPlugin on both platforms.

  UxCrash.drainAndReport (lib/src/crash.dart)
    - Pulls persisted crash records on boot, re-emits each via Log.f
      (tag `ux.crash`) so they flow out through whatever sink chain
      the app installed, then acks each id.
    - Tolerates MissingPluginException silently; PlatformException is
      logged as a single warn without throwing.

Tests:
  - log_http_test.dart: payload shape, batching, retry doubling on 5xx,
    drop on 4xx, queue overflow ordering, non-encodable field
    stringification, real loopback HTTP round-trip with the default
    sender.
  - log_http_e2e_test.dart: opt-in real-server round-trip gated by
    --dart-define=E2E_LOG_ENDPOINT/E2E_LOG_TOKEN.
  - crash_test.dart: drain + re-emit + ack across iOS and Android
    shapes, MissingPluginException tolerance, PlatformException
    warn-not-throw.
2026-05-11 12:07:26 +03:00
agra
3eba30358c ... 2026-05-10 16:37:23 +03:00
agra
6f73b53c5e 0.8.0: pretty logger (Log) + crash capture
- Log: static entry + scoped Loggers (Log.tag), six levels, lazy messages,
  structured fields, ANSI ConsoleSink, DeveloperSink, MemorySink, NoopSink.
  Sinks compose via +; throwing sinks are isolated.
- Log.configure wires FlutterError/PlatformDispatcher/isolate errors through
  Log.e by default; log-then-rethrow deduped via Expando.
- UxKeyboard: migrate kDebugMode print() to Log.tag('KB').d lazily.
- Depend on package:clock for testable timestamps.
2026-04-24 15:07:06 +03:00
agra
ae21d81eab automap 2026-04-23 13:21:44 +03:00
agra
785c6d3c31 pub.dev score: license, dartdoc, analysis, tests
- Add MIT license
- Add dartdoc to all public APIs (UxKeyboard, BendBox, Bezier, Json)
- Add analysis_options.yaml with flutter_lints and public_member_api_docs
- Add repository, issue_tracker, topics to pubspec.yaml
- Expand package description
- Fix BendBox constructor (const, super.key)
- Remove unused import in keyboard.dart
- Replace boilerplate tests with UxKeyboard smoke tests
- Move ux dependency to example's dependencies (not dev_dependencies)
2026-04-16 18:51:22 +03:00
alex
94ee74840a zzz 2020-05-04 10:58:19 +01:00
agrapine
810d060d44 red pill 2019-08-07 15:37:44 +01:00