Settings > Appearance shows a preset control plus Blur and Opacity sliders
(new NooSlider) while frosted glass is on; values persist and feed
NooBottomBar.frostedBlur/frostedOpacity. Default matches the old fixed look.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Share Extension's session had no delegate, so a fast upload finished
unseen and the invalidated session was discarded before the app could hear of
it - no summary notification. Results are now handled by a shared delegate in
whichever process is alive, started/finished notifications replace each
other in place, and the app refreshes the folder the extension uploaded to.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SwiftUI folder picker in the extension (WebDAV PROPFIND), account shared via
a Keychain group, upload on a background session that outlives the extension
and is finished by the app. The inbox + notification hand-off remains as the
fallback (no account / 'choose later'). Shared code moves to ios/Shared.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ShareExtension target copies shared files into group.dev.ayushya.noo and
leaves a manifest; the app consumes it on launch/activation through the
existing share_intent channel into the destination picker. Extension is
embedded before Flutter's script phases; versions follow FLUTTER_BUILD_*.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Swift implementation of the upload_service/download_service channels:
background PUT/GET against the account's WebDAV root, one summary local
notification per batch, Downloads visible in the Files app. Pure helpers
covered by RunnerTests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Share/pick/sync calls and streams no longer throw MissingPluginException at
startup; upload/download/sync-now show 'X isn't available on this platform
yet'. Offline/sync mirror dir no longer uses the Android-only external dir.
Info.plist: Face ID and photo library usage strings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In light mode surfaceContainerLow is slightly darker than the OS surface bg
maps to, so the selection bar and other cards blended into the page. Use
surfaceContainerLowest (light) / surfaceContainer (dark) for NooColors.surface.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hidden: hide / only hidden / all + hidden. External: cloud only / only
external / all + external; with 'all' Files shows the external items in a
collapsible section. Old show-hidden bool prefs are still read as fallback.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Translucent fill over a backdrop blur, both platforms and both bar styles.
Frosted bars draw over an extended body, so the shell's extendBody and the
tab clearance now key off NooBottomBar.drawsBehindBody.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
watch outside build asserts in debug builds, so taps on rows silently did
nothing in debug (release strips the assert, which hid it on Android).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add PolyForm Shield license, contributing and security docs, PR and commit
templates; rewrite README; ignore local Claude and signing files.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The Release-* workflow now only builds Noo-<version>.aab and attaches it to
the Gitea release. The now-unused upload script is removed with it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The title was a Flexible and the search an Expanded, so both defaulted to
flex 1 and split the free space evenly. The title is now a non-flex child
capped at 40% of the width, leaving the search all remaining space.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- The avatar menu card's border was going missing wherever an opaque
row sat against it (every corner but the header's) - a Container
combining `border` with its own `clipBehavior` paints the border as
part of the outer decoration, then the clipped child on top right up
to the same boundary, with no gap for the border's stroke to show
through. Fixed with the standard pattern: no clipBehavior on the
bordered Container, a 1px-inset ClipRRect around the filled, clipped
content instead.
- NooTopBar.androidTitleTrailing (renamed from androidTitleReplacement)
now sits beside the tab title instead of replacing it - the title
keeps its natural width (Flexible, ellipsizing only if truly out of
room) with the inline search bar taking the remaining space to its
right, rather than the title disappearing whenever search moves into
the top bar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Every tab now wraps its CustomScrollView in SafeArea(top: true) - the
pinned sort/filter row was riding up underneath the status bar once
the floating top bar above it fully collapsed, since that bar's own
internal SafeArea only reserved the inset while it had some height
left to put it in. Reserving it outside the scrolling region entirely
fixes this regardless of the floating header's state. Caught live on
device and backed by a new regression test.
- The avatar menu now covers the top bar (title included) instead of
sitting below it - positioned off just the status-bar inset rather
than the top bar's full height on top of that.
- The avatar menu's card now carries a second, tighter contact shadow
alongside nooDialogShadow - that shadow alone was essentially
invisible on a small card over a dark theme's near-black background.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- New Settings > Appearance > "Navigation menu" toggle
(SettingsController.navMenuStyle, NooNavMenuStyle.drawer/avatarMenu):
the hamburger-opens-a-Drawer pattern stays the default, or the avatar
button opens a full-width dropdown (showAvatarMenu) instead, holding
the same hidden-tabs + Settings content the drawer does. Removes one
of the two top-corner targets the shell asks a thumb to reach, with
no permanent chrome added.
- NooTopBar gains androidTitleReplacement so Android's top bar can swap
its title for an inline search-field-styled launcher
(ShellSearchLauncher) when search isn't in the bottom bar, instead of
a bare search icon beside it.
- ShellAvatarButton's onTap/label are now overridable so AppTopBar can
repoint it per the new setting without this file depending on
avatar_menu.dart (which already depends on it for shared helpers).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both the bottom-clearance and the top-bar-sliver work independently
added this import to files_view.dart and favorites_view.dart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Give the floating bar and search satellite the app's one shadow
(nooDialogShadow) plus a smaller, tighter height on both platforms.
- Let every tab's scrollable list compute its own bottom clearance
(bottomBarClearance) instead of a flat 100px, so the floating bar no
longer overlaps the last row/card.
- Square the Android upload FAB off to a rounded-square radius when the
bottom bar is attached, keeping it fully round only when floating.
- Let a swipe on a file row trigger its action directly once dragged
past 1.8x the reveal width, instead of requiring a second tap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each regular tab (Files/Photos/Favorites/Shares/Recent/Activity/Trash)
now plants its own AppTopBar as the first sliver in its own
CustomScrollView, via a new topBarSliver() helper (tab_state_slivers.dart)
wrapping the framework's SliverFloatingHeader, instead of one shared
instance living in Scaffold.appBar.
This gives each tab's top bar Material's native floating-app-bar
behavior - tied to that tab's own ScrollController - scrolling away as
the list scrolls down and reappearing the moment the drag reverses,
not only once scrolled all the way back to the top, which a single
fixed Scaffold.appBar (or the "put it in a SliverToBoxAdapter" escape
hatch NooTopBar's doc comment already mentioned) can't do. It sits
above each tab's own pinned in-content sticky header (the sort/filter
row, or the selection bar that replaces it), which keeps working
unchanged underneath it.
main.dart builds a separate, correctly-labelled AppTopBar per tab
(rather than one for "whichever tab is selected") and passes it through
buildAppTabView's new optional topBar parameter; it's null on desktop
(which keeps its own NooToolbar) and while picking, matching the old
Scaffold.appBar: pickRequest == null guard.
SliverFloatingHeader sizes itself from the bar's own natural layout
(like SliverToBoxAdapter) rather than a fixed extent declared up front,
so NooTopBar's own internal SafeArea keeps handling the status-bar inset
correctly with no extra height math needed on this side.
Adds a widget test (test/widgets/tabs/tab_state_slivers_test.dart)
driving a raw gesture mid-drag to verify the reveal-follows-finger
behavior, since a single tester.drag() only pumps after the gesture -
including release - has already finished, which doesn't exercise the
live per-frame reveal this change depends on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On mobile, AccountView used to render all 9 Settings sections inline
in one long, individually-collapsible column (_MobileList). Replace
that with a native-style menu: the account card stays pinned at the
top, and every other section becomes a NooSettingsRow in a
NooGroupedList that pushes a dedicated screen (NooTopBar +
NooTopBarBack) holding just that section's content. This removes the
scroll-depth problem outright instead of working around it with
per-section collapsing, so SettingsSection no longer needs
NooGroupedList's collapsible mode on mobile. Desktop's 2-column grid
is unchanged - it already shows every section at once.
Audited every lib/widgets/settings/*.dart file for the reported
"description and controls are flipped" row-layout bug: every row goes
through NooSettingsRow directly, or - for the few hand-rolled rows
(_SavedAccountRow, _ThemeRow/_BottomBarStyleRow, _CacheIntervalRow) -
preserves its icon/description-then-control order (the stacked
control-below-label shape used by _ThemeRow and _CacheIntervalRow is
an intentional, spec'd variant, not a flip). Found no instance of the
bug; no fix was needed.
Updates DESIGN_SYSTEM.md's Settings recipe and styling.md's
NooGroupedList notes to describe the new menu-then-pushed-screen
pattern, and adds a widget test covering the category menu and the
push/pop navigation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Row wrapping the pill + search satellite used
crossAxisAlignment.stretch, but Scaffold.bottomNavigationBar gives its
child a loose (unbounded-max) height constraint - stretch on an
unbounded cross axis blows up to fill all available space in
profile/release builds (the debug assertion that would catch it is
stripped there), same class of bug files_controls_row.dart hit earlier
from a different cause. Both children already sized themselves
explicitly, so stretch was never actually needed - wrapping the Row in
a fixed-height SizedBox instead fixes it.
Added a regression test that reproduces this inside a real Scaffold
(the bare SizedBox host the other tests use gives a bounded height and
doesn't trigger it) - confirmed it fails without the fix (height 600,
the full test viewport) and passes with it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A single-page, static site under website/ - no build step, no
framework, just HTML/CSS/vanilla JS plus the two Google Fonts the app
already uses. Colors, radii, type scale and motion timing are copied
from lib/theme/design_tokens.dart so it reads as the same product as
the app, not a separate marketing skin.
Sections: hero with an exploded, parallaxing stack of the app's own UI
pieces (file row, photo grid, accent swatches, a toggle, the floating
bar) that drift apart on scroll and tilt toward the pointer; feature
highlights; a Customize section with two live interactive demos
(accent color, floating vs attached bottom bar); a privacy section
covering Login Flow v2; and a closing CTA.
GitHub/download links are placeholders (documented in website/README.md)
until real release URLs exist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New Settings > Appearance toggle moves Search into the bottom bar and
out of the top bar, so there's only one search entry point on screen.
Tapping it pushes SearchView same as before - it's never highlighted,
since it isn't a real destination.
- Attached: Search becomes the row's last item, styled like any other
tab but permanently idle.
- Floating: Search gets its own separate, always fully round satellite
circle beside the bar instead.
Enabling it costs one regular tab: SettingsController.maxVisibleTabs
is now a dynamic getter (defaultMaxVisibleTabs - 1 while the toggle is
on) instead of the old flat constant, re-enforced immediately if the
user already has all 5 slots filled - the same cleanup path used for
a fresh install or an app update that added a new tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Settings > Appearance > "Bottom bar" now switches between the
original edge-to-edge bar and a floating one - inset 16px from both
side edges and clear of the bottom safe area, 28px corners (the same
radius the sheet top/drawer edge use), a 1px line border standing in
for elevation instead of a shadow (product UI stays flat). Persisted
via NooBottomBarStyle, defaulting to the existing attached behavior.
On Android, floating's idle tabs drop their reserved label space -
with no label to leave room for, the icon centers in the whole button
and renders a touch bigger (27 vs 24px) instead of sitting high with a
gap held open under it; the active tab's icon/pill/label position is
unchanged. iOS's tab content is unchanged either way, only the
container.
main.dart sets Scaffold.extendBody while floating so the body scrolls
behind the bar's transparent margin instead of stopping short of it,
matching how the design canvas explored it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uploads run entirely in ShareUploadService.kt, an Android foreground
service with no channel back to Dart once started - by design, so
closing the app mid-upload doesn't interrupt it. That meant
FilesController had no way to know a batch had finished, so a newly
uploaded file only appeared after a manual pull-to-refresh.
Adds a one-shot completion signal instead of a full stream: the
service publishes into a new UploadEventBus (in-process pub/sub,
mirroring SyncStatusBus) once a batch finishes with at least one
success, MainActivity forwards it to Dart over a new
dev.ayushya.noo/upload_service/status EventChannel, and
FilesController (subscribed in its own constructor, same pattern
OfflineController already uses for sync completion) calls
refreshData() when the event's destination folder matches
currentFolderPath. Covers both upload entry points (share-to-Noo and
Files' own "+" -> Upload file), since they already share the same
ShareUploadView/UploadService path.
Move/Copy needed no fix - ItemOperations already calls
files.invalidateCache() + refreshData() on success.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Every tab's RefreshIndicator+CustomScrollView was missing an explicit
AlwaysScrollableScrollPhysics, so pull-to-refresh silently couldn't be
triggered once the list was short enough to fit the viewport (empty,
or a single item) - the platform default physics only allows the
overscroll drag RefreshIndicator listens for once content overflows.
- StickyHeaderDelegate (Files'/Photos'/Favorites' pinned controls row)
painted its background from Theme.of(context).colorScheme.surface -
Flutter's own Material 3 scheme, reseeded by the user's accent color
choice - instead of context.nooColors.bg like the rest of the screen,
showing as a stray colored cast behind the row.
- NooSegmentedControl gains onSurface: true wherever one sits inside a
sheet (Files'/Photos' sort and type-filter controls) - it defaulted to
the same surface fill as the sheet itself, so the control had no
visible boundary showing it as a grouped unit.
- Shares tab: removed the per-row "..." overflow menu on every scope
(With you/By you/Links); tapping a row now opens the full Share sheet
for that item instead, fetched via the new
FilesController.fetchItemAtPath (a share only carries enough metadata
for its own row, not what the sheet's header needs).
- fetchShares() now reuses _shareFromJson instead of its own thinner
inline parser, so url/token/permissions/expireDate are actually
populated - a public-link share could never show "Copy link" before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- SharesController's fetchShares() built NextcloudShare objects through
its own thin inline parser instead of _shareFromJson, so url/token/
permissions/expireDate were always null - a public link share could
never show "Copy link" in the Shares tab. Reuses _shareFromJson.
- Upload/Move/Copy's destination picker hid External storage in its
filter sheet (showStorageScope: false) unlike Files' own controls
row, despite the design doc already calling for full parity.
- Files' and Photos' type filter (All/Files/Folders, All/Photos/Videos)
now render as one consistent style in both filter sheets: every icon
always shown, label only on the selected segment
(NooSegmentedControl's new labelOnlySelected), replacing Files' old
checkmark list and Photos' own always-labelled track.
- Fixes Photos' type filter only visually updating after closing and
reopening the filter sheet - it lived on PhotosView's own State, so
Listenable.merge([photos, files]) never rebuilt the sheet when it
changed; a StatefulBuilder now gives it that trigger.
- Replaces Settings' trailing section jump rail with per-section
collapsible cards (NooGroupedList's new collapsible param, expanded
by default) - one less parallel way to navigate a long screen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
addAccountActivatedListener/addAccountReadyListener fired once, to
whichever listeners were registered at that instant. Most per-tab
controllers are lazy providers, only constructed (and so only
registering) whenever something first reads them - if that happened
after the one-shot event already fired (e.g. a cold-start connectivity
misdetection delaying a tab's construction until after login
verified), that controller's initial fetch never ran, leaving its list
permanently empty. Both listeners now call back immediately on
registration if the account is already in the state being subscribed
to, closing the race regardless of construction timing.
Documents the failure class and the guardrail for future controllers
in architecture.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- FilesControlsRow (and Photos' own controls row) wrapped its Row in a
SingleChildScrollView, which gives a child unbounded width - silently
breaking the trailing Spacer (a flex child needs bounded width) and
leaving the List/Grid toggle sitting right after the filter chip
instead of pinned to the row's right edge, "not enough controls to
fill the row" or not. Removed the scroll wrapper (two chips and an
icon toggle never need it) and the dead scroll-hint plumbing that
went with it in files_view.dart.
- Photos' All/Photos/Videos chips moved into the filter sheet as a
full-width segmented control (matching Settings' Theme control),
instead of their own row of chips competing for space with the sort
and filter chips.
- Settings' Theme row: the System/Light/Dark segmented control now
sits on its own row below the label instead of squeezed into a
trailing slot beside it, where three icon+label segments were too
cramped to read comfortably - same shape as the sort sheet's
Ascending/Descending control.
- NooBanner (Trash's "Empty trash") and NooSummaryCard (Offline's
"Sync now") both move their action onto its own row below the
description text instead of squeezed onto the same line, where a
longer caption had no room to share with it.
- SharesController now caches each scope (with you / by you, which
also covers links) after its first fetch instead of re-fetching on
every single switch, including switching straight back to a scope
already loaded seconds ago.
- Shares' per-row overflow menu jumped straight to the unshare
confirmation with no menu at all - a link share had no way to copy
its own URL short of opening the full Share sheet. It's a real menu
now: "Copy link" (link shares only) plus remove/unshare.
- Settings gained a trailing jump rail on mobile (one small icon per
section) so a specific section can be reached in one tap instead of
scrolling the whole list by hand; desktop's 2-column grid doesn't
need one.
- The media viewer's PageView now disables its own left/right swipe
while the current photo is zoomed in, so panning around it doesn't
also swipe to the next item.
- The PDF preview no longer sets minScale below 1.0: pdfx's
PdfViewPinch hard-codes an *infinite* pan boundary whenever minScale
< 1 (not an exposed parameter, so there's no way to override it),
which let a page be panned arbitrarily far off-screen without ever
zooming.