- Offline tab is now FilesView(offline: true) over a FolderBrowser interface (FilesController / OfflineController) sharing controls, tiles, thumbnails - Synced files follow the Files Cache rule (default: refresh every 15 min): per-account WorkManager jobs, in-app timer, resume and pull triggers; root-etag shortcut skips full walks when nothing changed - Single sync at a time (shared lock); logging out stops that account's sync - Sync safety: PROPFIND failures skip the path instead of deleting local files - Mirror empty folders and remove deleted ones; drop synced paths deleted on the server; missing local files are re-downloaded - Notifications: silent per-account sync notifications, "Background sync notifications" setting, audible upload/download completion - Files refresh in place (no spinner flash); retry after network returns - Fix sync badges not updating (shared native status stream), "Sync off" on launch (eager providers), Files Cache section moved under Device Sync Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.9 KiB
Styling
Theme
All theming goes through AppTheme
(AppTheme.light/AppTheme.dark) — don't set colors/fonts ad hoc in
widgets. Key points:
- Material 3, seed-color based.
AppTheme.seedColorsis the picker list users choose from;defaultNextcloudBlue(#0082C9) is the fallback. - Dynamic color (Android 12+ Material You / desktop accent color) is
supported via
package:dynamic_color'sDynamicColorBuilderwrapping the whole app inmain.dart. When available anduseDynamicColoris on, the OS-providedColorSchemewins over the seed color — always thread bothdynamicSchemeanduseDynamicColorthrough when adding a theme knob. - Font: Inter via
google_fonts, applied throughGoogleFonts.interTextTheme(...). In tests, setGoogleFonts.config.allowRuntimeFetching = falseinsetUpAll— without it, the font-fetch call to Google's CDN can stallpumpAndSettleindefinitely (seestandards.md). - Cards: flat (
elevation: 0), 20px rounded corners,surfaceContainerLow. - App bars: flat, not centered,
surfacebackground. - Two Flutter defaults are deliberately overridden app-wide rather than
per-widget, each with a comment explaining why in
app_theme.dart: Android predictive-back page transitions, and the non-2023SliderTheme. Follow that pattern (a themed default + a comment) instead of overriding per-instance if you need the same behavior elsewhere. - Dark theme supports an
amoledflag that flattens every surface tone to pure black — extendcolorScheme.copyWith(...)there if a new surface role needs the same treatment, don't hardcodeColors.blackat call sites. - Scrollbars are deliberately not shown — every scrollable list in the
app is a plain
ListView/CustomScrollViewwith noScrollbarwrapper and noscrollbarThemeoverride (tried once, explicitly reverted). Don't reintroduce one without being asked.
Reusable chrome
-
MediaGridTile(lib/widgets/media_grid_tile.dart): the full-bleed image/video grid card with name/size scrim, shared by Files (server preview) and Offline (localFileImage, images only - no video frame-extraction plugin, so offline videos use the plain icon card).ItemThumbnailtakes an optionallocalFilefor the same offline-image case in list tiles. -
FilesControlsRow(lib/widgets/files_controls_row.dart): the sort/hidden/scope/type-filter/view-mode row, shared by the Files and Offline tabs (showStorageScope: falsefor Offline). -
getItemIcon/getIconColor/ItemThumbnail— the icon/color/thumbnail treatment for a file or folder, shared by any screen that listsNextcloudItems the way the Files tab does (currentlyfiles_view.dartandshare_upload_view.dart's destination picker). Extend this rather than re-deriving per-type icons/colors at a new call site. -
DetailsHeader— the icon-box/name/meta row every per-item bottom sheet opens on (DetailsSheet, the media viewer's collapsed peek state, andShareSheet). Takes apaddingoverride for callers whose own scroll view already applies horizontal insets (ShareSheet'sListView), so it doesn't get doubled up. Reuse this instead of a bare titleTextfor any new per-item sheet - a plain title reads as under-designed next to the other sheets (a real instance:ShareSheetused to be just that). -
FrostedGlassContainer— the blurred/translucent pill background shared by all floating chrome (bottom nav bar, media-viewer top/bottom bars and video transport controls). Reuse this for any new floating overlay instead of building a new blur/shadow combo. -
FloatingBottomNavBar— the main tab bar; opacity/blur are user-adjustable settings (SettingsController.bottomBarOpacity/bottomBarBlur), not constants — pull new adjustable visual knobs fromSettingsControllerthe same way rather than hardcoding them. -
SyncedHeaderScaffold— the pull-to-syncCustomScrollViewheader shared by all 8 tabs (seearchitecture.md); also where the pull-to-refresh gesture thresholds and the classic Material refresh spinner live. Its persistent compact chip (icon + "Sync off"/"Syncing…"/"Synced"/"Sync issue") reflects device-sync status (SyncStatusController.syncHeaderStatus), not the WebDAV-refresh loading state the pull gesture itself triggers - that has its own, separate floating spinner bubble, so nothing was lost by handing the persistent text/icon over. The expanded panel's headline is a separate, more detailed string (_syncSummaryinsynced_header_scaffold.dart) - counts of what's actually configured to sync ("2 folders & 1 file synced") rather than just repeating the chip's generic label. Deliberately doesn't add up the individual files inside a synced folder ("1 folder synced", not "1 folder & 4 items synced") - once a folder's synced, its file count is an implementation detail, not something the user picked. -
SyncStatusBadge— the small corner badge over a thumbnail showing per-item device-sync status (cloud_done/sync, nothing for not-synced/conflict); used in Files' list and grid tiles today. Reuse this rather than a new ad hoc badge if another view starts showing sync status per item. -
SeekBarPainter/SeekBarPreview— the fourMediaProgressBarStylepresets (Default/Wavy/Slim/Squiggly) for the video player's seek bar, plus a perpetually-animatedSeekBarPreviewwrapper used by the Settings style picker so every preview always matches the real widget exactly (same painter, just fed demoprogress/phasevalues). Add new seek-bar presets here, not by forking the painter. -
Chrome inside the media viewer (
file_viewer_screen.dart— the top bar's back button + filename, the bottom action bar, the video transport controls) all share one small hand-rolled icon-button pattern (_ActionIconButton:InkWell+Iconat a fixed 22px, colored fromcolorScheme.onSurfaceunless overridden) rather than plainIconButtons — match this instead of adding a bareIconButtonin that screen, since a default-styled one visibly stands out against the rest (this was a real bug: an unstyled back button read as "too large" next to everything else). -
A title/label that might overflow a fixed-width chrome bar (e.g. the media viewer's filename) should use
_MarqueeTitle-style logic — measure withTextPainterfirst and only switch to a scrollingMarqueewhen the text actually doesn't fit, rather than marqueeing unconditionally. -
Icons: prefer
Icons.*_rounded(matches the rest of the app) ormaterial_symbols_iconswhere Material Symbols are already in use; avoid mixing in the sharp/outlined default set.
Conventions
- No hardcoded colors for anything themeable — pull from
Theme.of(context).colorScheme, notColors.blueetc. (per-file-type icon tinting infiles_view.dart's_getIconColoris the one deliberate exception, since those colors are content-identity cues, not theme). - Use
colorScheme.surfaceContainer*/onSurfaceVarianttokens for elevation/secondary text rather than manual opacity on black/white. - Anywhere the app shows its own icon in-app (splash, lock screen,
login screen) uses
assets/icon/app_icon_monochrome.png— a plain white silhouette on transparent, tinted viaColorFiltered(colorFilter: ColorFilter.mode(colorScheme.onSurface, BlendMode.srcIn), ...)so it reads correctly in both light and dark mode. Never the full-colorapp_icon.png/adaptive-icon assets for in-app UI — those are for the launcher icon only (flutter_launcher_iconsinpubspec.yaml).