Applies the same treatment to the Join tab: label and button centred on both
axes, button filling the width at 56dp tall, and the same asymmetric screen
padding. An empty state that looks different depending on which tab you are
on reads as a bug rather than as variety.
The two shared dimensions move into ui/Dimens.kt rather than being duplicated
per screen, so the tabs cannot drift apart later.
Verified on an API 37 emulator: both tabs now present an identical empty
state. 65 unit and 17 instrumented tests still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splits the converter screen into a fixed header and a body region that
behaves differently per state. The empty state centres its label and button
on both axes in whatever space is left; the working states keep scrolling,
since format pickers and progress can exceed the screen and centring content
that overflows would push it out of reach.
The primary button now fills the width and stands 56dp tall rather than the
Material default of 40dp, so it reads as the main affordance instead of a
small control adrift in an otherwise empty screen. Horizontal screen padding
drops to 16dp while vertical stays at 24dp, which is what lets a full-width
button sit close to both edges.
Both dimensions are named constants rather than inline numbers, because the
same treatment is applied to the primary action in every other state.
No theme changes were needed. Dark mode already tracked the device: the
Compose theme reads isSystemInDarkTheme() and the activity theme has a
values-night variant. Verified on an API 37 emulator by toggling
`cmd uimode night` and capturing both -- mean luminance 245/255 in light
against 18/255 in dark, with the button recolouring and status bar icons
inverting correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the second engine and the rules that choose between them, turning a
video transcoder into a converter.
The router encodes capability boundaries, not preferences. Media3 handles
what it genuinely can and FFmpeg takes the rest:
MKV, and any container Media3 cannot mux
MP3, which Android cannot encode at any API level -- a platform gap
rather than a Media3 limitation
GIF and PNG frame sequences, which have no Media3 muxer
VP9 and AV1 targets: Transformer.setVideoMimeType accepts only
H.263/H.264/H.265/MP4V, so the WebM muxer has no encoder behind it
inputs with no platform decoder, since Transformer ignores ExoPlayer's
bundled software decoders and the dav1d extension does not rescue it
the Best quality tier, because CRF and two-pass come from x264/x265 and
no Android hardware encoder exposes either
Rule order matters and is deliberate: specific reasons are checked before
general ones because the reason is shown to the user. "Android has no
encoder for this format" is actionable for MP3; "this container needs
FFmpeg" is not. A test caught the original ordering getting this backwards.
Hardware support is vendor-declared and, per the platform's own docs,
"cannot be tested for correctness", so the static rules are backed by a
dynamic fallback: a Media3 export that fails is retried on FFmpeg rather
than surfaced as a failed conversion.
Joining files chooses between a stream copy and a re-encode by inspecting
the inputs. The concat demuxer requires matching codec, resolution and
timebase, and does not reliably fail when they differ -- it can emit a file
whose later segments are garbled. Unknown properties count as a mismatch,
because two nulls are not evidence of agreement.
The UI surfaces the routing decision rather than hiding it, so a slow job
explains itself, and offers a per-job engine override.
Navigation is adaptive: a bottom bar on phones, a side rail on wider
screens. Not cosmetic -- from targetSdk 37 Android ignores screenOrientation
and resizableActivity on displays at least 600dp wide, with no opt-out, so
the app is resized whether or not it is ready.
62 unit tests cover the routing matrix, the FFmpeg argument builder and the
concat planner on the JVM, against fabricated device profiles so branches
like "this device cannot encode HEVC" are reachable without that hardware.
Device-level tests for the FFmpeg formats are written but not yet run; the
emulator in this environment will not stay up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>