Compare commits

...

10 Commits

Author SHA1 Message Date
Pato05 10133755b7
Fix right click on album menu 2024-02-15 00:00:47 +01:00
Pato05 fa12f3120d
Linux CMakeLists.txt: remove old tarballs 2024-02-15 00:00:36 +01:00
Pato05 99fef45fb8
fix restoring queueIndex
build-linux: fix getting commit hash
2024-02-14 23:29:03 +01:00
Pato05 e782643ee0
Fix restore queueSource
Make queueSource responsive
Fix some warnings
2024-02-14 23:06:04 +01:00
Pato05 2a5a51e43f
fix player in just_audio_media_kit +
fix lyrics
add right click action to AlbumCard
add desktop file script for linux
automated tarball creation for linux
don't preload old queue
2024-02-14 22:39:35 +01:00
Pato05 a7661d168b
systray + try to put in youtube sans without success 2024-02-13 23:56:53 +01:00
Pato05 019961ca85
fix logout 2024-02-13 17:53:25 +01:00
Pato05 c28256f258
update to just_audio_media_kit 2.0.0
fix play button's border radius
2024-02-13 17:37:06 +01:00
Pato05 c42b9bc8e2
use pipe API for lyrics
Hive persistent cookie jar
Translated lyrics
2024-02-13 02:48:39 +01:00
Pato05 15490444a9
send cancel on widget dispose 2024-02-12 03:46:39 +01:00
66 changed files with 1944 additions and 1525 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "basic"
}

View File

@ -60,7 +60,7 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
shrinkResources true
shrinkResources false
minifyEnabled true
}
debug {

View File

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

BIN
assets/icon_mono_small.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/icon_mono_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icon_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

BIN
fonts/FreezerIcons.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/YouTubeSansLight.otf Normal file

Binary file not shown.

BIN
fonts/YouTubeSansMedium.otf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

46
icons-config.json Normal file
View File

@ -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"
}
]
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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';

148
lib/api/pipe_api.dart Normal file
View File

@ -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'])]);
}
}

View File

@ -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;

View File

@ -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());
}

107
lib/api/player/systray.dart Normal file
View File

@ -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) {}
}

50
lib/icons.dart Normal file
View File

@ -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);
}

View File

@ -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),
),
];

View File

@ -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);

View File

@ -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 {

View File

@ -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,

View File

@ -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)

View File

@ -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(';')};
}

View File

@ -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:

View File

@ -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,

View File

@ -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))
],
);
}

View File

@ -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) {

View File

@ -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)),
)),
],
);
}
}

View File

@ -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);
}
//===================

View File

@ -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);

View File

@ -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)
],
);
}

View File

@ -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'));

View File

@ -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'),

View File

@ -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(

View File

@ -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()

5
linux/create-tarball.sh Normal file
View File

@ -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" .

View File

@ -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);
}

View File

@ -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)

View File

@ -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"

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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_

View File

@ -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"))
}

View File

@ -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:

View File

@ -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.

View File

@ -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()

View File

@ -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"
}

View File

@ -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"));
}

View File

@ -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)

View File

@ -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)

View File

@ -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));
}
}

View File

@ -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_