From 1bd49e46efc24ebfd9331e2dd0821380b540f019 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Wed, 6 Nov 2024 00:53:15 +0200 Subject: [PATCH] core: migrate tracks.json to database - and 10000000x improve performance of `_addTheseTracksToAlbumGenreArtistEtc()`, this can be noticed while (editing tags/reindexing tracks/playing external files) --- lib/class/split_config.dart | 6 +- lib/class/track.dart | 6 +- lib/controller/backup_controller.dart | 3 +- lib/controller/indexer_controller.dart | 211 +++++++++++------- lib/core/constants.dart | 3 +- .../settings/backup_restore_settings.dart | 6 +- pubspec.yaml | 8 +- 7 files changed, 146 insertions(+), 97 deletions(-) diff --git a/lib/class/split_config.dart b/lib/class/split_config.dart index 86030e86..4168fbab 100644 --- a/lib/class/split_config.dart +++ b/lib/class/split_config.dart @@ -3,13 +3,13 @@ import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; class SplitArtistGenreConfigsWrapper { - final String path; + final String dbPath; final ArtistsSplitConfig artistsConfig; final GenresSplitConfig genresConfig; final GeneralSplitConfig generalConfig; const SplitArtistGenreConfigsWrapper({ - required this.path, + required this.dbPath, required this.artistsConfig, required this.genresConfig, required this.generalConfig, @@ -17,7 +17,7 @@ class SplitArtistGenreConfigsWrapper { factory SplitArtistGenreConfigsWrapper.settings() { return SplitArtistGenreConfigsWrapper( - path: AppPaths.TRACKS, + dbPath: AppPaths.TRACKS_DB_INFO.file.path, artistsConfig: ArtistsSplitConfig.settings(), genresConfig: GenresSplitConfig.settings(), generalConfig: GeneralSplitConfig(), diff --git a/lib/class/track.dart b/lib/class/track.dart index 888a2985..e52bf98b 100644 --- a/lib/class/track.dart +++ b/lib/class/track.dart @@ -317,6 +317,7 @@ class TrackExtended { } factory TrackExtended.fromJson( + String path, Map json, { required ArtistsSplitConfig artistsSplitConfig, required GenresSplitConfig genresSplitConfig, @@ -349,7 +350,7 @@ class TrackExtended { size: json['size'] ?? 0, dateAdded: json['dateAdded'] ?? 0, dateModified: json['dateModified'] ?? 0, - path: json['path'] ?? '', + path: path, comment: json['comment'] ?? '', description: json['description'] ?? '', synopsis: json['synopsis'] ?? '', @@ -372,7 +373,7 @@ class TrackExtended { ); } - Map toJson() { + Map toJsonWithoutPath() { return { if (title.isNotEmpty) 'title': title, if (originalArtist.isNotEmpty) 'originalArtist': originalArtist, @@ -387,7 +388,6 @@ class TrackExtended { if (size > 0) 'size': size, if (dateAdded > 0) 'dateAdded': dateAdded, if (dateModified > 0) 'dateModified': dateModified, - if (path.isNotEmpty) 'path': path, if (comment.isNotEmpty) 'comment': comment, if (description.isNotEmpty) 'description': description, if (synopsis.isNotEmpty) 'synopsis': synopsis, diff --git a/lib/controller/backup_controller.dart b/lib/controller/backup_controller.dart index a9b3719f..270263a7 100644 --- a/lib/controller/backup_controller.dart +++ b/lib/controller/backup_controller.dart @@ -45,7 +45,8 @@ class BackupController { final diff = DateTime.now().difference(lastModified).abs().inDays; if (diff > interval) { final itemsToBackup = [ - AppPaths.TRACKS, + AppPaths.TRACKS_OLD, + AppPaths.TRACKS_DB_INFO.file.path, AppPaths.TRACKS_STATS_OLD, AppPaths.TRACKS_STATS_DB_INFO.file.path, AppPaths.TOTAL_LISTEN_TIME, diff --git a/lib/controller/indexer_controller.dart b/lib/controller/indexer_controller.dart index 771b16e8..403ecaef 100644 --- a/lib/controller/indexer_controller.dart +++ b/lib/controller/indexer_controller.dart @@ -41,6 +41,13 @@ class Indexer { bool get _defaultUseMediaStore => settings.useMediaStore.value; bool get _includeVideosAsTracks => true; // TODO: settings.includeVideosAsTracks.value + void _clearTracksDBAndReOpen() { + _tracksDBManager + ..deleteEverything() + ..claimFreeSpaceAsync(); + } + + late final _tracksDBManager = DBWrapper.openFromInfo(fileInfo: AppPaths.TRACKS_DB_INFO, createIfNotExist: true); late final _trackStatsDBManager = DBWrapper.openFromInfo(fileInfo: AppPaths.TRACKS_STATS_DB_INFO, createIfNotExist: true); final isIndexing = false.obs; @@ -194,15 +201,14 @@ class Indexer { void prepareTracksFile() { _fetchMediaStoreTracks(); // to fill ids map - /// Only awaits if the track file exists, otherwise it will get into normally and start indexing. - if (File(AppPaths.TRACKS).existsAndValidSync()) { + final tracksDBPath = AppPaths.TRACKS_DB_INFO.file.path; + if (File(tracksDBPath).existsAndValidSync(21 * 1024) || File(AppPaths.TRACKS_OLD).existsAndValidSync()) { + // -- only block load if the track file exists.. _readTrackData(); _afterIndexing(); - } - - /// doesnt exists - else { - File(AppPaths.TRACKS).createSync(); + } else { + // -- otherwise it get into normally and start indexing. + File(tracksDBPath).createSync(); refreshLibraryAndCheckForDiff(forceReIndex: true, useMediaStore: _defaultUseMediaStore); } } @@ -463,34 +469,39 @@ class Indexer { final List addedFolders = []; final List addedFoldersVideos = []; + // -- this gurantees that [newlyAddedList] will not contain duplicates. + void addCustom(Map> map, K key, E item, List newlyAddedList) { + final list = map[key]; + if (list == null) { + map[key] = [item]; + newlyAddedList.add(key); + } else { + if (!list.contains(item)) { + list.add(item); + } + } + } + tracks.loop((tr) { final trExt = tr.toTrackExt(); // -- Assigning Albums - mainMapAlbums.addNoDuplicatesForce(trExt.albumIdentifier, tr); + addCustom(mainMapAlbums, trExt.albumIdentifier, tr, addedAlbums); // -- Assigning Artists trExt.artistsList.loop((artist) { - mainMapArtists.addNoDuplicatesForce(artist, tr); + addCustom(mainMapArtists, artist, tr, addedArtists); }); - mainMapAlbumArtists.addNoDuplicatesForce(trExt.albumArtist, tr); - mainMapComposer.addNoDuplicatesForce(trExt.composer, tr); + addCustom(mainMapAlbumArtists, trExt.albumArtist, tr, addedAlbumArtists); + addCustom(mainMapComposer, trExt.composer, tr, addedComposers); // -- Assigning Genres trExt.genresList.loop((genre) { - mainMapGenres.addNoDuplicatesForce(genre, tr); + addCustom(mainMapGenres, genre, tr, addedGenres); }); // -- Assigning Folders - tr is Video ? mainMapFoldersVideos.addNoDuplicatesForce(tr.folder, tr) : mainMapFolders.addNoDuplicatesForce(tr.folder, tr); - - // --- Adding media that was affected - addedAlbums.add(trExt.albumIdentifier); - addedArtists.addAll(trExt.artistsList); - addedAlbumArtists.add(trExt.albumArtist); - addedComposers.add(trExt.composer); - addedGenres.addAll(trExt.artistsList); - tr is Video ? addedFoldersVideos.add(tr.folder) : addedFolders.add(tr.folder); + tr is Video ? addCustom(mainMapFoldersVideos, tr.folder, tr, addedFoldersVideos) : addCustom(mainMapFolders, tr.folder, tr, addedFolders); }); final albumSorters = SearchSortController.inst.getMediaTracksSortingComparables(MediaType.album); @@ -499,29 +510,29 @@ class Indexer { final folderSorters = SearchSortController.inst.getMediaTracksSortingComparables(MediaType.folder); final folderVideosSorters = SearchSortController.inst.getMediaTracksSortingComparables(MediaType.folderVideo); - void cleanyLoopy(MediaType type, List added, Map> map, List Function(A tr)> sorters) { + void cleanyLoopy(MediaType type, List added, Map> map, List Function(A tr)> sorters, {bool printt = false}) { if (added.isEmpty) return; - added.removeDuplicates(); + + SearchSortController.inst.sortMedia(type); // main list sorting final reverse = settings.mediaItemsTrackSortingReverse.value[type] ?? false; if (reverse) { - added.loop((e) => map[e]?.sortByReverseAlts(sorters)); + added.loop((e) => map[e]?.sortByReverseAlts(sorters)); // sub-list sorting } else { - added.loop((e) => map[e]?.sortByAlts(sorters)); + added.loop((e) => map[e]?.sortByAlts(sorters)); // sub-list sorting } } cleanyLoopy(MediaType.album, addedAlbums, mainMapAlbums, albumSorters); - cleanyLoopy(MediaType.artist, addedArtists, mainMapArtists, artistSorters); - cleanyLoopy(MediaType.albumArtist, addedAlbumArtists, mainMapAlbumArtists, artistSorters); - cleanyLoopy(MediaType.composer, addedComposers, mainMapComposer, artistSorters); + cleanyLoopy(MediaType.artist, addedArtists, mainMapArtists, artistSorters, printt: true); + cleanyLoopy(MediaType.albumArtist, addedAlbumArtists, mainMapAlbumArtists, artistSorters, printt: true); + cleanyLoopy(MediaType.composer, addedComposers, mainMapComposer, artistSorters, printt: true); cleanyLoopy(MediaType.genre, addedGenres, mainMapGenres, genreSorters); cleanyLoopy(MediaType.folder, addedFolders, mainMapFolders, folderSorters); cleanyLoopy(MediaType.folderVideo, addedFoldersVideos, mainMapFoldersVideos, folderVideosSorters); Folders.tracks.onMapChanged(mainMapFolders); Folders.videos.onMapChanged(mainMapFoldersVideos); - _sortAll(); } TrackExtended? _convertTagToTrack({ @@ -717,6 +728,7 @@ class Indexer { void _addTrackToLists(TrackExtended trackExt, bool checkForDuplicates, FArtwork? artwork) { final tr = trackExt.asTrack() as T; allTracksMappedByPath[tr.path] = trackExt; + _tracksDBManager.putAsync(tr.path, trackExt.toJsonWithoutPath()); allTracksMappedByYTID.addForce(trackExt.youtubeID, tr); _currentFileNamesMap[trackExt.path.getFilename] = true; if (checkForDuplicates) { @@ -749,13 +761,14 @@ class Indexer { SearchSortController.inst.trackSearchList.value.remove(tr); SearchSortController.inst.trackSearchTemp.value.remove(tr); allTracksMappedByPath.remove(tr.path); + _tracksDBManager.deleteAsync(tr.path); + TrackTileManager.rebuildTrackInfo(tr); }, ); SearchSortController.inst.trackSearchList.refresh(); SearchSortController.inst.trackSearchTemp.refresh(); Folders.tracks.currentFolder.refresh(); Folders.videos.currentFolder.refresh(); - await _saveTrackFileToStorage(); recentlyDeltedFileWrite.flush().then((_) => recentlyDeltedFileWrite.close()); } @@ -780,6 +793,7 @@ class Indexer { } else { tracksMissing.add(tr); } + TrackTileManager.rebuildTrackInfo(tr); }); if (updateArtwork) { @@ -824,7 +838,7 @@ class Indexer { }); _addTheseTracksToAlbumGenreArtistEtc(finalTrack); Player.inst.refreshNotification(); - await _sortAndSaveTracks(); + _sortAndRefreshTracks(); onFinish(finalTrack.length); } @@ -847,9 +861,11 @@ class Indexer { oldTracks.add(ot); newTracks.add(nt); allTracksMappedByPath[ot.path] = e.value; + _tracksDBManager.putAsync(ot.path, e.value.toJsonWithoutPath()); allTracksMappedByYTID.addForce(e.value.youtubeID, ot); _currentFileNamesMap.remove(ot.filename); _currentFileNamesMap[nt.filename] = true; + TrackTileManager.rebuildTrackInfo(ot); if (artworkWasEdited) { // artwork extraction is not our business @@ -858,7 +874,10 @@ class Indexer { } oldTracks.loop((tr) => _removeThisTrackFromAlbumGenreArtistEtc(tr)); _addTheseTracksToAlbumGenreArtistEtc(newTracks); - await _sortAndSaveTracks(); + + Player.inst.refreshRxVariables(); + Player.inst.refreshNotification(); + SearchSortController.inst.searchAll(ScrollSearchController.inst.searchTextEditingController.text); } Future> convertPathsToTracksAndAddToLists(Iterable tracksPathPre) async { @@ -878,11 +897,7 @@ class Indexer { index++; } - bool saveToFile = false; - if (tracksToExtract.isNotEmpty) { - saveToFile = true; - final splitConfig = _createSplitConfig(); TrackExtended? extractFunction(FAudioModel item) => _convertTagToTrack( @@ -906,19 +921,18 @@ class Indexer { } _addTheseTracksToAlbumGenreArtistEtc(finalTracks); - if (saveToFile) await _sortAndSaveTracks(); + _sortAndRefreshTracks(); finalTracks.sortBy((e) => orderLookup[e.path] ?? 0); return finalTracks; } - Future _sortAndSaveTracks() async { + void _sortAndRefreshTracks() { Player.inst.refreshRxVariables(); Player.inst.refreshNotification(); SearchSortController.inst.searchAll(ScrollSearchController.inst.searchTextEditingController.text); SearchSortController.inst.sortMedia(MediaType.track); - await _saveTrackFileToStorage(); - await _createDefaultNamidaArtwork(); + _createDefaultNamidaArtworkIfRequired(); } void _clearLists() { @@ -926,6 +940,7 @@ class Indexer { artworksSizeInStorage.value = 0; tracksInfoList.clear(); allTracksMappedByPath.clear(); + _clearTracksDBAndReOpen(); allTracksMappedByYTID.clear(); _currentFileNamesMap.clear(); SearchSortController.inst.sortMedia(MediaType.track); @@ -960,7 +975,11 @@ class Indexer { printy("Audio Files Deleted: ${deletedPaths.length}"); if (deletedPaths.isNotEmpty) { - tracksInfoList.removeWhere((tr) => deletedPaths.contains(tr.path)); + tracksInfoList.removeWhere((tr) { + final remove = deletedPaths.contains(tr.path); + if (remove) _tracksDBManager.deleteAsync(tr.path); + return remove; + }); } final minDur = settings.indexMinDurationInSec.value; // Seconds @@ -970,6 +989,7 @@ class Indexer { final trs = await _fetchMediaStoreTracks(); tracksInfoList.clear(); allTracksMappedByPath.clear(); + _clearTracksDBAndReOpen(); allTracksMappedByYTID.clear(); _currentFileNamesMap.clear(); trs.loop((e) => _addTrackToLists(e.$1, false, null)); @@ -1033,22 +1053,30 @@ class Indexer { /// doing some checks to remove unqualified tracks. /// removes tracks after changing `duration` or `size`. - tracksInfoList.removeWhere((tr) => (tr.durationMS != 0 && tr.durationMS < minDur * 1000) || tr.size < minSize); + tracksInfoList.removeWhere((tr) { + final remove = (tr.durationMS != 0 && tr.durationMS < minDur * 1000) || tr.size < minSize; + if (remove) _tracksDBManager.deleteAsync(tr.path); + return remove; + }); /// removes duplicated tracks after a refresh if (prevDuplicated) { - final removedNumber = tracksInfoList.removeDuplicates((element) => element.filename); + final uniquedSet = {}; + final lengthBefore = tracksInfoList.value.length; + tracksInfoList.value.retainWhere((e) { + final keep = uniquedSet.add(e.filename); + if (!keep) _tracksDBManager.deleteAsync(e.path); + return keep; + }); + final lengthAfter = tracksInfoList.value.length; + final removedNumber = lengthBefore - lengthAfter; duplicatedTracksLength.value = removedNumber; } printy("FINAL: ${tracksInfoList.length}"); - await _sortAndSaveTracks(); - } - - Future _saveTrackFileToStorage() async { + _sortAndRefreshTracks(); TrackTileManager.onTrackItemPropChange(); - await File(AppPaths.TRACKS).writeAsJson(tracksInfoList.value.map((tr) => allTracksMappedByPath[tr.path]?.toJson()).toList()); } Future updateTrackDuration(Track track, Duration dur) async { @@ -1056,12 +1084,14 @@ class Indexer { if (durInMS > 0 && track.durationMS != durInMS) { final trx = allTracksMappedByPath[track.path]; if (trx != null) { - allTracksMappedByPath[track.path] = trx.copyWith(durationMS: durInMS); + final newTrExt = trx.copyWith(durationMS: durInMS); + allTracksMappedByPath[track.path] = newTrExt; + _tracksDBManager.putAsync(track.path, newTrExt.toJsonWithoutPath()); tracksInfoList.refresh(); SearchSortController.inst.trackSearchList.refresh(); SearchSortController.inst.trackSearchTemp.refresh(); } - await _saveTrackFileToStorage(); + TrackTileManager.rebuildTrackInfo(track); } } @@ -1090,7 +1120,7 @@ class Indexer { int? lastPositionInMs, }) async { if (ratingString != null || tagsString != null || moodsString != null) { - TrackTileManager.onTrackItemPropChange(); + TrackTileManager.rebuildTrackInfo(track); } final rating = ratingString != null @@ -1124,7 +1154,7 @@ class Indexer { }, ); - // -- migrating json to db + // -- migrating tracks stats json to db final statsJsonFile = File(AppPaths.TRACKS_STATS_OLD); if (statsJsonFile.existsSync()) { final list = statsJsonFile.readAsJsonSync() as List?; @@ -1152,39 +1182,54 @@ class Indexer { /// Reading actual track file. final splitconfig = _createSplitConfig(); - final tracksResult = _readTracksFileCompute(splitconfig); - allTracksMappedByPath = tracksResult.$1; - allTracksMappedByYTID = tracksResult.$2 as Map>; - tracksInfoList.value = tracksResult.$3 as List; - - printy("All Tracks Length From File: ${tracksInfoList.length}"); - } - - static (Map, Map>, List) _readTracksFileCompute(SplitArtistGenreConfigsWrapper config) { - final map = {}; - final idsMap = >{}; - final allTracks = []; - final list = File(config.path).readAsJsonSync() as List?; - if (list != null) { - for (int i = 0; i < list.length; i++) { - try { - final item = list[i]; + try { + _tracksDBManager.loadEverythingKeyed( + (path, item) { final trExt = TrackExtended.fromJson( + path, item, - artistsSplitConfig: config.artistsConfig, - genresSplitConfig: config.genresConfig, - generalSplitConfig: config.generalConfig, + artistsSplitConfig: splitconfig.artistsConfig, + genresSplitConfig: splitconfig.genresConfig, + generalSplitConfig: splitconfig.generalConfig, ); - final track = trExt.asTrack(); - map[track.path] = trExt; - allTracks.add(track); - idsMap.addForce(trExt.youtubeID, track); - } catch (e) { - continue; + final track = trExt.asTrack() as T; + allTracksMappedByPath[track.path] = trExt; + tracksInfoList.value.add(track); + allTracksMappedByYTID.addForce(trExt.youtubeID, track); + }, + ); + + // -- migrating tracks json to db + final tracksJsonFile = File(AppPaths.TRACKS_OLD); + if (tracksJsonFile.existsSync()) { + final list = tracksJsonFile.readAsJsonSync() as List?; + if (list != null) { + for (int i = 0; i < list.length; i++) { + try { + final item = list[i]; + final trExt = TrackExtended.fromJson( + item['path'] ?? '', + item, + artistsSplitConfig: splitconfig.artistsConfig, + genresSplitConfig: splitconfig.genresConfig, + generalSplitConfig: splitconfig.generalConfig, + ); + final track = trExt.asTrack() as T; + allTracksMappedByPath[track.path] = trExt; + tracksInfoList.value.add(track); + allTracksMappedByYTID.addForce(trExt.youtubeID, track); + _tracksDBManager.put(track.path, trExt.toJsonWithoutPath()); + } catch (_) {} + } } + tracksJsonFile.deleteSync(); } - } - return (map, idsMap, allTracks); + } catch (_) {} + + tracksInfoList.refresh(); + trackStatsMap.refresh(); + + printy("All Tracks Length From File: ${tracksInfoList.length}"); } static List splitArtist({ @@ -1491,12 +1536,12 @@ class Indexer { Future clearImageCache() async { await Directory(AppDirs.ARTWORKS).delete(recursive: true); await Directory(AppDirs.ARTWORKS).create(); - await _createDefaultNamidaArtwork(); + _createDefaultNamidaArtworkIfRequired(); calculateAllImageSizesInStorage(); } - Future _createDefaultNamidaArtwork() async { - if (!await File(AppPaths.NAMIDA_LOGO_MONET).exists()) { + Future _createDefaultNamidaArtworkIfRequired() async { + if (!File(AppPaths.NAMIDA_LOGO_MONET).existsSync()) { final byteData = await rootBundle.load('assets/namida_icon_monet.png'); final file = await File(AppPaths.NAMIDA_LOGO_MONET).create(recursive: true); await file.writeAsBytes(byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 3aa43628..2a5810b5 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -225,7 +225,8 @@ class AppPaths { static final SETTINGS_PLAYER = _join(_USER_DATA, 'namida_settings_player.json'); static final SETTINGS_YOUTUBE = _join(_USER_DATA, 'namida_settings_youtube.json'); static final SETTINGS_EXTRA = _join(_USER_DATA, 'namida_settings_extra.json'); - static final TRACKS = _join(_USER_DATA, 'tracks.json'); + static final TRACKS_OLD = _join(_USER_DATA, 'tracks.json'); + static final TRACKS_DB_INFO = DbWrapperFileInfo(directory: _USER_DATA, dbName: 'tracks'); static final TRACKS_STATS_OLD = _join(_USER_DATA, 'tracks_stats.json'); static final TRACKS_STATS_DB_INFO = DbWrapperFileInfo(directory: _USER_DATA, dbName: 'tracks_stats'); static final VIDEOS_LOCAL = _join(_USER_DATA, 'local_videos.json'); diff --git a/lib/ui/widgets/settings/backup_restore_settings.dart b/lib/ui/widgets/settings/backup_restore_settings.dart index 354a42e7..b28b425f 100644 --- a/lib/ui/widgets/settings/backup_restore_settings.dart +++ b/lib/ui/widgets/settings/backup_restore_settings.dart @@ -188,7 +188,8 @@ class BackupAndRestore extends SettingSubpageProvider { AppPaths.SETTINGS_PLAYER, AppPaths.SETTINGS_YOUTUBE, AppPaths.SETTINGS_EXTRA, - AppPaths.TRACKS, + AppPaths.TRACKS_OLD, + AppPaths.TRACKS_DB_INFO.file.path, AppPaths.TRACKS_STATS_OLD, AppPaths.TRACKS_STATS_DB_INFO.file.path, AppPaths.VIDEOS_LOCAL, @@ -371,7 +372,8 @@ class BackupAndRestore extends SettingSubpageProvider { title: lang.DATABASE, icon: Broken.box_1, items: [ - AppPaths.TRACKS, + AppPaths.TRACKS_OLD, + AppPaths.TRACKS_DB_INFO.file.path, AppPaths.TRACKS_STATS_OLD, AppPaths.TRACKS_STATS_DB_INFO.file.path, AppPaths.TOTAL_LISTEN_TIME, diff --git a/pubspec.yaml b/pubspec.yaml index 48aa6c2f..36d6f15b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: namida description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter publish_to: "none" -version: 4.6.68-beta+241105216 +version: 4.6.7-beta+241105228 environment: sdk: ">=3.4.0 <4.0.0" @@ -37,7 +37,7 @@ dependencies: permission_handler: ^11.0.1 logger: ^2.2.0 package_info_plus: ^8.0.0 - device_info_plus: ^10.1.0 + device_info_plus: ^11.1.0 intl: ^0.19.0 flutter_archive: ^6.0.3 url_launcher: ^6.3.0 @@ -64,7 +64,7 @@ dependencies: lrc: git: url: https://github.com/MSOB7YY/dart_lrc - vibration: ^1.8.4 + vibration: ^2.0.1 flutter_displaymode: ^0.6.0 flutter_udid: ^3.0.0 namico_db_wrapper: @@ -138,7 +138,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 + flutter_lints: ^5.0.0 flutter_native_splash: ^2.4.1 custom_lint: