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:
@@ -0,0 +1,160 @@
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/nextcloud_file_version.dart';
|
||||
import '../../models/nextcloud_item.dart';
|
||||
import '../../providers/server_provider.dart';
|
||||
import '../synced_header_scaffold.dart' show formatBytes;
|
||||
|
||||
/// File version history — a synthetic "Current version" row (from the
|
||||
/// item's own metadata) followed by whatever the DAV versions endpoint
|
||||
/// returns, each with Restore/Download actions.
|
||||
class DetailsVersionsTab extends StatefulWidget {
|
||||
final NextcloudItem item;
|
||||
|
||||
const DetailsVersionsTab({super.key, required this.item});
|
||||
|
||||
@override
|
||||
State<DetailsVersionsTab> createState() => _DetailsVersionsTabState();
|
||||
}
|
||||
|
||||
class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
||||
bool _requested = false;
|
||||
bool _isLoading = true;
|
||||
List<NextcloudFileVersion> _versions = [];
|
||||
|
||||
Future<void> _load() async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final result = await provider.fetchFileVersions(widget.item);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_versions = result;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _restore(NextcloudFileVersion version) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.restoreFileVersion(
|
||||
widget.item,
|
||||
version.versionLabel,
|
||||
);
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
success ? 'Restored version' : 'Could not restore version',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
if (success) _load();
|
||||
}
|
||||
|
||||
Future<void> _download(NextcloudFileVersion version) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempPath = p.join(
|
||||
tempDir.path,
|
||||
'${version.versionLabel}_${widget.item.name}',
|
||||
);
|
||||
final success = await provider.downloadVersion(
|
||||
widget.item,
|
||||
version.versionLabel,
|
||||
tempPath,
|
||||
);
|
||||
if (!success) throw Exception('download failed');
|
||||
final ext = p.extension(widget.item.name).replaceFirst('.', '');
|
||||
final baseName = p.basenameWithoutExtension(widget.item.name);
|
||||
await FileSaver.instance.saveFile(
|
||||
name: baseName,
|
||||
filePath: tempPath,
|
||||
ext: ext,
|
||||
);
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Version downloaded'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not download version'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_requested) {
|
||||
_requested = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _load());
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final items = [
|
||||
NextcloudFileVersion(
|
||||
versionLabel: 'current',
|
||||
timestamp: widget.item.lastModified,
|
||||
size: widget.item.size,
|
||||
isCurrent: true,
|
||||
),
|
||||
..._versions,
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final version = items[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
child: Icon(
|
||||
Icons.history_rounded,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
version.isCurrent
|
||||
? 'Current version'
|
||||
: DateFormat.yMMMd().add_jm().format(version.timestamp),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${DateFormat.yMMMd().format(version.timestamp)} • ${formatBytes(version.size)}',
|
||||
),
|
||||
trailing: version.isCurrent
|
||||
? null
|
||||
: PopupMenuButton<String>(
|
||||
onSelected: (value) => value == 'restore'
|
||||
? _restore(version)
|
||||
: _download(version),
|
||||
itemBuilder: (context) => const [
|
||||
PopupMenuItem(value: 'restore', child: Text('Restore')),
|
||||
PopupMenuItem(value: 'download', child: Text('Download')),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user