Add Trash/Shares/Recent tabs, file sharing, Details sheet, and rebrand to Noo

Major additions since the initial rebuild:
- New tabs: Trash, Shares, Recent, configurable tab visibility/order/default
- Nextcloud sharing: search users/teams, internal link, email shares,
  public links, "others with access" - in its own Share bottom sheet
- Details bottom sheet (Info/Versions/Activity) with a persistent peek
  panel in the media viewer, reachable from selection, swipe, and the
  media viewer
- Files/Photos: tap-and-hold multi-select with a sticky selection toolbar,
  swipe actions (favorite/delete/share), sticky sort/filter controls,
  list/grid views, breadcrumb chips, whole-account Photos view
- Media viewer: pinch/double-tap zoom on images, predictive-back support,
  fixed PDF zoom-out floor
- Settings: Material You theming incl. AMOLED black, configurable bottom
  bar tabs, swipe actions, media seek bar style, and folder-listing cache
  policy (never/periodic/manual) to cut down on refetching
- Login flow moved to an in-app Chrome Custom Tab with retry-tolerant
  polling
- Replaced ad hoc widgets with native Material 3 equivalents (toggle
  IconButtons, SliverAppBar selection toolbar, SearchBar, drag handles)
- Rebrand: app renamed to Noo throughout, package id -> dev.ayushya.noo,
  new adaptive launcher icon

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 22:44:42 -04:00
co-authored by Claude Sonnet 5
parent f62d912ef7
commit d68cc43368
75 changed files with 6491 additions and 1213 deletions
+47
View File
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
/// Every destination the bottom nav bar can show. Order here is only the
/// fallback default — actual order/visibility/default-tab are user
/// configurable and persisted in [ServerProvider].
enum AppTab { files, photos, activity, trash, shares, recent }
/// At most this many tabs may be visible in the bottom nav bar at once —
/// the rest are reachable through the "more" dropdown instead.
const int maxVisibleTabs = 5;
extension AppTabInfo on AppTab {
String get label {
switch (this) {
case AppTab.files:
return 'Files';
case AppTab.photos:
return 'Photos';
case AppTab.activity:
return 'Activity';
case AppTab.trash:
return 'Trash';
case AppTab.shares:
return 'Shares';
case AppTab.recent:
return 'Recent';
}
}
IconData get icon {
switch (this) {
case AppTab.files:
return Icons.folder_rounded;
case AppTab.photos:
return Icons.photo_library_rounded;
case AppTab.activity:
return Icons.electric_bolt_rounded;
case AppTab.trash:
return Icons.delete_outline_rounded;
case AppTab.shares:
return Icons.groups_rounded;
case AppTab.recent:
return Symbols.search_activity_rounded;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
/// One entry in a file's version history (Nextcloud's DAV versions API).
class NextcloudFileVersion {
/// The timestamp segment used in the DAV versions/restore URLs, e.g.
/// `1699999999`.
final String versionLabel;
final DateTime timestamp;
final int size;
final bool isCurrent;
const NextcloudFileVersion({
required this.versionLabel,
required this.timestamp,
required this.size,
this.isCurrent = false,
});
}
+11
View File
@@ -18,6 +18,11 @@ class NextcloudItem {
final int? imageHeight;
final String? mountType;
/// Trash-bin items only: the folder path the item was deleted from, and
/// when it was deleted. Null for items fetched from a regular directory.
final String? originalLocation;
final DateTime? deletedAt;
const NextcloudItem({
required this.id,
required this.name,
@@ -33,6 +38,8 @@ class NextcloudItem {
this.imageWidth,
this.imageHeight,
this.mountType,
this.originalLocation,
this.deletedAt,
}) : dateCreated = dateCreated ?? lastModified;
bool get isFolder => type == NextcloudItemType.folder;
@@ -55,6 +62,8 @@ class NextcloudItem {
int? imageWidth,
int? imageHeight,
String? mountType,
String? originalLocation,
DateTime? deletedAt,
}) {
return NextcloudItem(
id: id ?? this.id,
@@ -71,6 +80,8 @@ class NextcloudItem {
imageWidth: imageWidth ?? this.imageWidth,
imageHeight: imageHeight ?? this.imageHeight,
mountType: mountType ?? this.mountType,
originalLocation: originalLocation ?? this.originalLocation,
deletedAt: deletedAt ?? this.deletedAt,
);
}
+42
View File
@@ -0,0 +1,42 @@
import 'nextcloud_item.dart';
enum ShareType { user, group, publicLink, email, federated, other }
/// A Nextcloud OCS share — either something the current user has shared
/// with someone else, or something someone else has shared with them
/// (distinguished by [sharedWithMe]).
class NextcloudShare {
final String id;
final String path;
final String name;
final NextcloudItemType itemType;
final ShareType shareType;
final String ownerDisplayName;
final String? sharedWithDisplayName;
final DateTime sharedAt;
final bool sharedWithMe;
/// Only set for public-link shares.
final String? url;
final int permissions;
final String? token;
final DateTime? expireDate;
const NextcloudShare({
required this.id,
required this.path,
required this.name,
required this.itemType,
required this.shareType,
required this.ownerDisplayName,
this.sharedWithDisplayName,
required this.sharedAt,
required this.sharedWithMe,
this.url,
this.permissions = 1,
this.token,
this.expireDate,
});
bool get isFolder => itemType == NextcloudItemType.folder;
}
+30
View File
@@ -0,0 +1,30 @@
/// One result from a sharee search (`/ocs/.../sharees`) — a user or team
/// (group) that a file/folder can be shared with.
enum ShareeType { user, group, remote }
class NextcloudSharee {
final String shareWith;
final String label;
final ShareeType type;
final String? subtitle;
const NextcloudSharee({
required this.shareWith,
required this.label,
required this.type,
this.subtitle,
});
/// The numeric OCS `shareType` used when creating a share with this
/// sharee (0 = user, 1 = group, 6 = federated/remote).
int get shareTypeValue {
switch (type) {
case ShareeType.user:
return 0;
case ShareeType.group:
return 1;
case ShareeType.remote:
return 6;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
/// One bulk action available for the current multi-selection (e.g. favorite,
/// download, delete), shown as an icon button in the sticky selection
/// toolbar each tab (Files, Photos, ...) renders inline in its own content.
class SelectionAction {
final IconData icon;
final String label;
final Color? color;
final VoidCallback onTap;
const SelectionAction({
required this.icon,
required this.label,
required this.onTap,
this.color,
});
}