Replaces the Material 3 screens with the Noo design system: warm neutrals, one violet accent, pill controls, Schibsted Grotesk/ Instrument Sans, Lucide icons, no gradients/shadows. - New lib/widgets/noo/ component kit (core, lists, files, media, nav, overlays) plus lib/theme/design_tokens.dart for the color/type/space/ radius/motion tokens. - Rebuilt the app shell (top/bottom bars, drawer, desktop sidebar and toolbar), every tab (Files/Offline, Photos, Favorites, Recent, Activity, Trash, Shares), Settings, the lock and login screens, the media viewer, search, the details/share sheets, and the share-upload and move/copy destination pickers. - Added a design canvas (linked from DESIGN_SYSTEM.md) covering the screens the spec didn't already describe, with matching Android and iOS chrome; wrote up the approved recipes into DESIGN_SYSTEM.md §4. - Removed now-dead legacy widgets (media_grid_tile, swipeable_item, sync_status_badge, selectable_thumbnail) and updated architecture.md/styling.md/standards.md to describe the new structure and component/testing conventions. - Added widget tests for the noo/ component kit. This is a UI-only rework: no provider/model/service behavior changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
189 lines
5.5 KiB
Dart
189 lines
5.5 KiB
Dart
import 'package:flutter/widgets.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
import '../../../theme/design_tokens.dart';
|
|
|
|
/// File-type buckets from `DESIGN_SYSTEM.md` 1.2 (`KIND` in `noo-kit.js`).
|
|
/// Drives the tile tint + icon in `NooFileTile`, `NooFileRow` and
|
|
/// `NooFileTableRow`. [other] isn't in the spec table - it's the neutral
|
|
/// fallback (archive colors, plain `file` icon) for anything the table
|
|
/// doesn't cover (audio, binaries, unknown extensions) so those don't
|
|
/// masquerade as an archive or a document.
|
|
enum NooFileKind {
|
|
folder,
|
|
pdf,
|
|
doc,
|
|
sheet,
|
|
video,
|
|
image,
|
|
archive,
|
|
other;
|
|
|
|
/// Lucide icon for this kind.
|
|
IconData get icon {
|
|
switch (this) {
|
|
case NooFileKind.folder:
|
|
return LucideIcons.folder;
|
|
case NooFileKind.pdf:
|
|
case NooFileKind.doc:
|
|
return LucideIcons.fileText;
|
|
case NooFileKind.sheet:
|
|
return LucideIcons.sheet;
|
|
case NooFileKind.video:
|
|
return LucideIcons.film;
|
|
case NooFileKind.image:
|
|
return LucideIcons.image;
|
|
case NooFileKind.archive:
|
|
return LucideIcons.fileArchive;
|
|
case NooFileKind.other:
|
|
return LucideIcons.file;
|
|
}
|
|
}
|
|
|
|
/// Tile background - the soft half of the pair.
|
|
Color background(NooColors colors) {
|
|
switch (this) {
|
|
case NooFileKind.folder:
|
|
return colors.accentSoft;
|
|
case NooFileKind.pdf:
|
|
return colors.dangerSoft;
|
|
case NooFileKind.doc:
|
|
case NooFileKind.image:
|
|
return colors.infoSoft;
|
|
case NooFileKind.sheet:
|
|
return colors.successSoft;
|
|
case NooFileKind.video:
|
|
return colors.warningSoft;
|
|
case NooFileKind.archive:
|
|
case NooFileKind.other:
|
|
return colors.surface3;
|
|
}
|
|
}
|
|
|
|
/// Icon color - always the strong half of [background]'s pair.
|
|
Color foreground(NooColors colors) {
|
|
switch (this) {
|
|
case NooFileKind.folder:
|
|
return colors.accentText;
|
|
case NooFileKind.pdf:
|
|
return colors.danger;
|
|
case NooFileKind.doc:
|
|
case NooFileKind.image:
|
|
return colors.info;
|
|
case NooFileKind.sheet:
|
|
return colors.success;
|
|
case NooFileKind.video:
|
|
return colors.warning;
|
|
case NooFileKind.archive:
|
|
case NooFileKind.other:
|
|
return colors.fg2;
|
|
}
|
|
}
|
|
|
|
/// Derives a kind from whatever the caller has: [isDirectory] wins, then
|
|
/// [mimeType] (e.g. WebDAV `getcontenttype`), then [name]'s extension.
|
|
static NooFileKind from({
|
|
String? name,
|
|
String? mimeType,
|
|
bool isDirectory = false,
|
|
}) {
|
|
final mime = mimeType?.toLowerCase().trim();
|
|
if (isDirectory || mime == 'httpd/unix-directory') {
|
|
return NooFileKind.folder;
|
|
}
|
|
if (mime != null && mime.isNotEmpty) {
|
|
final byMime = _fromMime(mime);
|
|
if (byMime != null) return byMime;
|
|
}
|
|
if (name != null) {
|
|
final dot = name.lastIndexOf('.');
|
|
if (dot >= 0 && dot < name.length - 1) {
|
|
final byExt = _extensions[name.substring(dot + 1).toLowerCase()];
|
|
if (byExt != null) return byExt;
|
|
}
|
|
}
|
|
return NooFileKind.other;
|
|
}
|
|
|
|
static NooFileKind? _fromMime(String mime) {
|
|
if (mime == 'application/pdf') return NooFileKind.pdf;
|
|
if (mime.startsWith('image/')) return NooFileKind.image;
|
|
if (mime.startsWith('video/')) return NooFileKind.video;
|
|
if (mime.contains('spreadsheet') ||
|
|
mime.contains('ms-excel') ||
|
|
mime == 'text/csv') {
|
|
return NooFileKind.sheet;
|
|
}
|
|
if (mime.contains('zip') ||
|
|
mime.contains('compressed') ||
|
|
mime.contains('x-tar') ||
|
|
mime.contains('x-7z') ||
|
|
mime.contains('x-rar') ||
|
|
mime.contains('gzip')) {
|
|
return NooFileKind.archive;
|
|
}
|
|
if (mime.startsWith('text/') ||
|
|
mime.contains('wordprocessing') ||
|
|
mime.contains('msword') ||
|
|
mime.contains('opendocument.text') ||
|
|
mime.contains('presentation') ||
|
|
mime.contains('powerpoint') ||
|
|
mime == 'application/rtf') {
|
|
return NooFileKind.doc;
|
|
}
|
|
// Generic types (application/octet-stream etc.) fall through to the
|
|
// extension check.
|
|
return null;
|
|
}
|
|
|
|
static const _extensions = <String, NooFileKind>{
|
|
'pdf': NooFileKind.pdf,
|
|
// doc / text
|
|
'doc': NooFileKind.doc,
|
|
'docx': NooFileKind.doc,
|
|
'odt': NooFileKind.doc,
|
|
'rtf': NooFileKind.doc,
|
|
'txt': NooFileKind.doc,
|
|
'md': NooFileKind.doc,
|
|
'ppt': NooFileKind.doc,
|
|
'pptx': NooFileKind.doc,
|
|
'odp': NooFileKind.doc,
|
|
'pages': NooFileKind.doc,
|
|
// sheet
|
|
'xls': NooFileKind.sheet,
|
|
'xlsx': NooFileKind.sheet,
|
|
'ods': NooFileKind.sheet,
|
|
'csv': NooFileKind.sheet,
|
|
'numbers': NooFileKind.sheet,
|
|
// video
|
|
'mp4': NooFileKind.video,
|
|
'mov': NooFileKind.video,
|
|
'mkv': NooFileKind.video,
|
|
'webm': NooFileKind.video,
|
|
'avi': NooFileKind.video,
|
|
'm4v': NooFileKind.video,
|
|
'3gp': NooFileKind.video,
|
|
// image
|
|
'jpg': NooFileKind.image,
|
|
'jpeg': NooFileKind.image,
|
|
'png': NooFileKind.image,
|
|
'gif': NooFileKind.image,
|
|
'webp': NooFileKind.image,
|
|
'heic': NooFileKind.image,
|
|
'heif': NooFileKind.image,
|
|
'bmp': NooFileKind.image,
|
|
'svg': NooFileKind.image,
|
|
'tif': NooFileKind.image,
|
|
'tiff': NooFileKind.image,
|
|
'dng': NooFileKind.image,
|
|
// archive
|
|
'zip': NooFileKind.archive,
|
|
'tar': NooFileKind.archive,
|
|
'gz': NooFileKind.archive,
|
|
'tgz': NooFileKind.archive,
|
|
'bz2': NooFileKind.archive,
|
|
'xz': NooFileKind.archive,
|
|
'7z': NooFileKind.archive,
|
|
'rar': NooFileKind.archive,
|
|
};
|
|
}
|