mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'KRTirtho:master' into feature_duration_matching
This commit is contained in:
commit
0fce2c6347
@ -39,74 +39,96 @@ const imgMimeToExt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
||||||
final downloadDir = Directory(
|
try {
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)),
|
final downloadDir = Directory(
|
||||||
);
|
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)),
|
||||||
if (!await downloadDir.exists()) {
|
);
|
||||||
await downloadDir.create(recursive: true);
|
if (!await downloadDir.exists()) {
|
||||||
|
await downloadDir.create(recursive: true);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final entities = downloadDir.listSync(recursive: true);
|
||||||
|
|
||||||
|
final filesWithMetadata = (await Future.wait(
|
||||||
|
entities.map((e) => File(e.path)).where((file) {
|
||||||
|
final mimetype = lookupMimeType(file.path);
|
||||||
|
return mimetype != null && supportedAudioTypes.contains(mimetype);
|
||||||
|
}).map(
|
||||||
|
(f) async {
|
||||||
|
try {
|
||||||
|
final bytes = f.readAsBytes();
|
||||||
|
final mp3Instance = MP3Instance(await bytes);
|
||||||
|
|
||||||
|
bool isParsed = false;
|
||||||
|
try {
|
||||||
|
isParsed = mp3Instance.parseTagsSync();
|
||||||
|
} catch (e, stack) {
|
||||||
|
getLogger(MP3Instance).e("[parseTagsSync]", e, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
final imageFile = isParsed
|
||||||
|
? File(join(
|
||||||
|
(await getTemporaryDirectory()).path,
|
||||||
|
"spotube",
|
||||||
|
basenameWithoutExtension(f.path) +
|
||||||
|
imgMimeToExt[mp3Instance.metaTags["APIC"]?["mime"] ??
|
||||||
|
"image/jpeg"]!,
|
||||||
|
))
|
||||||
|
: null;
|
||||||
|
if (imageFile != null &&
|
||||||
|
!await imageFile.exists() &&
|
||||||
|
mp3Instance.metaTags["APIC"]?["base64"] != null) {
|
||||||
|
await imageFile.create(recursive: true);
|
||||||
|
await imageFile.writeAsBytes(
|
||||||
|
base64Decode(
|
||||||
|
mp3Instance.metaTags["APIC"]["base64"],
|
||||||
|
),
|
||||||
|
mode: FileMode.writeOnly,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Duration duration;
|
||||||
|
try {
|
||||||
|
duration = MP3Processor.fromBytes(await bytes).duration;
|
||||||
|
} catch (e, stack) {
|
||||||
|
getLogger(MP3Processor).e("[Parsing Mp3]", e, stack);
|
||||||
|
duration = Duration.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadata = await tagProcessor.getTagsFromByteArray(bytes);
|
||||||
|
return {
|
||||||
|
"metadata": metadata,
|
||||||
|
"file": f,
|
||||||
|
"art": imageFile?.path,
|
||||||
|
"duration": duration,
|
||||||
|
};
|
||||||
|
} catch (e, stack) {
|
||||||
|
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
|
||||||
|
return {
|
||||||
|
"metadata": <Tag>[],
|
||||||
|
"file": f,
|
||||||
|
"duration": Duration.zero,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final tracks = filesWithMetadata
|
||||||
|
.map(
|
||||||
|
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
|
||||||
|
fileWithMetadata["metadata"] as List<Tag>,
|
||||||
|
fileWithMetadata["file"] as File,
|
||||||
|
fileWithMetadata["duration"] as Duration,
|
||||||
|
fileWithMetadata["art"] as String?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
} catch (e, stack) {
|
||||||
|
getLogger(FutureProvider).e("[LocalTracksProvider]", e, stack);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final entities = downloadDir.listSync(recursive: true);
|
|
||||||
final filesWithMetadata = (await Future.wait(
|
|
||||||
entities.map((e) => File(e.path)).where((file) {
|
|
||||||
final mimetype = lookupMimeType(file.path);
|
|
||||||
return mimetype != null && supportedAudioTypes.contains(mimetype);
|
|
||||||
}).map(
|
|
||||||
(f) async {
|
|
||||||
final bytes = f.readAsBytes();
|
|
||||||
final mp3Instance = MP3Instance(await bytes);
|
|
||||||
|
|
||||||
final imageFile = mp3Instance.parseTagsSync()
|
|
||||||
? File(join(
|
|
||||||
(await getTemporaryDirectory()).path,
|
|
||||||
"spotube",
|
|
||||||
basenameWithoutExtension(f.path) +
|
|
||||||
imgMimeToExt[
|
|
||||||
mp3Instance.metaTags["APIC"]?["mime"] ?? "image/jpeg"]!,
|
|
||||||
))
|
|
||||||
: null;
|
|
||||||
if (imageFile != null &&
|
|
||||||
!await imageFile.exists() &&
|
|
||||||
mp3Instance.metaTags["APIC"]?["base64"] != null) {
|
|
||||||
await imageFile.create(recursive: true);
|
|
||||||
await imageFile.writeAsBytes(
|
|
||||||
base64Decode(
|
|
||||||
mp3Instance.metaTags["APIC"]["base64"],
|
|
||||||
),
|
|
||||||
mode: FileMode.writeOnly,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Duration duration;
|
|
||||||
try {
|
|
||||||
duration = MP3Processor.fromBytes(await bytes).duration;
|
|
||||||
} catch (e, stack) {
|
|
||||||
getLogger(MP3Processor).e("[Parsing Mp3]", e, stack);
|
|
||||||
duration = Duration.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
final metadata = await tagProcessor.getTagsFromByteArray(bytes);
|
|
||||||
return {
|
|
||||||
"metadata": metadata,
|
|
||||||
"file": f,
|
|
||||||
"art": imageFile?.path,
|
|
||||||
"duration": duration,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
final tracks = filesWithMetadata
|
|
||||||
.map(
|
|
||||||
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
|
|
||||||
fileWithMetadata["metadata"] as List<Tag>,
|
|
||||||
fileWithMetadata["file"] as File,
|
|
||||||
fileWithMetadata["duration"] as Duration,
|
|
||||||
fileWithMetadata["art"] as String?,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return tracks;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class UserLocalTracks extends HookConsumerWidget {
|
class UserLocalTracks extends HookConsumerWidget {
|
||||||
|
@ -63,6 +63,66 @@ class Settings extends HookConsumerWidget {
|
|||||||
constraints: const BoxConstraints(maxWidth: 1366),
|
constraints: const BoxConstraints(maxWidth: 1366),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
|
const Text(
|
||||||
|
" Account",
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
),
|
||||||
|
if (auth.isAnonymous)
|
||||||
|
AdaptiveListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.login_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
title: AutoSizeText(
|
||||||
|
"Login with your Spotify account",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: (context, update) => ElevatedButton(
|
||||||
|
child: Text("Connect with Spotify".toUpperCase()),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).push("/login");
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: MaterialStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (auth.isLoggedIn)
|
||||||
|
Builder(builder: (context) {
|
||||||
|
Auth auth = ref.watch(authProvider);
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.logout_rounded),
|
||||||
|
title: const AutoSizeText(
|
||||||
|
"Log out of this account",
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
child: const Text("Logout"),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all(Colors.red),
|
||||||
|
foregroundColor:
|
||||||
|
MaterialStateProperty.all(Colors.white),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
auth.logout();
|
||||||
|
GoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const Text(
|
||||||
|
" Appearance",
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
),
|
||||||
AdaptiveListTile(
|
AdaptiveListTile(
|
||||||
leading: const Icon(Icons.dark_mode_outlined),
|
leading: const Icon(Icons.dark_mode_outlined),
|
||||||
title: const Text("Theme"),
|
title: const Text("Theme"),
|
||||||
@ -122,6 +182,55 @@ class Settings extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onTap: pickColorScheme(ColorSchemeType.background),
|
onTap: pickColorScheme(ColorSchemeType.background),
|
||||||
),
|
),
|
||||||
|
const Text(
|
||||||
|
" Playback",
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
),
|
||||||
|
AdaptiveListTile(
|
||||||
|
leading: const Icon(Icons.multitrack_audio_rounded),
|
||||||
|
title: const Text("Audio Quality"),
|
||||||
|
trailing: (context, update) =>
|
||||||
|
DropdownButton<AudioQuality>(
|
||||||
|
value: preferences.audioQuality,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text(
|
||||||
|
"High",
|
||||||
|
),
|
||||||
|
value: AudioQuality.high,
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text("Low"),
|
||||||
|
value: AudioQuality.low,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
preferences.setAudioQuality(value);
|
||||||
|
update?.call(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.fast_forward_rounded),
|
||||||
|
title: const Text(
|
||||||
|
"Skip non-music segments (SponsorBlock)",
|
||||||
|
),
|
||||||
|
trailing: Switch.adaptive(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
value: preferences.skipSponsorSegments,
|
||||||
|
onChanged: (state) {
|
||||||
|
preferences.setSkipSponsorSegments(state);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
" Search",
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
),
|
||||||
AdaptiveListTile(
|
AdaptiveListTile(
|
||||||
leading: const Icon(Icons.shopping_bag_rounded),
|
leading: const Icon(Icons.shopping_bag_rounded),
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -155,16 +264,6 @@ class Settings extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.file_download_outlined),
|
|
||||||
title: const Text("Download Location"),
|
|
||||||
subtitle: Text(preferences.downloadLocation),
|
|
||||||
trailing: ElevatedButton(
|
|
||||||
child: const Icon(Icons.folder_rounded),
|
|
||||||
onPressed: pickDownloadLocation,
|
|
||||||
),
|
|
||||||
onTap: pickDownloadLocation,
|
|
||||||
),
|
|
||||||
AdaptiveListTile(
|
AdaptiveListTile(
|
||||||
leading: const Icon(Icons.screen_search_desktop_rounded),
|
leading: const Icon(Icons.screen_search_desktop_rounded),
|
||||||
title: const AutoSizeText(
|
title: const AutoSizeText(
|
||||||
@ -195,66 +294,6 @@ class Settings extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.fast_forward_rounded),
|
|
||||||
title: const Text(
|
|
||||||
"Skip non-music segments (SponsorBlock)",
|
|
||||||
),
|
|
||||||
trailing: Switch.adaptive(
|
|
||||||
activeColor: Theme.of(context).primaryColor,
|
|
||||||
value: preferences.skipSponsorSegments,
|
|
||||||
onChanged: (state) {
|
|
||||||
preferences.setSkipSponsorSegments(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.lyrics_rounded),
|
|
||||||
title: const Text("Download lyrics along with the Track"),
|
|
||||||
trailing: Switch.adaptive(
|
|
||||||
activeColor: Theme.of(context).primaryColor,
|
|
||||||
value: preferences.saveTrackLyrics,
|
|
||||||
onChanged: (state) {
|
|
||||||
preferences.setSaveTrackLyrics(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (auth.isAnonymous)
|
|
||||||
AdaptiveListTile(
|
|
||||||
leading: Icon(
|
|
||||||
Icons.login_rounded,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
title: AutoSizeText(
|
|
||||||
"Login with your Spotify account",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: (context, update) => ElevatedButton(
|
|
||||||
child: Text("Connect with Spotify".toUpperCase()),
|
|
||||||
onPressed: () {
|
|
||||||
GoRouter.of(context).push("/login");
|
|
||||||
},
|
|
||||||
style: ButtonStyle(
|
|
||||||
shape: MaterialStateProperty.all(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.update_rounded),
|
|
||||||
title: const Text("Check for Update"),
|
|
||||||
trailing: Switch.adaptive(
|
|
||||||
activeColor: Theme.of(context).primaryColor,
|
|
||||||
value: preferences.checkUpdate,
|
|
||||||
onChanged: (checked) =>
|
|
||||||
preferences.setCheckUpdate(checked),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AdaptiveListTile(
|
AdaptiveListTile(
|
||||||
leading: const Icon(Icons.low_priority_rounded),
|
leading: const Icon(Icons.low_priority_rounded),
|
||||||
title: const AutoSizeText(
|
title: const AutoSizeText(
|
||||||
@ -294,56 +333,37 @@ class Settings extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AdaptiveListTile(
|
const Text(
|
||||||
leading: const Icon(Icons.multitrack_audio_rounded),
|
" Downloads",
|
||||||
title: const Text("Audio Quality"),
|
style:
|
||||||
trailing: (context, update) =>
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
DropdownButton<AudioQuality>(
|
),
|
||||||
value: preferences.audioQuality,
|
ListTile(
|
||||||
items: const [
|
leading: const Icon(Icons.file_download_outlined),
|
||||||
DropdownMenuItem(
|
title: const Text("Download Location"),
|
||||||
child: Text(
|
subtitle: Text(preferences.downloadLocation),
|
||||||
"High",
|
trailing: ElevatedButton(
|
||||||
),
|
child: const Icon(Icons.folder_rounded),
|
||||||
value: AudioQuality.high,
|
onPressed: pickDownloadLocation,
|
||||||
),
|
),
|
||||||
DropdownMenuItem(
|
onTap: pickDownloadLocation,
|
||||||
child: Text("Low"),
|
),
|
||||||
value: AudioQuality.low,
|
ListTile(
|
||||||
),
|
leading: const Icon(Icons.lyrics_rounded),
|
||||||
],
|
title: const Text("Download lyrics along with the Track"),
|
||||||
onChanged: (value) {
|
trailing: Switch.adaptive(
|
||||||
if (value != null) {
|
activeColor: Theme.of(context).primaryColor,
|
||||||
preferences.setAudioQuality(value);
|
value: preferences.saveTrackLyrics,
|
||||||
update?.call(() {});
|
onChanged: (state) {
|
||||||
}
|
preferences.setSaveTrackLyrics(state);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (auth.isLoggedIn)
|
const Text(
|
||||||
Builder(builder: (context) {
|
" About",
|
||||||
Auth auth = ref.watch(authProvider);
|
style:
|
||||||
return ListTile(
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
leading: const Icon(Icons.logout_rounded),
|
),
|
||||||
title: const AutoSizeText(
|
|
||||||
"Log out of this account",
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
trailing: ElevatedButton(
|
|
||||||
child: const Text("Logout"),
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor:
|
|
||||||
MaterialStateProperty.all(Colors.red),
|
|
||||||
foregroundColor:
|
|
||||||
MaterialStateProperty.all(Colors.white),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
auth.logout();
|
|
||||||
GoRouter.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
AdaptiveListTile(
|
AdaptiveListTile(
|
||||||
leading: const Icon(
|
leading: const Icon(
|
||||||
Icons.favorite_border_rounded,
|
Icons.favorite_border_rounded,
|
||||||
@ -373,6 +393,16 @@ class Settings extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.update_rounded),
|
||||||
|
title: const Text("Check for Update"),
|
||||||
|
trailing: Switch.adaptive(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
value: preferences.checkUpdate,
|
||||||
|
onChanged: (checked) =>
|
||||||
|
preferences.setCheckUpdate(checked),
|
||||||
|
),
|
||||||
|
),
|
||||||
const About()
|
const About()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -64,6 +64,20 @@ class Id3Tags {
|
|||||||
"genre": genre,
|
"genre": genre,
|
||||||
"picture": picture,
|
"picture": picture,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String? get artist => tpe2;
|
||||||
|
String? get year => tdrc;
|
||||||
|
|
||||||
|
Map<String, String> toAndroidJson(String artwork) {
|
||||||
|
return {
|
||||||
|
"title": title ?? "Unknown",
|
||||||
|
"artist": artist ?? "Unknown",
|
||||||
|
"album": album ?? "Unknown",
|
||||||
|
"genre": genre ?? "Unknown",
|
||||||
|
"artwork": artwork,
|
||||||
|
"year": year ?? "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CommentJson on Comment {
|
extension CommentJson on Comment {
|
||||||
|
@ -16,6 +16,7 @@ import 'package:spotube/models/SpotubeTrack.dart';
|
|||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
|
||||||
|
|
||||||
@ -96,11 +97,15 @@ class Downloader with ChangeNotifier {
|
|||||||
"[addToQueue] Download of ${file.path} is done successfully",
|
"[addToQueue] Download of ${file.path} is done successfully",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tagging isn't supported in Android currently
|
||||||
|
if (kIsAndroid) return;
|
||||||
|
|
||||||
|
final imageUri = TypeConversionUtils.image_X_UrlString(
|
||||||
|
track.album?.images ?? [],
|
||||||
|
);
|
||||||
final response = await get(
|
final response = await get(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
imageUri,
|
||||||
track.album?.images ?? [],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final picture = AttachedPicture.base64(
|
final picture = AttachedPicture.base64(
|
||||||
|
@ -463,13 +463,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
eztags:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: eztags
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
fading_edge_scrollview:
|
fading_edge_scrollview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1417,5 +1410,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.5 <3.0.0"
|
dart: ">=2.17.1 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user