Compare commits
10 Commits
b8f0bb2140
...
10133755b7
Author | SHA1 | Date |
---|---|---|
Pato05 | 10133755b7 | |
Pato05 | fa12f3120d | |
Pato05 | 99fef45fb8 | |
Pato05 | e782643ee0 | |
Pato05 | 2a5a51e43f | |
Pato05 | a7661d168b | |
Pato05 | 019961ca85 | |
Pato05 | c28256f258 | |
Pato05 | c42b9bc8e2 | |
Pato05 | 15490444a9 |
|
@ -66,3 +66,7 @@ testfiles/
|
|||
|
||||
# Exceptions to above rules.
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
|
||||
translations/crowdin.zip
|
||||
translations/freezer.json
|
||||
translations/exp
|
||||
|
|
20
.metadata
20
.metadata
|
@ -4,7 +4,7 @@
|
|||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "2f708eb8396e362e280fac22cf171c2cb467343c"
|
||||
revision: "41456452f29d64e8deb623a3c927524bcf9f111b"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
@ -13,20 +13,14 @@ project_type: app
|
|||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
- platform: ios
|
||||
create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: linux
|
||||
create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
- platform: macos
|
||||
create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: windows
|
||||
create_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
base_revision: 2f708eb8396e362e280fac22cf171c2cb467343c
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
|
||||
# User provided section
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
}
|
|
@ -60,7 +60,7 @@ android {
|
|||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
shrinkResources true
|
||||
shrinkResources false
|
||||
minifyEnabled true
|
||||
}
|
||||
debug {
|
||||
|
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 814 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "FreezerIcons",
|
||||
"css_prefix_text": "",
|
||||
"css_use_suffix": false,
|
||||
"hinting": true,
|
||||
"units_per_em": 1000,
|
||||
"ascent": 850,
|
||||
"glyphs": [
|
||||
{
|
||||
"uid": "efc3f240ef9ea8ea869a4136920bfefb",
|
||||
"css": "spotify",
|
||||
"code": 61884,
|
||||
"src": "fontawesome5"
|
||||
},
|
||||
{
|
||||
"uid": "1e8b62f6676597cb577d0c5252b65da5",
|
||||
"css": "sort_alpha_down",
|
||||
"code": 61789,
|
||||
"src": "fontawesome5"
|
||||
},
|
||||
{
|
||||
"uid": "04107d9128cc4ad96ff0d33e7853b34b",
|
||||
"css": "sort_alpha_up",
|
||||
"code": 61790,
|
||||
"src": "fontawesome5"
|
||||
},
|
||||
{
|
||||
"uid": "fd8d9ae4422e55d3ca23f55d9cf4b20a",
|
||||
"css": "waves",
|
||||
"code": 59392,
|
||||
"src": "typicons"
|
||||
},
|
||||
{
|
||||
"uid": "e011416dc6fca0b491d7857b1a4adfae",
|
||||
"css": "lastfm",
|
||||
"code": 61954,
|
||||
"src": "fontawesome5"
|
||||
},
|
||||
{
|
||||
"uid": "e9f1a450968111eab72aacfdce5a3d9f",
|
||||
"css": "primitive-dot",
|
||||
"code": 62220,
|
||||
"src": "octicons"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -47,10 +47,16 @@
|
|||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<!-- For HTTP local server -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<!-- Set Background mode for playing audio -->
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
|
||||
class HiveStorage implements Storage {
|
||||
final String boxName;
|
||||
final String? boxPath;
|
||||
HiveStorage(this.boxName, {this.boxPath});
|
||||
|
||||
bool _initialized = false;
|
||||
late final Box<String> _box;
|
||||
|
||||
Future<void>? _initFuture;
|
||||
|
||||
Future<void> init(bool persistSession, bool ignoreExpires) =>
|
||||
_initFuture ??= _init(persistSession, ignoreExpires);
|
||||
|
||||
Future<void> _init(bool persistSession, bool ignoreExpires) async {
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
_box = await Hive.openBox(boxName, path: boxPath);
|
||||
print('init() finished');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> read(String key) async {
|
||||
await _initFuture;
|
||||
return _box.get(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) => _box.put(key, value);
|
||||
@override
|
||||
Future<void> delete(String key) => _box.delete(key);
|
||||
@override
|
||||
Future<void> deleteAll(List<String> keys) => _box.deleteAll(keys);
|
||||
}
|
|
@ -11,10 +11,12 @@ import 'package:freezer/settings.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'cookie_jar_hive_storage.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
||||
final deezerAPI = DeezerAPI();
|
||||
final cookieJar = PersistCookieJar(storage: HiveStorage('cookies'));
|
||||
|
||||
class DeezerAPI {
|
||||
// from deemix: https://gitlab.com/RemixDev/deemix-js/-/blob/main/deemix/utils/deezer.js?ref_type=heads#L6
|
||||
|
@ -60,15 +62,10 @@ class DeezerAPI {
|
|||
String? favoritesPlaylistId;
|
||||
String? sid;
|
||||
|
||||
// JWT for pipe.deezer.com
|
||||
String? jwt;
|
||||
int jwtExpiration = 0;
|
||||
|
||||
late String licenseToken;
|
||||
late bool canStreamLossless;
|
||||
late bool canStreamHQ;
|
||||
|
||||
final cookieJar = DefaultCookieJar();
|
||||
late final dio = Dio(BaseOptions(
|
||||
headers: headers,
|
||||
responseType: ResponseType.json,
|
||||
|
@ -91,6 +88,10 @@ class DeezerAPI {
|
|||
};
|
||||
|
||||
Future<void> logout() async {
|
||||
// actual logout from deezer API
|
||||
await dio.get('https://www.deezer.com/logout.php');
|
||||
await dio.get('https://auth.deezer.com/logout');
|
||||
|
||||
// delete all cookies
|
||||
await cookieJar.deleteAll();
|
||||
updateHeaders();
|
||||
|
@ -100,19 +101,6 @@ class DeezerAPI {
|
|||
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
|
||||
Future<Map<dynamic, dynamic>> callApi(String method,
|
||||
{Map<dynamic, dynamic>? params,
|
||||
|
@ -246,30 +234,6 @@ class DeezerAPI {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> authorizePipeAPI() async {
|
||||
// authorize on pipe.deezer.com
|
||||
|
||||
if (DateTime.now().millisecondsSinceEpoch > jwtExpiration) {
|
||||
// only continue if JWT expired!
|
||||
}
|
||||
|
||||
// arl should be contained in cookies, so we should be fine
|
||||
final res =
|
||||
await dio.post('https://auth.deezer.com/login/arl?jo=p&rto=c&i=c');
|
||||
if (res.statusCode != 200 ||
|
||||
res.data['jwt'] == null ||
|
||||
res.data['jwt'] == '') {
|
||||
throw Exception('Pipe authentication failed!');
|
||||
}
|
||||
|
||||
jwt = res.data['jwt'];
|
||||
_logger.fine('got jwt: $jwt');
|
||||
// decode JWT
|
||||
final parts = jwt!.split('.');
|
||||
final data = jsonDecode(utf8.decode(base64Url.decode(parts[1])));
|
||||
jwtExpiration = data['exp'];
|
||||
}
|
||||
|
||||
//URL/Link parser
|
||||
Future<DeezerLinkResponse?> parseLink(String url) async {
|
||||
Uri uri = Uri.parse(url);
|
||||
|
@ -365,17 +329,6 @@ class DeezerAPI {
|
|||
}).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
|
||||
Future<SearchResults> search(String? query) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
||||
|
@ -383,11 +336,16 @@ class DeezerAPI {
|
|||
return SearchResults.fromPrivateJson(data['results']);
|
||||
}
|
||||
|
||||
Future<List<Track>> getTracks(List<String> ids) async {
|
||||
final data = await callApi('song.getListData', params: {'sng_ids': ids});
|
||||
|
||||
return (data['results']['data'] as List)
|
||||
.map<Track>((t) => Track.fromPrivateJson(t as Map))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<Track> track(String id) async {
|
||||
Map<dynamic, dynamic> data = await callApi('song.getListData', params: {
|
||||
'sng_ids': [id]
|
||||
});
|
||||
return Track.fromPrivateJson(data['results']['data'][0]);
|
||||
return (await getTracks([id]))[0];
|
||||
}
|
||||
|
||||
//Get album details, tracks
|
||||
|
|
|
@ -295,6 +295,7 @@ class DeezerAudio {
|
|||
}) async {
|
||||
final String actualTrackToken;
|
||||
if (isTokenExpired(expiration)) {
|
||||
// get new token via pipe API
|
||||
final newTrack = await deezerAPI.track(trackId);
|
||||
actualTrackToken = newTrack.trackToken!;
|
||||
} else {
|
||||
|
|
|
@ -758,7 +758,9 @@ class Lyric {
|
|||
@HiveField(2)
|
||||
String? lrcTimestamp;
|
||||
|
||||
Lyric({this.offset, this.text, this.lrcTimestamp});
|
||||
String? translated;
|
||||
|
||||
Lyric({this.offset, this.text, this.lrcTimestamp, this.translated});
|
||||
|
||||
//JSON
|
||||
factory Lyric.fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||
|
@ -769,7 +771,8 @@ class Lyric {
|
|||
offset:
|
||||
Duration(milliseconds: int.parse(json['milliseconds'].toString())),
|
||||
text: json['line'],
|
||||
lrcTimestamp: json['lrc_timestamp']);
|
||||
translated: json['lineTranslated'],
|
||||
lrcTimestamp: json['lrcTimestamp'] ?? json['lrc_timestamp']);
|
||||
}
|
||||
|
||||
factory Lyric.fromJson(Map<String, dynamic> json) => _$LyricFromJson(json);
|
||||
|
|
|
@ -82,7 +82,6 @@ class DownloadManager {
|
|||
FlutterBackgroundService().invoke(method, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> addOfflineTrack(d.Track track,
|
||||
{bool private = true, BuildContext? context, isSingleton = false}) async {
|
||||
//Permission
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer_audio.dart';
|
||||
import 'package:freezer/api/download_manager/database.dart';
|
||||
import 'package:freezer/api/download_manager/service_interface.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final pipeAPI = PipeAPI._();
|
||||
|
||||
class PipeAPI {
|
||||
PipeAPI._();
|
||||
|
||||
// JWT for pipe.deezer.com
|
||||
String? _jwt;
|
||||
int _jwtExpiration = 0;
|
||||
|
||||
final _logger = Logger('PipeAPI');
|
||||
|
||||
Dio get dio => deezerAPI.dio;
|
||||
|
||||
Future<void> authorize() async {
|
||||
// authorize on pipe.deezer.com
|
||||
|
||||
if (DateTime.now().millisecondsSinceEpoch ~/ 1000 < _jwtExpiration) {
|
||||
// only continue if JWT expired!
|
||||
return;
|
||||
}
|
||||
|
||||
// arl should be contained in cookies, so we should be fine
|
||||
var res = await dio.post('https://auth.deezer.com/login/arl?jo=p&rto=c&i=c',
|
||||
options: Options(responseType: ResponseType.plain));
|
||||
final data = jsonDecode(res.data);
|
||||
|
||||
if (res.statusCode == 400) {
|
||||
// renew token (refresh token should be in cookies)
|
||||
res = await dio.post('https://auth.deezer.com/login/renew?jo=p&rto=c&i=c',
|
||||
options: Options(responseType: ResponseType.plain));
|
||||
}
|
||||
|
||||
if (res.statusCode != 200 || data['jwt'] == null || data['jwt'] == '') {
|
||||
throw Exception('Pipe authentication failed!');
|
||||
}
|
||||
|
||||
_jwt = data['jwt'];
|
||||
_logger.fine('got jwt: $_jwt');
|
||||
// decode JWT
|
||||
final parts = _jwt!.split('.');
|
||||
final jwtData = jsonDecode(utf8.decode(base64Url.decode(parts[1])));
|
||||
_jwtExpiration = jwtData['exp'];
|
||||
}
|
||||
|
||||
Future<Map<dynamic, dynamic>> callApi(
|
||||
String operationName, String query, Map<String, dynamic> variables,
|
||||
{CancelToken? cancelToken}) async {
|
||||
// authorize if necessary.
|
||||
await authorize();
|
||||
|
||||
final res = await dio.post('https://pipe.deezer.com/api',
|
||||
data: jsonEncode({
|
||||
'operationName': operationName,
|
||||
'variables': variables,
|
||||
'query': query,
|
||||
}),
|
||||
options: Options(headers: {'Authorization': 'Bearer $_jwt'}),
|
||||
cancelToken: cancelToken);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// -- Not working --
|
||||
Future<(String, int)> getTrackToken(String trackId) async {
|
||||
final data = await callApi(
|
||||
'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},
|
||||
);
|
||||
|
||||
print('[getTrackToken] $data');
|
||||
|
||||
return (
|
||||
data['data']['track']['media']['token']['payload'] as String,
|
||||
data['data']['track']['media']['token']['expiresAt'] as int
|
||||
);
|
||||
}
|
||||
|
||||
Future<Lyrics?> lyrics(String trackId, {CancelToken? cancelToken}) async {
|
||||
final data = await callApi(
|
||||
'SynchronizedTrackLyrics',
|
||||
r'''query SynchronizedTrackLyrics($trackId: String!) {
|
||||
track(trackId: $trackId) {
|
||||
...SynchronizedTrackLyrics
|
||||
__typename
|
||||
}
|
||||
}
|
||||
|
||||
fragment SynchronizedTrackLyrics on Track {
|
||||
id
|
||||
lyrics {
|
||||
...Lyrics
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
|
||||
fragment Lyrics on Lyrics {
|
||||
id
|
||||
copyright
|
||||
text
|
||||
writers
|
||||
synchronizedLines {
|
||||
...LyricsSynchronizedLines
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
|
||||
fragment LyricsSynchronizedLines on LyricsSynchronizedLine {
|
||||
lrcTimestamp
|
||||
line
|
||||
lineTranslated
|
||||
milliseconds
|
||||
duration
|
||||
__typename
|
||||
}''',
|
||||
{'trackId': trackId},
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
final lyrics = data['data']['track']['lyrics'] as Map?;
|
||||
if (lyrics == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lyrics['synchronizedLines'] != null) {
|
||||
return Lyrics(
|
||||
id: lyrics['id'],
|
||||
writers: lyrics['writers'],
|
||||
sync: true,
|
||||
lyrics: (lyrics['synchronizedLines'] as List)
|
||||
.map<Lyric>((lrc) => Lyric.fromPrivateJson(lrc as Map))
|
||||
.toList(growable: false));
|
||||
}
|
||||
|
||||
return Lyrics(
|
||||
id: lyrics['id'],
|
||||
writers: lyrics['writers'],
|
||||
sync: false,
|
||||
lyrics: [Lyric(text: lyrics['text'])]);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/audio_sources/deezer_audio_source.dart';
|
||||
|
@ -8,6 +7,7 @@ import 'package:freezer/api/audio_sources/offline_audio_source.dart';
|
|||
import 'package:freezer/api/paths.dart';
|
||||
import 'package:freezer/api/player/player_helper.dart';
|
||||
import 'package:freezer/api/audio_sources/url_audio_source.dart';
|
||||
import 'package:freezer/api/player/systray.dart';
|
||||
import 'package:freezer/ui/android_auto.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
@ -28,6 +28,7 @@ import 'dart:convert';
|
|||
|
||||
PlayerHelper playerHelper = PlayerHelper();
|
||||
late AudioHandler audioHandler;
|
||||
bool failsafe = false;
|
||||
|
||||
class AudioPlayerTaskInitArguments {
|
||||
final bool ignoreInterruptions;
|
||||
|
@ -70,6 +71,8 @@ class AudioPlayerTaskInitArguments {
|
|||
class AudioPlayerTask extends BaseAudioHandler {
|
||||
final _logger = Logger('AudioPlayerTask');
|
||||
|
||||
bool _disposed = false;
|
||||
|
||||
late AudioPlayer _player;
|
||||
late ConcatenatingAudioSource _audioSource;
|
||||
late DeezerAPI _deezerAPI;
|
||||
|
@ -88,11 +91,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
int _queueAutoIncrement = 0;
|
||||
|
||||
//Stream subscriptions
|
||||
StreamSubscription? _eventSubscription;
|
||||
StreamSubscription? _bufferPositionSubscription;
|
||||
StreamSubscription? _audioSessionSubscription;
|
||||
StreamSubscription? _visualizerSubscription;
|
||||
StreamSubscription? _connectivitySubscription;
|
||||
List<StreamSubscription> _subscriptions = [];
|
||||
bool _isConnectivityPluginAvailable = true;
|
||||
|
||||
/// Android Auto helper class for navigation
|
||||
|
@ -109,6 +108,10 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
/// Last playback position (used for restoring position when player died)
|
||||
Duration? _lastPosition;
|
||||
|
||||
/// Last playback queueIndex (used for restoring when player died)
|
||||
int? _lastQueueIndex;
|
||||
|
||||
AudioServiceRepeatMode _repeatMode = AudioServiceRepeatMode.none;
|
||||
|
||||
/// LastFM API
|
||||
|
@ -133,6 +136,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
/// When playback begun (in SECONDS)
|
||||
int? _timestamp;
|
||||
|
||||
bool _ignoreInterruptions = false;
|
||||
|
||||
MediaItem get currentMediaItem => queue.value[_queueIndex];
|
||||
|
||||
bool get currentMediaItemIsShow =>
|
||||
|
@ -143,14 +148,15 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
AudioPlayerTask([AudioPlayerTaskInitArguments? initArgs]) {
|
||||
if (initArgs == null) {
|
||||
unawaited(AudioPlayerTaskInitArguments.loadSettings().then(_init));
|
||||
unawaited(AudioPlayerTaskInitArguments.loadSettings().then(_start));
|
||||
return;
|
||||
}
|
||||
unawaited(_init(initArgs));
|
||||
unawaited(_start(initArgs));
|
||||
}
|
||||
|
||||
Future<void> _init(AudioPlayerTaskInitArguments initArgs) async {
|
||||
// Linux/Windows specific options
|
||||
Future<void> _start(AudioPlayerTaskInitArguments initArgs) async {
|
||||
// Linux and Windows support
|
||||
JustAudioMediaKit.ensureInitialized();
|
||||
JustAudioMediaKit.title = 'Freezer';
|
||||
JustAudioMediaKit.protocolWhitelist = const ['http'];
|
||||
//JustAudioMediaKit.bufferSize = 128;
|
||||
|
@ -159,72 +165,95 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
_androidAuto = AndroidAuto(deezerAPI: _deezerAPI);
|
||||
_shouldLogTracks = initArgs.logListen;
|
||||
_seekAsSkip = initArgs.seekAsSkip;
|
||||
_ignoreInterruptions = initArgs.ignoreInterruptions;
|
||||
|
||||
final session = await AudioSession.instance;
|
||||
session.configure(const AudioSessionConfiguration.music());
|
||||
await session.configure(const AudioSessionConfiguration.music());
|
||||
|
||||
_box = await Hive.openLazyBox('playback', path: await Paths.cacheDir());
|
||||
_init();
|
||||
|
||||
await _loadQueueFile();
|
||||
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
unawaited(sysTray.init());
|
||||
}
|
||||
|
||||
if (initArgs.lastFMUsername != null && initArgs.lastFMPassword != null) {
|
||||
unawaited(
|
||||
_authorizeLastFM(initArgs.lastFMUsername!, initArgs.lastFMPassword!));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
_player = AudioPlayer(
|
||||
handleInterruptions: !initArgs.ignoreInterruptions,
|
||||
handleInterruptions: !_ignoreInterruptions,
|
||||
androidApplyAudioAttributes: true,
|
||||
handleAudioSessionActivation: true,
|
||||
);
|
||||
|
||||
if (initArgs.ignoreInterruptions) {
|
||||
//session.interruptionEventStream.listen((_) {});
|
||||
//session.becomingNoisyEventStream.listen((_) {});
|
||||
}
|
||||
_subscriptions = [
|
||||
_player.currentIndexStream.listen((index) {
|
||||
if (index != null && queue.value.isNotEmpty) {
|
||||
// Update track index + update media item
|
||||
_queueIndex = index;
|
||||
mediaItem.add(currentMediaItem);
|
||||
|
||||
//Update track index
|
||||
_player.currentIndexStream.listen((index) {
|
||||
if (index != null && queue.value.isNotEmpty) {
|
||||
_queueIndex = index;
|
||||
mediaItem.add(currentMediaItem);
|
||||
// log previous track
|
||||
if (index != 0 &&
|
||||
_lastTrackId != null &&
|
||||
_lastTrackId! != currentMediaItem.id) {
|
||||
unawaited(_logListenedTrack(_lastTrackId!,
|
||||
sync: _amountPaused == 0 && _amountSeeked == 0));
|
||||
}
|
||||
|
||||
// log previous track
|
||||
if (index != 0 &&
|
||||
_lastTrackId != null &&
|
||||
_lastTrackId! != currentMediaItem.id) {
|
||||
_logListenedTrack(_lastTrackId!,
|
||||
sync: _amountPaused == 0 && _amountSeeked == 0);
|
||||
_lastTrackId = currentMediaItem.id;
|
||||
_amountSeeked = 0;
|
||||
_amountPaused = 0;
|
||||
_timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
//LastFM
|
||||
if (_queueIndex >= queue.value.length) return;
|
||||
if (_scrobblenaut != null && currentMediaItem.id != _loggedTrackId) {
|
||||
_loggedTrackId = currentMediaItem.id;
|
||||
unawaited(_scrobblenaut!.track.scrobble(
|
||||
track: currentMediaItem.title,
|
||||
artist: currentMediaItem.artist!,
|
||||
album: currentMediaItem.album,
|
||||
duration: currentMediaItem.duration,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
_lastTrackId = currentMediaItem.id;
|
||||
_amountSeeked = 0;
|
||||
_amountPaused = 0;
|
||||
_timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
if (index == queue.value.length - 1) {
|
||||
// if the queue is ended, load more tracks if applicable
|
||||
unawaited(_onQueueEnd());
|
||||
}
|
||||
}),
|
||||
//Update state on all clients on change
|
||||
_player.playbackEventStream.listen((event) {
|
||||
//Update
|
||||
_broadcastState();
|
||||
}, onError: (Object e, StackTrace st) {
|
||||
_logger.severe('A stream error occurred: $e');
|
||||
}),
|
||||
_player.processingStateStream.listen((state) {
|
||||
switch (state) {
|
||||
case ProcessingState.completed:
|
||||
//Player ended, get more songs
|
||||
if (_queueIndex == queue.value.length - 1) {
|
||||
_onQueueEnd();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}),
|
||||
|
||||
if (index == queue.value.length - 1) {
|
||||
unawaited(_onQueueEnd());
|
||||
}
|
||||
});
|
||||
//Update state on all clients on change
|
||||
_eventSubscription = _player.playbackEventStream.listen((event) {
|
||||
//Update
|
||||
_broadcastState();
|
||||
}, onError: (Object e, StackTrace st) {
|
||||
_logger.severe('A stream error occurred: $e');
|
||||
});
|
||||
_player.processingStateStream.listen((state) {
|
||||
switch (state) {
|
||||
case ProcessingState.completed:
|
||||
//Player ended, get more songs
|
||||
if (_queueIndex == queue.value.length - 1) {
|
||||
customEvent.add(
|
||||
{'action': 'queueEnd', 'queueSource': queueSource!.toJson()});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
_bufferPositionSubscription =
|
||||
_player.bufferedPositionStream.listen((bufferPosition) {
|
||||
customEvent.add({'action': 'bufferPosition', 'data': bufferPosition});
|
||||
});
|
||||
_player.bufferedPositionStream.listen((bufferPosition) {
|
||||
customEvent.add({'action': 'bufferPosition', 'data': bufferPosition});
|
||||
}),
|
||||
];
|
||||
|
||||
//Audio session
|
||||
// _audioSessionSubscription =
|
||||
|
@ -232,25 +261,21 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
// customEvent.add({'action': 'audioSession', 'id': event});
|
||||
// });
|
||||
|
||||
//Load queue
|
||||
// queue.add(_queue);
|
||||
|
||||
// Determine audio quality to use
|
||||
// Determine audio quality to use (based on whether mobile or wifi)
|
||||
// also checks if we can use the Connectivity plugin on this platform
|
||||
// ex. Linux without NetworkManager
|
||||
if (await _determineAudioQuality()) {
|
||||
// listen for connectivity changes
|
||||
_connectivitySubscription = Connectivity()
|
||||
_subscriptions.add(Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen(_determineAudioQualityByResult);
|
||||
.listen(_determineAudioQualityByResult));
|
||||
}
|
||||
}
|
||||
|
||||
await _loadQueueFile();
|
||||
Future<void> _maybeResume() {
|
||||
if (!_disposed) return Future.value();
|
||||
|
||||
if (initArgs.lastFMUsername != null && initArgs.lastFMPassword != null) {
|
||||
await _authorizeLastFM(
|
||||
initArgs.lastFMUsername!, initArgs.lastFMPassword!);
|
||||
}
|
||||
|
||||
customEvent.add({'action': 'onLoad'});
|
||||
return _init();
|
||||
}
|
||||
|
||||
/// Determine the [AudioQuality] to use according to current connection
|
||||
|
@ -296,9 +321,16 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future skipToQueueItem(int index) async {
|
||||
await _maybeResume();
|
||||
_lastPosition = null;
|
||||
|
||||
unawaited(_logListenedTrack(currentMediaItem.id, sync: false));
|
||||
_lastQueueIndex = null;
|
||||
// next or prev track?
|
||||
unawaited(_logListenedTrack(
|
||||
currentMediaItem.id,
|
||||
sync: false,
|
||||
next: _queueIndex + 1 == index,
|
||||
prev: _queueIndex - 1 == index,
|
||||
));
|
||||
//Skip in player
|
||||
await _player.seek(Duration.zero, index: index);
|
||||
_queueIndex = index;
|
||||
|
@ -307,23 +339,13 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future play() async {
|
||||
await _maybeResume();
|
||||
_player.play();
|
||||
//Restore position on play
|
||||
//Restore position and queue index on play
|
||||
if (_lastPosition != null) {
|
||||
_player.seek(_lastPosition);
|
||||
_player.seek(_lastPosition, index: _lastQueueIndex);
|
||||
_lastPosition = null;
|
||||
}
|
||||
|
||||
//LastFM
|
||||
if (_queueIndex >= queue.value.length) return;
|
||||
if (_scrobblenaut != null && currentMediaItem.id != _loggedTrackId) {
|
||||
_loggedTrackId = currentMediaItem.id;
|
||||
await _scrobblenaut!.track.scrobble(
|
||||
track: currentMediaItem.title,
|
||||
artist: currentMediaItem.artist!,
|
||||
album: currentMediaItem.album,
|
||||
duration: currentMediaItem.duration,
|
||||
);
|
||||
_lastQueueIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,13 +356,15 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration? position) {
|
||||
Future<void> seek(Duration? position) async {
|
||||
await _maybeResume();
|
||||
_amountSeeked++;
|
||||
return _player.seek(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> fastForward() {
|
||||
Future<void> fastForward() async {
|
||||
await _maybeResume();
|
||||
print('fast forward called');
|
||||
if (currentMediaItemIsShow) {
|
||||
return _seekRelative(const Duration(seconds: 30));
|
||||
|
@ -352,7 +376,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> rewind() {
|
||||
Future<void> rewind() async {
|
||||
await _maybeResume();
|
||||
print('rewind called');
|
||||
if (currentMediaItemIsShow) {
|
||||
return _seekRelative(-const Duration(seconds: 30));
|
||||
|
@ -400,6 +425,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
await _maybeResume();
|
||||
_lastPosition = null;
|
||||
if (_queueIndex == queue.value.length - 1) return;
|
||||
//Update buffering state
|
||||
|
@ -411,6 +437,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
await _maybeResume();
|
||||
if (_queueIndex == 0) return;
|
||||
//Update buffering state
|
||||
//_skipState = AudioProcessingState.skippingToPrevious;
|
||||
|
@ -518,7 +545,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
MediaAction.seek,
|
||||
MediaAction.fastForward,
|
||||
MediaAction.rewind,
|
||||
MediaAction.stop,
|
||||
},
|
||||
processingState: _convertProcessingState(_player.processingState),
|
||||
playing: _player.playing,
|
||||
|
@ -548,7 +574,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
_lastPosition = null;
|
||||
//just_audio
|
||||
_originalQueue = null;
|
||||
_player.stop();
|
||||
|
||||
// assign unique ids
|
||||
_queueAutoIncrement = 0;
|
||||
|
@ -562,7 +587,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
}
|
||||
|
||||
//Load queue to just_audio
|
||||
Future _loadQueue() async {
|
||||
Future _loadQueue({bool preload = true}) async {
|
||||
//Don't reset queue index by starting player
|
||||
if (!queue.hasValue || queue.value.isEmpty) {
|
||||
return;
|
||||
|
@ -582,7 +607,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
await _player.setAudioSource(_audioSource,
|
||||
initialIndex: _queueIndex,
|
||||
initialPosition: Duration.zero,
|
||||
preload: kIsWeb || defaultTargetPlatform != TargetPlatform.linux);
|
||||
preload: preload);
|
||||
} catch (e) {
|
||||
//Error loading tracks
|
||||
}
|
||||
|
@ -742,12 +767,11 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
@override
|
||||
Future<void> stop() async {
|
||||
await _saveQueue();
|
||||
_player.stop();
|
||||
_eventSubscription?.cancel();
|
||||
_audioSessionSubscription?.cancel();
|
||||
_visualizerSubscription?.cancel();
|
||||
_bufferPositionSubscription?.cancel();
|
||||
_connectivitySubscription?.cancel();
|
||||
_disposed = true;
|
||||
_player.dispose();
|
||||
for (final subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
|
@ -767,26 +791,23 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
//Restore queue & playback info from path
|
||||
Future<void> _loadQueueFile() async {
|
||||
if (_box.isEmpty) {
|
||||
await _loadQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
final q = ((await _box.get('queue')) as List?)?.cast<MediaItem>();
|
||||
_queueIndex = await _box.get('index', defaultValue: 0);
|
||||
_lastQueueIndex = _queueIndex = await _box.get('index', defaultValue: 0);
|
||||
_lastPosition = await _box.get('position', defaultValue: Duration.zero);
|
||||
queueSource =
|
||||
await _box.get('queueSource', defaultValue: const QueueSource());
|
||||
_repeatMode =
|
||||
await _box.get('repeatMode', defaultValue: AudioServiceRepeatMode.none);
|
||||
//Restore queue
|
||||
if (q != null) {
|
||||
_queueAutoIncrement = q.length;
|
||||
queue.add(q);
|
||||
await _loadQueue();
|
||||
mediaItem.add(currentMediaItem);
|
||||
} else {
|
||||
await _loadQueue();
|
||||
}
|
||||
if (q == null) return;
|
||||
|
||||
_queueAutoIncrement = q.length;
|
||||
queue.add(q);
|
||||
await _loadQueue(preload: false);
|
||||
|
||||
//Send restored queue source to ui
|
||||
customEvent.add({
|
||||
'action': 'onRestore',
|
||||
|
@ -857,6 +878,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
@override
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||
switch (shuffleMode) {
|
||||
// TODO: migrate to native implementation once we fix this in just_audio_media_kit
|
||||
case AudioServiceShuffleMode.none:
|
||||
queue.add(_originalQueue!);
|
||||
_originalQueue = null;
|
||||
|
|
|
@ -15,7 +15,6 @@ class PlayerHelper {
|
|||
late StreamSubscription _customEventSubscription;
|
||||
late StreamSubscription _mediaItemSubscription;
|
||||
late StreamSubscription _playbackStateStreamSubscription;
|
||||
QueueSource? queueSource;
|
||||
AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none;
|
||||
bool equalizerOpen = false;
|
||||
bool _shuffleEnabled = false;
|
||||
|
@ -30,6 +29,9 @@ class PlayerHelper {
|
|||
// StreamController _visualizerController = StreamController.broadcast();
|
||||
// Stream get visualizerStream => _visualizerController.stream;
|
||||
|
||||
final _queueSourceSubject = BehaviorSubject<QueueSource>();
|
||||
ValueStream<QueueSource> get queueSource => _queueSourceSubject.stream;
|
||||
|
||||
final _streamInfoSubject = BehaviorSubject<StreamQualityInfo>();
|
||||
ValueStream<StreamQualityInfo> get streamInfo => _streamInfoSubject.stream;
|
||||
|
||||
|
@ -57,6 +59,12 @@ class PlayerHelper {
|
|||
int get queueIndex => _queueIndex;
|
||||
|
||||
Future<void> initAudioHandler() async {
|
||||
if (failsafe) {
|
||||
Fluttertoast.showToast(msg: 'what the fuck?');
|
||||
return;
|
||||
}
|
||||
failsafe = true;
|
||||
|
||||
final initArgs = AudioPlayerTaskInitArguments.from(
|
||||
settings: settings, deezerAPI: deezerAPI);
|
||||
// initialize our audiohandler instance
|
||||
|
@ -84,11 +92,9 @@ class PlayerHelper {
|
|||
if (event is! Map) return;
|
||||
Logger('PlayerHelper').fine("event received: ${event['action']}");
|
||||
switch (event['action']) {
|
||||
case 'onLoad':
|
||||
break;
|
||||
case 'onRestore':
|
||||
//Load queueSource from isolate
|
||||
queueSource = event['queueSource'] as QueueSource;
|
||||
_queueSourceSubject.add(event['queueSource'] as QueueSource);
|
||||
repeatType = event['repeatMode'] as AudioServiceRepeatMode;
|
||||
_queueIndex = getQueueIndex();
|
||||
break;
|
||||
|
@ -198,13 +204,13 @@ class PlayerHelper {
|
|||
|
||||
//Play mix by track
|
||||
Future playMix(String trackId, String trackTitle) async {
|
||||
List<Track> tracks = (await deezerAPI.playMix(trackId))!;
|
||||
List<Track> tracks = await deezerAPI.playMix(trackId);
|
||||
await playFromTrackList(
|
||||
tracks,
|
||||
tracks[0].id,
|
||||
QueueSource(
|
||||
id: trackId,
|
||||
text: '${'Mix based on'.i18n} $trackTitle',
|
||||
text: 'Mix based on %s'.i18n.fill([trackTitle]),
|
||||
source: 'mix'));
|
||||
}
|
||||
|
||||
|
@ -214,7 +220,7 @@ class PlayerHelper {
|
|||
null,
|
||||
QueueSource(
|
||||
id: track.id,
|
||||
text: "${'Mix based on'.i18n} ${track.title}",
|
||||
text: 'Mix based on %s'.i18n.fill([track.title!]),
|
||||
source: 'searchMix'));
|
||||
List<Track> tracks = await deezerAPI.getSearchTrackMix(track.id, false);
|
||||
// discard first track (if it is the searched track)
|
||||
|
@ -233,7 +239,7 @@ class PlayerHelper {
|
|||
null, // we can avoid passing it, as the index is 0
|
||||
QueueSource(
|
||||
id: trackId,
|
||||
text: "${'Mix based on'.i18n} $trackTitle",
|
||||
text: 'Mix based on %s'.i18n.fill([trackTitle]),
|
||||
source: 'searchMix'));
|
||||
}
|
||||
|
||||
|
@ -273,7 +279,7 @@ class PlayerHelper {
|
|||
Future<void> playFromTrackList(
|
||||
List<Track> tracks, String? trackId, QueueSource queueSource) async {
|
||||
final queue =
|
||||
tracks.map<MediaItem>((track) => track!.toMediaItem()).toList();
|
||||
tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
|
||||
await setQueueSource(queueSource);
|
||||
await _loadQueuePlay(
|
||||
queue, trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId));
|
||||
|
@ -307,7 +313,7 @@ class PlayerHelper {
|
|||
}
|
||||
|
||||
Future setQueueSource(QueueSource queueSource) async {
|
||||
this.queueSource = queueSource;
|
||||
_queueSourceSubject.add(queueSource);
|
||||
await audioHandler.customAction('queueSource', queueSource.toJson());
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezer/api/player/audio_handler.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final sysTray = SysTray._();
|
||||
|
||||
class SysTray with TrayListener {
|
||||
SysTray._();
|
||||
|
||||
static String getIcon({bool forcePng = false}) {
|
||||
if (Platform.isWindows && !forcePng) {
|
||||
if (settings.useColorTrayIcon) {
|
||||
return 'assets/icon.ico';
|
||||
}
|
||||
return 'assets/icon_mono_small.ico';
|
||||
}
|
||||
|
||||
if (settings.useColorTrayIcon) {
|
||||
return 'assets/icon_small.png';
|
||||
}
|
||||
|
||||
return 'assets/icon_mono_small.png';
|
||||
}
|
||||
|
||||
bool _inited = false;
|
||||
Future<void> init() async {
|
||||
if (_inited) return;
|
||||
_inited = true;
|
||||
|
||||
updateIcon();
|
||||
try {
|
||||
await trayManager.setToolTip('freezer');
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
|
||||
await updateContextMenu();
|
||||
|
||||
trayManager.addListener(this);
|
||||
|
||||
playerHelper.playing
|
||||
.listen((playing) => updateContextMenu(playing: playing));
|
||||
audioHandler.mediaItem
|
||||
.listen((mediaItem) => updateContextMenu(mediaItem: mediaItem));
|
||||
}
|
||||
|
||||
Future<void> updateIcon() {
|
||||
return trayManager.setIcon(getIcon());
|
||||
}
|
||||
|
||||
Future<void> updateContextMenu({bool? playing, MediaItem? mediaItem}) async {
|
||||
playing ??= playerHelper.playing.valueOrNull ?? false;
|
||||
mediaItem ??= audioHandler.mediaItem.valueOrNull;
|
||||
// create context menu
|
||||
final menu = Menu(items: [
|
||||
if (mediaItem != null) ...[
|
||||
MenuItem(label: mediaItem.title, disabled: true),
|
||||
MenuItem(label: mediaItem.artist!, disabled: true),
|
||||
],
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: 'Previous'.i18n,
|
||||
onClick: (menuItem) => audioHandler.skipToPrevious()),
|
||||
playing
|
||||
? MenuItem(
|
||||
label: 'Pause'.i18n, onClick: (menuItem) => audioHandler.pause())
|
||||
: MenuItem(
|
||||
label: 'Play'.i18n, onClick: (menuItem) => audioHandler.play()),
|
||||
MenuItem(
|
||||
label: 'Next'.i18n, onClick: (menuItem) => audioHandler.skipToNext()),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: 'Show'.i18n,
|
||||
// we can safely ignore it if it errors, as it's expected
|
||||
onClick: (menuItem) => windowManager.show().catchError((e) {})),
|
||||
MenuItem(
|
||||
label: 'Exit'.i18n,
|
||||
onClick: (menuItem) async {
|
||||
await audioHandler.pause();
|
||||
SystemNavigator.pop();
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
// set context menu
|
||||
await trayManager.setContextMenu(menu);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconMouseUp() async {
|
||||
try {
|
||||
await windowManager.show();
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseUp() => trayManager.popUpContextMenu();
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/// Flutter icons FreezerIcons
|
||||
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
|
||||
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
|
||||
///
|
||||
/// To use this font, place it in your fonts/ directory and include the
|
||||
/// following in your pubspec.yaml
|
||||
///
|
||||
/// flutter:
|
||||
/// fonts:
|
||||
/// - family: FreezerIcons
|
||||
/// fonts:
|
||||
/// - asset: fonts/FreezerIcons.ttf
|
||||
///
|
||||
///
|
||||
/// * Typicons, (c) Stephen Hutchings 2012
|
||||
/// Author: Stephen Hutchings
|
||||
/// License: SIL (http://scripts.sil.org/OFL)
|
||||
/// Homepage: http://typicons.com/
|
||||
/// * Font Awesome 5, Copyright (C) 2016 by Dave Gandy
|
||||
/// Author: Dave Gandy
|
||||
/// License: SIL (https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt)
|
||||
/// Homepage: http://fortawesome.github.com/Font-Awesome/
|
||||
/// * Octicons, Copyright (C) 2020 by GitHub Inc.
|
||||
/// Author: GitHub
|
||||
/// License: MIT (http://opensource.org/licenses/mit-license.php)
|
||||
/// Homepage: https://primer.style/octicons/
|
||||
///
|
||||
// ignore_for_file: dangling_library_doc_comments
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FreezerIcons {
|
||||
FreezerIcons._();
|
||||
|
||||
static const _kFontFam = 'FreezerIcons';
|
||||
static const String? _kFontPkg = null;
|
||||
|
||||
static const IconData waves =
|
||||
IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sort_alpha_down =
|
||||
IconData(0xf15d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sort_alpha_up =
|
||||
IconData(0xf15e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData spotify =
|
||||
IconData(0xf1bc, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData lastfm =
|
||||
IconData(0xf202, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData primitive_dot =
|
||||
IconData(0xf30c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
}
|
|
@ -12,10 +12,10 @@ import 'package:flutter_cache_manager_hive/flutter_cache_manager_hive.dart';
|
|||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/paths.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/page_routes/blur_slide.dart';
|
||||
import 'package:freezer/page_routes/fade.dart';
|
||||
import 'package:freezer/page_routes/scale_fade.dart';
|
||||
|
@ -122,7 +122,8 @@ void main() async {
|
|||
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// ignore: avoid_print
|
||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||
print(
|
||||
'${record.level.name}: ${record.time}: [${record.loggerName}] ${record.message}');
|
||||
});
|
||||
|
||||
if (kDebugMode) {
|
||||
|
@ -160,7 +161,7 @@ class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
|
|||
break;
|
||||
|
||||
default:
|
||||
print('lifecycle: $state');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,6 +193,8 @@ class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
|
|||
DynamicColorBuilder(builder: (lightScheme, darkScheme) {
|
||||
final lightTheme = settings.materialYouAccent
|
||||
? ThemeData(
|
||||
textTheme: settings.textTheme,
|
||||
fontFamily: settings.fontFamily,
|
||||
colorScheme: lightScheme,
|
||||
useMaterial3: true,
|
||||
appBarTheme: const AppBarTheme(
|
||||
|
@ -207,6 +210,8 @@ class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
|
|||
: settings.themeData;
|
||||
final darkTheme = settings.materialYouAccent
|
||||
? ThemeData(
|
||||
textTheme: settings.textTheme,
|
||||
fontFamily: settings.fontFamily,
|
||||
colorScheme: darkScheme,
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
|
@ -292,12 +297,12 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
|
|||
|
||||
Future _logOut() async {
|
||||
await deezerAPI.logout();
|
||||
await settings.save();
|
||||
await Cache.wipe();
|
||||
setState(() {
|
||||
settings.arl = null;
|
||||
settings.offlineMode = false;
|
||||
});
|
||||
await settings.save();
|
||||
await Cache.wipe();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -637,7 +642,7 @@ class MainScreenState extends State<MainScreen>
|
|||
label: Text('Settings'.i18n),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(FontAwesome5.spotify),
|
||||
icon: const Icon(FreezerIcons.spotify),
|
||||
label: Text('Importer'.i18n),
|
||||
),
|
||||
];
|
||||
|
|
|
@ -175,6 +175,10 @@ class Settings {
|
|||
@HiveField(49, defaultValue: true)
|
||||
bool enableMaterial3PlayButton = true;
|
||||
|
||||
// DESKTOP ONLY -- TRAY ICON
|
||||
@HiveField(50, defaultValue: false)
|
||||
bool useColorTrayIcon = false;
|
||||
|
||||
static LazyBox<Settings>? __box;
|
||||
static Future<LazyBox<Settings>> get _box async =>
|
||||
__box ??= await Hive.openLazyBox<Settings>('settings');
|
||||
|
@ -218,9 +222,10 @@ class Settings {
|
|||
return _themeData[theme] ?? ThemeData();
|
||||
}
|
||||
|
||||
final customFonts = ['System', 'YouTube Sans', 'Deezer'];
|
||||
//Get all available fonts
|
||||
List<String> get fonts {
|
||||
return ['System', 'Deezer', ...GoogleFonts.asMap().keys];
|
||||
return [...customFonts, ...GoogleFonts.asMap().keys];
|
||||
}
|
||||
|
||||
//JSON to forward into download service
|
||||
|
@ -310,11 +315,11 @@ class Settings {
|
|||
|
||||
static const deezerBg = Color(0xFF1F1A16);
|
||||
static const deezerBottom = Color(0xFF1b1714);
|
||||
TextTheme? get textTheme => (font == 'Deezer' || font == 'System')
|
||||
TextTheme? get textTheme => customFonts.contains(font)
|
||||
? null
|
||||
: GoogleFonts.getTextTheme(font,
|
||||
isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme);
|
||||
String? get fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
|
||||
String? get fontFamily => (font == 'Deezer') ? 'Mabry Pro' : null;
|
||||
|
||||
final _elevation1Black = Color.alphaBlend(Colors.white12, Colors.black);
|
||||
|
||||
|
|
|
@ -42,6 +42,12 @@ extension Localization on String {
|
|||
static final _t = Translations.byLocale("en_US") + language_en_us + crowdin;
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
|
||||
String plural(value) {
|
||||
return replaceAll("%d", value.toString());
|
||||
}
|
||||
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
}
|
||||
|
||||
class Language {
|
||||
|
|
|
@ -2,11 +2,11 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player/audio_handler.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/search.dart';
|
||||
|
@ -1074,8 +1074,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
|
|
|
@ -42,9 +42,7 @@ class _ErrorScreenState extends State<ErrorScreen> {
|
|||
color: Colors.red,
|
||||
size: 64.0,
|
||||
),
|
||||
Container(
|
||||
height: 4.0,
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(widget.message ??
|
||||
'Please check your connection and try again later...'.i18n),
|
||||
if (checkArl)
|
||||
|
|
|
@ -45,7 +45,7 @@ class _ExternalLinkRouteState extends State<ExternalLinkRoute> {
|
|||
}
|
||||
|
||||
Future<Map<String, String>> _resolveHeaders(Uri uri) async {
|
||||
List<Cookie> cookies = await deezerAPI.cookieJar.loadForRequest(uri);
|
||||
List<Cookie> cookies = await cookieJar.loadForRequest(uri);
|
||||
print(cookies);
|
||||
return {'Cookie': cookies.join(';')};
|
||||
}
|
||||
|
|
|
@ -382,9 +382,9 @@ class HomePageItemWidget extends StatelessWidget {
|
|||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AlbumDetails(item.value));
|
||||
},
|
||||
onHold: () {
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(item.value);
|
||||
m.defaultAlbumMenu(item.value, details: details);
|
||||
},
|
||||
);
|
||||
case HomePageItemType.ARTIST:
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/importer.dart';
|
||||
import 'package:freezer/api/player/audio_handler.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
|
@ -137,7 +137,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
// title: Text('Importer'.i18n),
|
||||
// children: [
|
||||
// ListTile(
|
||||
// leading: const Icon(FontAwesome5.spotify),
|
||||
// leading: const Icon(FreezerIcons.spotify),
|
||||
// title: Text('Spotify v1'.i18n),
|
||||
// subtitle: Text(
|
||||
// 'Import Spotify playlists up to 100 tracks without any login.'
|
||||
|
@ -150,7 +150,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
// },
|
||||
// ),
|
||||
// ListTile(
|
||||
// leading: const Icon(FontAwesome5.spotify),
|
||||
// leading: const Icon(FreezerIcons.spotify),
|
||||
// title: Text('Spotify v2'.i18n),
|
||||
// subtitle: Text(
|
||||
// 'Import any Spotify playlist, import from own Spotify library. Requires free account.'
|
||||
|
@ -420,8 +420,8 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
|
@ -536,7 +536,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
),
|
||||
const SizedBox(height: 8.0),
|
||||
for (final track in allTracks)
|
||||
TrackTile.fromTrack(track!, onTap: () {
|
||||
TrackTile.fromTrack(track, onTap: () {
|
||||
playerHelper.playFromTrackList(
|
||||
allTracks,
|
||||
track.id,
|
||||
|
@ -631,8 +631,8 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
|
@ -843,8 +843,8 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
|
@ -1027,8 +1027,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:dio/dio.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/ui/login_screen.dart';
|
||||
import 'package:freezer/utils.dart';
|
||||
import 'package:pointycastle/export.dart' as pc;
|
||||
import 'package:pointycastle/src/platform_check/platform_check.dart';
|
||||
|
@ -117,7 +118,7 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
});
|
||||
}
|
||||
|
||||
void _loginUsingArl(String arl) async {
|
||||
void _loginWithArl(String arl) async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
|
@ -167,7 +168,8 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
ScaffoldMessenger.of(context).snack('Logged in successfully!'.i18n);
|
||||
}
|
||||
|
||||
void _cancel() async {
|
||||
@override
|
||||
void dispose() async {
|
||||
if (_step2) {
|
||||
final hash =
|
||||
Hmac(sha512, key.bytes).convert(utf8.encode(_code)).toString();
|
||||
|
@ -175,7 +177,7 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
data: jsonEncode({'_': 'cancel', 'hash': hash}));
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -183,14 +185,65 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
return AlertDialog(
|
||||
title: Text('Login on other device'.i18n),
|
||||
content: _step2
|
||||
? Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (_error != null)
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
OutlinedButton(
|
||||
onPressed:
|
||||
_loading ? null : () => _loginUsingArl(settings.arl!),
|
||||
child: Text('Login with current ARL'.i18n)),
|
||||
])
|
||||
? (_loading
|
||||
? const CircularProgressIndicator()
|
||||
: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (_error != null)
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
OutlinedButton(
|
||||
onPressed: () => _loginWithArl(settings.arl!),
|
||||
child: Text('Login with current ARL (one-click)'.i18n)),
|
||||
const SizedBox(height: 16.0),
|
||||
Text('Or, login with external ARL'.i18n),
|
||||
const SizedBox(height: 16.0),
|
||||
if (LoginBrowser.supported()) ...[
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) =>
|
||||
LoginBrowser(_loginWithArl));
|
||||
},
|
||||
child: Text('Login using browser'.i18n)),
|
||||
const SizedBox(height: 16.0),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
String arl = '';
|
||||
final key = GlobalKey<FormFieldState<String>>();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
submit(String arl) {
|
||||
if (!key.currentState!.validate()) return;
|
||||
Navigator.pop(context);
|
||||
_loginWithArl(arl);
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Enter ARL'.i18n),
|
||||
content: TextFormField(
|
||||
key: key,
|
||||
validator: (value) =>
|
||||
value?.trim().length == 192
|
||||
? null
|
||||
: 'Invalid ARL length!'.i18n,
|
||||
onChanged: (value) => arl = value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Token (ARL)'.i18n),
|
||||
onFieldSubmitted: submit,
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () => submit(arl),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Text('Login with ARL'.i18n)),
|
||||
]))
|
||||
: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
|
@ -203,16 +256,25 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
validator: (value) {
|
||||
value ??= '';
|
||||
final p = value.split(':');
|
||||
if (p.length != 2) return 'Invalid IP and Port';
|
||||
if (p.length != 2) {
|
||||
return 'Invalid IP and Port'.i18n;
|
||||
}
|
||||
|
||||
final ip = p[0];
|
||||
final port = int.tryParse(p[1]);
|
||||
if (port == null || port > 65535) return 'Invalid port';
|
||||
if (port == null || port > 65535) {
|
||||
return 'Invalid port'.i18n;
|
||||
}
|
||||
|
||||
final ipParts = ip.split('.');
|
||||
if (ipParts.length != 4) return 'Invalid IP';
|
||||
if (ipParts.length != 4) {
|
||||
return 'Invalid IP'.i18n;
|
||||
}
|
||||
|
||||
for (final part in ipParts) {
|
||||
final a = int.tryParse(part);
|
||||
if (a == null || a < 0 || a > 255) {
|
||||
return 'Invalid IP';
|
||||
return 'Invalid IP'.i18n;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,7 +288,7 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
validator: (value) {
|
||||
value ??= '';
|
||||
if (value.length != 6 || int.tryParse(value) == null) {
|
||||
return 'Invalid code';
|
||||
return 'Invalid code'.i18n;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -237,12 +299,14 @@ class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
|
|||
]),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Cancel'.i18n)),
|
||||
if (!_step2)
|
||||
TextButton(
|
||||
onPressed: _loading ? null : _doHandshake,
|
||||
child: Text('Login'.i18n),
|
||||
),
|
||||
TextButton(onPressed: _cancel, child: Text('Cancel'.i18n))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,20 +3,16 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:encrypt/encrypt_io.dart';
|
||||
import 'package:pointycastle/asymmetric/api.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/utils.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
|
@ -119,7 +115,9 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
});
|
||||
}
|
||||
|
||||
void _update() async {
|
||||
void _update(String arl) async {
|
||||
settings.arl = arl;
|
||||
await settings.save();
|
||||
setState(() => {});
|
||||
|
||||
//Try logging in
|
||||
|
@ -154,9 +152,8 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
node.unfocus();
|
||||
}
|
||||
controller.clear();
|
||||
settings.arl = _arl.trim();
|
||||
Navigator.of(context).pop();
|
||||
_update();
|
||||
_update(_arl.trim());
|
||||
}
|
||||
|
||||
void _loginBrowser() async {
|
||||
|
@ -231,7 +228,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
builder: (context) =>
|
||||
OtherDeviceLogin(_update));
|
||||
}),
|
||||
const SizedBox(height: 16.0),
|
||||
const SizedBox(height: 2.0),
|
||||
//Email login dialog (Not working anymore)
|
||||
// ElevatedButton(
|
||||
// child: Text(
|
||||
|
@ -247,7 +244,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
// const SizedBox(height: 2.0),
|
||||
|
||||
// only supported on android
|
||||
if (Platform.isAndroid) ...[
|
||||
if (LoginBrowser.supported()) ...[
|
||||
ElevatedButton(
|
||||
onPressed: _loginBrowser,
|
||||
child: Text('Login using browser'.i18n),
|
||||
|
@ -255,7 +252,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
const SizedBox(height: 2.0),
|
||||
],
|
||||
ElevatedButton(
|
||||
child: Text('Login using token'.i18n),
|
||||
child: Text('Login with ARL'.i18n),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -353,8 +350,10 @@ class LoadingWindowWait extends StatelessWidget {
|
|||
}
|
||||
|
||||
class LoginBrowser extends StatefulWidget {
|
||||
final Function updateParent;
|
||||
const LoginBrowser(this.updateParent, {super.key});
|
||||
static bool supported() => Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
final void Function(String arl) arlCallback;
|
||||
const LoginBrowser(this.arlCallback, {super.key});
|
||||
|
||||
@override
|
||||
State<LoginBrowser> createState() => _LoginBrowserState();
|
||||
|
@ -363,15 +362,9 @@ class LoginBrowser extends StatefulWidget {
|
|||
class _LoginBrowserState extends State<LoginBrowser> {
|
||||
late final WebViewController _controller;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = WebViewController()
|
||||
..clearLocalStorage()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
// Chrome on Android 14
|
||||
|
@ -396,9 +389,8 @@ class _LoginBrowserState extends State<LoginBrowser> {
|
|||
Uri linkUri = Uri.parse(uri.queryParameters['link']!);
|
||||
String? arl = linkUri.queryParameters['arl'];
|
||||
if (arl != null) {
|
||||
settings.arl = arl;
|
||||
Navigator.of(context).pop();
|
||||
widget.updateParent();
|
||||
widget.arlCallback(arl);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -421,6 +413,14 @@ class _LoginBrowserState extends State<LoginBrowser> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// clear everything
|
||||
unawaited(_controller.clearCache());
|
||||
unawaited(_controller.clearLocalStorage());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
|
@ -432,8 +432,8 @@ class _LoginBrowserState extends State<LoginBrowser> {
|
|||
}
|
||||
|
||||
class OtherDeviceLogin extends StatefulWidget {
|
||||
final VoidCallback callback;
|
||||
const OtherDeviceLogin(this.callback, {super.key});
|
||||
final void Function(String arl) arlCallback;
|
||||
const OtherDeviceLogin(this.arlCallback, {super.key});
|
||||
|
||||
@override
|
||||
State<OtherDeviceLogin> createState() => _OtherDeviceLoginState();
|
||||
|
@ -591,9 +591,8 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
|
|||
request.response.write(jsonEncode({'ok': true}));
|
||||
request.response.close();
|
||||
|
||||
settings.arl = decryptedArl;
|
||||
Navigator.pop(context);
|
||||
widget.callback();
|
||||
widget.arlCallback(decryptedArl);
|
||||
break;
|
||||
case 'cancel':
|
||||
if (!data.containsKey('hash') || data['hash'] is! String) {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/pipe_api.dart';
|
||||
import 'package:freezer/api/player/audio_handler.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
@ -47,14 +46,15 @@ class LyricsWidget extends StatefulWidget {
|
|||
|
||||
class _LyricsWidgetState extends State<LyricsWidget>
|
||||
with WidgetsBindingObserver {
|
||||
late StreamSubscription _mediaItemSub;
|
||||
late StreamSubscription _playbackStateSub;
|
||||
StreamSubscription? _mediaItemSub;
|
||||
StreamSubscription? _positionSub;
|
||||
int? _currentIndex = -1;
|
||||
Duration _nextOffset = Duration.zero;
|
||||
Duration _currentOffset = Duration.zero;
|
||||
String? _currentTrackId;
|
||||
final ScrollController _controller = ScrollController();
|
||||
final double height = 90;
|
||||
static const double height = 110.0;
|
||||
static const double additionalTranslationHeight = 40.0;
|
||||
BoxConstraints? _widgetConstraints;
|
||||
Lyrics? _lyrics;
|
||||
bool _loading = true;
|
||||
|
@ -65,6 +65,9 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
bool _animatedScroll = false;
|
||||
bool _syncedLyrics = false;
|
||||
|
||||
bool _showTranslation = false;
|
||||
bool _availableTranslation = false;
|
||||
|
||||
Future<void> _loadForId(String trackId) async {
|
||||
if (_currentTrackId == trackId) return;
|
||||
_currentTrackId = trackId;
|
||||
|
@ -77,20 +80,29 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
_nextOffset = Duration.zero;
|
||||
|
||||
//Fetch
|
||||
if (_loading == false && _lyrics != null) {
|
||||
setState(() {
|
||||
_freeScroll = false;
|
||||
_loading = true;
|
||||
_lyrics = null;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
_freeScroll = false;
|
||||
_loading = true;
|
||||
_lyrics = null;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
_lyricsCancelToken = CancelToken();
|
||||
final lyrics =
|
||||
await deezerAPI.lyrics(trackId, cancelToken: _lyricsCancelToken);
|
||||
await pipeAPI.lyrics(trackId, cancelToken: _lyricsCancelToken);
|
||||
if (lyrics == null) {
|
||||
setState(() {
|
||||
_error = 'No lyrics available.';
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
_syncedLyrics = lyrics.sync;
|
||||
_availableTranslation = lyrics.lyrics![0].translated != null;
|
||||
if (!_availableTranslation) {
|
||||
_showTranslation = false;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
|
@ -116,13 +128,15 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
void _scrollToLyric() {
|
||||
if (!_controller.hasClients) return;
|
||||
//Lyric height, screen height, appbar height
|
||||
final actualHeight =
|
||||
height + (_showTranslation ? additionalTranslationHeight : 0.0);
|
||||
double scrollTo;
|
||||
if (_widgetConstraints == null) {
|
||||
scrollTo = (height * _currentIndex!) -
|
||||
scrollTo = (actualHeight * _currentIndex!) -
|
||||
(MediaQuery.of(context).size.height / 4 + height / 2);
|
||||
} else {
|
||||
final widgetHeight = _widgetConstraints!.maxHeight;
|
||||
final minScroll = height * _currentIndex!;
|
||||
final minScroll = actualHeight * _currentIndex!;
|
||||
scrollTo = minScroll - widgetHeight / 2 + height / 2;
|
||||
}
|
||||
|
||||
|
@ -162,7 +176,8 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
}
|
||||
|
||||
void _makeSubscriptions() {
|
||||
_playbackStateSub = AudioService.position.listen(_updatePosition);
|
||||
if (_mediaItemSub != null || _positionSub != null) return;
|
||||
_positionSub = AudioService.position.listen(_updatePosition);
|
||||
|
||||
/// Track change = reload new lyrics
|
||||
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
|
||||
|
@ -172,6 +187,13 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
});
|
||||
}
|
||||
|
||||
void _cancelSubscriptions() {
|
||||
_mediaItemSub?.cancel();
|
||||
_positionSub?.cancel();
|
||||
_mediaItemSub = null;
|
||||
_positionSub = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
|
@ -186,10 +208,10 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
print('fuck? $state');
|
||||
switch (state) {
|
||||
case AppLifecycleState.paused:
|
||||
_mediaItemSub.cancel();
|
||||
_playbackStateSub.cancel();
|
||||
_cancelSubscriptions();
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
_makeSubscriptions();
|
||||
|
@ -201,9 +223,8 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_mediaItemSub.cancel();
|
||||
_playbackStateSub.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_cancelSubscriptions();
|
||||
//Stop visualizer
|
||||
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
|
||||
super.dispose();
|
||||
|
@ -219,61 +240,69 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
if (_freeScroll && !_loading)
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _freeScroll = false);
|
||||
_scrollToLyric();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(Colors.white)),
|
||||
child: Text(
|
||||
_currentIndex! >= 0
|
||||
? (_lyrics?.lyrics?[_currentIndex!].text ?? '...')
|
||||
: '...',
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
),
|
||||
Expanded(
|
||||
child: _error != null
|
||||
?
|
||||
//Shouldn't really happen, empty lyrics have own text
|
||||
ErrorScreen(message: _error.toString())
|
||||
:
|
||||
// Loading lyrics
|
||||
_loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: LayoutBuilder(builder: (context, constraints) {
|
||||
_widgetConstraints = constraints;
|
||||
return NotificationListener<ScrollStartNotification>(
|
||||
onNotification: (ScrollStartNotification notification) {
|
||||
if (!_syncedLyrics) return false;
|
||||
final extentDiff =
|
||||
(notification.metrics.extentBefore -
|
||||
notification.metrics.extentAfter)
|
||||
.abs();
|
||||
// avoid accidental clicks
|
||||
const extentThreshold = 10.0;
|
||||
if (extentDiff >= extentThreshold &&
|
||||
!_animatedScroll &&
|
||||
!_loading &&
|
||||
!_freeScroll) {
|
||||
setState(() => _freeScroll = true);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ScrollConfiguration(
|
||||
behavior: _scrollBehavior,
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
itemCount: _lyrics!.lyrics!.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0),
|
||||
child: Container(
|
||||
return Stack(
|
||||
children: [
|
||||
Column(children: [
|
||||
if (_freeScroll && !_loading)
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _freeScroll = false);
|
||||
_scrollToLyric();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(Colors.white)),
|
||||
child: Text(
|
||||
_currentIndex! >= 0
|
||||
? (_lyrics?.lyrics?[_currentIndex!].text ?? '...')
|
||||
: '...',
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
),
|
||||
Expanded(
|
||||
child: _error != null
|
||||
?
|
||||
//Shouldn't really happen, empty lyrics have own text
|
||||
ErrorScreen(message: _error.toString())
|
||||
:
|
||||
// Loading lyrics
|
||||
_loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: LayoutBuilder(builder: (context, constraints) {
|
||||
_widgetConstraints = constraints;
|
||||
return NotificationListener<ScrollStartNotification>(
|
||||
onNotification:
|
||||
(ScrollStartNotification notification) {
|
||||
if (!_syncedLyrics) return false;
|
||||
final extentDiff =
|
||||
(notification.metrics.extentBefore -
|
||||
notification.metrics.extentAfter)
|
||||
.abs();
|
||||
// avoid accidental clicks
|
||||
const extentThreshold = 10.0;
|
||||
if (extentDiff >= extentThreshold &&
|
||||
!_animatedScroll &&
|
||||
!_loading &&
|
||||
!_freeScroll) {
|
||||
setState(() => _freeScroll = true);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ScrollConfiguration(
|
||||
behavior: _scrollBehavior,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0),
|
||||
controller: _controller,
|
||||
itemExtent: !_syncedLyrics
|
||||
? null
|
||||
: height +
|
||||
(_showTranslation
|
||||
? additionalTranslationHeight
|
||||
: 0.0),
|
||||
itemCount: _lyrics!.lyrics!.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8.0),
|
||||
|
@ -281,7 +310,6 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
? Colors.grey.withOpacity(0.25)
|
||||
: Colors.transparent,
|
||||
),
|
||||
height: _syncedLyrics ? height : null,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8.0),
|
||||
|
@ -291,33 +319,64 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
|||
_lyrics!.lyrics![i].offset!)
|
||||
: null,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: _currentIndex == i
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 1.0),
|
||||
child: Text(
|
||||
_lyrics!.lyrics![i].text!,
|
||||
textAlign: _syncedLyrics
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: _syncedLyrics
|
||||
? 26.0
|
||||
: 20.0,
|
||||
fontWeight:
|
||||
(_currentIndex == i)
|
||||
? FontWeight.bold
|
||||
: FontWeight
|
||||
.normal),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_lyrics!.lyrics![i].text!,
|
||||
textAlign: _syncedLyrics
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: _syncedLyrics
|
||||
? 26.0
|
||||
: 20.0,
|
||||
fontWeight:
|
||||
(_currentIndex == i)
|
||||
? FontWeight
|
||||
.bold
|
||||
: FontWeight
|
||||
.normal),
|
||||
),
|
||||
if (_showTranslation)
|
||||
Text(
|
||||
_lyrics!.lyrics![i]
|
||||
.translated!,
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Color.lerp(
|
||||
Theme.of(
|
||||
context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
Colors.black,
|
||||
0.12),
|
||||
fontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
))));
|
||||
},
|
||||
)));
|
||||
}),
|
||||
),
|
||||
]);
|
||||
)));
|
||||
},
|
||||
)));
|
||||
}),
|
||||
),
|
||||
]),
|
||||
if (_availableTranslation)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _showTranslation = !_showTranslation);
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((_) => _scrollToLyric());
|
||||
},
|
||||
child: Text(_showTranslation
|
||||
? 'Without translation'.i18n
|
||||
: 'With translation'.i18n)),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ class MenuSheetOption {
|
|||
|
||||
class MenuSheet {
|
||||
final BuildContext context;
|
||||
final Function? navigateCallback;
|
||||
final VoidCallback? navigateCallback;
|
||||
|
||||
MenuSheet(this.context, {this.navigateCallback});
|
||||
|
||||
|
@ -233,12 +233,14 @@ class MenuSheet {
|
|||
void defaultTrackMenu(
|
||||
Track track, {
|
||||
List<MenuSheetOption> options = const [],
|
||||
List<MenuSheetOption> optionsTop = const [],
|
||||
Function? onRemove,
|
||||
TapUpDetails? details,
|
||||
}) {
|
||||
showWithTrack(
|
||||
track,
|
||||
<MenuSheetOption>[
|
||||
...optionsTop,
|
||||
addToQueueNext(track),
|
||||
addToQueue(track),
|
||||
(cache.checkTrackFavorite(track))
|
||||
|
@ -433,7 +435,7 @@ class MenuSheet {
|
|||
offlineAlbum(album),
|
||||
shareTile('album', album.id),
|
||||
...options
|
||||
]);
|
||||
], details: details);
|
||||
}
|
||||
|
||||
//===================
|
||||
|
|
|
@ -308,7 +308,7 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
|
|||
duration: const Duration(milliseconds: 250),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: snapshot.data == true
|
||||
? BorderRadius.circular(24.0)
|
||||
? BorderRadius.circular(widget.size / 3)
|
||||
: BorderRadius.circular(widget.size * 0.5),
|
||||
color: widget.color),
|
||||
child: child);
|
||||
|
|
|
@ -318,7 +318,8 @@ class PlayerScreenDesktop extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
Flexible(
|
||||
AspectRatio(
|
||||
aspectRatio: 9 / 16,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
|
@ -327,8 +328,8 @@ class PlayerScreenDesktop extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: PlayerScreenTopRow(
|
||||
textSize: 10.sp,
|
||||
iconSize: 17.sp,
|
||||
textSize: 12.h,
|
||||
iconSize: 21.h,
|
||||
showQueueButton: false,
|
||||
),
|
||||
),
|
||||
|
@ -339,18 +340,18 @@ class PlayerScreenDesktop extends StatelessWidget {
|
|||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: PlayerTextSubtext(textSize: 18.sp),
|
||||
child: PlayerTextSubtext(textSize: 22.h),
|
||||
),
|
||||
SeekBar(textSize: 12.sp),
|
||||
SeekBar(textSize: 16.h),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: PlaybackControls(24.sp),
|
||||
child: PlaybackControls(28.h),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||
child: BottomBarControls(
|
||||
size: 16.sp,
|
||||
size: 20.h,
|
||||
desktopMode: true,
|
||||
),
|
||||
)
|
||||
|
@ -786,41 +787,46 @@ class PlaybackControls extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
playerHelper.queueSource?.source != 'show'
|
||||
? ShuffleButton(iconSize: size * 0.75)
|
||||
: const ForwardReplay30Button(forward: false),
|
||||
PrevNextButton(size, prev: true),
|
||||
if (settings.enableFilledPlayButton)
|
||||
Consumer<BackgroundProvider>(builder: (context, provider, _) {
|
||||
final color = provider.dominantColor == null
|
||||
? Colors.transparent
|
||||
: Theme.of(context).brightness == Brightness.light
|
||||
? provider.dominantColor!
|
||||
: darken(provider.dominantColor!);
|
||||
return PlayPauseButton(size * 2.25,
|
||||
filled: true,
|
||||
material3: settings.enableMaterial3PlayButton,
|
||||
color: color,
|
||||
iconColor: Color.lerp(
|
||||
(ThemeData.estimateBrightnessForColor(color) ==
|
||||
Brightness.light
|
||||
? Colors.black
|
||||
: Colors.white),
|
||||
color,
|
||||
0.25));
|
||||
})
|
||||
else
|
||||
PlayPauseButton(size * 1.25),
|
||||
PrevNextButton(size),
|
||||
playerHelper.queueSource?.source != 'show'
|
||||
? RepeatButton(size * 0.75)
|
||||
: const ForwardReplay30Button(forward: true),
|
||||
],
|
||||
),
|
||||
child: StreamBuilder<QueueSource>(
|
||||
stream: playerHelper.queueSource,
|
||||
builder: (context, snapshot) {
|
||||
final queueSource = snapshot.data;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
queueSource?.source != 'show'
|
||||
? ShuffleButton(iconSize: size * 0.75)
|
||||
: const ForwardReplay30Button(forward: false),
|
||||
PrevNextButton(size, prev: true),
|
||||
if (settings.enableFilledPlayButton)
|
||||
Consumer<BackgroundProvider>(builder: (context, provider, _) {
|
||||
final color = provider.dominantColor == null
|
||||
? Colors.transparent
|
||||
: Theme.of(context).brightness == Brightness.light
|
||||
? provider.dominantColor!
|
||||
: darken(provider.dominantColor!);
|
||||
return PlayPauseButton(size * 2.25,
|
||||
filled: true,
|
||||
material3: settings.enableMaterial3PlayButton,
|
||||
color: color,
|
||||
iconColor: Color.lerp(
|
||||
(ThemeData.estimateBrightnessForColor(color) ==
|
||||
Brightness.light
|
||||
? Colors.black
|
||||
: Colors.white),
|
||||
color,
|
||||
0.25));
|
||||
})
|
||||
else
|
||||
PlayPauseButton(size * 1.25),
|
||||
PrevNextButton(size),
|
||||
queueSource?.source != 'show'
|
||||
? RepeatButton(size * 0.75)
|
||||
: const ForwardReplay30Button(forward: true),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -832,7 +838,7 @@ class BigAlbumArt extends StatefulWidget {
|
|||
State<BigAlbumArt> createState() => _BigAlbumArtState();
|
||||
}
|
||||
|
||||
class _BigAlbumArtState extends State<BigAlbumArt> {
|
||||
class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
||||
final _pageController = PageController(
|
||||
initialPage: playerHelper.queueIndex,
|
||||
keepPage: false,
|
||||
|
@ -842,14 +848,15 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
|
||||
/// is true on pointer down event
|
||||
/// used to distinguish between [PageController.animateToPage] and user gesture
|
||||
bool _userScroll = false;
|
||||
bool _userScroll = true;
|
||||
|
||||
/// whether the user has already scrolled the [PageView],
|
||||
/// so to avoid calling [PageController.animateToPage] again.
|
||||
bool _initiatedByUser = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
void _listenForMediaItemChanges() {
|
||||
if (_currentItemSub != null) return;
|
||||
|
||||
_currentItemSub = audioHandler.mediaItem.listen((event) async {
|
||||
if (_initiatedByUser) {
|
||||
_initiatedByUser = false;
|
||||
|
@ -859,12 +866,32 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
if (_pageController.page?.toInt() == playerHelper.queueIndex) return;
|
||||
print('animating controller to page');
|
||||
|
||||
_userScroll = false;
|
||||
await _pageController.animateToPage(playerHelper.queueIndex,
|
||||
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
_userScroll = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_listenForMediaItemChanges();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.paused:
|
||||
_currentItemSub?.cancel();
|
||||
case AppLifecycleState.resumed:
|
||||
_listenForMediaItemChanges();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.didChangeAppLifecycleState(state);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_currentItemSub?.cancel();
|
||||
|
@ -874,11 +901,6 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = GestureDetector(
|
||||
// onVerticalDragUpdate: (DragUpdateDetails details) {
|
||||
// if (details.delta.dy > 16) {
|
||||
// Navigator.of(context).pop();
|
||||
// }
|
||||
// },
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
FadePageRoute(
|
||||
|
@ -889,29 +911,7 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
return ZoomableImageRoute(
|
||||
imageUrl: mediaItem.artUri.toString(), heroKey: mediaItem.id);
|
||||
},
|
||||
)
|
||||
// PageRouteBuilder(
|
||||
// opaque: false, // transparent background
|
||||
// barrierDismissible: true,
|
||||
// pageBuilder: (context, animation, __) {
|
||||
// return FadeTransition(
|
||||
// opacity: animation,
|
||||
// child: PhotoView(
|
||||
// imageProvider: CachedNetworkImageProvider(
|
||||
// audioHandler.mediaItem.value!.artUri.toString()),
|
||||
// maxScale: 8.0,
|
||||
// minScale: 0.2,
|
||||
// heroAttributes: PhotoViewHeroAttributes(
|
||||
// tag: audioHandler.mediaItem.value!.id),
|
||||
// backgroundDecoration: const BoxDecoration(
|
||||
// color: Color.fromARGB(0x90, 0, 0, 0))),
|
||||
// );
|
||||
// }),
|
||||
),
|
||||
onHorizontalDragDown: (_) => _userScroll = true,
|
||||
// delayed a bit, so to make sure that the page view updated.
|
||||
onHorizontalDragEnd: (_) => Future.delayed(
|
||||
const Duration(milliseconds: 100), () => _userScroll = false),
|
||||
)),
|
||||
child: StreamBuilder<List<MediaItem>>(
|
||||
stream: audioHandler.queue,
|
||||
initialData: audioHandler.queue.valueOrNull,
|
||||
|
@ -927,10 +927,10 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
if (!_userScroll) return;
|
||||
Logger('BigAlbumArt')
|
||||
.fine('page changed, skipping to media item');
|
||||
// if (queue[index].id == audioHandler.mediaItem.value?.id) {
|
||||
// return;
|
||||
// }
|
||||
_initiatedByUser = true;
|
||||
if (queue[index].id == audioHandler.mediaItem.value?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
audioHandler.skipToQueueItem(index);
|
||||
},
|
||||
itemCount: queue.length,
|
||||
|
@ -1002,23 +1002,29 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||
iconSize: size,
|
||||
splashRadius: size * 1.5,
|
||||
),
|
||||
if (playerHelper.queueSource != null)
|
||||
Expanded(
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
text: TextSpan(children: [
|
||||
if (!short)
|
||||
TextSpan(
|
||||
text:
|
||||
'${'Playing from:'.i18n.toUpperCase().withoutLast(1)}\n',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: (textSize ?? 38.sp) * 0.85)),
|
||||
TextSpan(text: playerHelper.queueSource!.text ?? '')
|
||||
], style: TextStyle(fontSize: textSize ?? 38.sp))),
|
||||
),
|
||||
Expanded(
|
||||
child: StreamBuilder<QueueSource>(
|
||||
stream: playerHelper.queueSource,
|
||||
builder: (context, snapshot) {
|
||||
final queueSource = snapshot.data;
|
||||
if (queueSource == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
text: TextSpan(children: [
|
||||
if (!short)
|
||||
TextSpan(
|
||||
text: '${'PLAYING FROM'.i18n}\n',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: (textSize ?? 38.sp) * 0.85)),
|
||||
TextSpan(text: queueSource.text ?? '')
|
||||
], style: TextStyle(fontSize: textSize ?? 38.sp)));
|
||||
}),
|
||||
),
|
||||
showQueueButton
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
|
@ -1163,7 +1169,8 @@ class BottomBarControls extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (playerHelper.queueSource?.source == 'show') {
|
||||
final iconSize = size * 0.9;
|
||||
if (playerHelper.queueSource.valueOrNull?.source == 'show') {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
|
@ -1195,7 +1202,7 @@ class BottomBarControls extends StatelessWidget {
|
|||
Icons.sentiment_very_dissatisfied,
|
||||
semanticLabel: "Dislike".i18n,
|
||||
),
|
||||
iconSize: size * 0.85,
|
||||
iconSize: iconSize,
|
||||
onPressed: () async {
|
||||
unawaited(
|
||||
deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id));
|
||||
|
@ -1221,10 +1228,10 @@ class BottomBarControls extends StatelessWidget {
|
|||
// toastLength: Toast.LENGTH_SHORT);
|
||||
// },
|
||||
// ),
|
||||
FavoriteButton(size: size * 0.85),
|
||||
FavoriteButton(size: iconSize),
|
||||
desktopMode
|
||||
? PlayerMenuButtonDesktop(size: size)
|
||||
: PlayerMenuButton(size: size)
|
||||
? PlayerMenuButtonDesktop(size: iconSize)
|
||||
: PlayerMenuButton(size: iconSize)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,11 +6,10 @@ import 'package:dio/dio.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:fluttericon/typicons_icons.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player/audio_handler.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/notifiers/list_notifier.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
|
@ -29,7 +28,11 @@ FutureOr openScreenByURL(BuildContext context, String url) async {
|
|||
switch (res.type) {
|
||||
case DeezerLinkType.TRACK:
|
||||
Track t = await deezerAPI.track(res.id!);
|
||||
MenuSheet(context).defaultTrackMenu(t);
|
||||
MenuSheet(context).defaultTrackMenu(t, optionsTop: [
|
||||
MenuSheetOption(Text('Play'.i18n),
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onTap: () => playerHelper.playSearchMixDeferred(t)),
|
||||
]);
|
||||
break;
|
||||
case DeezerLinkType.ALBUM:
|
||||
Album a = await deezerAPI.album(res.id);
|
||||
|
@ -271,7 +274,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
SearchBrowseCard(
|
||||
color: const Color(0xff11b192),
|
||||
text: 'Flow'.i18n,
|
||||
icon: const Icon(Typicons.waves),
|
||||
icon: const Icon(FreezerIcons.waves),
|
||||
onTap: () async {
|
||||
await playerHelper.playFromSmartTrackList(
|
||||
SmartTrackList(id: 'flow'));
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:country_pickers/country.dart';
|
||||
|
@ -7,13 +9,12 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||
import 'package:fluttericon/web_symbols_icons.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/player/systray.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/ui/login_on_other_device.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
@ -463,6 +464,21 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
.toList(growable: false),
|
||||
)),
|
||||
),
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS)
|
||||
SwitchListTile(
|
||||
title: Text('Use colorful tray icon'.i18n),
|
||||
secondary:
|
||||
Image.asset(SysTray.getIcon(forcePng: true), height: 24.0),
|
||||
value: settings.useColorTrayIcon,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
settings.useColorTrayIcon = value;
|
||||
unawaited(settings.save());
|
||||
sysTray.updateIcon();
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
//Display mode (Android only!)
|
||||
if (defaultTargetPlatform == TargetPlatform.android)
|
||||
ListTile(
|
||||
|
@ -1260,7 +1276,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: const Icon(Icons.insert_drive_file)),
|
||||
ListTile(
|
||||
title: Text('Artist separator'.i18n),
|
||||
leading: const Icon(WebSymbols.tag),
|
||||
leading: const Icon(Icons.safety_divider),
|
||||
trailing: SizedBox(
|
||||
width: 75.0,
|
||||
child: TextField(
|
||||
|
@ -1431,7 +1447,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
settings.lastFMUsername != null)
|
||||
? 'Log out'.i18n
|
||||
: 'Login to enable scrobbling.'.i18n),
|
||||
leading: const Icon(FontAwesome5.lastfm),
|
||||
leading: const Icon(FreezerIcons.lastfm),
|
||||
onTap: () async {
|
||||
//Log out
|
||||
if (settings.lastFMPassword != null &&
|
||||
|
@ -1792,43 +1808,6 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
const FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Telegram Channel'.i18n),
|
||||
subtitle: Text('To get latest releases'.i18n),
|
||||
leading: const Icon(FontAwesome5.telegram,
|
||||
color: Color(0xFF27A2DF), size: 36.0),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://t.me/joinchat/Se4zLEBvjS1NCiY9'));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Telegram Group'.i18n),
|
||||
subtitle: Text('Official chat'.i18n),
|
||||
leading: const Icon(FontAwesome5.telegram,
|
||||
color: Colors.cyan, size: 36.0),
|
||||
onTap: () => launchUrl(Uri.parse('https://t.me/freezerandroid')),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Discord'.i18n),
|
||||
subtitle: Text('Official Discord server'.i18n),
|
||||
leading: const Icon(FontAwesome5.discord,
|
||||
color: Color(0xff7289da), size: 36.0),
|
||||
onTap: () => launchUrl(Uri.parse('https://discord.gg/qwJpa3r4dQ')),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('${'Repository'.i18n} (unavailable)'),
|
||||
subtitle: Text('Source code, report issues there.'.i18n),
|
||||
leading: const Icon(Icons.code, color: Colors.green, size: 36.0),
|
||||
enabled: false,
|
||||
),
|
||||
const ListTile(
|
||||
enabled: false,
|
||||
title: Text('Don\'t Donate'),
|
||||
subtitle: Text(
|
||||
'You should rather support your favorite artists, instead of this app!'),
|
||||
leading: Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
||||
),
|
||||
const FreezerDivider(),
|
||||
const ListTile(
|
||||
title: Text('Pato05'),
|
||||
subtitle: Text('Current Developer - best of all'),
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/octicons_icons.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player/audio_handler.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/main.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
|
@ -48,8 +48,8 @@ class TrackTile extends StatelessWidget {
|
|||
this.onSecondary,
|
||||
this.trailing,
|
||||
this.checkTrackOffline = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
factory TrackTile.fromTrack(Track track,
|
||||
{VoidCallback? onTap,
|
||||
|
@ -130,7 +130,7 @@ class TrackTile extends StatelessWidget {
|
|||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
||||
child: Icon(
|
||||
Octicons.primitive_dot,
|
||||
FreezerIcons.primitive_dot,
|
||||
color: Colors.green,
|
||||
size: 12.0,
|
||||
),
|
||||
|
@ -602,15 +602,16 @@ class _SmartTrackListTileState extends State<SmartTrackListTile> {
|
|||
class AlbumCard extends StatelessWidget {
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
final SecondaryTapCallback? onSecondary;
|
||||
|
||||
const AlbumCard(this.album, {super.key, this.onTap, this.onHold});
|
||||
const AlbumCard(this.album, {super.key, this.onTap, this.onSecondary});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onLongPress: normalizeSecondary(onSecondary),
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
|
|
|
@ -105,6 +105,10 @@ install(CODE "
|
|||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
# Install desktop script
|
||||
install(FILES "install-desktop-file.sh" DESTINATION "${BUILD_BUNDLE_DIR}"
|
||||
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
|
@ -123,9 +127,6 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
|||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# add app icon
|
||||
install(FILES "app_icon.ico" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}")
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
|
@ -140,3 +141,17 @@ if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
|||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# === Create Tarball ===
|
||||
# Make a tarball out of the install bundle (only in Release mode)
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
# Remove old tarballs
|
||||
file(GLOB REMOVE_OLD_TARBALLS ${PROJECT_BINARY_DIR}/freezer-linux-*.tar.gz)
|
||||
file(REMOVE ${REMOVE_OLD_TARBALLS})
|
||||
|
||||
# Configure the script to create a new tarball, and run it as a target
|
||||
configure_file(create-tarball.sh ${PROJECT_BINARY_DIR} @ONLY
|
||||
FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ)
|
||||
add_custom_target(create_tar ALL COMMAND "${PROJECT_BINARY_DIR}/create-tarball.sh")
|
||||
add_dependencies(create_tar ${BINARY_NAME})
|
||||
endif()
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
cd "@PROJECT_BINARY_DIR@/../../../.."
|
||||
COMMIT_HASH=`git log -1 --pretty=format:%h`
|
||||
cd "@BUILD_BUNDLE_DIR@"
|
||||
@CMAKE_COMMAND@ -E tar cvzf "@PROJECT_BINARY_DIR@/freezer-linux-${COMMIT_HASH}.tar.gz" .
|
|
@ -9,7 +9,10 @@
|
|||
#include <dynamic_color/dynamic_color_plugin.h>
|
||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
||||
|
@ -21,7 +24,16 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
}
|
||||
|
|
|
@ -6,11 +6,13 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
dynamic_color
|
||||
isar_flutter_libs
|
||||
media_kit_libs_linux
|
||||
screen_retriever
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
media_kit_native_event_loop
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
#!/bin/bash
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
if [ -z "$HOME" ]; then
|
||||
echo "HOME variable not set. Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXECUTABLE="${SCRIPT_DIR}/freezer"
|
||||
ICON="${SCRIPT_DIR}/data/flutter_assets/assets/icon.ico"
|
||||
if [ ! -x "$EXECUTABLE" ] || [ ! -f "$ICON" ]; then
|
||||
echo "Icon or executable not found. Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APPLICATIONS_DIR="$HOME/.local/share/applications"
|
||||
if [ ! -w "$APPLICATIONS_DIR" ]; then
|
||||
echo "Cannot write to $APPLICATIONS_DIR. Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=freezer
|
||||
GenericName=No DRM Deezer client
|
||||
Comment=No DRM Deezer client to listen and download your songs
|
||||
Exec=$EXECUTABLE
|
||||
Icon=$ICON
|
||||
Categories=Audio;Music;Player;AudioVideo;
|
||||
Keywords=Music;Player;Streaming;Online;
|
||||
Terminal=false
|
||||
EOF > "$APPLICATIONS_DIR/freezer.desktop"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
|
|
|
@ -1,118 +1,104 @@
|
|||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication
|
||||
{
|
||||
GtkApplication parent_instance;
|
||||
char **dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication *application)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(application);
|
||||
GtkWindow *window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen *screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen))
|
||||
{
|
||||
const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0)
|
||||
{
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// set icon
|
||||
gtk_window_set_icon_from_file(window, "data/app_icon.ico", NULL);
|
||||
if (use_header_bar)
|
||||
{
|
||||
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "Freezer");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_window_set_title(window, "Freezer");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView *view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error))
|
||||
{
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject *object)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass *klass)
|
||||
{
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication *self) {}
|
||||
|
||||
MyApplication *my_application_new()
|
||||
{
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "freezer");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "freezer");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
|
|
|
@ -15,10 +15,13 @@ import just_audio
|
|||
import network_info_plus
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import screen_retriever
|
||||
import share_plus
|
||||
import sqflite
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||
|
@ -31,8 +34,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
|
372
pubspec.lock
372
pubspec.lock
|
@ -21,10 +21,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03"
|
||||
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.6"
|
||||
version: "3.4.10"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -37,10 +37,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd"
|
||||
sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.5.2"
|
||||
async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -61,10 +61,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_service_mpris
|
||||
sha256: "31be5de2db0c71b217157afce1974ac6d0ad329bd91deb1f19ad094d29340d8e"
|
||||
sha256: a8d1583f9143d17b2facc994a99bd1ea257cec43adcb8d7349458555c62b570f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
version: "0.1.3"
|
||||
audio_service_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -85,10 +85,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_session
|
||||
sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad"
|
||||
sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.16"
|
||||
version: "0.1.18"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -117,34 +117,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65"
|
||||
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.0.1"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8"
|
||||
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b"
|
||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
version: "2.4.8"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185
|
||||
sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.11"
|
||||
version: "7.3.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -157,34 +157,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74
|
||||
sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.6.3"
|
||||
version: "8.9.0"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
|
||||
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.3.1"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
|
||||
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "4.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
|
||||
sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -213,10 +213,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677"
|
||||
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "4.10.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -229,10 +229,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
|
||||
sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
version: "5.0.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -269,10 +269,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c"
|
||||
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+6"
|
||||
version: "0.3.3+8"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -318,18 +318,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263"
|
||||
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.8"
|
||||
version: "0.7.10"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||
sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.3"
|
||||
version: "5.4.0"
|
||||
dio_cookie_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -350,10 +350,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f"
|
||||
sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.8"
|
||||
version: "1.6.9"
|
||||
encrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -422,10 +422,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flex_color_picker
|
||||
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
|
||||
sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.3.1"
|
||||
flex_seed_scheme:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -443,18 +443,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_background_service
|
||||
sha256: "5ec79841c3e9f3bd1885b06c5d7502d6df415cb1665e6717792cc0e51716619f"
|
||||
sha256: "94d9a143852729140e17254a53769383b03738cd92b6e588a8762003e6cd9dd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.5"
|
||||
flutter_background_service_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_background_service_android
|
||||
sha256: a295c7604782b3723fa356679e5b14c5e0fb694d77a7299af135364fa851ee1a
|
||||
sha256: "30863ebafd8214b8e76d5e5c9f27887dc5cc303fcf3e89f71534f621fc486782"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.2.2"
|
||||
flutter_background_service_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -500,18 +500,18 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.1"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "501ed9d54f1c8c0535b7991bade36f9e7e3b45a2346401f03775c1ec7a3c06ae"
|
||||
sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.1.2"
|
||||
version: "16.3.2"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -559,22 +559,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluttericon:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttericon
|
||||
sha256: "252fa8043826e93d972a602497a260cb3d62b5aea6d045793e4381590f2c1e99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
fluttertoast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c"
|
||||
sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.2"
|
||||
version: "8.2.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -583,6 +575,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
gettext_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gettext_parser
|
||||
sha256: "9565c9dd1033ec125e1fbc7ccba6c0d2d753dd356122ba1a17e6aa7dc868f34a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -595,10 +595,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: e20ff62b158b96f392bfc8afe29dee1503c94fbea2cbe8186fd59b756b8ae982
|
||||
sha256: f0b8d115a13ecf827013ec9fc883390ccc0e87a96ed5347a3114cac177ef18e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "6.1.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -659,10 +659,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -683,18 +683,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: i18n_extension
|
||||
sha256: db45cd88cf3114f5b9368d975aebebe4ac37fa634fbc5643634289cdfd4d3631
|
||||
sha256: "813da89a434e617e3065a5d729f148a4d2d93d227e4d1ed4e77660eda6fa58c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.2"
|
||||
version: "10.0.3"
|
||||
i18n_extension_importer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: i18n_extension_importer
|
||||
sha256: "4fd651ff47ac52f604b34b5cd80ee225d38fe589d51b042f00178081b476252f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.6"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
|
||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.3"
|
||||
version: "4.1.7"
|
||||
infinite_listview:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -771,43 +779,41 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio
|
||||
sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b"
|
||||
sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.35"
|
||||
version: "0.9.36"
|
||||
just_audio_media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: dcb7d1aa74a96b3dd892b3f65ec83f5d77352dfa
|
||||
url: "https://github.com/Pato05/just_audio_media_kit.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
path: "../just_audio_media_kit"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.0.0"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_platform_interface
|
||||
sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df
|
||||
sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
version: "4.2.2"
|
||||
just_audio_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_web
|
||||
sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13
|
||||
sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.8"
|
||||
version: "0.4.9"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -844,12 +850,12 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: "3dffc6d0c19117d51fbc42a7f89612e0595665800a596289ab7a80bdd93e0ad1"
|
||||
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.9"
|
||||
version: "1.1.10+1"
|
||||
media_kit_libs_linux:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_linux
|
||||
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
|
||||
|
@ -857,21 +863,21 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.3"
|
||||
media_kit_libs_windows_audio:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_windows_audio
|
||||
sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
media_kit_native_event_loop:
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_native_event_loop
|
||||
sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e
|
||||
name: menu_base
|
||||
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "0.1.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -884,10 +890,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
move_to_background:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -972,10 +978,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
||||
sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "5.0.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1004,26 +1010,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
|
||||
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.2"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1036,10 +1042,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1060,26 +1066,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "3c84d49f0a5e1915364707159ab71f11b3b8a429532176d3a6248a45718ad4f9"
|
||||
sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.1"
|
||||
version: "11.3.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: a5ebaa420cee8fd880ef10dedd42c6b3f493e7dbe27d7e0a7e1798669373082a
|
||||
sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.4"
|
||||
version: "12.0.5"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "6ca25ee52518a8a26e80aaefe3c71caf6e2dfd809c1b20900d0882df6faed36e"
|
||||
sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.1"
|
||||
version: "9.4.0"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1092,10 +1098,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c"
|
||||
sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.2.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1108,10 +1114,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
version: "6.0.2"
|
||||
photo_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1124,26 +1130,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.3"
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1156,10 +1162,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.5"
|
||||
version: "6.1.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1180,34 +1186,34 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: quick_actions
|
||||
sha256: "3930e1cf78a0574495b4ea741ee197323c4a9081321d6ae384b3bfcd84c7ea83"
|
||||
sha256: b17da113df7a7005977f64adfa58ccc49c829d3ccc6e8e770079a8c7fbf2da9e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.7"
|
||||
quick_actions_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_android
|
||||
sha256: df67c20583e05f5038a24c47bfa1b7b2977703ec2d162663017c5f9ef8707699
|
||||
sha256: adb42f20a46b22fee4caef421c00ff9eb209f9d441010bc5d6e9afa824288cf6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
version: "1.0.10"
|
||||
quick_actions_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_ios
|
||||
sha256: f086cf98884421188c7c5c13f61b62aeb5b6fb88f197a0601db45108b1444ea6
|
||||
sha256: dd355101d0e9fef6176fa2ae2bf738bcafa8df09a1e17057fcb56475719793de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
version: "1.0.10"
|
||||
quick_actions_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_platform_interface
|
||||
sha256: d2a8566b56eec49f93934528b62033906199c60f4ffaef0cba9ef02fcfed8a81
|
||||
sha256: "81a1e40c519bb3cacfec38b3008b13cef665a75bd270da94f40091b57f0f9236"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
random_string:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1232,6 +1238,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever
|
||||
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.9"
|
||||
scrobblenaut:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1245,10 +1259,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
|
||||
sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.1"
|
||||
version: "7.2.2"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1273,6 +1287,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shortid
|
||||
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -1282,10 +1304,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1306,10 +1328,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: spotify
|
||||
sha256: e967c5e295792e9d38f4c5e9e60d7c2868ed9cb2a8fac2a67c75303f8395e374
|
||||
sha256: "5c4d80a3d6a263c26d4922faf0cb9688234c721760ea3a0dd72e0172bb6fa72c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
version: "0.13.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1322,18 +1344,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
|
||||
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
|
||||
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.5.3"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1370,10 +1392,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.0+1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1394,10 +1416,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: time
|
||||
sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124"
|
||||
sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1414,6 +1436,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1474,66 +1504,66 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.14"
|
||||
version: "6.2.4"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
|
||||
sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "6.2.2"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
|
||||
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
version: "6.2.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
|
||||
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.1.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
|
||||
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.1.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
|
||||
sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.3.1"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
|
||||
sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.20"
|
||||
version: "2.2.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.1.1"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1562,10 +1592,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: f45a6c03aa3f8322e0a9d7f4a0482721c8789cb41d555407367650b8f9c26018
|
||||
sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "1.1.4"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1602,10 +1632,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: d81b68e88cc353e546afb93fb38958e3717282c5ac6e5d3be4a4aef9fc3c1413
|
||||
sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
version: "4.7.0"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1618,10 +1648,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
|
||||
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
version: "2.10.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1634,26 +1664,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.9"
|
||||
version: "5.2.0"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.8"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.4"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.5.0"
|
||||
xxh3:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
61
pubspec.yaml
61
pubspec.yaml
|
@ -25,7 +25,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
spotify: ^0.12.0
|
||||
spotify: ^0.13.1
|
||||
flutter_displaymode: ^0.6.0
|
||||
crypto: ^3.0.3
|
||||
http: ^1.1.0
|
||||
|
@ -42,7 +42,7 @@ dependencies:
|
|||
flutter_material_color_picker: ^1.0.5
|
||||
country_pickers: ^2.0.0
|
||||
move_to_background: ^1.0.1
|
||||
flutter_local_notifications: ^15.1.0+1
|
||||
flutter_local_notifications: ^16.3.2
|
||||
collection: ^1.17.1
|
||||
random_string: ^2.0.1
|
||||
async: ^2.6.1
|
||||
|
@ -51,8 +51,7 @@ dependencies:
|
|||
marquee: ^2.2.0
|
||||
flutter_cache_manager: ^3.0.0
|
||||
cached_network_image: ^3.1.0
|
||||
i18n_extension: ^9.0.2
|
||||
fluttericon: ^2.0.0
|
||||
i18n_extension: ^10.0.3
|
||||
url_launcher: ^6.0.5
|
||||
uni_links: ^0.5.1
|
||||
numberpicker: ^2.1.1
|
||||
|
@ -65,25 +64,32 @@ dependencies:
|
|||
open_file: ^3.0.3
|
||||
version: ^3.0.2
|
||||
wakelock_plus: ^1.1.1
|
||||
google_fonts: ^5.1.0
|
||||
google_fonts: ^6.1.0
|
||||
audio_session: ^0.1.6
|
||||
audio_service: ^0.18.1
|
||||
provider: ^6.0.0
|
||||
hive_flutter: ^1.1.0
|
||||
connectivity_plus: ^4.0.1
|
||||
connectivity_plus: ^5.0.2
|
||||
share_plus: ^7.0.2
|
||||
disk_space_plus: ^0.2.3
|
||||
dynamic_color: ^1.6.6
|
||||
package_info_plus: ^4.0.2
|
||||
package_info_plus: ^5.0.1
|
||||
encrypt: ^5.0.1
|
||||
dart_blowfish:
|
||||
git:
|
||||
url: https://github.com/Pato05/dart_blowfish.git
|
||||
ref: main
|
||||
logging: ^1.2.0
|
||||
|
||||
# Player
|
||||
just_audio: ^0.9.35
|
||||
# Player plugin for Linux and Windows
|
||||
just_audio_media_kit:
|
||||
git: https://github.com/Pato05/just_audio_media_kit.git
|
||||
path: ../just_audio_media_kit
|
||||
# Player libs for Linux and windows
|
||||
media_kit_libs_linux: any
|
||||
media_kit_libs_windows_audio: any
|
||||
|
||||
rxdart: ^0.27.7
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1
|
||||
|
@ -94,9 +100,13 @@ dependencies:
|
|||
flutter_cache_manager_hive:
|
||||
git: https://github.com/Pato05/flutter_cache_manager_hive.git
|
||||
flex_color_picker: ^3.3.0
|
||||
webview_flutter:
|
||||
^4.4.4
|
||||
webview_flutter: ^4.4.4
|
||||
network_info_plus: ^4.1.0+1
|
||||
pointycastle: ^3.7.4
|
||||
i18n_extension_importer: ^0.0.6
|
||||
tray_manager: ^0.2.1
|
||||
window_manager:
|
||||
^0.3.8
|
||||
#deezcryptor:
|
||||
#path: deezcryptor/
|
||||
|
||||
|
@ -106,7 +116,7 @@ dev_dependencies:
|
|||
json_serializable: ^6.0.1
|
||||
build_runner: ^2.4.6
|
||||
hive_generator: ^2.0.0
|
||||
flutter_lints: ^2.0.3
|
||||
flutter_lints: ^3.0.1
|
||||
isar_generator: ^3.1.0+1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
|
@ -127,27 +137,30 @@ flutter:
|
|||
assets:
|
||||
- assets/cover.jpg
|
||||
- assets/cover_thumb.jpg
|
||||
- assets/icon.png
|
||||
- assets/favorites_thumb.jpg
|
||||
- assets/browse_icon.png
|
||||
- assets/icon.png
|
||||
- assets/icon_small.png
|
||||
- assets/icon_mono_small.png
|
||||
- assets/icon.ico
|
||||
- assets/icon_mono_small.ico
|
||||
|
||||
fonts:
|
||||
# - family: Montserrat
|
||||
# fonts:
|
||||
# - asset: assets/fonts/Montserrat-Regular.ttf
|
||||
# - asset: assets/fonts/Montserrat-Bold.ttf
|
||||
# weight: 700
|
||||
# - asset: assets/fonts/Montserrat-Italic.ttf
|
||||
# style: italic
|
||||
- family: MabryPro
|
||||
- family: FreezerIcons
|
||||
fonts:
|
||||
- asset: assets/fonts/MabryPro.otf
|
||||
- asset: assets/fonts/MabryProItalic.otf
|
||||
- asset: fonts/FreezerIcons.ttf
|
||||
- family: "Mabry Pro"
|
||||
fonts:
|
||||
- asset: fonts/MabryPro.otf
|
||||
- asset: fonts/MabryProItalic.otf
|
||||
style: italic
|
||||
- asset: assets/fonts/MabryProBold.otf
|
||||
- asset: fonts/MabryProBold.otf
|
||||
weight: 700
|
||||
- asset: assets/fonts/MabryProBlack.otf
|
||||
- asset: fonts/MabryProBlack.otf
|
||||
weight: 900
|
||||
- family: "YouTube Sans"
|
||||
fonts:
|
||||
- asset: fonts/YouTubeSansMedium.otf
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import zipfile
|
||||
import json
|
||||
import os
|
||||
|
||||
lang_crowdin = {
|
||||
'ar': 'ar_ar',
|
||||
|
@ -48,6 +49,19 @@ def generate_dart():
|
|||
out = f'const crowdin = {data};'
|
||||
f.write(out)
|
||||
|
||||
def dart_to_json():
|
||||
with open('../lib/languages/crowdin.dart', 'r') as f:
|
||||
content = f.read()
|
||||
a = content[len('const crowdin = '):-1].replace('\\$', '$')
|
||||
translation_map = json.loads(a)
|
||||
for lang in translation_map:
|
||||
lang_ = list(lang_crowdin.keys())[list(lang_crowdin.values()).index(lang)]
|
||||
os.mkdir(f'exp/{lang_}')
|
||||
with open(f'exp/{lang_}/freezer.json', 'w') as f:
|
||||
f.write(json.dumps(translation_map[lang]))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_dart()
|
||||
dart_to_json()
|
||||
# generate_dart()
|
||||
|
|
|
@ -1,226 +0,0 @@
|
|||
{
|
||||
"Home": "Home",
|
||||
"Search": "Search",
|
||||
"Library": "Library",
|
||||
"Offline mode, can't play flow or smart track lists.":
|
||||
"Offline mode, can't play flow or smart track lists.",
|
||||
"Added to library": "Added to library",
|
||||
"Download": "Download",
|
||||
"Disk": "Disk",
|
||||
"Offline": "Offline",
|
||||
"Top Tracks": "Top Tracks",
|
||||
"Show more tracks": "Show more tracks",
|
||||
"Top": "Top",
|
||||
"Top Albums": "Top Albums",
|
||||
"Show all albums": "Show all albums",
|
||||
"Discography": "Discography",
|
||||
"Default": "Default",
|
||||
"Reverse": "Reverse",
|
||||
"Alphabetic": "Alphabetic",
|
||||
"Artist": "Artist",
|
||||
"Post processing...": "Post processing...",
|
||||
"Done": "Done",
|
||||
"Delete": "Delete",
|
||||
"Are you sure you want to delete this download?":
|
||||
"Are you sure you want to delete this download?",
|
||||
"Cancel": "Cancel",
|
||||
"Downloads": "Downloads",
|
||||
"Clear queue": "Clear queue",
|
||||
"This won't delete currently downloading item":
|
||||
"This won't delete currently downloading item",
|
||||
"Are you sure you want to delete all queued downloads?":
|
||||
"Are you sure you want to delete all queued downloads?",
|
||||
"Clear downloads history": "Clear downloads history",
|
||||
"WARNING: This will only clear non-offline (external downloads)":
|
||||
"WARNING: This will only clear non-offline (external downloads)",
|
||||
"Please check your connection and try again later...":
|
||||
"Please check your connection and try again later...",
|
||||
"Show more": "Show more",
|
||||
"Importer": "Importer",
|
||||
"Currently supporting only Spotify, with 100 tracks limit":
|
||||
"Currently supporting only Spotify, with 100 tracks limit",
|
||||
"Due to API limitations": "Due to API limitations",
|
||||
"Enter your playlist link below": "Enter your playlist link below",
|
||||
"Error loading URL!": "Error loading URL!",
|
||||
"Convert": "Convert",
|
||||
"Download only": "Download only",
|
||||
"Downloading is currently stopped, click here to resume.":
|
||||
"Downloading is currently stopped, click here to resume.",
|
||||
"Tracks": "Tracks",
|
||||
"Albums": "Albums",
|
||||
"Artists": "Artists",
|
||||
"Playlists": "Playlists",
|
||||
"Import": "Import",
|
||||
"Import playlists from Spotify": "Import playlists from Spotify",
|
||||
"Statistics": "Statistics",
|
||||
"Offline tracks": "Offline tracks",
|
||||
"Offline albums": "Offline albums",
|
||||
"Offline playlists": "Offline playlists",
|
||||
"Offline size": "Offline size",
|
||||
"Free space": "Free space",
|
||||
"Loved tracks": "Loved tracks",
|
||||
"Favorites": "Favorites",
|
||||
"All offline tracks": "All offline tracks",
|
||||
"Create new playlist": "Create new playlist",
|
||||
"Cannot create playlists in offline mode":
|
||||
"Cannot create playlists in offline mode",
|
||||
"Error": "Error",
|
||||
"Error logging in! Please check your token and internet connection and try again.":
|
||||
"Error logging in! Please check your token and internet connection and try again.",
|
||||
"Dismiss": "Dismiss",
|
||||
"Welcome to": "Welcome to",
|
||||
"Please login using your Deezer account.":
|
||||
"Please login using your Deezer account.",
|
||||
"Login using browser": "Login using browser",
|
||||
"Login using token": "Login using token",
|
||||
"Enter ARL": "Enter ARL",
|
||||
"Token (ARL)": "Token (ARL)",
|
||||
"Save": "Save",
|
||||
"If you don't have account, you can register on deezer.com for free.":
|
||||
"If you don't have account, you can register on deezer.com for free.",
|
||||
"Open in browser": "Open in browser",
|
||||
"By using this app, you don't agree with the Deezer ToS":
|
||||
"By using this app, you don't agree with the Deezer ToS",
|
||||
"Play next": "Play next",
|
||||
"Add to queue": "Add to queue",
|
||||
"Add track to favorites": "Add track to favorites",
|
||||
"Add to playlist": "Add to playlist",
|
||||
"Select playlist": "Select playlist",
|
||||
"Track added to": "Track added to",
|
||||
"Remove from playlist": "Remove from playlist",
|
||||
"Track removed from": "Track removed from",
|
||||
"Remove favorite": "Remove favorite",
|
||||
"Track removed from library": "Track removed from library",
|
||||
"Go to": "Go to",
|
||||
"Make offline": "Make offline",
|
||||
"Add to library": "Add to library",
|
||||
"Remove album": "Remove album",
|
||||
"Album removed": "Album removed",
|
||||
"Remove from favorites": "Remove from favorites",
|
||||
"Artist removed from library": "Artist removed from library",
|
||||
"Add to favorites": "Add to favorites",
|
||||
"Remove from library": "Remove from library",
|
||||
"Add playlist to library": "Add playlist to library",
|
||||
"Added playlist to library": "Added playlist to library",
|
||||
"Make playlist offline": "Make playlist offline",
|
||||
"Download playlist": "Download playlist",
|
||||
"Create playlist": "Create playlist",
|
||||
"Title": "Title",
|
||||
"Description": "Description",
|
||||
"Private": "Private",
|
||||
"Collaborative": "Collaborative",
|
||||
"Create": "Create",
|
||||
"Playlist created!": "Playlist created!",
|
||||
"Playing from:": "Playing from:",
|
||||
"Queue": "Queue",
|
||||
"Offline search": "Offline search",
|
||||
"Search Results": "Search Results",
|
||||
"No results!": "No results!",
|
||||
"Show all tracks": "Show all tracks",
|
||||
"Show all playlists": "Show all playlists",
|
||||
"Settings": "Settings",
|
||||
"General": "General",
|
||||
"Appearance": "Appearance",
|
||||
"Quality": "Quality",
|
||||
"Deezer": "Deezer",
|
||||
"Theme": "Theme",
|
||||
"Currently": "Currently",
|
||||
"Select theme": "Select theme",
|
||||
"Dark": "Dark",
|
||||
"Black (AMOLED)": "Black (AMOLED)",
|
||||
"Deezer (Dark)": "Deezer (Dark)",
|
||||
"Primary color": "Primary color",
|
||||
"Selected color": "Selected color",
|
||||
"Use album art primary color": "Use album art primary color",
|
||||
"Warning: might be buggy": "Warning: might be buggy",
|
||||
"Mobile streaming": "Mobile streaming",
|
||||
"Wifi streaming": "Wifi streaming",
|
||||
"External downloads": "External downloads",
|
||||
"Content language": "Content language",
|
||||
"Not app language, used in headers. Now":
|
||||
"Not app language, used in headers. Now",
|
||||
"Select language": "Select language",
|
||||
"Content country": "Content country",
|
||||
"Country used in headers. Now": "Country used in headers. Now",
|
||||
"Log tracks": "Log tracks",
|
||||
"Send track listen logs to Deezer, enable it for features like Flow to work properly":
|
||||
"Send track listen logs to Deezer, enable it for features like Flow to work properly",
|
||||
"Offline mode": "Offline mode",
|
||||
"Will be overwritten on start.": "Will be overwritten on start.",
|
||||
"Error logging in, check your internet connections.":
|
||||
"Error logging in, check your internet connections.",
|
||||
"Logging in...": "Logging in...",
|
||||
"Download path": "Download path",
|
||||
"Downloads naming": "Downloads naming",
|
||||
"Downloaded tracks filename": "Downloaded tracks filename",
|
||||
"Valid variables are": "Valid variables are",
|
||||
"Reset": "Reset",
|
||||
"Clear": "Clear",
|
||||
"Create folders for artist": "Create folders for artist",
|
||||
"Create folders for albums": "Create folders for albums",
|
||||
"Separate albums by discs": "Separate albums by disks",
|
||||
"Overwrite already downloaded files": "Overwrite already downloaded files",
|
||||
"Copy ARL": "Copy ARL",
|
||||
"Copy userToken/ARL Cookie for use in other apps.":
|
||||
"Copy userToken/ARL Cookie for use in other apps.",
|
||||
"Copied": "Copied",
|
||||
"Log out": "Log out",
|
||||
"Due to plugin incompatibility, login using browser is unavailable without restart.":
|
||||
"Due to plugin incompatibility, login using browser is unavailable without restart.",
|
||||
"(ARL ONLY) Continue": "(ARL ONLY) Continue",
|
||||
"Log out & Exit": "Log out & Exit",
|
||||
"Pick-a-Path": "Pick-a-Path",
|
||||
"Select storage": "Select storage",
|
||||
"Go up": "Go up",
|
||||
"Permission denied": "Permission denied",
|
||||
"Language": "Language",
|
||||
"Language changed, please restart Freezer to apply!":
|
||||
"Language changed, please restart Freezer to apply!",
|
||||
"Importing...": "Importing...",
|
||||
"Radio": "Radio",
|
||||
"Flow": "Flow",
|
||||
"Track is not available on Deezer!": "Track is not available on Deezer!",
|
||||
"Failed to download track! Please restart.": "Failed to download track! Please restart.",
|
||||
"Storage permission denied!": "Storage permission denied!",
|
||||
"Failed": "Failed",
|
||||
"Queued": "Queued",
|
||||
"External": "Storage",
|
||||
"Restart failed downloads": "Restart failed downloads",
|
||||
"Clear failed": "Clear failed",
|
||||
"Download Settings": "Download Settings",
|
||||
"Create folder for playlist": "Create folder for playlist",
|
||||
"Download .LRC lyrics": "Download .LRC lyrics",
|
||||
"Proxy": "Proxy",
|
||||
"Not set": "Not set",
|
||||
"Search or paste URL": "Search or paste URL",
|
||||
"History": "History",
|
||||
"Download threads": "Concurrent downloads",
|
||||
"Lyrics unavailable, empty or failed to load!": "Lyrics unavailable, empty or failed to load!",
|
||||
"About": "About",
|
||||
"Telegram Channel": "Telegram Channel",
|
||||
"To get latest releases": "To get latest releases",
|
||||
"Official chat": "Official chat",
|
||||
"Telegram Group": "Telegram Group",
|
||||
"Huge thanks to all the contributors! <3": "Huge thanks to all the contributors! <3",
|
||||
"Edit playlist": "Edit playlist",
|
||||
"Update": "Update",
|
||||
"Playlist updated!": "Playlist updated!",
|
||||
"Downloads added!": "Downloads added!",
|
||||
"Save cover file for every track": "Save cover file for every track",
|
||||
"Download Log": "Download Log",
|
||||
"Repository": "Repository",
|
||||
"Source code, report issues there.": "Source code, report issues there.",
|
||||
"Use system theme": "Use system theme",
|
||||
"Light": "Light",
|
||||
"Popularity": "Popularity",
|
||||
"User": "User",
|
||||
"Track count": "Track count",
|
||||
"If you want to use custom directory naming - use '/' as directory separator.": "If you want to use custom directory naming - use '/' as directory separator.",
|
||||
"Share": "Share",
|
||||
"Save album cover": "Save album cover",
|
||||
"Warning": "Warning",
|
||||
"Using too many concurrent downloads on older/weaker devices might cause crashes!": "Using too many concurrent downloads on older/weaker devices might cause crashes!",
|
||||
"Create .nomedia files": "Create .nomedia files",
|
||||
"To prevent gallery being filled with album art": "To prevent gallery being filled with album art"
|
||||
}
|
||||
|
|
@ -11,8 +11,11 @@
|
|||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
|
@ -25,8 +28,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
TrayManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
}
|
||||
|
|
|
@ -8,12 +8,14 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
isar_flutter_libs
|
||||
media_kit_libs_windows_audio
|
||||
permission_handler_windows
|
||||
screen_retriever
|
||||
share_plus
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
media_kit_native_event_loop
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the build version.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
|
||||
# Add dependency libraries and include directories. Add any application-specific
|
||||
# dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the build version.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
|
||||
# Add dependency libraries and include directories. Add any application-specific
|
||||
# dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
|
|
@ -1,288 +1,288 @@
|
|||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
/// Window attribute that enables dark mode window decorations.
|
||||
///
|
||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||
/// version 10.0.22000.0.
|
||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
/// Registry key for app theme preference.
|
||||
///
|
||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||
/// value indicates apps should use light mode.
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
}
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registrar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
RegisterClass(&window_class);
|
||||
class_registered_ = true;
|
||||
}
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::Create(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
static_cast<LONG>(origin.y)};
|
||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTheme(window);
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
bool Win32Window::Show() {
|
||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||
UpdateTheme(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
void Win32Window::UpdateTheme(HWND const window) {
|
||||
DWORD light_mode;
|
||||
DWORD light_mode_size = sizeof(light_mode);
|
||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||
kGetPreferredBrightnessRegValue,
|
||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||
&light_mode_size);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
BOOL enable_dark_mode = light_mode == 0;
|
||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||
}
|
||||
}
|
||||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
/// Window attribute that enables dark mode window decorations.
|
||||
///
|
||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||
/// version 10.0.22000.0.
|
||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
/// Registry key for app theme preference.
|
||||
///
|
||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||
/// value indicates apps should use light mode.
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
}
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registrar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
RegisterClass(&window_class);
|
||||
class_registered_ = true;
|
||||
}
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::Create(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
static_cast<LONG>(origin.y)};
|
||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTheme(window);
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
bool Win32Window::Show() {
|
||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||
UpdateTheme(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
void Win32Window::UpdateTheme(HWND const window) {
|
||||
DWORD light_mode;
|
||||
DWORD light_mode_size = sizeof(light_mode);
|
||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||
kGetPreferredBrightnessRegValue,
|
||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||
&light_mode_size);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
BOOL enable_dark_mode = light_mode == 0;
|
||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,102 +1,102 @@
|
|||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
: width(width), height(height) {}
|
||||
};
|
||||
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates a win32 window with |title| that is positioned and sized using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size this function will scale the inputted width and height as
|
||||
// as appropriate for the default monitor. The window is invisible until
|
||||
// |Show| is called. Returns true if the window was created successfully.
|
||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||
|
||||
// Show the current window. Returns true if the window was successfully shown.
|
||||
bool Show();
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
|
||||
// Inserts |content| into the window tree.
|
||||
void SetChildContent(HWND content);
|
||||
|
||||
// Returns the backing Window handle to enable clients to set icon and other
|
||||
// window properties. Returns nullptr if the window has been destroyed.
|
||||
HWND GetHandle();
|
||||
|
||||
// If true, closing this window will quit the application.
|
||||
void SetQuitOnClose(bool quit_on_close);
|
||||
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
virtual LRESULT MessageHandler(HWND window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Called when CreateAndShow is called, allowing subclass window-related
|
||||
// setup. Subclasses should return false if setup fails.
|
||||
virtual bool OnCreate();
|
||||
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
// Update the window frame's theme to match the system theme.
|
||||
static void UpdateTheme(HWND const window);
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
// window handle for top level window.
|
||||
HWND window_handle_ = nullptr;
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
: width(width), height(height) {}
|
||||
};
|
||||
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates a win32 window with |title| that is positioned and sized using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size this function will scale the inputted width and height as
|
||||
// as appropriate for the default monitor. The window is invisible until
|
||||
// |Show| is called. Returns true if the window was created successfully.
|
||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||
|
||||
// Show the current window. Returns true if the window was successfully shown.
|
||||
bool Show();
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
|
||||
// Inserts |content| into the window tree.
|
||||
void SetChildContent(HWND content);
|
||||
|
||||
// Returns the backing Window handle to enable clients to set icon and other
|
||||
// window properties. Returns nullptr if the window has been destroyed.
|
||||
HWND GetHandle();
|
||||
|
||||
// If true, closing this window will quit the application.
|
||||
void SetQuitOnClose(bool quit_on_close);
|
||||
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
virtual LRESULT MessageHandler(HWND window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Called when CreateAndShow is called, allowing subclass window-related
|
||||
// setup. Subclasses should return false if setup fails.
|
||||
virtual bool OnCreate();
|
||||
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
// Update the window frame's theme to match the system theme.
|
||||
static void UpdateTheme(HWND const window);
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
// window handle for top level window.
|
||||
HWND window_handle_ = nullptr;
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
|
|
Loading…
Reference in New Issue