diff --git a/android/app/build.gradle b/android/app/build.gradle index cf89f3b..a0078ad 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,14 +1,13 @@ plugins { id "com.android.application" id "kotlin-android" - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } android { namespace = "tubesync.app" compileSdk = flutter.compileSdkVersion - ndkVersion = "25.1.8937393" //flutter.ndkVersion + ndkVersion = "25.1.8937393" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -21,13 +20,8 @@ android { defaultConfig { applicationId = "tubesync.app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - // https://stackoverflow.com/questions/67974978/android-execute-binary-from-application - // This app will never be on Google Play anyway - //noinspection ExpiredTargetSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode @@ -37,7 +31,6 @@ android { buildTypes { release { // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.debug } } diff --git a/android/build.gradle b/android/build.gradle index d2ffbff..e1473fe 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,6 +6,17 @@ allprojects { } rootProject.buildDir = "../build" + +// FIX for Flutter 3.24 +subprojects { + afterEvaluate { + android { + compileSdkVersion 34 + } + } +} + + subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } diff --git a/lib/app/home/library_tab.dart b/lib/app/home/library_tab.dart index 9409333..391b081 100644 --- a/lib/app/home/library_tab.dart +++ b/lib/app/home/library_tab.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:isar/isar.dart'; import 'package:provider/provider.dart'; import 'package:tube_sync/app/playlist/import_playlist_dialog.dart'; import 'package:tube_sync/app/playlist/playlist_entry_builder.dart'; @@ -24,7 +25,10 @@ class LibraryTab extends StatelessWidget { final playlist = library.entries[index]; return PlaylistEntryBuilder(playlist, onTap: () { notifier.value = ChangeNotifierProvider( - create: (_) => PlaylistProvider(playlist), + create: (_) => PlaylistProvider( + context.read(), + playlist, + ), child: PlaylistTab(notifier: notifier), ); }); diff --git a/lib/main.dart b/lib/main.dart index d7c0397..7df6052 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,12 +22,12 @@ void main() async { dragDevices: PointerDeviceKind.values.toSet(), ), theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.red), useMaterial3: true, ), darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, + seedColor: Colors.red, brightness: Brightness.dark, ), useMaterial3: true, diff --git a/lib/model/media.dart b/lib/model/media.dart index 0ed877f..3afef17 100644 --- a/lib/model/media.dart +++ b/lib/model/media.dart @@ -17,6 +17,7 @@ class Media { final Thumbnails thumbnail; + @ignore Duration? get duration => durationMs == null ? null : Duration(milliseconds: durationMs!); diff --git a/lib/model/playlist.dart b/lib/model/playlist.dart index e7fb020..220b61d 100644 --- a/lib/model/playlist.dart +++ b/lib/model/playlist.dart @@ -16,6 +16,8 @@ class Playlist { final Thumbnails thumbnail; final int videoCount; + final List videoIds; + Playlist( this.id, this.title, @@ -23,6 +25,7 @@ class Playlist { this.thumbnail, this.videoCount, this.description, + this.videoIds, ); factory Playlist.fromYTPlaylist(yt.Playlist playlist) => Playlist( @@ -32,6 +35,7 @@ class Playlist { Thumbnails.fromYTThumbnails(playlist.thumbnails), playlist.videoCount ?? -1, playlist.description.isNotEmpty ? playlist.description : null, + List.empty(growable: true), ); @override diff --git a/lib/model/playlist.g.dart b/lib/model/playlist.g.dart index da3bb9c..caa8eac 100644 --- a/lib/model/playlist.g.dart +++ b/lib/model/playlist.g.dart @@ -45,6 +45,10 @@ const PlaylistSchema = IsarGeneratedSchema( name: 'videoCount', type: IsarType.long, ), + IsarPropertySchema( + name: 'videoIds', + type: IsarType.stringList, + ), ], indexes: [], ), @@ -76,6 +80,14 @@ int serializePlaylist(IsarWriter writer, Playlist object) { IsarCore.endObject(writer, objectWriter); } IsarCore.writeLong(writer, 6, object.videoCount); + { + final list = object.videoIds; + final listWriter = IsarCore.beginList(writer, 7, list.length); + for (var i = 0; i < list.length; i++) { + IsarCore.writeString(listWriter, i, list[i]); + } + IsarCore.endList(writer, listWriter); + } return Isar.fastHash(object.id); } @@ -106,6 +118,23 @@ Playlist deserializePlaylist(IsarReader reader) { _videoCount = IsarCore.readLong(reader, 6); final String? _description; _description = IsarCore.readString(reader, 4); + final List _videoIds; + { + final length = IsarCore.readList(reader, 7, IsarCore.readerPtrPtr); + { + final reader = IsarCore.readerPtr; + if (reader.isNull) { + _videoIds = const []; + } else { + final list = List.filled(length, '', growable: true); + for (var i = 0; i < length; i++) { + list[i] = IsarCore.readString(reader, i) ?? ''; + } + IsarCore.freeReader(reader); + _videoIds = list; + } + } + } final object = Playlist( _id, _title, @@ -113,6 +142,7 @@ Playlist deserializePlaylist(IsarReader reader) { _thumbnail, _videoCount, _description, + _videoIds, ); return object; } @@ -145,6 +175,23 @@ dynamic deserializePlaylistProp(IsarReader reader, int property) { } case 6: return IsarCore.readLong(reader, 6); + case 7: + { + final length = IsarCore.readList(reader, 7, IsarCore.readerPtrPtr); + { + final reader = IsarCore.readerPtr; + if (reader.isNull) { + return const []; + } else { + final list = List.filled(length, '', growable: true); + for (var i = 0; i < length; i++) { + list[i] = IsarCore.readString(reader, i) ?? ''; + } + IsarCore.freeReader(reader); + return list; + } + } + } default: throw ArgumentError('Unknown property: $property'); } @@ -1089,6 +1136,198 @@ extension PlaylistQueryFilter ); }); } + + QueryBuilder + videoIdsElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EqualCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementGreaterThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementGreaterThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + GreaterOrEqualCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementLessThan( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementLessThanOrEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + LessOrEqualCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementBetween( + String lower, + String upper, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + BetweenCondition( + property: 7, + lower: lower, + upper: upper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + StartsWithCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + EndsWithCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + ContainsCondition( + property: 7, + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + MatchesCondition( + property: 7, + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + videoIdsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const EqualCondition( + property: 7, + value: '', + ), + ); + }); + } + + QueryBuilder + videoIdsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterCondition( + property: 7, + value: '', + ), + ); + }); + } + + QueryBuilder videoIdsIsEmpty() { + return not().videoIdsIsNotEmpty(); + } + + QueryBuilder videoIdsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const GreaterOrEqualCondition(property: 7, value: null), + ); + }); + } } extension PlaylistQueryObject @@ -1298,6 +1537,12 @@ extension PlaylistQueryWhereDistinct return query.addDistinctBy(6); }); } + + QueryBuilder distinctByVideoIds() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(7); + }); + } } extension PlaylistQueryProperty1 @@ -1337,6 +1582,12 @@ extension PlaylistQueryProperty1 return query.addProperty(6); }); } + + QueryBuilder, QAfterProperty> videoIdsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(7); + }); + } } extension PlaylistQueryProperty2 @@ -1376,6 +1627,12 @@ extension PlaylistQueryProperty2 return query.addProperty(6); }); } + + QueryBuilder), QAfterProperty> videoIdsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(7); + }); + } } extension PlaylistQueryProperty3 @@ -1416,4 +1673,11 @@ extension PlaylistQueryProperty3 return query.addProperty(6); }); } + + QueryBuilder), QOperations> + videoIdsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addProperty(7); + }); + } } diff --git a/lib/provider/library_provider.dart b/lib/provider/library_provider.dart index 04170c3..f30e683 100644 --- a/lib/provider/library_provider.dart +++ b/lib/provider/library_provider.dart @@ -29,7 +29,7 @@ class LibraryProvider extends ChangeNotifier { ); entries.add(Playlist.fromYTPlaylist(playlist)); - isar.write((isar) => isar.playlists.put(entries.last)); + isar.writeAsyncWith(entries.last, (db, data) => db.playlists.put(data)); notifyListeners(); } diff --git a/lib/provider/playlist_provider.dart b/lib/provider/playlist_provider.dart index bc1f791..a46af8a 100644 --- a/lib/provider/playlist_provider.dart +++ b/lib/provider/playlist_provider.dart @@ -1,19 +1,34 @@ import 'package:flutter/foundation.dart'; +import 'package:isar/isar.dart'; import 'package:tube_sync/model/media.dart'; import 'package:tube_sync/model/playlist.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart' as yt; class PlaylistProvider extends ChangeNotifier { + final Isar isar; final _ytClient = yt.YoutubeExplode().playlists; final Playlist playlist; final List medias = List.empty(growable: true); - PlaylistProvider(this.playlist); + PlaylistProvider(this.isar, this.playlist) { + for (final id in playlist.videoIds) { + final media = isar.medias.where().idEqualTo(id).findFirst(); + if (media != null) medias.add(media); + } + } Future refresh() async { final vids = await _ytClient.getVideos(playlist.id).toList(); medias.clear(); medias.addAll(vids.map(Media.fromYTVideo)); + + // Update playlist + playlist.videoIds.clear(); + playlist.videoIds.addAll(medias.map((m) => m.id)); + // Save to DB + isar.writeAsyncWith(playlist, (db, data) => db.playlists.put(data)); + isar.writeAsyncWith(medias, (db, data) => db.medias.putAll(data)); + notifyListeners(); } }