improved unknown file preview handling with probe failure fallbacks and switched to an explicit TabController in the share view to prevent build-time layout issues
This commit is contained in:
@@ -17,7 +17,19 @@ class QrShareView extends StatefulWidget {
|
|||||||
State<QrShareView> createState() => _QrShareViewState();
|
State<QrShareView> createState() => _QrShareViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QrShareViewState extends State<QrShareView> {
|
class _QrShareViewState extends State<QrShareView>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
// Owning the TabController explicitly (instead of DefaultTabController) is
|
||||||
|
// a workaround for a Flutter framework bug where TabBarView's
|
||||||
|
// didChangeDependencies issues a jumpToPage mid-build, whose scroll
|
||||||
|
// notification then calls setState on _TabStyle while the frame is still
|
||||||
|
// being built — only reproduces reliably with a bottomNavigationBar in the
|
||||||
|
// Scaffold underneath.
|
||||||
|
late final TabController _tabController = TabController(
|
||||||
|
length: 2,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
ScreenBrightness.instance.setApplicationScreenBrightness(1.0);
|
ScreenBrightness.instance.setApplicationScreenBrightness(1.0);
|
||||||
@@ -26,6 +38,7 @@ class _QrShareViewState extends State<QrShareView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
ScreenBrightness.instance.resetApplicationScreenBrightness();
|
ScreenBrightness.instance.resetApplicationScreenBrightness();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -45,53 +58,52 @@ class _QrShareViewState extends State<QrShareView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => DefaultTabController(
|
Widget build(BuildContext context) => Scaffold(
|
||||||
length: 2,
|
appBar: AppBar(
|
||||||
child: Scaffold(
|
title: const Text('Teile die App'),
|
||||||
appBar: AppBar(
|
bottom: TabBar(
|
||||||
title: const Text('Teile die App'),
|
controller: _tabController,
|
||||||
bottom: const TabBar(
|
tabs: const [
|
||||||
tabs: [
|
Tab(icon: Icon(Icons.android_outlined), text: 'Android'),
|
||||||
Tab(icon: Icon(Icons.android_outlined), text: 'Android'),
|
Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'),
|
||||||
Tab(icon: Icon(Icons.apple_outlined), text: 'iOS & iPadOS'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: const TabBarView(
|
|
||||||
children: [
|
|
||||||
AppSharePlatformView('Für Android', _androidUrl),
|
|
||||||
AppSharePlatformView('Für iOS & iPad', _iosUrl),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: SafeArea(
|
),
|
||||||
child: Material(
|
body: TabBarView(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
controller: _tabController,
|
||||||
child: Column(
|
children: const [
|
||||||
mainAxisSize: MainAxisSize.min,
|
AppSharePlatformView('Für Android', _androidUrl),
|
||||||
children: [
|
AppSharePlatformView('Für iOS & iPad', _iosUrl),
|
||||||
const Divider(height: 1),
|
],
|
||||||
Builder(
|
),
|
||||||
builder: (innerCtx) => ListTile(
|
bottomNavigationBar: SafeArea(
|
||||||
leading: const Icon(Icons.share_outlined),
|
child: Material(
|
||||||
title: const Text('Per Link teilen'),
|
color: Theme.of(context).colorScheme.surface,
|
||||||
trailing: const Icon(Icons.arrow_right),
|
child: Column(
|
||||||
onTap: () => _shareLink(innerCtx),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
|
const Divider(height: 1),
|
||||||
|
Builder(
|
||||||
|
builder: (innerCtx) => ListTile(
|
||||||
|
leading: const Icon(Icons.share_outlined),
|
||||||
|
title: const Text('Per Link teilen'),
|
||||||
|
trailing: const Icon(Icons.arrow_right),
|
||||||
|
onTap: () => _shareLink(innerCtx),
|
||||||
),
|
),
|
||||||
ListTile(
|
),
|
||||||
leading: const Icon(Icons.android_outlined),
|
ListTile(
|
||||||
title: const Text('Android-Link kopieren'),
|
leading: const Icon(Icons.android_outlined),
|
||||||
trailing: const Icon(Icons.copy),
|
title: const Text('Android-Link kopieren'),
|
||||||
onTap: () => copyToClipboard(context, _androidUrl),
|
trailing: const Icon(Icons.copy),
|
||||||
),
|
onTap: () => copyToClipboard(context, _androidUrl),
|
||||||
ListTile(
|
),
|
||||||
leading: const Icon(Icons.apple_outlined),
|
ListTile(
|
||||||
title: const Text('iOS-Link kopieren'),
|
leading: const Icon(Icons.apple_outlined),
|
||||||
trailing: const Icon(Icons.copy),
|
title: const Text('iOS-Link kopieren'),
|
||||||
onTap: () => copyToClipboard(context, _iosUrl),
|
trailing: const Icon(Icons.copy),
|
||||||
),
|
onTap: () => copyToClipboard(context, _iosUrl),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+47
-24
@@ -508,8 +508,6 @@ class _FileViewerState extends State<FileViewer> {
|
|||||||
Widget _buildUnknownPlaceholder() {
|
Widget _buildUnknownPlaceholder() {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final descriptors = _availableActions();
|
final descriptors = _availableActions();
|
||||||
final remote = widget.remoteFile;
|
|
||||||
final hasPreview = remote?.hasPreview == true;
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
children: [
|
children: [
|
||||||
@@ -517,16 +515,8 @@ class _FileViewerState extends State<FileViewer> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_UnknownPreviewHeader(remoteFile: remote),
|
_UnknownPreviewBlock(remoteFile: widget.remoteFile),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (!hasPreview) ...[
|
|
||||||
Text(
|
|
||||||
'Vorschau nicht verfügbar',
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
],
|
|
||||||
Text(
|
Text(
|
||||||
widget.path.split('/').last,
|
widget.path.split('/').last,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
@@ -607,24 +597,48 @@ class _TextPayload {
|
|||||||
const _TextPayload({required this.content, required this.truncated});
|
const _TextPayload({required this.content, required this.truncated});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Header for the "Vorschau nicht verfügbar" screen: tries to fetch the
|
/// Header block for the "Vorschau nicht verfügbar" screen.
|
||||||
/// Nextcloud thumbnail and shows it mid-sized if available, otherwise
|
///
|
||||||
/// falls back to the generic file icon.
|
/// Two visual modes — kept layout-equivalent so the screen looks identical
|
||||||
class _UnknownPreviewHeader extends StatelessWidget {
|
/// whether the server already said "no preview" or the probe failed late:
|
||||||
|
/// * **No preview available** (server said no, no remoteFile, or probe
|
||||||
|
/// errored): compact "file icon + 'Vorschau nicht verfügbar' text".
|
||||||
|
/// * **Preview rendering / loaded**: mid-sized thumbnail without text.
|
||||||
|
class _UnknownPreviewBlock extends StatefulWidget {
|
||||||
final RemoteFileRef? remoteFile;
|
final RemoteFileRef? remoteFile;
|
||||||
const _UnknownPreviewHeader({required this.remoteFile});
|
const _UnknownPreviewBlock({required this.remoteFile});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_UnknownPreviewBlock> createState() => _UnknownPreviewBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UnknownPreviewBlockState extends State<_UnknownPreviewBlock> {
|
||||||
static const double _previewSize = 180;
|
static const double _previewSize = 180;
|
||||||
|
bool _failed = false;
|
||||||
|
|
||||||
Widget _fallbackIcon() =>
|
Widget _compact(ThemeData theme) => Column(
|
||||||
const Icon(Icons.insert_drive_file_outlined, size: 60);
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.insert_drive_file_outlined, size: 60),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Vorschau nicht verfügbar',
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final remote = remoteFile;
|
final theme = Theme.of(context);
|
||||||
// Skip the probe outright when the server already told us there is no
|
final remote = widget.remoteFile;
|
||||||
// preview — saves an HTTP request that would 404 anyway.
|
final canProbe =
|
||||||
if (remote == null || remote.hasPreview == false) return _fallbackIcon();
|
remote != null &&
|
||||||
|
remote.hasPreview != false &&
|
||||||
|
remote.fileId != null &&
|
||||||
|
!_failed;
|
||||||
|
if (!canProbe) return _compact(theme);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _previewSize,
|
width: _previewSize,
|
||||||
height: _previewSize,
|
height: _previewSize,
|
||||||
@@ -633,10 +647,19 @@ class _UnknownPreviewHeader extends StatelessWidget {
|
|||||||
imageUrl: _ncPreviewUrl(remote, width: 360),
|
imageUrl: _ncPreviewUrl(remote, width: 360),
|
||||||
fadeInDuration: Duration.zero,
|
fadeInDuration: Duration.zero,
|
||||||
fadeOutDuration: Duration.zero,
|
fadeOutDuration: Duration.zero,
|
||||||
errorListener: (_) {},
|
// Late probe failure: re-render into the compact layout so the
|
||||||
|
// screen doesn't keep a 180×180 box around a tiny icon. Deferred
|
||||||
|
// to the next frame because setState during build is illegal.
|
||||||
|
errorListener: (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) setState(() => _failed = true);
|
||||||
|
});
|
||||||
|
},
|
||||||
placeholder: (_, _) =>
|
placeholder: (_, _) =>
|
||||||
const Center(child: AppProgressIndicator.large()),
|
const Center(child: AppProgressIndicator.large()),
|
||||||
errorWidget: (_, _, _) => Center(child: _fallbackIcon()),
|
// Briefly empty while the post-frame setState swaps layouts.
|
||||||
|
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||||
imageBuilder: (_, imageProvider) => ClipRRect(
|
imageBuilder: (_, imageProvider) => ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Image(
|
child: Image(
|
||||||
|
|||||||
Reference in New Issue
Block a user