♻️ Unified the track tile widget
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:groovybox/data/db.dart';
|
||||
import 'package:groovybox/data/playlist_repository.dart';
|
||||
import 'package:groovybox/providers/audio_provider.dart';
|
||||
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||
import 'package:groovybox/ui/widgets/universal_image.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class AlbumDetailScreen extends HookConsumerWidget {
|
||||
final AlbumData album;
|
||||
@@ -24,7 +26,7 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: Text(album.album),
|
||||
background: album.artUri != null
|
||||
? Image.file(File(album.artUri!), fit: BoxFit.cover)
|
||||
? UniversalImage(uri: album.artUri!, fit: BoxFit.cover)
|
||||
: Container(
|
||||
color: Colors.grey[800],
|
||||
child: const Icon(
|
||||
@@ -85,17 +87,17 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
||||
|
||||
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
||||
final track = tracks[index];
|
||||
return ListTile(
|
||||
return TrackTile(
|
||||
track: track,
|
||||
leading: Text(
|
||||
'${index + 1}',
|
||||
'${index + 1}'.padLeft(2, '0'),
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
title: Text(track.title),
|
||||
subtitle: Text(_formatDuration(track.duration)),
|
||||
).padding(right: 16),
|
||||
showTrailingIcon: false,
|
||||
onTap: () {
|
||||
_playAlbum(ref, tracks, initialIndex: index);
|
||||
},
|
||||
trailing: const Icon(Icons.play_circle_outline),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,12 +105,4 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
||||
final audioHandler = ref.read(audioHandlerProvider);
|
||||
audioHandler.playTracks(tracks, initialIndex: initialIndex);
|
||||
}
|
||||
|
||||
String _formatDuration(int? durationMs) {
|
||||
if (durationMs == null) return '--:--';
|
||||
final d = Duration(milliseconds: durationMs);
|
||||
final minutes = d.inMinutes;
|
||||
final seconds = d.inSeconds % 60;
|
||||
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -9,12 +8,11 @@ import 'package:groovybox/data/playlist_repository.dart';
|
||||
import 'package:groovybox/data/track_repository.dart';
|
||||
import 'package:groovybox/logic/lyrics_parser.dart';
|
||||
import 'package:groovybox/providers/audio_provider.dart';
|
||||
import 'package:groovybox/providers/remote_provider.dart';
|
||||
import 'package:groovybox/providers/watch_folder_provider.dart';
|
||||
import 'package:groovybox/ui/screens/settings_screen.dart';
|
||||
import 'package:groovybox/ui/tabs/albums_tab.dart';
|
||||
import 'package:groovybox/ui/tabs/playlists_tab.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -483,37 +481,19 @@ class LibraryScreen extends HookConsumerWidget {
|
||||
SnackBar(content: Text('Deleted "${track.title}"')),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: _buildAlbumArt(track, ref),
|
||||
),
|
||||
title: Text(
|
||||
track.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${track.artist ?? 'Unknown Artist'} • ${_formatDuration(track.duration)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: isSelectionMode
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
_showTrackOptions(context, ref, track);
|
||||
},
|
||||
),
|
||||
child: TrackTile(
|
||||
track: track,
|
||||
showTrailingIcon: true,
|
||||
onTrailingPressed: () =>
|
||||
_showTrackOptions(context, ref, track),
|
||||
onTap: () {
|
||||
final audio = ref.read(audioHandlerProvider);
|
||||
audio.playTrack(track);
|
||||
},
|
||||
onLongPress: () {
|
||||
// Enter selection mode
|
||||
toggleSelection(track.id);
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -853,81 +833,6 @@ class LibraryScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlbumArt(Track track, WidgetRef ref) {
|
||||
// Check if this is a remote track
|
||||
final urlResolver = ref.watch(remoteUrlResolverProvider);
|
||||
final isRemote = urlResolver.isProtocolUrl(track.path);
|
||||
|
||||
if (isRemote && track.artUri != null) {
|
||||
// For remote tracks, fetch album art directly
|
||||
return FutureBuilder<Uint8List?>(
|
||||
future: _fetchRemoteAlbumArt(track.artUri!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Container(
|
||||
color: Colors.grey[800],
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white54),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasData && snapshot.data != null) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: DecorationImage(
|
||||
image: MemoryImage(snapshot.data!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Colors.grey[800],
|
||||
child: const Icon(Icons.music_note, color: Colors.white54),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// For local tracks, use existing logic
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: track.artUri != null
|
||||
? DecorationImage(
|
||||
image: FileImage(File(track.artUri!)),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: track.artUri == null
|
||||
? const Icon(Icons.music_note, color: Colors.white54)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> _fetchRemoteAlbumArt(String url) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
return response.bodyBytes;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _formatDuration(int? durationMs) {
|
||||
if (durationMs == null) return '--:--';
|
||||
final d = Duration(milliseconds: durationMs);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:groovybox/data/db.dart';
|
||||
import 'package:groovybox/data/playlist_repository.dart';
|
||||
import 'package:groovybox/providers/audio_provider.dart';
|
||||
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class PlaylistDetailScreen extends HookConsumerWidget {
|
||||
@@ -93,17 +94,17 @@ class PlaylistDetailScreen extends HookConsumerWidget {
|
||||
|
||||
Widget _buildTrackTile(WidgetRef ref, List<Track> tracks, int index) {
|
||||
final track = tracks[index];
|
||||
return ListTile(
|
||||
return TrackTile(
|
||||
track: track,
|
||||
leading: Text(
|
||||
'${index + 1}',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
title: Text(track.title),
|
||||
subtitle: Text(track.artist ?? 'Unknown Artist'),
|
||||
trailing: Text(_formatDuration(track.duration)),
|
||||
showTrailingIcon: false,
|
||||
onTap: () {
|
||||
_playPlaylist(ref, tracks, initialIndex: index);
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,12 +116,4 @@ class PlaylistDetailScreen extends HookConsumerWidget {
|
||||
final audioHandler = ref.read(audioHandlerProvider);
|
||||
audioHandler.playTracks(tracks, initialIndex: initialIndex);
|
||||
}
|
||||
|
||||
String _formatDuration(int? durationMs) {
|
||||
if (durationMs == null) return '--:--';
|
||||
final d = Duration(milliseconds: durationMs);
|
||||
final minutes = d.inMinutes;
|
||||
final seconds = d.inSeconds % 60;
|
||||
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:groovybox/data/track_repository.dart';
|
||||
import 'package:groovybox/providers/settings_provider.dart';
|
||||
import 'package:groovybox/providers/watch_folder_provider.dart';
|
||||
import 'package:groovybox/providers/remote_provider.dart';
|
||||
@@ -305,6 +306,42 @@ class SettingsScreen extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Database Management Section
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Database Management',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).padding(horizontal: 16, bottom: 8, top: 16),
|
||||
const Text(
|
||||
'Manage your music database and cached files.',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
ListTile(
|
||||
title: const Text('Reset Track Database'),
|
||||
subtitle: const Text(
|
||||
'Remove all tracks from database and delete cached files. This action cannot be undone.',
|
||||
),
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () => _resetTrackDatabase(context, ref),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -491,4 +528,48 @@ class SettingsScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetTrackDatabase(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Reset Track Database'),
|
||||
content: const Text(
|
||||
'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop(); // Close confirmation dialog
|
||||
|
||||
try {
|
||||
final repository = ref.read(trackRepositoryProvider.notifier);
|
||||
await repository.clearAllTracks();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Track database has been reset'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error resetting database: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:groovybox/data/playlist_repository.dart';
|
||||
import 'package:groovybox/ui/screens/album_detail_screen.dart';
|
||||
import 'package:groovybox/ui/widgets/universal_image.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class AlbumsTab extends HookConsumerWidget {
|
||||
@@ -49,16 +49,12 @@ class AlbumsTab extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: album.artUri != null
|
||||
? Image.file(File(album.artUri!), fit: BoxFit.cover)
|
||||
: Container(
|
||||
color: Colors.grey[800],
|
||||
child: const Icon(
|
||||
Icons.album,
|
||||
size: 48,
|
||||
color: Colors.white54,
|
||||
),
|
||||
),
|
||||
child: UniversalImage(
|
||||
uri: album.artUri,
|
||||
fit: BoxFit.cover,
|
||||
fallbackIcon: Icons.album,
|
||||
fallbackIconSize: 48,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
||||
@@ -355,29 +355,35 @@ class _DesktopMiniPlayer extends HookConsumerWidget {
|
||||
),
|
||||
const Gap(8),
|
||||
// Title & Artist
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currentMetadata?.title ??
|
||||
Uri.parse(media.uri).pathSegments.last,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
currentMetadata?.artist ?? 'Unknown Artist',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currentMetadata?.title ??
|
||||
Uri.parse(media.uri).pathSegments.last,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
currentMetadata?.artist ?? 'Unknown Artist',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:groovybox/data/db.dart' as db;
|
||||
import 'package:groovybox/ui/widgets/universal_image.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class TrackTile extends StatelessWidget {
|
||||
final db.Track track;
|
||||
@@ -41,29 +41,20 @@ class TrackTile extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding:
|
||||
padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
contentPadding: padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
?leading,
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: track.artUri != null
|
||||
? DecorationImage(
|
||||
image: FileImage(File(track.artUri!)),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: track.artUri == null
|
||||
? const Icon(Icons.music_note, color: Colors.white54)
|
||||
: null,
|
||||
),
|
||||
child: UniversalImage(
|
||||
uri: track.artUri,
|
||||
fit: BoxFit.cover,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
fallbackIcon: Icons.music_note,
|
||||
fallbackIconSize: 24,
|
||||
).clipRRect(all: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
130
lib/ui/widgets/universal_image.dart
Normal file
130
lib/ui/widgets/universal_image.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'dart:io';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
final String? uri;
|
||||
final BoxFit? fit;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Widget? fallback;
|
||||
final IconData? fallbackIcon;
|
||||
final double? fallbackIconSize;
|
||||
final Color? fallbackIconColor;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool useDecorationImage;
|
||||
|
||||
const UniversalImage({
|
||||
super.key,
|
||||
this.uri,
|
||||
this.fit = BoxFit.cover,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fallback,
|
||||
this.fallbackIcon = Icons.image,
|
||||
this.fallbackIconSize = 48,
|
||||
this.fallbackIconColor = Colors.white54,
|
||||
this.borderRadius,
|
||||
this.useDecorationImage = false,
|
||||
});
|
||||
|
||||
bool _isNetworkUri(String uri) {
|
||||
return uri.startsWith('http://') || uri.startsWith('https://');
|
||||
}
|
||||
|
||||
Widget _buildFallback() {
|
||||
if (fallback != null) {
|
||||
return fallback!;
|
||||
}
|
||||
|
||||
final icon = Icon(
|
||||
fallbackIcon,
|
||||
size: fallbackIconSize,
|
||||
color: fallbackIconColor,
|
||||
);
|
||||
|
||||
if (borderRadius != null) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: Colors.grey[800],
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNetworkImage() {
|
||||
if (useDecorationImage) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: uri!,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
placeholder: (context, url) => Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: Colors.grey[800],
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) => _buildFallback(),
|
||||
);
|
||||
}
|
||||
|
||||
return CachedNetworkImage(
|
||||
imageUrl: uri!,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
placeholder: (context, url) => Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: Colors.grey[800],
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) => _buildFallback(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileImage() {
|
||||
if (useDecorationImage) {
|
||||
return Image.file(
|
||||
File(uri!),
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
errorBuilder: (context, error, stackTrace) => _buildFallback(),
|
||||
);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
File(uri!),
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
errorBuilder: (context, error, stackTrace) => _buildFallback(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (uri == null || uri!.isEmpty) {
|
||||
return _buildFallback();
|
||||
}
|
||||
|
||||
if (_isNetworkUri(uri!)) {
|
||||
return _buildNetworkImage();
|
||||
} else {
|
||||
return _buildFileImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user