when a song is played in search, now its mix gets played instead
This commit is contained in:
parent
8fd84c3929
commit
faec2af805
|
|
@ -47,5 +47,10 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
|
import 'package:freezer/api/paths.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
|
@ -8,46 +8,82 @@ part 'cache.g.dart';
|
||||||
|
|
||||||
late Cache cache;
|
late Cache cache;
|
||||||
|
|
||||||
|
class CacheEntryAdapter extends TypeAdapter<CacheEntry> {
|
||||||
|
@override
|
||||||
|
final int typeId = 20;
|
||||||
|
|
||||||
|
@override
|
||||||
|
CacheEntry read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return CacheEntry(
|
||||||
|
fields[0],
|
||||||
|
updatedAt: fields[1] as DateTime?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, CacheEntry obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(2)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.value)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is CacheEntryAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheEntry<T> {
|
||||||
|
final T value;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
CacheEntry(this.value, {DateTime? updatedAt})
|
||||||
|
: updatedAt = updatedAt ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
//Cache for miscellaneous things
|
//Cache for miscellaneous things
|
||||||
@HiveType(typeId: 22)
|
@HiveType(typeId: 22)
|
||||||
@JsonSerializable()
|
|
||||||
class Cache {
|
class Cache {
|
||||||
static Future<LazyBox<Cache>> get _box =>
|
static Future<LazyBox<Cache>> get _box async =>
|
||||||
Hive.openLazyBox<Cache>('metacache');
|
Hive.openLazyBox<Cache>('metacache', path: await Paths.cacheDir());
|
||||||
//ID's of tracks that are in library
|
//ID's of tracks that are in library
|
||||||
@HiveField(0, defaultValue: [])
|
@HiveField(0, defaultValue: [])
|
||||||
@JsonKey(defaultValue: [])
|
|
||||||
List<String> libraryTracks = [];
|
List<String> libraryTracks = [];
|
||||||
|
|
||||||
//Track ID of logged track, to prevent duplicates
|
//Track ID of logged track, to prevent duplicates
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
|
||||||
String? loggedTrackId;
|
String? loggedTrackId;
|
||||||
|
|
||||||
@HiveField(2)
|
@HiveField(2)
|
||||||
@JsonKey(defaultValue: [])
|
|
||||||
List<Track> history = [];
|
List<Track> history = [];
|
||||||
|
|
||||||
//All sorting cached
|
//All sorting cached
|
||||||
@HiveField(3)
|
@HiveField(3)
|
||||||
@JsonKey(defaultValue: [])
|
|
||||||
List<Sorting?> sorts = [];
|
List<Sorting?> sorts = [];
|
||||||
|
|
||||||
//Sleep timer
|
//Sleep timer
|
||||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
|
||||||
DateTime? sleepTimerTime;
|
DateTime? sleepTimerTime;
|
||||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
|
||||||
// ignore: cancel_subscriptions
|
// ignore: cancel_subscriptions
|
||||||
StreamSubscription? sleepTimer;
|
StreamSubscription? sleepTimer;
|
||||||
|
|
||||||
//Search history
|
//Search history
|
||||||
@HiveField(4)
|
@HiveField(4)
|
||||||
@JsonKey(name: 'searchHistory2', defaultValue: [])
|
List<DeezerMediaItem> searchHistory = [];
|
||||||
List<SearchHistoryItem> searchHistory = [];
|
|
||||||
|
|
||||||
//If download threads warning was shown
|
//If download threads warning was shown
|
||||||
@HiveField(5)
|
@HiveField(5)
|
||||||
@JsonKey(defaultValue: false)
|
|
||||||
bool threadsWarning = false;
|
bool threadsWarning = false;
|
||||||
|
|
||||||
//Last time update check
|
//Last time update check
|
||||||
|
|
@ -62,9 +98,17 @@ class Cache {
|
||||||
@HiveField(9, defaultValue: false)
|
@HiveField(9, defaultValue: false)
|
||||||
bool canStreamLossless = false;
|
bool canStreamLossless = false;
|
||||||
|
|
||||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
|
||||||
bool wakelock = false;
|
bool wakelock = false;
|
||||||
|
|
||||||
|
@HiveField(10, defaultValue: null)
|
||||||
|
CacheEntry<Map<String, Playlist>>? favoritePlaylists;
|
||||||
|
@HiveField(11, defaultValue: null)
|
||||||
|
CacheEntry<Map<String, Artist>>? favoriteArtists;
|
||||||
|
@HiveField(12, defaultValue: null)
|
||||||
|
CacheEntry<Map<String, Album>>? favoriteAlbums;
|
||||||
|
@HiveField(13, defaultValue: null)
|
||||||
|
CacheEntry<List<Track>>? favoriteTracks;
|
||||||
|
|
||||||
Cache();
|
Cache();
|
||||||
|
|
||||||
//Wrapper to test if track is favorite against cache
|
//Wrapper to test if track is favorite against cache
|
||||||
|
|
@ -74,27 +118,34 @@ class Cache {
|
||||||
return libraryTracks.contains(t.id);
|
return libraryTracks.contains(t.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add [item] to the corresponding favorite* cached item
|
||||||
|
void addFavorite(DeezerMediaItem item) {
|
||||||
|
switch (item) {
|
||||||
|
case final Track track:
|
||||||
|
favoriteTracks?.value.add(track);
|
||||||
|
libraryTracks.add(track.id);
|
||||||
|
break;
|
||||||
|
case final Album album:
|
||||||
|
favoriteAlbums?.value[album.id!] = album;
|
||||||
|
break;
|
||||||
|
case final Playlist playlist:
|
||||||
|
favoritePlaylists?.value[playlist.id] = playlist;
|
||||||
|
break;
|
||||||
|
case final Artist artist:
|
||||||
|
favoriteArtists?.value[artist.id] = artist;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Add to history
|
//Add to history
|
||||||
void addToSearchHistory(DeezerMediaItem item) async {
|
void addToSearchHistory(DeezerMediaItem item) async {
|
||||||
// Remove duplicate
|
// Remove duplicate
|
||||||
int i = searchHistory.indexWhere((e) => e.data.id == item.id);
|
int i = searchHistory.indexWhere((e) => e.id == item.id);
|
||||||
if (i != -1) {
|
if (i != -1) {
|
||||||
searchHistory.removeAt(i);
|
searchHistory.removeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is Track) {
|
searchHistory.add(item);
|
||||||
searchHistory.add(SearchHistoryItem(item, SearchHistoryItemType.track));
|
|
||||||
}
|
|
||||||
if (item is Album) {
|
|
||||||
searchHistory.add(SearchHistoryItem(item, SearchHistoryItemType.album));
|
|
||||||
}
|
|
||||||
if (item is Artist) {
|
|
||||||
searchHistory.add(SearchHistoryItem(item, SearchHistoryItemType.artist));
|
|
||||||
}
|
|
||||||
if (item is Playlist) {
|
|
||||||
searchHistory
|
|
||||||
.add(SearchHistoryItem(item, SearchHistoryItemType.playlist));
|
|
||||||
}
|
|
||||||
|
|
||||||
await save();
|
await save();
|
||||||
}
|
}
|
||||||
|
|
@ -134,64 +185,9 @@ class Cache {
|
||||||
await box.clear();
|
await box.clear();
|
||||||
await box.put(0, this);
|
await box.put(0, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
//JSON
|
|
||||||
factory Cache.fromJson(Map<String, dynamic> json) => _$CacheFromJson(json);
|
|
||||||
Map<String, dynamic> toJson() => _$CacheToJson(this);
|
|
||||||
|
|
||||||
//Search History JSON
|
|
||||||
// static List<SearchHistoryItem> _searchHistoryFromJson(List<dynamic>? json) {
|
|
||||||
// return (json ?? [])
|
|
||||||
// .map<SearchHistoryItem>((i) => _searchHistoryItemFromJson(i))
|
|
||||||
// .toList();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// static SearchHistoryItem _searchHistoryItemFromJson(
|
|
||||||
// Map<String, dynamic> json) {
|
|
||||||
// SearchHistoryItemType type = SearchHistoryItemType.values[json['type']];
|
|
||||||
// dynamic data;
|
|
||||||
// switch (type) {
|
|
||||||
// case SearchHistoryItemType.TRACK:
|
|
||||||
// data = Track.fromJson(json['data']);
|
|
||||||
// break;
|
|
||||||
// case SearchHistoryItemType.ALBUM:
|
|
||||||
// data = Album.fromJson(json['data']);
|
|
||||||
// break;
|
|
||||||
// case SearchHistoryItemType.ARTIST:
|
|
||||||
// data = Artist.fromJson(json['data']);
|
|
||||||
// break;
|
|
||||||
// case SearchHistoryItemType.PLAYLIST:
|
|
||||||
// data = Playlist.fromJson(json['data']);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// return SearchHistoryItem(data, type);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
@HiveType(typeId: 20)
|
|
||||||
@JsonSerializable()
|
|
||||||
class SearchHistoryItem {
|
|
||||||
@HiveField(0)
|
|
||||||
@JsonKey(
|
|
||||||
toJson: _searchHistoryItemTypeToJson,
|
|
||||||
fromJson: _searchHistoryItemTypeFromJson)
|
|
||||||
SearchHistoryItemType type;
|
|
||||||
@HiveField(1)
|
|
||||||
// TODO: make this type-safe
|
|
||||||
dynamic data;
|
|
||||||
|
|
||||||
SearchHistoryItem(this.data, this.type);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SearchHistoryItemToJson(this);
|
|
||||||
factory SearchHistoryItem.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SearchHistoryItemFromJson(json);
|
|
||||||
|
|
||||||
static int _searchHistoryItemTypeToJson(SearchHistoryItemType type) =>
|
|
||||||
type.index;
|
|
||||||
static SearchHistoryItemType _searchHistoryItemTypeFromJson(int index) =>
|
|
||||||
SearchHistoryItemType.values[index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@deprecated
|
||||||
@HiveType(typeId: 21)
|
@HiveType(typeId: 21)
|
||||||
enum SearchHistoryItemType {
|
enum SearchHistoryItemType {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cookie_jar/cookie_jar.dart';
|
import 'package:cookie_jar/cookie_jar.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
@ -44,8 +46,13 @@ class DeezerAPI {
|
||||||
cookieJar.delete(Uri.https('www.deezer.com'));
|
cookieJar.delete(Uri.https('www.deezer.com'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cookieJar
|
cookieJar.saveFromResponse(Uri.https('www.deezer.com'), [
|
||||||
.saveFromResponse(Uri.https('www.deezer.com'), [Cookie('arl', arl)]);
|
Cookie('arl', arl)
|
||||||
|
..domain = '.deezer.com'
|
||||||
|
..httpOnly = true
|
||||||
|
..sameSite = SameSite.none
|
||||||
|
..secure = true
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? token;
|
String? token;
|
||||||
|
|
@ -89,9 +96,24 @@ class DeezerAPI {
|
||||||
dio.options.headers = headers;
|
dio.options.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<dynamic, dynamic>> callPipeApi(
|
||||||
|
String operationName, String query, Map<String, dynamic> variables,
|
||||||
|
{CancelToken? cancelToken}) async {
|
||||||
|
final res = await dio.post('https://pipe.deezer.com/api',
|
||||||
|
data: jsonEncode({
|
||||||
|
'operationName': operationName,
|
||||||
|
'variables': variables,
|
||||||
|
'query': query,
|
||||||
|
}),
|
||||||
|
cancelToken: cancelToken);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
//Call private API
|
//Call private API
|
||||||
Future<Map<dynamic, dynamic>> callApi(String method,
|
Future<Map<dynamic, dynamic>> callApi(String method,
|
||||||
{Map<dynamic, dynamic>? params, String? gatewayInput}) async {
|
{Map<dynamic, dynamic>? params,
|
||||||
|
String? gatewayInput,
|
||||||
|
CancelToken? cancelToken}) async {
|
||||||
//Post
|
//Post
|
||||||
final res = await dio.post('https://www.deezer.com/ajax/gw-light.php',
|
final res = await dio.post('https://www.deezer.com/ajax/gw-light.php',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
|
|
@ -102,7 +124,8 @@ class DeezerAPI {
|
||||||
//Used for homepage
|
//Used for homepage
|
||||||
if (gatewayInput != null) 'gateway_input': gatewayInput
|
if (gatewayInput != null) 'gateway_input': gatewayInput
|
||||||
},
|
},
|
||||||
data: jsonEncode(params));
|
data: jsonEncode(params),
|
||||||
|
cancelToken: cancelToken);
|
||||||
final body = res.data;
|
final body = res.data;
|
||||||
|
|
||||||
// In case of error "Invalid CSRF token" retrieve new one and retry the same call
|
// In case of error "Invalid CSRF token" retrieve new one and retry the same call
|
||||||
|
|
@ -310,6 +333,17 @@ class DeezerAPI {
|
||||||
}).toList(growable: false);
|
}).toList(growable: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Not working
|
||||||
|
Future<(String, DateTime)> getTrackToken(String trackId) async {
|
||||||
|
final data = await callPipeApi(
|
||||||
|
'TrackMediaToken',
|
||||||
|
"query TrackMediaToken(\$trackId: String!) {\n track(trackId: \$trackId) {\n media {\n token {\n payload\n expiresAt\n __typename\n }\n __typename\n }\n __typename\n }\n}",
|
||||||
|
{'trackId': trackId},
|
||||||
|
);
|
||||||
|
|
||||||
|
return data['data']['track']['media']['token'];
|
||||||
|
}
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
Future<SearchResults> search(String? query) async {
|
Future<SearchResults> search(String? query) async {
|
||||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
||||||
|
|
@ -395,6 +429,9 @@ class DeezerAPI {
|
||||||
await callApi('artist.addFavorite', params: {'ART_ID': id});
|
await callApi('artist.addFavorite', params: {'ART_ID': id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> addFavoriteShow(String id) =>
|
||||||
|
callApi('show.addFavorite', params: {'SHOW_ID': id});
|
||||||
|
|
||||||
//Remove artist from favorites/library
|
//Remove artist from favorites/library
|
||||||
Future removeArtist(String? id) async {
|
Future removeArtist(String? id) async {
|
||||||
await callApi('artist.deleteFavorite', params: {'ART_ID': id});
|
await callApi('artist.deleteFavorite', params: {'ART_ID': id});
|
||||||
|
|
@ -430,7 +467,7 @@ class DeezerAPI {
|
||||||
//Get users playlists
|
//Get users playlists
|
||||||
Future<List<Playlist>> getPlaylists() async {
|
Future<List<Playlist>> getPlaylists() async {
|
||||||
Map data = await callApi('deezer.pageProfile',
|
Map data = await callApi('deezer.pageProfile',
|
||||||
params: {'nb': 100, 'tab': 'playlists', 'user_id': userId});
|
params: {'nb': 2000, 'tab': 'playlists', 'user_id': userId});
|
||||||
return data['results']['TAB']['playlists']['data']
|
return data['results']['TAB']['playlists']['data']
|
||||||
.map<Playlist>((json) => Playlist.fromPrivateJson(json, library: true))
|
.map<Playlist>((json) => Playlist.fromPrivateJson(json, library: true))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
@ -439,7 +476,7 @@ class DeezerAPI {
|
||||||
//Get favorite albums
|
//Get favorite albums
|
||||||
Future<List<Album>> getAlbums() async {
|
Future<List<Album>> getAlbums() async {
|
||||||
Map data = await callApi('deezer.pageProfile',
|
Map data = await callApi('deezer.pageProfile',
|
||||||
params: {'nb': 50, 'tab': 'albums', 'user_id': userId});
|
params: {'nb': 2000, 'tab': 'albums', 'user_id': userId});
|
||||||
List albumList = data['results']['TAB']['albums']['data'];
|
List albumList = data['results']['TAB']['albums']['data'];
|
||||||
List<Album> albums = albumList
|
List<Album> albums = albumList
|
||||||
.map<Album>((json) => Album.fromPrivateJson(json, library: true))
|
.map<Album>((json) => Album.fromPrivateJson(json, library: true))
|
||||||
|
|
@ -448,15 +485,18 @@ class DeezerAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remove album from library
|
//Remove album from library
|
||||||
Future removeAlbum(String? id) async {
|
Future<void> removeAlbum(String? id) async {
|
||||||
await callApi('album.deleteFavorite', params: {'ALB_ID': id});
|
await callApi('album.deleteFavorite', params: {'ALB_ID': id});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remove track from favorites
|
//Remove track from favorites
|
||||||
Future removeFavorite(String id) async {
|
Future<void> removeFavorite(String id) async {
|
||||||
await callApi('favorite_song.remove', params: {'SNG_ID': id});
|
await callApi('favorite_song.remove', params: {'SNG_ID': id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeFavoriteShow(String id) =>
|
||||||
|
callApi('show.deleteFavorite', params: {'SHOW_ID': id});
|
||||||
|
|
||||||
//Get favorite artists
|
//Get favorite artists
|
||||||
Future<List<Artist>?> getArtists() async {
|
Future<List<Artist>?> getArtists() async {
|
||||||
Map data = await callApi('deezer.pageProfile',
|
Map data = await callApi('deezer.pageProfile',
|
||||||
|
|
@ -467,11 +507,13 @@ class DeezerAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get lyrics by track id
|
//Get lyrics by track id
|
||||||
Future<Lyrics> lyrics(String? trackId) async {
|
Future<Lyrics> lyrics(String? trackId, {CancelToken? cancelToken}) async {
|
||||||
Map data = await callApi('song.getLyrics', params: {'sng_id': trackId});
|
Map data = await callApi('song.getLyrics',
|
||||||
|
params: {'sng_id': trackId}, cancelToken: cancelToken);
|
||||||
if (data['error'] != null && data['error'].length > 0) {
|
if (data['error'] != null && data['error'].length > 0) {
|
||||||
return Lyrics.error();
|
throw Exception('Deezer reported error: ${data['error']}');
|
||||||
}
|
}
|
||||||
|
print(data);
|
||||||
return Lyrics.fromPrivateJson(data['results']);
|
return Lyrics.fromPrivateJson(data['results']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,7 +547,8 @@ class DeezerAPI {
|
||||||
'show',
|
'show',
|
||||||
'smarttracklist',
|
'smarttracklist',
|
||||||
'track',
|
'track',
|
||||||
'user'
|
'user',
|
||||||
|
'external-link'
|
||||||
];
|
];
|
||||||
Map data = await callApi('page.get',
|
Map data = await callApi('page.get',
|
||||||
gatewayInput: jsonEncode({
|
gatewayInput: jsonEncode({
|
||||||
|
|
@ -663,9 +706,10 @@ class DeezerAPI {
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>?> searchSuggestions(String? query) async {
|
Future<List<String>?> searchSuggestions(String? query,
|
||||||
Map data =
|
{CancelToken? cancelToken}) async {
|
||||||
await callApi('search_getSuggestedQueries', params: {'QUERY': query});
|
Map data = await callApi('search_getSuggestedQueries',
|
||||||
|
params: {'QUERY': query}, cancelToken: cancelToken);
|
||||||
return (data['results']['SUGGESTION'] as List?)
|
return (data['results']['SUGGESTION'] as List?)
|
||||||
?.map<String>((s) => s['QUERY'] as String)
|
?.map<String>((s) => s['QUERY'] as String)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
@ -702,11 +746,24 @@ class DeezerAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get similar tracks for track with id [trackId]
|
//Get similar tracks for track with id [trackId]
|
||||||
Future<List<Track>?> playMix(String? trackId) async {
|
Future<List<Track>> playMix(String? trackId) async {
|
||||||
Map data = await callApi('song.getContextualTrackMix', params: {
|
Map data = await callApi('song.getContextualTrackMix', params: {
|
||||||
'sng_ids': [trackId]
|
'sng_ids': [trackId]
|
||||||
});
|
});
|
||||||
return data['results']['data']
|
return data['results']['data']!
|
||||||
|
.map<Track>((t) => Track.fromPrivateJson(t))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Track>> getSearchTrackMix(String trackId,
|
||||||
|
[bool? startWithInputTrack = true]) async {
|
||||||
|
Map data = await callApi('song.getSearchTrackMix', params: {
|
||||||
|
'sng_id': trackId,
|
||||||
|
if (startWithInputTrack != null)
|
||||||
|
'start_with_input_track': startWithInputTrack,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data['results']['data']!
|
||||||
.map<Track>((t) => Track.fromPrivateJson(t))
|
.map<Track>((t) => Track.fromPrivateJson(t))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
@ -725,3 +782,9 @@ class DeezerAPI {
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PipeAPI {
|
||||||
|
PipeAPI._();
|
||||||
|
|
||||||
|
Future<void> getTrackToken(String trackId) async {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class Track extends DeezerMediaItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
//MediaItem
|
//MediaItem
|
||||||
Future<MediaItem> toMediaItem() async {
|
MediaItem toMediaItem() {
|
||||||
return MediaItem(
|
return MediaItem(
|
||||||
title: title!,
|
title: title!,
|
||||||
album: album!.title!,
|
album: album!.title!,
|
||||||
|
|
@ -641,7 +641,7 @@ class DeezerImageDetails extends ImageDetails {
|
||||||
@override
|
@override
|
||||||
String get full => size(1000, 1000);
|
String get full => size(1000, 1000);
|
||||||
@override
|
@override
|
||||||
String get thumb => size(140, 140);
|
String get thumb => size(264, 264);
|
||||||
|
|
||||||
String size(int width, int height,
|
String size(int width, int height,
|
||||||
{int num = 80, String id = '000000', String format = 'jpg'}) =>
|
{int num = 80, String id = '000000', String format = 'jpg'}) =>
|
||||||
|
|
@ -957,18 +957,18 @@ class HomePageSection {
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
static HomePageSection? fromPrivateJson(Map<dynamic, dynamic> json) {
|
static HomePageSection? fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||||
final layout = {
|
final layout = const <String, HomePageSectionLayout>{
|
||||||
'horizontal-grid': HomePageSectionLayout.ROW,
|
'horizontal-grid': HomePageSectionLayout.row,
|
||||||
'filterable-grid': HomePageSectionLayout.ROW,
|
'filterable-grid': HomePageSectionLayout.row,
|
||||||
'grid-preview-two': HomePageSectionLayout.ROW,
|
'grid-preview-two': HomePageSectionLayout.row,
|
||||||
'grid': HomePageSectionLayout.GRID
|
'grid': HomePageSectionLayout.grid,
|
||||||
|
'slideshow': HomePageSectionLayout.slideshow,
|
||||||
}[json['layout'] ?? ''];
|
}[json['layout'] ?? ''];
|
||||||
if (layout == null) {
|
if (layout == null) {
|
||||||
_logger.warning('UNKNOWN LAYOUT: ${json['layout']}');
|
_logger.warning('UNKNOWN LAYOUT: ${json['layout']}');
|
||||||
_logger.warning('LAYOUT DATA:');
|
|
||||||
_logger.warning(json);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
_logger.fine('LAYOUT: $layout');
|
||||||
final items = <HomePageItem>[];
|
final items = <HomePageItem>[];
|
||||||
for (var i in (json['items'] ?? [])) {
|
for (var i in (json['items'] ?? [])) {
|
||||||
HomePageItem? hpi = HomePageItem.fromPrivateJson(i);
|
HomePageItem? hpi = HomePageItem.fromPrivateJson(i);
|
||||||
|
|
@ -1020,7 +1020,7 @@ class HomePageItem {
|
||||||
case 'channel':
|
case 'channel':
|
||||||
return HomePageItem(
|
return HomePageItem(
|
||||||
type: HomePageItemType.CHANNEL,
|
type: HomePageItemType.CHANNEL,
|
||||||
value: DeezerChannel.fromPrivateJson(json));
|
value: DeezerChannel.fromPrivateJson(json, false));
|
||||||
case 'album':
|
case 'album':
|
||||||
return HomePageItem(
|
return HomePageItem(
|
||||||
type: HomePageItemType.ALBUM,
|
type: HomePageItemType.ALBUM,
|
||||||
|
|
@ -1029,6 +1029,10 @@ class HomePageItem {
|
||||||
return HomePageItem(
|
return HomePageItem(
|
||||||
type: HomePageItemType.SHOW,
|
type: HomePageItemType.SHOW,
|
||||||
value: Show.fromPrivateJson(json['data']));
|
value: Show.fromPrivateJson(json['data']));
|
||||||
|
case 'external-link':
|
||||||
|
return HomePageItem(
|
||||||
|
type: HomePageItemType.EXTERNAL_LINK,
|
||||||
|
value: DeezerChannel.fromPrivateJson(json, true));
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1060,6 +1064,10 @@ class HomePageItem {
|
||||||
return HomePageItem(
|
return HomePageItem(
|
||||||
type: HomePageItemType.SHOW,
|
type: HomePageItemType.SHOW,
|
||||||
value: Show.fromPrivateJson(json['value']));
|
value: Show.fromPrivateJson(json['value']));
|
||||||
|
case 'EXTERNAL_LINK':
|
||||||
|
return HomePageItem(
|
||||||
|
type: HomePageItemType.EXTERNAL_LINK,
|
||||||
|
value: DeezerChannel.fromJson(json['value']));
|
||||||
default:
|
default:
|
||||||
throw Exception('Unexpected type $t for HomePageItem');
|
throw Exception('Unexpected type $t for HomePageItem');
|
||||||
}
|
}
|
||||||
|
|
@ -1090,15 +1098,21 @@ class DeezerChannel {
|
||||||
@HiveField(5, defaultValue: null)
|
@HiveField(5, defaultValue: null)
|
||||||
final DeezerImageDetails? picture;
|
final DeezerImageDetails? picture;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
@HiveField(6, defaultValue: false)
|
||||||
|
final bool isExternalLink;
|
||||||
|
|
||||||
const DeezerChannel(
|
const DeezerChannel(
|
||||||
{this.id,
|
{this.id,
|
||||||
this.title,
|
this.title,
|
||||||
this.backgroundColor = Colors.blue,
|
this.backgroundColor = Colors.blue,
|
||||||
this.target,
|
this.target,
|
||||||
this.logo,
|
this.logo,
|
||||||
this.picture});
|
this.picture,
|
||||||
|
this.isExternalLink = false});
|
||||||
|
|
||||||
factory DeezerChannel.fromPrivateJson(Map<dynamic, dynamic> json) =>
|
factory DeezerChannel.fromPrivateJson(
|
||||||
|
Map<dynamic, dynamic> json, bool isExternalLink) =>
|
||||||
DeezerChannel(
|
DeezerChannel(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
title: json['title'],
|
title: json['title'],
|
||||||
|
|
@ -1112,7 +1126,8 @@ class DeezerChannel {
|
||||||
: null,
|
: null,
|
||||||
picture: json.containsKey('pictures') && json['pictures'].length > 0
|
picture: json.containsKey('pictures') && json['pictures'].length > 0
|
||||||
? DeezerImageDetails.fromPrivateJson(json['pictures'][0])
|
? DeezerImageDetails.fromPrivateJson(json['pictures'][0])
|
||||||
: null);
|
: null,
|
||||||
|
isExternalLink: isExternalLink);
|
||||||
|
|
||||||
factory DeezerChannel.fromJson(Map<String, dynamic> json) =>
|
factory DeezerChannel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$DeezerChannelFromJson(json);
|
_$DeezerChannelFromJson(json);
|
||||||
|
|
@ -1136,14 +1151,21 @@ enum HomePageItemType {
|
||||||
ALBUM,
|
ALBUM,
|
||||||
@HiveField(5)
|
@HiveField(5)
|
||||||
SHOW,
|
SHOW,
|
||||||
|
|
||||||
|
@HiveField(6)
|
||||||
|
EXTERNAL_LINK,
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: 3)
|
@HiveType(typeId: 3)
|
||||||
enum HomePageSectionLayout {
|
enum HomePageSectionLayout {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
ROW,
|
row,
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
GRID,
|
grid,
|
||||||
|
|
||||||
|
/// ROW but bigger
|
||||||
|
@HiveField(2)
|
||||||
|
slideshow,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RepeatType { NONE, LIST, TRACK }
|
enum RepeatType { NONE, LIST, TRACK }
|
||||||
|
|
|
||||||
|
|
@ -338,15 +338,15 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get all offline available tracks
|
//Get all offline available tracks
|
||||||
Future<List<Track?>> allOfflineTracks() async {
|
Future<List<Track>> allOfflineTracks() async {
|
||||||
if (!isSupported) return [];
|
if (!isSupported) return [];
|
||||||
|
|
||||||
List rawTracks =
|
List rawTracks =
|
||||||
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
||||||
List<Track?> out = [];
|
List<Track> out = [];
|
||||||
//Load track meta individually
|
//Load track meta individually
|
||||||
for (Map rawTrack in rawTracks as Iterable<Map<dynamic, dynamic>>) {
|
for (Map rawTrack in rawTracks as Iterable<Map<dynamic, dynamic>>) {
|
||||||
out.add(await getOfflineTrack(rawTrack['id']));
|
out.add((await getOfflineTrack(rawTrack['id']))!);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ class DeezerImageDetails {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@collection
|
@embedded
|
||||||
class Lyrics {
|
class Lyrics {
|
||||||
late final String lyricsId;
|
late final String lyricsId;
|
||||||
late final String writers;
|
late final String writers;
|
||||||
|
|
|
||||||
|
|
@ -893,11 +893,13 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
case 'mix':
|
case 'mix':
|
||||||
tracks = await _deezerAPI.playMix(queueSource!.id);
|
tracks = await _deezerAPI.playMix(queueSource!.id);
|
||||||
// Deduplicate tracks with the same id
|
// Deduplicate tracks with the same id
|
||||||
List<String> queueIds = queue.value.map((e) => e.id).toList();
|
// List<String> queueIds = queue.value.map((e) => e.id).toList();
|
||||||
tracks?.removeWhere((track) => queueIds.contains(track.id));
|
// tracks?.removeWhere((track) => queueIds.contains(track.id));
|
||||||
break;
|
break;
|
||||||
case 'smarttracklist':
|
case 'smarttracklist':
|
||||||
tracks = (await _deezerAPI.smartTrackList(queueSource!.id!)).tracks;
|
tracks = (await _deezerAPI.smartTrackList(queueSource!.id!)).tracks;
|
||||||
|
case 'searchMix':
|
||||||
|
tracks = await _deezerAPI.getSearchTrackMix(queueSource!.id!, null);
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
// print(queueSource.toJson());
|
// print(queueSource.toJson());
|
||||||
|
|
@ -908,8 +910,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
'Failed to fetch more queue songs (queueSource: ${queueSource!.toJson()})');
|
'Failed to fetch more queue songs (queueSource: ${queueSource!.toJson()})');
|
||||||
}
|
}
|
||||||
|
|
||||||
final mi = await Future.wait(
|
final mi =
|
||||||
tracks.map<Future<MediaItem>>((t) => t.toMediaItem()));
|
tracks.map<MediaItem>((t) => t.toMediaItem()).toList(growable: false);
|
||||||
await addQueueItems(mi);
|
await addQueueItems(mi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,12 +185,12 @@ class PlayerHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Replace queue, play specified track id
|
//Replace queue, play specified track id
|
||||||
Future<void> _loadQueuePlay(List<MediaItem> queue, String? trackId) async {
|
Future<void> _loadQueuePlay(List<MediaItem> queue, int? index) async {
|
||||||
await settings.updateAudioServiceQuality();
|
await settings.updateAudioServiceQuality();
|
||||||
|
|
||||||
await audioHandler.customAction('setIndex', {
|
if (index != null) {
|
||||||
'index': trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId)
|
await audioHandler.customAction('setIndex', {'index': index});
|
||||||
});
|
}
|
||||||
await audioHandler.updateQueue(queue);
|
await audioHandler.updateQueue(queue);
|
||||||
// if (queue[0].id != trackId)
|
// if (queue[0].id != trackId)
|
||||||
// await AudioService.skipToQueueItem(trackId);
|
// await AudioService.skipToQueueItem(trackId);
|
||||||
|
|
@ -206,7 +206,7 @@ class PlayerHelper {
|
||||||
//Play mix by track
|
//Play mix by track
|
||||||
Future playMix(String trackId, String trackTitle) async {
|
Future playMix(String trackId, String trackTitle) async {
|
||||||
List<Track> tracks = (await deezerAPI.playMix(trackId))!;
|
List<Track> tracks = (await deezerAPI.playMix(trackId))!;
|
||||||
playFromTrackList(
|
await playFromTrackList(
|
||||||
tracks,
|
tracks,
|
||||||
tracks[0].id,
|
tracks[0].id,
|
||||||
QueueSource(
|
QueueSource(
|
||||||
|
|
@ -215,6 +215,35 @@ class PlayerHelper {
|
||||||
source: 'mix'));
|
source: 'mix'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> playSearchMixDeferred(Track track) async {
|
||||||
|
final playFuture = playFromTrackList(
|
||||||
|
[track],
|
||||||
|
null,
|
||||||
|
QueueSource(
|
||||||
|
id: track.id,
|
||||||
|
text: "${'Mix based on'.i18n} ${track.title}",
|
||||||
|
source: 'searchMix'));
|
||||||
|
List<Track> tracks = await deezerAPI.getSearchTrackMix(track.id, false);
|
||||||
|
// discard first track (if it is the searched track)
|
||||||
|
if (tracks[0].id == track.id) tracks.removeAt(0);
|
||||||
|
await playFuture; // avoid race conditions
|
||||||
|
|
||||||
|
// update queue with mix
|
||||||
|
await audioHandler.addQueueItems(
|
||||||
|
tracks.map((e) => e.toMediaItem()).toList(growable: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> playSearchMix(String trackId, String trackTitle) async {
|
||||||
|
List<Track> tracks = await deezerAPI.getSearchTrackMix(trackId, true);
|
||||||
|
await playFromTrackList(
|
||||||
|
tracks,
|
||||||
|
null, // we can avoid passing it, as the index is 0
|
||||||
|
QueueSource(
|
||||||
|
id: trackId,
|
||||||
|
text: "${'Mix based on'.i18n} $trackTitle",
|
||||||
|
source: 'searchMix'));
|
||||||
|
}
|
||||||
|
|
||||||
//Play from artist top tracks
|
//Play from artist top tracks
|
||||||
Future playFromTopTracks(
|
Future playFromTopTracks(
|
||||||
List<Track> tracks, String trackId, Artist artist) async {
|
List<Track> tracks, String trackId, Artist artist) async {
|
||||||
|
|
@ -249,17 +278,17 @@ class PlayerHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load tracks as queue, play track id, set queue source
|
//Load tracks as queue, play track id, set queue source
|
||||||
Future playFromTrackList(
|
Future<void> playFromTrackList(
|
||||||
List<Track?> tracks, String? trackId, QueueSource queueSource) async {
|
List<Track> tracks, String? trackId, QueueSource queueSource) async {
|
||||||
final queue = await Future.wait(tracks
|
final queue =
|
||||||
.map<Future<MediaItem>>((track) => track!.toMediaItem())
|
tracks.map<MediaItem>((track) => track!.toMediaItem()).toList();
|
||||||
.toList());
|
|
||||||
await setQueueSource(queueSource);
|
await setQueueSource(queueSource);
|
||||||
await _loadQueuePlay(queue, trackId);
|
await _loadQueuePlay(
|
||||||
|
queue, trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load smart track list as queue, start from beginning
|
//Load smart track list as queue, start from beginning
|
||||||
Future playFromSmartTrackList(SmartTrackList stl) async {
|
Future<void> playFromSmartTrackList(SmartTrackList stl) async {
|
||||||
//Load from API if no tracks
|
//Load from API if no tracks
|
||||||
if (stl.tracks == null || stl.tracks!.isEmpty) {
|
if (stl.tracks == null || stl.tracks!.isEmpty) {
|
||||||
if (settings.offlineMode) {
|
if (settings.offlineMode) {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ void main() async {
|
||||||
..registerAdapter(SortingAdapter())
|
..registerAdapter(SortingAdapter())
|
||||||
..registerAdapter(SortTypeAdapter())
|
..registerAdapter(SortTypeAdapter())
|
||||||
..registerAdapter(SortSourceTypesAdapter())
|
..registerAdapter(SortSourceTypesAdapter())
|
||||||
..registerAdapter(SearchHistoryItemAdapter())
|
..registerAdapter(CacheEntryAdapter())
|
||||||
..registerAdapter(SearchHistoryItemTypeAdapter())
|
..registerAdapter(SearchHistoryItemTypeAdapter())
|
||||||
..registerAdapter(CacheAdapter())
|
..registerAdapter(CacheAdapter())
|
||||||
..registerAdapter(ColorAdapter())
|
..registerAdapter(ColorAdapter())
|
||||||
|
|
@ -92,7 +92,7 @@ void main() async {
|
||||||
..registerAdapter(QueueSourceAdapter())
|
..registerAdapter(QueueSourceAdapter())
|
||||||
..registerAdapter(HomePageAdapter())
|
..registerAdapter(HomePageAdapter())
|
||||||
..registerAdapter(NavigationRailAppearanceAdapter())
|
..registerAdapter(NavigationRailAppearanceAdapter())
|
||||||
..registerAdapter(HiveCacheObjectAdapter(typeId: 35));
|
..registerAdapter(HiveCacheObjectAdapter(typeId: 35)); // not working?
|
||||||
|
|
||||||
Hive.init(await Paths.dataDirectory());
|
Hive.init(await Paths.dataDirectory());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,7 @@ class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
maxHeight: MediaQuery.of(context).size.height / 3),
|
maxHeight: MediaQuery.of(context).size.height / 3),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ZoomableImage(
|
child: ZoomableImage(
|
||||||
|
|
@ -368,7 +369,7 @@ class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
MediaQuery.of(context).size.width / 16, 60.0)),
|
MediaQuery.of(context).size.width / 16, 60.0)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|
@ -847,6 +848,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
setState(() {
|
setState(() {
|
||||||
playlist = p;
|
playlist = p;
|
||||||
});
|
});
|
||||||
|
// update cache
|
||||||
|
cache.favoritePlaylists?.value[playlist!.id] = p;
|
||||||
//Load tracks
|
//Load tracks
|
||||||
_load();
|
_load();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
|
|
@ -872,7 +875,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 4.0),
|
const SizedBox(height: 4.0),
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints.tight(
|
constraints: BoxConstraints.loose(
|
||||||
Size.fromHeight(MediaQuery.of(context).size.height / 3)),
|
Size.fromHeight(MediaQuery.of(context).size.height / 3)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|
|
||||||
51
lib/ui/external_link_route.dart
Normal file
51
lib/ui/external_link_route.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:cookie_jar/cookie_jar.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as fwv;
|
||||||
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
import 'package:freezer/settings.dart';
|
||||||
|
|
||||||
|
class ExternalLinkRoute extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String target;
|
||||||
|
const ExternalLinkRoute(
|
||||||
|
{required this.title, required this.target, super.key});
|
||||||
|
|
||||||
|
Uri _resolveTarget(String target) {
|
||||||
|
print('target: $target');
|
||||||
|
if (target == 'story/mdy' || target == '/story/mdy') {
|
||||||
|
// my deezer year redirect to iframe URL
|
||||||
|
return Uri.parse(
|
||||||
|
'https://mydeezerstory.deezer.com/inapp?campaign=mdy&lang=${settings.deezerLanguage}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve relative to www.deezer.com
|
||||||
|
return Uri.https('www.deezer.com', '/us').resolve(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> _resolveHeaders(Uri uri) async {
|
||||||
|
List<Cookie> cookies = await deezerAPI.cookieJar.loadForRequest(uri);
|
||||||
|
print(cookies);
|
||||||
|
return {'Cookie': cookies.join(';')};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Uri uriTarget = _resolveTarget(target);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(title)),
|
||||||
|
body: fwv.InAppWebView(
|
||||||
|
onWebViewCreated: (controller) async {
|
||||||
|
final uri = _resolveTarget(target);
|
||||||
|
final headers = await _resolveHeaders(uri);
|
||||||
|
controller.addWebMessageListener(fwv.WebMessageListener(
|
||||||
|
jsObjectName: 'jsObjectName',
|
||||||
|
onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) =>
|
||||||
|
print('message: $message, sourceOrigin: $sourceOrigin'),
|
||||||
|
));
|
||||||
|
controller.loadUrl(
|
||||||
|
urlRequest: fwv.URLRequest(url: uri, headers: headers));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/player/audio_handler.dart';
|
import 'package:freezer/api/player/audio_handler.dart';
|
||||||
import 'package:freezer/main.dart';
|
import 'package:freezer/main.dart';
|
||||||
import 'package:freezer/ui/error.dart';
|
import 'package:freezer/ui/error.dart';
|
||||||
|
import 'package:freezer/ui/external_link_route.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'tiles.dart';
|
import 'tiles.dart';
|
||||||
|
|
@ -43,15 +44,26 @@ class HomeScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
final actualScrollConfiguration = ScrollConfiguration.of(context);
|
||||||
child: Scaffold(
|
return ScrollConfiguration(
|
||||||
body: NestedScrollView(
|
behavior: actualScrollConfiguration.copyWith(
|
||||||
floatHeaderSlivers: true,
|
dragDevices: {
|
||||||
headerSliverBuilder: (context, _) => [
|
PointerDeviceKind.mouse,
|
||||||
SliverPersistentHeader(
|
PointerDeviceKind.stylus,
|
||||||
delegate: _SearchHeaderDelegate(), floating: true)
|
PointerDeviceKind.touch,
|
||||||
],
|
PointerDeviceKind.trackpad
|
||||||
body: const HomePageWidget(cacheable: true),
|
},
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
body: NestedScrollView(
|
||||||
|
floatHeaderSlivers: true,
|
||||||
|
headerSliverBuilder: (context, _) => [
|
||||||
|
SliverPersistentHeader(
|
||||||
|
delegate: _SearchHeaderDelegate(), floating: true)
|
||||||
|
],
|
||||||
|
body: const HomePageWidget(cacheable: true),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -189,9 +201,10 @@ class _HomePageWidgetState extends State<HomePageWidget> {
|
||||||
|
|
||||||
Widget getSectionChild(HomePageSection section) {
|
Widget getSectionChild(HomePageSection section) {
|
||||||
switch (section.layout) {
|
switch (section.layout) {
|
||||||
case HomePageSectionLayout.GRID:
|
case HomePageSectionLayout.grid:
|
||||||
return HomePageGridSection(section);
|
return HomePageGridSection(section);
|
||||||
case HomePageSectionLayout.ROW:
|
case HomePageSectionLayout.slideshow:
|
||||||
|
case HomePageSectionLayout.row:
|
||||||
default:
|
default:
|
||||||
return HomepageRowSection(section);
|
return HomepageRowSection(section);
|
||||||
}
|
}
|
||||||
|
|
@ -205,33 +218,22 @@ class _HomePageWidgetState extends State<HomePageWidget> {
|
||||||
sections = _homePage!.sections;
|
sections = _homePage!.sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
final actualScrollConfiguration = ScrollConfiguration.of(context);
|
return RefreshIndicator(
|
||||||
return ScrollConfiguration(
|
key: _indicatorKey,
|
||||||
behavior: actualScrollConfiguration.copyWith(
|
onRefresh: _load,
|
||||||
dragDevices: {
|
child: _homePage == null
|
||||||
PointerDeviceKind.mouse,
|
? const SizedBox.expand(child: SingleChildScrollView())
|
||||||
PointerDeviceKind.stylus,
|
: ListView.builder(
|
||||||
PointerDeviceKind.touch,
|
itemBuilder: (context, index) {
|
||||||
PointerDeviceKind.trackpad
|
return Padding(
|
||||||
},
|
padding: index == 0
|
||||||
),
|
? EdgeInsets.zero
|
||||||
child: RefreshIndicator(
|
: const EdgeInsets.only(top: 16.0),
|
||||||
key: _indicatorKey,
|
child: getSectionChild(sections![index]));
|
||||||
onRefresh: _load,
|
},
|
||||||
child: _homePage == null
|
itemCount: sections!.length,
|
||||||
? const SizedBox.expand(child: SingleChildScrollView())
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
: ListView.builder(
|
),
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return Padding(
|
|
||||||
padding: index == 0
|
|
||||||
? EdgeInsets.zero
|
|
||||||
: const EdgeInsets.only(top: 16.0),
|
|
||||||
child: getSectionChild(sections![index]));
|
|
||||||
},
|
|
||||||
itemCount: sections!.length,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -356,7 +358,8 @@ class HomePageGridSection extends StatelessWidget {
|
||||||
|
|
||||||
class HomePageItemWidget extends StatelessWidget {
|
class HomePageItemWidget extends StatelessWidget {
|
||||||
final HomePageItem item;
|
final HomePageItem item;
|
||||||
const HomePageItemWidget(this.item, {super.key});
|
final Size? itemSize;
|
||||||
|
const HomePageItemWidget(this.item, {super.key, this.itemSize});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -415,6 +418,19 @@ class HomePageItemWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
case HomePageItemType.EXTERNAL_LINK:
|
||||||
|
return ChannelTile(
|
||||||
|
item.value,
|
||||||
|
onTap: () {
|
||||||
|
final channel = item.value as DeezerChannel;
|
||||||
|
Navigator.of(context).pushRoute(
|
||||||
|
builder: (context) => ExternalLinkRoute(
|
||||||
|
target: channel.target!,
|
||||||
|
title: channel.title ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
case HomePageItemType.SHOW:
|
case HomePageItemType.SHOW:
|
||||||
return ShowCard(
|
return ShowCard(
|
||||||
item.value,
|
item.value,
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
bool _loadingTracks = false;
|
bool _loadingTracks = false;
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
List<Track> tracks = [];
|
List<Track> tracks = [];
|
||||||
List<Track?> allTracks = [];
|
List<Track> allTracks = [];
|
||||||
int? trackCount;
|
int? trackCount;
|
||||||
Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
|
Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
|
||||||
|
|
||||||
|
|
@ -377,7 +377,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _loadAllOffline() async {
|
Future _loadAllOffline() async {
|
||||||
List<Track?> tracks = await downloadManager.allOfflineTracks();
|
List<Track> tracks = await downloadManager.allOfflineTracks();
|
||||||
setState(() {
|
setState(() {
|
||||||
allTracks = tracks;
|
allTracks = tracks;
|
||||||
});
|
});
|
||||||
|
|
@ -955,10 +955,33 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _load() async {
|
Future _load() async {
|
||||||
|
if (cache.favoritePlaylists != null) {
|
||||||
|
setState(() => _playlists =
|
||||||
|
cache.favoritePlaylists!.value.values.toList(growable: false));
|
||||||
|
|
||||||
|
if (DateTime.now().difference(cache.favoritePlaylists!.updatedAt) <
|
||||||
|
const Duration(hours: 1)) return;
|
||||||
|
}
|
||||||
if (!settings.offlineMode) {
|
if (!settings.offlineMode) {
|
||||||
try {
|
try {
|
||||||
final List<Playlist> playlists = await deezerAPI.getPlaylists();
|
final List<Playlist> playlists = await deezerAPI.getPlaylists();
|
||||||
setState(() => _playlists = playlists);
|
setState(() => _playlists = playlists);
|
||||||
|
if (cache.favoritePlaylists == null) {
|
||||||
|
cache.favoritePlaylists =
|
||||||
|
CacheEntry({for (final p in playlists) p.id: p});
|
||||||
|
} else {
|
||||||
|
// update non-destructively
|
||||||
|
final oldEntry = cache.favoritePlaylists!.value;
|
||||||
|
final newEntry = <String, Playlist>{};
|
||||||
|
for (final playlist in playlists) {
|
||||||
|
if (oldEntry.containsKey(playlist.id)) {
|
||||||
|
newEntry[playlist.id] = oldEntry[playlist.id]!;
|
||||||
|
} else {
|
||||||
|
newEntry[playlist.id] = playlist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
return;
|
return;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
|
@ -48,13 +49,14 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
late StreamSubscription _mediaItemSub;
|
late StreamSubscription _mediaItemSub;
|
||||||
late StreamSubscription _playbackStateSub;
|
late StreamSubscription _playbackStateSub;
|
||||||
int? _currentIndex = -1;
|
int? _currentIndex = -1;
|
||||||
Duration _nextPosition = Duration.zero;
|
Duration _nextOffset = Duration.zero;
|
||||||
|
Duration _currentOffset = Duration.zero;
|
||||||
final ScrollController _controller = ScrollController();
|
final ScrollController _controller = ScrollController();
|
||||||
final double height = 90;
|
final double height = 90;
|
||||||
BoxConstraints? _widgetConstraints;
|
BoxConstraints? _widgetConstraints;
|
||||||
Lyrics? _lyrics;
|
Lyrics? _lyrics;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
CancelableOperation<Lyrics>? _lyricsCancelable;
|
CancelToken? _lyricsCancelToken;
|
||||||
Object? _error;
|
Object? _error;
|
||||||
|
|
||||||
bool _freeScroll = false;
|
bool _freeScroll = false;
|
||||||
|
|
@ -63,7 +65,10 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
|
|
||||||
Future<void> _loadForId(String trackId) async {
|
Future<void> _loadForId(String trackId) async {
|
||||||
// cancel current request, if applicable
|
// cancel current request, if applicable
|
||||||
await _lyricsCancelable?.cancel();
|
_lyricsCancelToken?.cancel();
|
||||||
|
_currentIndex = -1;
|
||||||
|
_currentOffset = Duration.zero;
|
||||||
|
_nextOffset = Duration.zero;
|
||||||
|
|
||||||
//Fetch
|
//Fetch
|
||||||
if (_loading == false && _lyrics != null) {
|
if (_loading == false && _lyrics != null) {
|
||||||
|
|
@ -75,10 +80,9 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_lyricsCancelable =
|
_lyricsCancelToken = CancelToken();
|
||||||
CancelableOperation.fromFuture(deezerAPI.lyrics(trackId));
|
final lyrics =
|
||||||
final lyrics = await _lyricsCancelable!.valueOrCancellation(null);
|
await deezerAPI.lyrics(trackId, cancelToken: _lyricsCancelToken);
|
||||||
if (lyrics == null) return;
|
|
||||||
_syncedLyrics = lyrics.sync;
|
_syncedLyrics = lyrics.sync;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -86,9 +90,10 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
_lyrics = lyrics;
|
_lyrics = lyrics;
|
||||||
});
|
});
|
||||||
|
|
||||||
_nextPosition = Duration.zero;
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback(
|
SchedulerBinding.instance.addPostFrameCallback(
|
||||||
(_) => _updatePosition(audioHandler.playbackState.value.position));
|
(_) => _updatePosition(audioHandler.playbackState.value.position));
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type != DioExceptionType.cancel) rethrow;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -126,21 +131,22 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
void _updatePosition(Duration position) {
|
void _updatePosition(Duration position) {
|
||||||
if (_loading) return;
|
if (_loading) return;
|
||||||
if (!_syncedLyrics) return;
|
if (!_syncedLyrics) return;
|
||||||
if (position < _nextPosition) return;
|
if (position < _nextOffset && position > _currentOffset) return;
|
||||||
|
|
||||||
_currentIndex =
|
_currentIndex =
|
||||||
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
|
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
|
||||||
//Scroll to current lyric
|
|
||||||
if (_currentIndex! < 0) return;
|
if (_currentIndex! < 0) return;
|
||||||
//Update current lyric index
|
|
||||||
if (_currentIndex! < _lyrics!.lyrics!.length) {
|
if (_currentIndex! < _lyrics!.lyrics!.length - 1) {
|
||||||
// update nextPosition
|
// update nextOffset
|
||||||
_nextPosition = _lyrics!.lyrics![_currentIndex! + 1].offset!;
|
_nextOffset = _lyrics!.lyrics![_currentIndex! + 1].offset!;
|
||||||
} else {
|
} else {
|
||||||
// dummy position so that the before-hand condition always returns false
|
// dummy position so that the before-hand condition always returns false
|
||||||
_nextPosition = const Duration(days: 69);
|
_nextOffset = const Duration(days: 69);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_currentOffset = _lyrics!.lyrics![_currentIndex!].offset!;
|
||||||
|
|
||||||
setState(() => _currentIndex);
|
setState(() => _currentIndex);
|
||||||
if (_freeScroll) return;
|
if (_freeScroll) return;
|
||||||
_scrollToLyric();
|
_scrollToLyric();
|
||||||
|
|
@ -153,9 +159,6 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
||||||
_playbackStateSub = AudioService.position.listen(_updatePosition);
|
_playbackStateSub = AudioService.position.listen(_updatePosition);
|
||||||
});
|
});
|
||||||
if (audioHandler.mediaItem.value != null) {
|
|
||||||
_loadForId(audioHandler.mediaItem.value!.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Track change = ~exit~ reload lyrics
|
/// Track change = ~exit~ reload lyrics
|
||||||
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
|
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,13 @@ class MenuSheet {
|
||||||
Text('Play mix'.i18n),
|
Text('Play mix'.i18n),
|
||||||
icon: const Icon(Icons.online_prediction),
|
icon: const Icon(Icons.online_prediction),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
playerHelper.playMix(track.id, track.title!);
|
// I couldn't find this API request within the Deezer app, but the
|
||||||
|
// same button uses the getSearchTrackMix API call, so let's use that
|
||||||
|
// instead.
|
||||||
|
|
||||||
|
// playerHelper.playMix(track.id, track.title!);
|
||||||
|
|
||||||
|
playerHelper.playSearchMix(track.id, track.title!);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,24 @@ class PlayerBar extends StatelessWidget {
|
||||||
initialData: audioHandler.mediaItem.valueOrNull,
|
initialData: audioHandler.mediaItem.valueOrNull,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.data == null) {
|
if (snapshot.data == null) {
|
||||||
return Material(
|
// lazy way to prevent dragging up
|
||||||
child: ListTile(
|
return GestureDetector(
|
||||||
dense: true,
|
behavior: HitTestBehavior.opaque,
|
||||||
visualDensity: VisualDensity.standard,
|
onVerticalDragEnd: (_) {},
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
onVerticalDragUpdate: (_) {},
|
||||||
horizontal: 16.0, vertical: 6.0),
|
child: Material(
|
||||||
leading: Image.asset(
|
child: ListTile(
|
||||||
'assets/cover_thumb.jpg',
|
dense: true,
|
||||||
width: 48.0,
|
visualDensity: VisualDensity.standard,
|
||||||
height: 48.0,
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0, vertical: 6.0),
|
||||||
|
leading: Image.asset(
|
||||||
|
'assets/cover_thumb.jpg',
|
||||||
|
width: 48.0,
|
||||||
|
height: 48.0,
|
||||||
|
),
|
||||||
|
title: Text('Nothing is currently playing'.i18n),
|
||||||
),
|
),
|
||||||
title: Text('Nothing is currently playing'.i18n),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +78,7 @@ class PlayerBar extends StatelessWidget {
|
||||||
? Hero(tag: currentMediaItem.id, child: image)
|
? Hero(tag: currentMediaItem.id, child: image)
|
||||||
: image;
|
: image;
|
||||||
return Material(
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
tileColor: _backgroundColor,
|
tileColor: _backgroundColor,
|
||||||
|
|
|
||||||
|
|
@ -333,9 +333,11 @@ class PlayerScreenDesktop extends StatelessWidget {
|
||||||
showQueueButton: false,
|
showQueueButton: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ConstrainedBox(
|
Flexible(
|
||||||
constraints: BoxConstraints.loose(const Size.square(500)),
|
child: ConstrainedBox(
|
||||||
child: const BigAlbumArt()),
|
constraints: BoxConstraints.loose(const Size.square(500)),
|
||||||
|
child: const BigAlbumArt()),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: PlayerTextSubtext(textSize: 18.sp),
|
child: PlayerTextSubtext(textSize: 18.sp),
|
||||||
|
|
@ -435,7 +437,6 @@ class _FitOrScrollTextState extends State<FitOrScrollText> {
|
||||||
);
|
);
|
||||||
|
|
||||||
textPainter.layout(maxWidth: constraints.maxWidth);
|
textPainter.layout(maxWidth: constraints.maxWidth);
|
||||||
print(textPainter.didExceedMaxLines);
|
|
||||||
|
|
||||||
return !(textPainter.didExceedMaxLines ||
|
return !(textPainter.didExceedMaxLines ||
|
||||||
textPainter.height > constraints.maxHeight ||
|
textPainter.height > constraints.maxHeight ||
|
||||||
|
|
@ -458,7 +459,7 @@ class _FitOrScrollTextState extends State<FitOrScrollText> {
|
||||||
startPadding: 0.0,
|
startPadding: 0.0,
|
||||||
accelerationDuration: const Duration(seconds: 1),
|
accelerationDuration: const Duration(seconds: 1),
|
||||||
pauseAfterRound: const Duration(seconds: 2),
|
pauseAfterRound: const Duration(seconds: 2),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
fadingEdgeEndFraction: 0.05,
|
fadingEdgeEndFraction: 0.05,
|
||||||
fadingEdgeStartFraction: 0.05,
|
fadingEdgeStartFraction: 0.05,
|
||||||
);
|
);
|
||||||
|
|
@ -483,12 +484,15 @@ class PlayerTextSubtext extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FitOrScrollText(
|
SizedBox(
|
||||||
key: Key(currentMediaItem.displayTitle!),
|
height: 1.5 * textSize,
|
||||||
text: currentMediaItem.displayTitle!,
|
child: FitOrScrollText(
|
||||||
maxLines: 1,
|
key: Key(currentMediaItem.displayTitle!),
|
||||||
style: TextStyle(
|
text: currentMediaItem.displayTitle!,
|
||||||
fontSize: textSize, fontWeight: FontWeight.bold)),
|
maxLines: 1,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: textSize, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
// child: currentMediaItem.displayTitle!.length >= 26
|
// child: currentMediaItem.displayTitle!.length >= 26
|
||||||
// ? Marquee(
|
// ? Marquee(
|
||||||
// key: Key(currentMediaItem.displayTitle!),
|
// key: Key(currentMediaItem.displayTitle!),
|
||||||
|
|
@ -511,7 +515,6 @@ class PlayerTextSubtext extends StatelessWidget {
|
||||||
// style: TextStyle(
|
// style: TextStyle(
|
||||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||||
// )),
|
// )),
|
||||||
const SizedBox(height: 2.0),
|
|
||||||
Text(
|
Text(
|
||||||
currentMediaItem.displaySubtitle ?? '',
|
currentMediaItem.displaySubtitle ?? '',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
@ -59,6 +60,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
final _suggestions = ListNotifier<String>([]);
|
final _suggestions = ListNotifier<String>([]);
|
||||||
final _showingSuggestions = ValueNotifier(false);
|
final _showingSuggestions = ValueNotifier(false);
|
||||||
final _loading = ValueNotifier(false);
|
final _loading = ValueNotifier(false);
|
||||||
|
CancelToken? _searchCancelToken;
|
||||||
Timer? _searchTimer;
|
Timer? _searchTimer;
|
||||||
final _focus = FocusNode();
|
final _focus = FocusNode();
|
||||||
final _textFieldFocusNode = FocusNode();
|
final _textFieldFocusNode = FocusNode();
|
||||||
|
|
@ -121,16 +123,24 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
_controller.text.length < 2 ||
|
_controller.text.length < 2 ||
|
||||||
_controller.text.startsWith('http')) return;
|
_controller.text.startsWith('http')) return;
|
||||||
_loading.value = true;
|
_loading.value = true;
|
||||||
|
_searchCancelToken?.cancel();
|
||||||
|
|
||||||
//Load
|
//Load
|
||||||
List<String>? sugg;
|
final List<String>? suggestions;
|
||||||
try {
|
try {
|
||||||
sugg = await deezerAPI.searchSuggestions(_controller.text);
|
_searchCancelToken = CancelToken();
|
||||||
|
suggestions = await deezerAPI.searchSuggestions(_controller.text,
|
||||||
|
cancelToken: _searchCancelToken);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.type != DioExceptionType.cancel) rethrow;
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print(e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
_loading.value = false;
|
|
||||||
|
|
||||||
if (sugg != null) _suggestions.value = sugg;
|
_loading.value = false;
|
||||||
|
if (suggestions != null) _suggestions.value = suggestions;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _removeHistoryItemWidget(int index) {
|
Widget _removeHistoryItemWidget(int index) {
|
||||||
|
|
@ -193,8 +203,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
_suggestions.clear();
|
_suggestions.clear();
|
||||||
} else {
|
} else {
|
||||||
_searchTimer ??= Timer(
|
_searchTimer ??=
|
||||||
const Duration(milliseconds: 300), () {
|
Timer(const Duration(milliseconds: 1), () {
|
||||||
_searchTimer = null;
|
_searchTimer = null;
|
||||||
_loadSuggestions();
|
_loadSuggestions();
|
||||||
});
|
});
|
||||||
|
|
@ -274,16 +284,13 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
),
|
),
|
||||||
...List.generate(min(cache.searchHistory.length, 10),
|
...List.generate(min(cache.searchHistory.length, 10),
|
||||||
(int i) {
|
(int i) {
|
||||||
final data = cache.searchHistory[i].data;
|
switch (cache.searchHistory[i]) {
|
||||||
switch (cache.searchHistory[i].type) {
|
case final Track data:
|
||||||
case SearchHistoryItemType.track:
|
|
||||||
return TrackTile.fromTrack(
|
return TrackTile.fromTrack(
|
||||||
data,
|
data,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
List<Track?> queue = cache.searchHistory
|
final queue = cache.searchHistory
|
||||||
.where((h) =>
|
.whereType<Track>()
|
||||||
h.type == SearchHistoryItemType.track)
|
|
||||||
.map<Track>((t) => t.data)
|
|
||||||
.toList();
|
.toList();
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playFromTrackList(
|
||||||
queue,
|
queue,
|
||||||
|
|
@ -297,7 +304,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
.defaultTrackMenu(data, details: details),
|
.defaultTrackMenu(data, details: details),
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
case SearchHistoryItemType.album:
|
case final Album data:
|
||||||
return AlbumTile(
|
return AlbumTile(
|
||||||
data,
|
data,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
@ -308,7 +315,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
.defaultAlbumMenu(data, details: details),
|
.defaultAlbumMenu(data, details: details),
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
case SearchHistoryItemType.artist:
|
case final Artist data:
|
||||||
return ArtistHorizontalTile(
|
return ArtistHorizontalTile(
|
||||||
data,
|
data,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
@ -320,7 +327,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
MenuSheet(context).defaultArtistMenu(data),
|
MenuSheet(context).defaultArtistMenu(data),
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
case SearchHistoryItemType.playlist:
|
case final Playlist data:
|
||||||
return PlaylistTile(
|
return PlaylistTile(
|
||||||
data,
|
data,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
@ -480,13 +487,7 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
.getRange(0, min(results.tracks!.length, 3)))
|
.getRange(0, min(results.tracks!.length, 3)))
|
||||||
TrackTile.fromTrack(track, onTap: () {
|
TrackTile.fromTrack(track, onTap: () {
|
||||||
cache.addToSearchHistory(track);
|
cache.addToSearchHistory(track);
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playSearchMixDeferred(track);
|
||||||
results.tracks!,
|
|
||||||
track.id,
|
|
||||||
QueueSource(
|
|
||||||
text: 'Search'.i18n,
|
|
||||||
id: query,
|
|
||||||
source: 'search'));
|
|
||||||
}, onSecondary: (details) {
|
}, onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(track, details: details);
|
m.defaultTrackMenu(track, details: details);
|
||||||
|
|
@ -495,12 +496,8 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
title: Text('Show all tracks'.i18n),
|
title: Text('Show all tracks'.i18n),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => TrackListScreen(
|
builder: (context) =>
|
||||||
results.tracks,
|
TrackListScreen(results.tracks, null));
|
||||||
QueueSource(
|
|
||||||
id: query,
|
|
||||||
source: 'search',
|
|
||||||
text: 'Search'.i18n)));
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
|
|
@ -699,8 +696,8 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
|
|
||||||
//List all tracks
|
//List all tracks
|
||||||
class TrackListScreen extends StatelessWidget {
|
class TrackListScreen extends StatelessWidget {
|
||||||
final QueueSource queueSource;
|
final QueueSource? queueSource;
|
||||||
final List<Track?>? tracks;
|
final List<Track>? tracks;
|
||||||
|
|
||||||
const TrackListScreen(this.tracks, this.queueSource, {super.key});
|
const TrackListScreen(this.tracks, this.queueSource, {super.key});
|
||||||
|
|
||||||
|
|
@ -711,11 +708,16 @@ class TrackListScreen extends StatelessWidget {
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: tracks!.length,
|
itemCount: tracks!.length,
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
Track t = tracks![i]!;
|
Track t = tracks![i];
|
||||||
return TrackTile.fromTrack(
|
return TrackTile.fromTrack(
|
||||||
t,
|
t,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playerHelper.playFromTrackList(tracks!, t.id, queueSource);
|
if (queueSource == null) {
|
||||||
|
playerHelper.playSearchMixDeferred(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerHelper.playFromTrackList(tracks!, t.id, queueSource!);
|
||||||
},
|
},
|
||||||
onSecondary: (details) {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,10 @@
|
||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue