Files
noo/lib/widgets/details/details_versions_tab.dart
T
ayushyaandClaude Sonnet 5 5299ac5c2c Add share-to-upload, per-folder sort, and media viewer polish
- Accept Android's "Share to..." intent (ACTION_SEND/SEND_MULTIPLE) to
  upload a shared file, with a new destination-folder picker screen
- Unlink Files/Photos sort settings and make Files remember sort
  per folder (like per-folder view settings), instead of one shared
  global sort field for both tabs
- Media viewer: show the file name (marquee-scrolling if too long) in
  the top bar next to a resized back button, slide the top bar out of
  view instead of fading it, and unify icon color/contrast across the
  top bar, bottom action bar, and video transport controls
- Add a scrollbar thumb to every scrollable list in the app (all tab
  views via SyncedHeaderScaffold, search results, settings, the new
  upload folder picker, share sheet, and the Details sheet's tabs)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 11:05:54 -04:00

163 lines
5.0 KiB
Dart

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 Scrollbar(
child: 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')),
],
),
);
},
),
);
}
}