Implement App Icon selection
- added 2 additional app icons to choose from - added translation string for settings screen - smaller changes + formatting some touched files
@@ -27,6 +27,7 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"
|
||||
android:required="false"
|
||||
android:minSdkVersion="30"/>
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
@@ -38,7 +39,10 @@
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="ReFreezer"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true">
|
||||
@@ -59,7 +63,6 @@
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:banner="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported = "true">
|
||||
@@ -76,10 +79,10 @@
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!--<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />-->
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- Deep Links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -113,12 +116,70 @@
|
||||
android:scheme="https"
|
||||
android:host="www.deezer.page.link" />
|
||||
</intent-filter>
|
||||
<!-- New short domain -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="dzr.page.link" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.dzr.page.link" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name="r.r.refreezer.DefaultIconActivity"
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:targetActivity="r.r.refreezer.MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name="r.r.refreezer.CatIconActivity"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_cat"
|
||||
android:roundIcon="@mipmap/ic_launcher_cat_round"
|
||||
android:targetActivity="r.r.refreezer.MainActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name="r.r.refreezer.DeezerBlueIconActivity"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_deezer_blue"
|
||||
android:roundIcon="@mipmap/ic_launcher_deezer_blue_round"
|
||||
android:targetActivity="r.r.refreezer.MainActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!--
|
||||
Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB |
115
android/app/src/main/java/r/r/refreezer/ChangeIconPlugin.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package r.r.refreezer;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ChangeIconPlugin {
|
||||
private final Context context;
|
||||
|
||||
public ChangeIconPlugin(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void initWith(BinaryMessenger binaryMessenger) {
|
||||
MethodChannel channel = new MethodChannel(binaryMessenger, "change_icon");
|
||||
channel.setMethodCallHandler((call, result) -> {
|
||||
if (call.method.equals("changeIcon")) {
|
||||
String iconName = call.argument("iconName");
|
||||
if (iconName != null) {
|
||||
LauncherIcon icon = LauncherIcon.fromKey(iconName);
|
||||
if (icon != null) {
|
||||
setIcon(icon);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.error("INVALID_ICON", "Invalid icon name", null);
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_ARGUMENT", "Icon name is required", null);
|
||||
}
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void tryFixLauncherIconIfNeeded() {
|
||||
for (LauncherIcon icon : LauncherIcon.values()) {
|
||||
if (isEnabled(icon)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setIcon(LauncherIcon.DEFAULT);
|
||||
}
|
||||
|
||||
public boolean isEnabled(LauncherIcon icon) {
|
||||
int state = context.getPackageManager().getComponentEnabledSetting(icon.getComponentName(context));
|
||||
return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED ||
|
||||
(state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT && icon == LauncherIcon.DEFAULT);
|
||||
}
|
||||
|
||||
public void setIcon(LauncherIcon icon) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
// Enable the new icon first
|
||||
pm.setComponentEnabledSetting(
|
||||
icon.getComponentName(context),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
);
|
||||
|
||||
// Disable all other icons (except the newly enabled one)
|
||||
for (LauncherIcon i : LauncherIcon.values()) {
|
||||
if (i != icon) {
|
||||
pm.setComponentEnabledSetting(
|
||||
i.getComponentName(context),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum LauncherIcon {
|
||||
DEFAULT("DefaultIcon"),
|
||||
CAT("CatIcon"),
|
||||
DEEZER("DeezerBlueIcon");
|
||||
// Add more icons as needed
|
||||
|
||||
private final String key;
|
||||
private static final Map<String, String> activityMap = new HashMap<>();
|
||||
private ComponentName componentName;
|
||||
|
||||
static {
|
||||
activityMap.put("DefaultIcon", "r.r.refreezer.DefaultIconActivity");
|
||||
activityMap.put("CatIcon", "r.r.refreezer.CatIconActivity");
|
||||
activityMap.put("DeezerBlueIcon", "r.r.refreezer.DeezerBlueIconActivity");
|
||||
}
|
||||
|
||||
LauncherIcon(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public ComponentName getComponentName(Context context) {
|
||||
if (componentName == null) {
|
||||
componentName = new ComponentName(context.getPackageName(), Objects.requireNonNull(activityMap.get(key)));
|
||||
}
|
||||
return componentName;
|
||||
}
|
||||
|
||||
public static LauncherIcon fromKey(String key) {
|
||||
for (LauncherIcon icon : values()) {
|
||||
if (icon.key.equals(key)) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,13 @@ public class MainActivity extends AudioServiceActivity {
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine);
|
||||
|
||||
// Initialize ChangeIconPlugin
|
||||
ChangeIconPlugin changeIconPlugin = new ChangeIconPlugin(this);
|
||||
changeIconPlugin.initWith(flutterEngine.getDartExecutor().getBinaryMessenger());
|
||||
changeIconPlugin.tryFixLauncherIconIfNeeded();
|
||||
|
||||
//Flutter method channel
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(((call, result) -> {
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.6"
|
||||
android:scaleY="0.6"
|
||||
android:translateX="21.6"
|
||||
android:translateY="21.6">
|
||||
<path
|
||||
android:pathData="M86.46,25.7C87.35,20.56 88.65,17.33 90.09,17.32L90.1,17.32C92.78,17.33 94.96,28.54 94.96,42.38C94.96,56.22 92.78,67.43 90.09,67.43C88.99,67.43 87.97,65.53 87.15,62.34C85.85,74.02 83.17,82.05 80.06,82.05C77.65,82.05 75.49,77.23 74.04,69.62C73.04,84.09 70.55,94.36 67.64,94.36C65.81,94.36 64.14,90.29 62.91,83.67C61.43,97.34 58,106.92 54,106.92C50,106.92 46.57,97.34 45.09,83.67C43.87,90.29 42.2,94.36 40.36,94.36C37.45,94.36 34.96,84.09 33.97,69.62C32.51,77.23 30.36,82.05 27.95,82.05C24.84,82.05 22.15,74.03 20.85,62.34C20.04,65.54 19.01,67.43 17.91,67.43C15.22,67.43 13.04,56.22 13.04,42.38C13.04,28.54 15.22,17.32 17.91,17.32C19.36,17.32 20.65,20.56 21.55,25.7C22.98,16.84 25.31,11.08 27.95,11.08C31.08,11.08 33.79,19.22 35.07,31.05C36.33,22.44 38.23,16.95 40.37,16.95C43.35,16.95 45.9,27.75 46.84,42.81C48.61,35.09 51.17,30.24 54.01,30.24C56.84,30.24 59.41,35.09 61.17,42.81C62.12,27.75 64.66,16.95 67.65,16.95C69.78,16.95 71.68,22.44 72.94,31.05C74.22,19.22 76.93,11.08 80.06,11.08C82.69,11.08 85.03,16.84 86.46,25.7ZM6.08,39.91C6.08,33.73 7.31,28.71 8.84,28.71C10.37,28.71 11.61,33.73 11.61,39.91C11.61,46.1 10.37,51.12 8.84,51.12C7.31,51.12 6.08,46.1 6.08,39.91ZM96.39,39.91C96.39,33.73 97.63,28.71 99.16,28.71C100.68,28.71 101.92,33.73 101.92,39.91C101.92,46.1 100.68,51.12 99.16,51.12C97.63,51.12 96.39,46.1 96.39,39.91Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="54"
|
||||
android:startY="11.08"
|
||||
android:endX="54"
|
||||
android:endY="106.92"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF4C9EFE"/>
|
||||
<item android:offset="0.4" android:color="#FF4151FF"/>
|
||||
<item android:offset="1" android:color="#FF4C9EFE"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,47 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.6111111"
|
||||
android:scaleY="0.6111111"
|
||||
android:translateX="21.61111"
|
||||
android:translateY="21">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m48.19,106.01c-2,-0.29 -4.25,-1.9 -5.32,-3.81 -0.45,-0.81 -0.61,-0.93 -1.49,-1.19C24.56,96.09 18.89,67.57 33.44,61.04l0.89,-0.4 0.05,-2.82c0.09,-4.95 1.35,-9.73 3.64,-13.9l0.27,-0.5 -0.86,-1.06c-0.47,-0.58 -0.92,-1.04 -1,-1.01 -0.08,0.02 -0.71,0.41 -1.4,0.85 -3.19,2.06 -4.38,2.06 -4.38,0.02 0,-1 0.22,-1.23 2.5,-2.6l1.68,-1.01 -0.48,-0.84C34.08,37.31 33.85,36.91 33.83,36.89 33.81,36.86 33.56,36.96 33.26,37.11 31.09,38.19 28.32,38.55 27.75,37.82 26.66,36.43 27.35,35.68 30.79,34.56L32.81,33.91 32.48,33.05 32.14,32.19 28.9,32.08C27.11,32.02 25.63,31.96 25.61,31.94 25,31.48 24.91,30.23 25.45,29.6L25.88,29.1 28.92,29.02 31.95,28.95 31.83,27.94C31.76,27.38 31.42,25.89 31.07,24.62 28.35,14.68 28.05,5.77 30.42,5.33 32.42,4.95 43.27,8.79 46.31,10.95l0.5,0.35 1.32,-0.36C51.72,9.95 56.49,9.91 60.21,10.84l1.14,0.29 1.95,-1c6.08,-3.12 13.69,-5.42 15.13,-4.56 1.48,0.88 1.04,8.77 -1.01,18.01 -0.44,1.99 -0.85,4.01 -0.92,4.48l-0.11,0.85 2.58,0.06c3.33,0.07 4.21,0.46 3.9,1.73 -0.28,1.13 -0.76,1.31 -3.87,1.41l-2.85,0.09 -0.2,0.82c-0.24,0.98 -0.37,0.89 2.07,1.55 2.7,0.73 3.39,1.71 2.13,3 -0.57,0.58 -3.47,0.27 -5.73,-0.62 -0.26,-0.1 -0.9,0.86 -0.9,1.36 0,0.09 0.76,0.61 1.68,1.16 2.73,1.62 3.27,2.51 2.25,3.71 -0.7,0.81 -1.4,0.67 -3.73,-0.75l-1.99,-1.22 -0.98,1.02 -0.98,1.02 0.36,0.59c2.22,3.65 3.86,9.48 4.07,14.38l0.1,2.47 1.45,0.81c0.8,0.45 1.64,1.04 1.87,1.32 0.82,0.98 0.85,0.25 0.09,-2.29 -2.35,-7.83 -0.82,-15.3 3.87,-18.86 6.45,-4.91 12.86,-0.32 7.6,5.45 -2.7,2.95 -3,6.19 -1.12,11.81 3.24,9.68 3.25,17.48 0.01,24.15 -2.09,4.31 -7.45,9.82 -9.55,9.82 -0.16,0 -0.92,0.56 -1.69,1.25 -3.31,2.97 -6.34,5 -9.09,6.09 -1.36,0.54 -1.47,0.62 -1.74,1.35 -1.48,3.89 -8.82,5.72 -17.79,4.43z"
|
||||
android:strokeWidth="0.68"/>
|
||||
<path
|
||||
android:pathData="M58.39,47.1C64.95,44.86 69.63,40.41 72.17,33.98 74.36,28.42 73.96,26.63 69.1,20.08l-1.26,-1.7 0.7,-0.68c0.39,-0.38 1.65,-1.75 2.8,-3.05 1.16,-1.3 2.79,-3.14 3.63,-4.09l1.53,-1.72 -0.77,0.12c-3.19,0.51 -11.17,3.78 -12.83,5.25l-0.89,0.79 -1.11,-0.4C56.42,13 51.79,12.94 47.57,14.42l-1.51,0.53 -0.71,-0.65C43.59,12.71 32.1,8.13 32.1,9.03c0,0.19 4.42,5.41 6.56,7.75 1.84,2 1.8,1.92 1.04,2.63 -2.25,2.13 -5.06,6.51 -5.06,7.9 0,12.12 13.43,23.31 23.74,19.79z"
|
||||
android:strokeWidth="0.68"
|
||||
android:fillColor="#2549FF"/>
|
||||
<path
|
||||
android:pathData="m60.69,102.6c1.71,-0.76 2.51,-1.92 2.73,-3.95 0.18,-1.63 2.53,-13.65 3.57,-18.26 3.61,-15.99 3.62,-16.04 3.74,-20.47 0.13,-4.64 -0.43,-7.63 -2.17,-11.7 -1,-2.33 -1.33,-2.83 -1.6,-2.39 -0.93,1.52 -7.34,4.61 -10.53,5.08 -4.88,0.72 -11.2,-1.25 -14.83,-4.61l-0.77,-0.71 -0.92,1.88c-3.59,7.34 -3.34,12.43 1.61,33.74 1.37,5.89 2.64,12.34 3.35,17.02 0.56,3.7 2.88,5.25 6.85,4.59l0.66,-0.11 -0.1,-14.38 -0.1,-14.38 1.29,-0.13c0.71,-0.07 1.54,-0.07 1.86,-0l0.57,0.13 -0.13,13.9c-0.07,7.65 -0.14,14.15 -0.15,14.45l-0.02,0.55 0.91,0.12c1.37,0.18 3.32,0.01 4.16,-0.37z"
|
||||
android:strokeWidth="0.68"
|
||||
android:fillColor="#3463FE"/>
|
||||
<path
|
||||
android:pathData="m68.97,96.07c4.53,-2.37 8.5,-6.88 9.89,-11.2 2.63,-8.24 1.05,-16.01 -4.09,-20.1 -0.9,-0.72 -0.82,-0.82 -1.35,1.77 -0.61,3.03 -1.7,7.92 -2.99,13.48 -0.61,2.61 -1.47,6.63 -1.92,8.95 -0.45,2.31 -0.98,4.88 -1.18,5.71 -0.61,2.56 -0.6,2.57 1.64,1.4z"
|
||||
android:strokeWidth="0.68"
|
||||
android:fillColor="#2549FF"/>
|
||||
<path
|
||||
android:pathData="m41.62,96.96c-0.06,-0.28 -0.27,-1.12 -0.46,-1.87 -0.19,-0.75 -0.55,-2.4 -0.79,-3.65 -0.71,-3.64 -1.47,-7.21 -2.72,-12.69C37.01,75.94 36.06,71.54 35.55,68.98 34.4,63.27 34.65,63.48 32.1,66.06c-7.8,7.86 -3.65,24.49 7.63,30.57 1.83,0.99 2.05,1.03 1.89,0.34z"
|
||||
android:strokeWidth="0.68"
|
||||
android:fillColor="#2549FF"/>
|
||||
<path
|
||||
android:pathData="m83.05,85.15c4.6,-4.62 5.31,-14.06 1.89,-25.3 -2.05,-6.74 -1.62,-10.83 1.49,-14.15 3.19,-3.41 -0.07,-4.5 -3.47,-1.16 -3.69,3.61 -4.08,7.85 -1.63,17.47 0.95,3.72 1.09,4.99 0.62,5.52 -0.26,0.29 -0.22,0.48 0.37,2.19 1.6,4.62 1.6,9.9 -0.01,14.63 -0.66,1.96 -0.54,2.09 0.73,0.8z"
|
||||
android:strokeWidth="0.68"
|
||||
android:fillColor="#3463FE"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m44.48,35.37c-1.81,-0.91 -3.15,-2.94 -3.41,-5.16l-0.12,-1.06 0.71,0.12c5.1,0.89 7.19,2.27 7.96,5.27 0.36,1.41 0.4,1.39 -1.97,1.39 -1.92,0 -2.12,-0.03 -3.17,-0.56z"
|
||||
android:strokeWidth="0.68"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m58.13,35.26c0.47,-2.89 2.48,-4.65 6.45,-5.63 2.52,-0.62 2.58,-0.59 2.12,1.11 -0.91,3.36 -3.37,5.18 -7.01,5.18l-1.66,0z"
|
||||
android:strokeWidth="0.68"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m52.72,43.22c-2.78,-2.58 -2.55,-3.81 0.73,-3.96 4.02,-0.18 4.61,0.94 1.95,3.66 -1.25,1.27 -1.59,1.31 -2.68,0.3z"
|
||||
android:strokeWidth="0.68"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.6"
|
||||
android:scaleY="0.6"
|
||||
android:translateX="21.6"
|
||||
android:translateY="21.6">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M86.46,25.7C87.35,20.56 88.65,17.33 90.09,17.32L90.1,17.32C92.78,17.33 94.96,28.54 94.96,42.38C94.96,56.22 92.78,67.43 90.09,67.43C88.99,67.43 87.97,65.53 87.15,62.34C85.85,74.02 83.17,82.05 80.06,82.05C77.65,82.05 75.49,77.23 74.04,69.62C73.04,84.09 70.55,94.36 67.64,94.36C65.81,94.36 64.14,90.29 62.91,83.67C61.43,97.34 58,106.92 54,106.92C50,106.92 46.57,97.34 45.09,83.67C43.87,90.29 42.2,94.36 40.36,94.36C37.45,94.36 34.96,84.09 33.97,69.62C32.51,77.23 30.36,82.05 27.95,82.05C24.84,82.05 22.15,74.03 20.85,62.34C20.04,65.54 19.01,67.43 17.91,67.43C15.22,67.43 13.04,56.22 13.04,42.38C13.04,28.54 15.22,17.32 17.91,17.32C19.36,17.32 20.65,20.56 21.55,25.7C22.98,16.84 25.31,11.08 27.95,11.08C31.08,11.08 33.79,19.22 35.07,31.05C36.33,22.44 38.23,16.95 40.37,16.95C43.35,16.95 45.9,27.75 46.84,42.81C48.61,35.09 51.17,30.24 54.01,30.24C56.84,30.24 59.41,35.09 61.17,42.81C62.12,27.75 64.66,16.95 67.65,16.95C69.78,16.95 71.68,22.44 72.94,31.05C74.22,19.22 76.93,11.08 80.06,11.08C82.69,11.08 85.03,16.84 86.46,25.7ZM6.08,39.91C6.08,33.73 7.31,28.71 8.84,28.71C10.37,28.71 11.61,33.73 11.61,39.91C11.61,46.1 10.37,51.12 8.84,51.12C7.31,51.12 6.08,46.1 6.08,39.91ZM96.39,39.91C96.39,33.73 97.63,28.71 99.16,28.71C100.68,28.71 101.92,33.73 101.92,39.91C101.92,46.1 100.68,51.12 99.16,51.12C97.63,51.12 96.39,46.1 96.39,39.91Z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -2,5 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_cat_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_cat_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_cat_mono_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_cat_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_cat_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_cat_mono_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_deezer_blue_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_deezer_blue_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_deezer_mono_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_deezer_blue_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_deezer_blue_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_deezer_mono_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_cat.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_cat_round.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_deezer_blue.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_cat.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_cat_round.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_deezer_blue.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_cat.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_cat_round.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_cat.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_cat_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cat.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_cat_background">#1C1C14</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_deezer_blue_background">#1C1C14</color>
|
||||
</resources>
|
||||
BIN
assets/icon_deezer.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/icons/CatIcon.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/icons/DeezerBlueIcon.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/icons/DefaultIcon.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
@@ -3888,7 +3888,9 @@ const crowdin = {
|
||||
'Crowdin': 'Crowdin',
|
||||
'Help translating this app on Crowdin!':
|
||||
'Aidez-nous à traduire cette application sur Crowdin !',
|
||||
'Allow screen to turn off': "Permettre à l'écran de s'éteindre"
|
||||
'Allow screen to turn off': "Permettre à l'écran de s'éteindre",
|
||||
'Selecting a new icon will exit the app to apply the change!':
|
||||
"Sélectionner une nouvelle icône quittera l'application pour appliquer la modification !"
|
||||
},
|
||||
'he-IL': {
|
||||
'Home': 'מסך הבית',
|
||||
@@ -6850,7 +6852,9 @@ const crowdin = {
|
||||
'Toestemming geweigerd, download geannuleerd!',
|
||||
'Help translating this app on Crowdin!':
|
||||
'Help deze app te vertalen op Crowdin!',
|
||||
'Allow screen to turn off': 'Scherm uitschakelen toestaan'
|
||||
'Allow screen to turn off': 'Scherm uitschakelen toestaan',
|
||||
'Selecting a new icon will exit the app to apply the change!':
|
||||
'Het selecteren van een nieuw icoon zal de app afsluiten om de wijziging toe te passen!'
|
||||
},
|
||||
'pl-PL': {
|
||||
'Home': 'Strona główna',
|
||||
|
||||
@@ -454,5 +454,9 @@ const language_en_us = {
|
||||
|
||||
// 0.7.15
|
||||
'Allow screen to turn off': 'Allow screen to turn off',
|
||||
|
||||
// 0.7.16
|
||||
'Selecting a new icon will exit the app to apply the change!':
|
||||
'Selecting a new icon will exit the app to apply the change!',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +39,8 @@ Future<AudioPlayerHandler> initAudioService() async {
|
||||
);
|
||||
}
|
||||
|
||||
class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
class AudioPlayerHandler extends BaseAudioHandler
|
||||
with QueueHandler, SeekHandler {
|
||||
AudioPlayerHandler() {
|
||||
_init();
|
||||
}
|
||||
@@ -48,7 +49,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
int? _prevAudioSession;
|
||||
bool _equalizerOpen = false;
|
||||
|
||||
final AndroidAuto _androidAuto = AndroidAuto(); // Create an instance of AndroidAuto
|
||||
final AndroidAuto _androidAuto =
|
||||
AndroidAuto(); // Create an instance of AndroidAuto
|
||||
|
||||
// for some reason, dart can decide not to respect the 'await' due to weird task sceduling ...
|
||||
final Completer<void> _playerInitializedCompleter = Completer<void>();
|
||||
@@ -70,7 +72,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
QueueSource? queueSource;
|
||||
StreamSubscription? _queueStateSub;
|
||||
StreamSubscription? _mediaItemSub;
|
||||
final BehaviorSubject<QueueState> _queueStateSubject = BehaviorSubject<QueueState>();
|
||||
final BehaviorSubject<QueueState> _queueStateSubject =
|
||||
BehaviorSubject<QueueState>();
|
||||
Stream<QueueState> get queueStateStream => _queueStateSubject.stream;
|
||||
QueueState get queueState => _queueStateSubject.value;
|
||||
int currentIndex = 0;
|
||||
@@ -85,13 +88,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
_player.sequenceStateStream
|
||||
.map((state) {
|
||||
try {
|
||||
return state?.effectiveSequence.map((source) => source.tag as MediaItem).toList();
|
||||
return state?.effectiveSequence
|
||||
.map((source) => source.tag as MediaItem)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
if (e is RangeError) {
|
||||
// This is caused by just_audio not updating the currentIndex first in the _broadcastSequence method.
|
||||
// Because in shufflemode it's out of range after removing items from the playlist.
|
||||
// Might be fixed in future
|
||||
Logger.root.severe('RangeError occurred while accessing effectiveSequence: $e');
|
||||
Logger.root.severe(
|
||||
'RangeError occurred while accessing effectiveSequence: $e');
|
||||
// Return null to indicate that the queue could/should not be broadcasted
|
||||
return null;
|
||||
}
|
||||
@@ -103,20 +109,25 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
.pipe(queue);
|
||||
|
||||
// Update current QueueState
|
||||
_queueStateSub = Rx.combineLatest3<List<MediaItem>, PlaybackState, List<int>, QueueState>(
|
||||
_queueStateSub = Rx.combineLatest3<List<MediaItem>, PlaybackState,
|
||||
List<int>, QueueState>(
|
||||
queue,
|
||||
playbackState,
|
||||
_player.shuffleIndicesStream.whereType<List<int>>(),
|
||||
(queue, playbackState, shuffleIndices) => QueueState(
|
||||
queue,
|
||||
playbackState.queueIndex,
|
||||
playbackState.shuffleMode == AudioServiceShuffleMode.all ? shuffleIndices : null,
|
||||
playbackState.shuffleMode == AudioServiceShuffleMode.all
|
||||
? shuffleIndices
|
||||
: null,
|
||||
playbackState.repeatMode,
|
||||
playbackState.shuffleMode,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
(state) => state.shuffleIndices == null || state.queue.length == state.shuffleIndices!.length,
|
||||
(state) =>
|
||||
state.shuffleIndices == null ||
|
||||
state.queue.length == state.shuffleIndices!.length,
|
||||
)
|
||||
.distinct()
|
||||
.listen(_queueStateSubject.add);
|
||||
@@ -124,7 +135,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
// Broadcast media item changes after track or position in queue change,
|
||||
// only emit value when different from previous item
|
||||
_mediaItemSub = Rx.combineLatest3<int?, List<MediaItem>, bool, MediaItem?>(
|
||||
_player.currentIndexStream, queue, _player.shuffleModeEnabledStream, (index, queue, shuffleModeEnabled) {
|
||||
_player.currentIndexStream, queue, _player.shuffleModeEnabledStream,
|
||||
(index, queue, shuffleModeEnabled) {
|
||||
// Don't broadcast while shuffling to avoid intermediate MediaItem change
|
||||
if (_rearranging) return null;
|
||||
|
||||
@@ -155,11 +167,14 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
});
|
||||
|
||||
// Propagate all events from the audio player to AudioService clients.
|
||||
_player.playbackEventStream.listen(_broadcastState, onError: _playbackError);
|
||||
_player.playbackEventStream
|
||||
.listen(_broadcastState, onError: _playbackError);
|
||||
|
||||
_player.shuffleModeEnabledStream.listen((enabled) => _broadcastState(_player.playbackEvent));
|
||||
_player.shuffleModeEnabledStream
|
||||
.listen((enabled) => _broadcastState(_player.playbackEvent));
|
||||
|
||||
_player.loopModeStream.listen((mode) => _broadcastState(_player.playbackEvent));
|
||||
_player.loopModeStream
|
||||
.listen((mode) => _broadcastState(_player.playbackEvent));
|
||||
|
||||
_player.processingStateStream.listen((state) {
|
||||
if (state == ProcessingState.completed && _player.playing) {
|
||||
@@ -225,7 +240,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playFromMediaId(String mediaId, [Map<String, dynamic>? extras]) async {
|
||||
Future<void> playFromMediaId(String mediaId,
|
||||
[Map<String, dynamic>? extras]) async {
|
||||
// Check if the mediaId is for Android Auto
|
||||
if (mediaId.startsWith(AndroidAuto.prefix)) {
|
||||
// Forward the event to Android Auto
|
||||
@@ -238,7 +254,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
if (index != -1) {
|
||||
_player.seek(
|
||||
Duration.zero,
|
||||
index: _player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
|
||||
index:
|
||||
_player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
|
||||
);
|
||||
} else {
|
||||
Logger.root.severe('playFromMediaId: MediaItem not found');
|
||||
@@ -252,11 +269,12 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
// Save queue before stopping player to save player details
|
||||
Logger.root.info('saving queue');
|
||||
_saveQueueToFile();
|
||||
Logger.root.info('stopping player');
|
||||
await _player.stop();
|
||||
await super.stop();
|
||||
Logger.root.info('saving queue');
|
||||
_saveQueueToFile();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -283,10 +301,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateQueue(List<MediaItem> newQueue) async {
|
||||
Future<void> updateQueue(List<MediaItem> queue) async {
|
||||
await _playlist.clear();
|
||||
if (newQueue.isNotEmpty) {
|
||||
await _playlist.addAll(await _itemsToSources(newQueue));
|
||||
if (queue.isNotEmpty) {
|
||||
await _playlist.addAll(await _itemsToSources(queue));
|
||||
} else {
|
||||
if (mediaItem.hasValue) {
|
||||
mediaItem.add(null);
|
||||
@@ -343,7 +361,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
|
||||
_player.seek(
|
||||
Duration.zero,
|
||||
index: _player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
|
||||
index:
|
||||
_player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,6 +410,33 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
// Start internal methods native to AudioHandler
|
||||
//----------------------------------------------
|
||||
|
||||
/// Wait for the player to be in a ready state before performing operations
|
||||
Future<void> _waitForPlayerReadiness() async {
|
||||
if (_player.processingState == ProcessingState.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
Completer<void> readyCompleter = Completer<void>();
|
||||
|
||||
late StreamSubscription subscription;
|
||||
subscription = _player.processingStateStream.listen((state) {
|
||||
if (state == ProcessingState.ready) {
|
||||
if (!readyCompleter.isCompleted) {
|
||||
readyCompleter.complete();
|
||||
}
|
||||
subscription.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
return readyCompleter.future.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
subscription.cancel();
|
||||
Logger.root.warning('Timed out waiting for player to be ready');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startSession() async {
|
||||
Logger.root.info('starting audio service...');
|
||||
final session = await AudioSession.instance;
|
||||
@@ -406,13 +452,15 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
_player = AudioPlayer();
|
||||
}
|
||||
|
||||
_loadEmptyPlaylist().then((_) => Logger.root.info('audio player initialized!'));
|
||||
_loadEmptyPlaylist()
|
||||
.then((_) => Logger.root.info('audio player initialized!'));
|
||||
}
|
||||
|
||||
/// Broadcasts the current state to all clients.
|
||||
void _broadcastState(PlaybackEvent event) {
|
||||
final playing = _player.playing;
|
||||
currentIndex = _getQueueIndex(_player.currentIndex ?? 0, shuffleModeEnabled: _player.shuffleModeEnabled);
|
||||
currentIndex = _getQueueIndex(_player.currentIndex ?? 0,
|
||||
shuffleModeEnabled: _player.shuffleModeEnabled);
|
||||
|
||||
playbackState.add(
|
||||
playbackState.value.copyWith(
|
||||
@@ -421,9 +469,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
// Custom Stop
|
||||
const MediaControl(androidIcon: 'drawable/ic_action_stop', label: 'stop', action: MediaAction.stop),
|
||||
const MediaControl(
|
||||
androidIcon: 'drawable/ic_action_stop',
|
||||
label: 'stop',
|
||||
action: MediaAction.stop),
|
||||
],
|
||||
systemActions: const {MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward},
|
||||
systemActions: const {
|
||||
MediaAction.seek,
|
||||
MediaAction.seekForward,
|
||||
MediaAction.seekBackward
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
processingState: const {
|
||||
ProcessingState.idle: AudioProcessingState.idle,
|
||||
@@ -478,7 +533,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
|
||||
Future _getTrackUrl(MediaItem mediaItem) async {
|
||||
//Check if offline
|
||||
String offlinePath = p.join((await getExternalStorageDirectory())!.path, 'offline/');
|
||||
String offlinePath =
|
||||
p.join((await getExternalStorageDirectory())!.path, 'offline/');
|
||||
File f = File(p.join(offlinePath, mediaItem.id));
|
||||
if (await f.exists()) {
|
||||
//return f.path;
|
||||
@@ -495,14 +551,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
//This just returns fake url that contains metadata
|
||||
int quality = await getStreamQuality();
|
||||
|
||||
List? streamPlaybackDetails = jsonDecode(mediaItem.extras?['playbackDetails']);
|
||||
List? streamPlaybackDetails =
|
||||
jsonDecode(mediaItem.extras?['playbackDetails']);
|
||||
String streamItemId = mediaItem.id;
|
||||
|
||||
//If Deezer provided a FALLBACK track, use the playbackDetails and id from the fallback track
|
||||
//for streaming (original stream unavailable)
|
||||
if (mediaItem.extras?['fallbackId'] != null) {
|
||||
streamItemId = mediaItem.extras?['fallbackId'];
|
||||
streamPlaybackDetails = jsonDecode(mediaItem.extras?['playbackDetailsFallback']);
|
||||
streamPlaybackDetails =
|
||||
jsonDecode(mediaItem.extras?['playbackDetailsFallback']);
|
||||
}
|
||||
|
||||
if ((streamPlaybackDetails ?? []).length < 3) return null;
|
||||
@@ -522,7 +580,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
|
||||
/// Load new queue of MediaItems to just_audio & seek to given index & position
|
||||
Future _loadQueueAtIndex(List<MediaItem> newQueue, int index, {Duration position = Duration.zero}) async {
|
||||
Future _loadQueueAtIndex(List<MediaItem> newQueue, int index,
|
||||
{Duration position = Duration.zero}) async {
|
||||
//Set requested index
|
||||
_requestedIndex = index;
|
||||
|
||||
@@ -532,6 +591,9 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
// Convert new queue to AudioSources playlist & add to just_audio (Concurrent approach)
|
||||
await _playlist.addAll(await _itemsToSources(newQueue));
|
||||
|
||||
// Wait for player to be ready before seeking
|
||||
await _waitForPlayerReadiness();
|
||||
|
||||
//Seek to correct position & index
|
||||
try {
|
||||
await _player.seek(position, index: index);
|
||||
@@ -542,7 +604,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
|
||||
//Replace queue, play specified item index
|
||||
Future _loadQueueAndPlayAtIndex(QueueSource newQueueSource, List<MediaItem> newQueue, int index) async {
|
||||
Future _loadQueueAndPlayAtIndex(
|
||||
QueueSource newQueueSource, List<MediaItem> newQueue, int index) async {
|
||||
// Pauze platback if playing (Player seems to crash on some devices otherwise)
|
||||
await pause();
|
||||
//Set requested index
|
||||
@@ -582,7 +645,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
// Get current position
|
||||
int pos = queue.value.length;
|
||||
// Load 25 more tracks from playlist
|
||||
tracks = await deezerAPI.playlistTracksPage(queueSource!.id!, pos, nb: 25);
|
||||
tracks =
|
||||
await deezerAPI.playlistTracksPage(queueSource!.id!, pos, nb: 25);
|
||||
break;
|
||||
default:
|
||||
Logger.root.info('Reached end of queue source: ${queueSource!.source}');
|
||||
@@ -592,13 +656,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
// Deduplicate tracks already in queue with the same id
|
||||
List<String> queueIds = queue.value.map((mi) => mi.id).toList();
|
||||
tracks.removeWhere((track) => queueIds.contains(track.id));
|
||||
List<MediaItem> extraTracks = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
List<MediaItem> extraTracks =
|
||||
tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
addQueueItems(extraTracks);
|
||||
}
|
||||
|
||||
void _playbackError(err) {
|
||||
Logger.root.severe('Playback Error from audioservice: ${err.code}', err);
|
||||
if (err is PlatformException && err.code == 'abort' && err.message == 'Connection aborted') {
|
||||
if (err is PlatformException &&
|
||||
err.code == 'abort' &&
|
||||
err.message == 'Connection aborted') {
|
||||
return;
|
||||
}
|
||||
_onError(err, null);
|
||||
@@ -647,7 +714,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
Map data = {
|
||||
'index': _player.currentIndex,
|
||||
'queue': queue.value.map<Map<String, dynamic>>((mi) => MediaItemConverter.mediaItemToMap(mi)).toList(),
|
||||
'queue': queue.value
|
||||
.map<Map<String, dynamic>>(
|
||||
(mi) => MediaItemConverter.mediaItemToMap(mi))
|
||||
.toList(),
|
||||
'position': _player.position.inMilliseconds,
|
||||
'queueSource': (queueSource ?? QueueSource()).toJson(),
|
||||
'loopMode': LoopMode.values.indexOf(_player.loopMode)
|
||||
@@ -674,20 +744,44 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
//Restore queue & playback info from path
|
||||
Future loadQueueFromFile() async {
|
||||
Logger.root.info('looking for saved queue file...');
|
||||
File f = File(await _getQueueFilePath());
|
||||
if (await f.exists()) {
|
||||
Logger.root.info('saved queue file found, loading...');
|
||||
Map<String, dynamic> json = jsonDecode(await f.readAsString());
|
||||
List<MediaItem> savedQueue =
|
||||
(json['queue'] ?? []).map<MediaItem>((mi) => (MediaItemConverter.mediaItemFromMap(mi))).toList();
|
||||
final int lastIndex = json['index'] ?? 0;
|
||||
final Duration lastPos = Duration(milliseconds: json['position'] ?? 0);
|
||||
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
|
||||
var repeatType = LoopMode.values[(json['loopMode'] ?? 0)];
|
||||
_player.setLoopMode(repeatType);
|
||||
//Restore queue & Broadcast
|
||||
await _loadQueueAtIndex(savedQueue, lastIndex, position: lastPos);
|
||||
Logger.root.info('saved queue loaded from file!');
|
||||
try {
|
||||
File f = File(await _getQueueFilePath());
|
||||
if (await f.exists()) {
|
||||
Logger.root.info('saved queue file found, loading...');
|
||||
|
||||
try {
|
||||
String fileContent = await f.readAsString();
|
||||
if (fileContent.isEmpty) {
|
||||
Logger.root.warning('saved queue file is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, dynamic> json = jsonDecode(fileContent);
|
||||
List<MediaItem> savedQueue = (json['queue'] ?? [])
|
||||
.map<MediaItem>((mi) => (MediaItemConverter.mediaItemFromMap(mi)))
|
||||
.toList();
|
||||
|
||||
final int lastIndex = json['index'] ?? 0;
|
||||
final Duration lastPos =
|
||||
Duration(milliseconds: json['position'] ?? 0);
|
||||
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
|
||||
var repeatType = LoopMode.values[(json['loopMode'] ?? 0)];
|
||||
|
||||
await _player.setLoopMode(repeatType);
|
||||
|
||||
// Restore queue & Broadcast
|
||||
await _loadQueueAtIndex(savedQueue, lastIndex, position: lastPos);
|
||||
Logger.root.info('saved queue loaded from file!');
|
||||
} catch (e) {
|
||||
Logger.root.severe('Error parsing queue file: $e');
|
||||
// Delete corrupted file to prevent future errors
|
||||
await f.delete();
|
||||
await _loadEmptyPlaylist();
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
Logger.root.severe('Error loading queue from file', e, st);
|
||||
await _loadEmptyPlaylist();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +800,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
String password = settings.lastFMPassword ?? '';
|
||||
try {
|
||||
LastFM lastFM = await LastFM.authenticateWithPasswordHash(
|
||||
apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: username, passwordHash: password);
|
||||
apiKey: Env.lastFmApiKey,
|
||||
apiSecret: Env.lastFmApiSecret,
|
||||
username: username,
|
||||
passwordHash: password);
|
||||
_scrobblenaut = Scrobblenaut(lastFM: lastFM);
|
||||
_scrobblenautReady = true;
|
||||
} catch (e) {
|
||||
@@ -721,7 +818,9 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
|
||||
Future toggleShuffle() async {
|
||||
await setShuffleMode(_player.shuffleModeEnabled ? AudioServiceShuffleMode.none : AudioServiceShuffleMode.all);
|
||||
await setShuffleMode(_player.shuffleModeEnabled
|
||||
? AudioServiceShuffleMode.none
|
||||
: AudioServiceShuffleMode.all);
|
||||
}
|
||||
|
||||
LoopMode getLoopMode() {
|
||||
@@ -749,53 +848,71 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
if (_player.playing) {
|
||||
// Pauze playback if playing (Player seems to crash on some devices otherwise)
|
||||
await pause();
|
||||
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0, position: _player.position);
|
||||
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0,
|
||||
position: _player.position);
|
||||
await _player.play();
|
||||
} else {
|
||||
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0, position: _player.position);
|
||||
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0,
|
||||
position: _player.position);
|
||||
}
|
||||
}
|
||||
|
||||
//Play track from album
|
||||
Future playFromAlbum(Album album, String trackId) async {
|
||||
await playFromTrackList(album.tracks ?? [], trackId, QueueSource(id: album.id, text: album.title, source: 'album'));
|
||||
await playFromTrackList(album.tracks ?? [], trackId,
|
||||
QueueSource(id: album.id, text: album.title, source: 'album'));
|
||||
}
|
||||
|
||||
//Play mix by track
|
||||
Future playMix(String trackId, String trackTitle) async {
|
||||
List<Track> tracks = await deezerAPI.playMix(trackId);
|
||||
playFromTrackList(tracks, tracks[0].id ?? '',
|
||||
QueueSource(id: trackId, text: 'Mix based on'.i18n + ' $trackTitle', source: 'mix'));
|
||||
playFromTrackList(
|
||||
tracks,
|
||||
tracks[0].id ?? '',
|
||||
QueueSource(
|
||||
id: trackId,
|
||||
text: 'Mix based on'.i18n + ' $trackTitle',
|
||||
source: 'mix'));
|
||||
}
|
||||
|
||||
//Play from artist top tracks
|
||||
Future playFromTopTracks(List<Track> tracks, String trackId, Artist artist) async {
|
||||
Future playFromTopTracks(
|
||||
List<Track> tracks, String trackId, Artist artist) async {
|
||||
await playFromTrackList(
|
||||
tracks, trackId, QueueSource(id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
|
||||
tracks,
|
||||
trackId,
|
||||
QueueSource(
|
||||
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
|
||||
}
|
||||
|
||||
Future playFromPlaylist(Playlist playlist, String trackId) async {
|
||||
await playFromTrackList(
|
||||
playlist.tracks ?? [], trackId, QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
||||
await playFromTrackList(playlist.tracks ?? [], trackId,
|
||||
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
||||
}
|
||||
|
||||
//Play episode from show, load whole show as queue
|
||||
Future playShowEpisode(Show show, List<ShowEpisode> episodes, {int index = 0}) async {
|
||||
QueueSource showQueueSource = QueueSource(id: show.id, text: show.name, source: 'show');
|
||||
Future playShowEpisode(Show show, List<ShowEpisode> episodes,
|
||||
{int index = 0}) async {
|
||||
QueueSource showQueueSource =
|
||||
QueueSource(id: show.id, text: show.name, source: 'show');
|
||||
//Generate media items
|
||||
List<MediaItem> episodeQueue = episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
|
||||
List<MediaItem> episodeQueue =
|
||||
episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
|
||||
|
||||
//Load and play
|
||||
await _loadQueueAndPlayAtIndex(showQueueSource, episodeQueue, index);
|
||||
}
|
||||
|
||||
//Load tracks as queue, play track id, set queue source
|
||||
Future playFromTrackList(List<Track> tracks, String trackId, QueueSource trackQueueSource) async {
|
||||
Future playFromTrackList(
|
||||
List<Track> tracks, String trackId, QueueSource trackQueueSource) async {
|
||||
//Generate media items
|
||||
List<MediaItem> trackQueue = tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
|
||||
List<MediaItem> trackQueue =
|
||||
tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
|
||||
|
||||
//Load and play
|
||||
await _loadQueueAndPlayAtIndex(trackQueueSource, trackQueue, trackQueue.indexWhere((m) => m.id == trackId));
|
||||
await _loadQueueAndPlayAtIndex(trackQueueSource, trackQueue,
|
||||
trackQueue.indexWhere((m) => m.id == trackId));
|
||||
}
|
||||
|
||||
//Load smart track list as queue, start from beginning
|
||||
@@ -820,8 +937,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
QueueSource queueSource = QueueSource(
|
||||
id: stl.id,
|
||||
source: (stl.id == 'flow') ? 'flow' : 'smarttracklist',
|
||||
text: stl.title ?? ((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
|
||||
await playFromTrackList(stl.tracks ?? [], stl.tracks?[0].id ?? '', queueSource);
|
||||
text: stl.title ??
|
||||
((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
|
||||
await playFromTrackList(
|
||||
stl.tracks ?? [], stl.tracks?[0].id ?? '', queueSource);
|
||||
}
|
||||
|
||||
//Start visualizer
|
||||
@@ -852,7 +971,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
||||
}
|
||||
|
||||
class QueueState {
|
||||
static const QueueState empty = QueueState([], 0, [], AudioServiceRepeatMode.none, AudioServiceShuffleMode.none);
|
||||
static const QueueState empty = QueueState(
|
||||
[], 0, [], AudioServiceRepeatMode.none, AudioServiceShuffleMode.none);
|
||||
|
||||
final List<MediaItem> queue;
|
||||
final int? queueIndex;
|
||||
@@ -860,10 +980,15 @@ class QueueState {
|
||||
final AudioServiceRepeatMode repeatMode;
|
||||
final AudioServiceShuffleMode shuffleMode;
|
||||
|
||||
const QueueState(this.queue, this.queueIndex, this.shuffleIndices, this.repeatMode, this.shuffleMode);
|
||||
const QueueState(this.queue, this.queueIndex, this.shuffleIndices,
|
||||
this.repeatMode, this.shuffleMode);
|
||||
|
||||
bool get hasPrevious => repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) > 0;
|
||||
bool get hasNext => repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) + 1 < queue.length;
|
||||
bool get hasPrevious =>
|
||||
repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) > 0;
|
||||
bool get hasNext =>
|
||||
repeatMode != AudioServiceRepeatMode.none ||
|
||||
(queueIndex ?? 0) + 1 < queue.length;
|
||||
|
||||
List<int> get indices => shuffleIndices ?? List.generate(queue.length, (i) => i);
|
||||
List<int> get indices =>
|
||||
shuffleIndices ?? List.generate(queue.length, (i) => i);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
@@ -15,6 +16,7 @@ import 'api/download.dart';
|
||||
import 'main.dart';
|
||||
import 'service/audio_service.dart';
|
||||
import 'ui/cached_image.dart';
|
||||
import 'utils/app_icon_changer.dart';
|
||||
|
||||
part 'settings.g.dart';
|
||||
|
||||
@@ -126,6 +128,9 @@ class Settings {
|
||||
bool useArtColor = false;
|
||||
StreamSubscription? _useArtColorSub;
|
||||
|
||||
@JsonKey(defaultValue: 'DefaultIcon')
|
||||
String? appIcon;
|
||||
|
||||
//Deezer
|
||||
@JsonKey(defaultValue: 'en')
|
||||
late String deezerLanguage;
|
||||
@@ -171,11 +176,28 @@ class Settings {
|
||||
return ['Deezer', ...GoogleFonts.asMap().keys];
|
||||
}
|
||||
|
||||
// Get all available app icons
|
||||
List<String> get availableIcons {
|
||||
return AppIconChanger.availableIcons.map((icon) => icon.key).toList();
|
||||
}
|
||||
|
||||
//JSON to forward into download service
|
||||
Map getServiceSettings() {
|
||||
return {'json': jsonEncode(toJson())};
|
||||
}
|
||||
|
||||
Future<void> updateAppIcon(String iconKey) async {
|
||||
try {
|
||||
LauncherIcon icon =
|
||||
LauncherIcon.values.firstWhere((e) => e.key == iconKey);
|
||||
await AppIconChanger.changeIcon(icon);
|
||||
appIcon = iconKey;
|
||||
await save();
|
||||
} catch (e) {
|
||||
Logger.root.severe('Error updating app icon: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void updateUseArtColor(bool v) {
|
||||
useArtColor = v;
|
||||
if (v) {
|
||||
@@ -403,7 +425,6 @@ class Settings {
|
||||
primaryColor: primaryColor,
|
||||
sliderTheme: _sliderTheme,
|
||||
scaffoldBackgroundColor: deezerBg,
|
||||
dialogBackgroundColor: deezerBottom,
|
||||
bottomSheetTheme:
|
||||
const BottomSheetThemeData(backgroundColor: deezerBottom),
|
||||
cardColor: deezerBg,
|
||||
@@ -459,7 +480,7 @@ class Settings {
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom)),
|
||||
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom), dialogTheme: DialogThemeData(backgroundColor: deezerBottom)),
|
||||
Themes.Black: ThemeData(
|
||||
useMaterial3: false,
|
||||
brightness: Brightness.dark,
|
||||
@@ -467,7 +488,6 @@ class Settings {
|
||||
fontFamily: _fontFamily,
|
||||
primaryColor: primaryColor,
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
dialogBackgroundColor: Colors.black,
|
||||
sliderTheme: _sliderTheme,
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: Colors.black,
|
||||
@@ -524,7 +544,7 @@ class Settings {
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black))
|
||||
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black), dialogTheme: DialogThemeData(backgroundColor: Colors.black))
|
||||
};
|
||||
|
||||
Future<String> getPath() async =>
|
||||
|
||||
@@ -74,6 +74,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
|
||||
..primaryColor =
|
||||
Settings._colorFromJson((json['primaryColor'] as num?)?.toInt())
|
||||
..useArtColor = json['useArtColor'] as bool? ?? false
|
||||
..appIcon = json['appIcon'] as String? ?? 'DefaultIcon'
|
||||
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
|
||||
..deezerCountry = json['deezerCountry'] as String? ?? 'US'
|
||||
..logListen = json['logListen'] as bool? ?? false
|
||||
@@ -121,6 +122,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||
'displayMode': instance.displayMode,
|
||||
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
||||
'useArtColor': instance.useArtColor,
|
||||
'appIcon': instance.appIcon,
|
||||
'deezerLanguage': instance.deezerLanguage,
|
||||
'deezerCountry': instance.deezerCountry,
|
||||
'logListen': instance.logListen,
|
||||
|
||||
@@ -62,21 +62,25 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
//BG Image
|
||||
if (settings.blurPlayerBackground) {
|
||||
setState(() {
|
||||
_blurImage =
|
||||
NetworkImage(audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri);
|
||||
_blurImage = NetworkImage(
|
||||
audioHandler.mediaItem.value?.extras?['thumb'] ??
|
||||
audioHandler.mediaItem.value?.artUri);
|
||||
});
|
||||
}
|
||||
|
||||
//Run in isolate
|
||||
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(
|
||||
audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri));
|
||||
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
|
||||
CachedNetworkImageProvider(
|
||||
audioHandler.mediaItem.value?.extras?['thumb'] ??
|
||||
audioHandler.mediaItem.value?.artUri));
|
||||
|
||||
//Update notification
|
||||
if (settings.blurPlayerBackground) {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: palette.dominantColor!.color.withOpacity(0.25),
|
||||
systemNavigationBarColor:
|
||||
Color.alphaBlend(palette.dominantColor!.color.withOpacity(0.25), scaffoldBackgroundColor)));
|
||||
systemNavigationBarColor: Color.alphaBlend(
|
||||
palette.dominantColor!.color.withOpacity(0.25),
|
||||
scaffoldBackgroundColor)));
|
||||
}
|
||||
|
||||
//Color gradient
|
||||
@@ -85,10 +89,16 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
statusBarColor: palette.dominantColor!.color.withOpacity(0.7),
|
||||
));
|
||||
setState(() => _bgGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [palette.dominantColor!.color.withOpacity(0.7), const Color.fromARGB(0, 0, 0, 0)],
|
||||
stops: const [0.0, 0.6]));
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
palette.dominantColor!.color.withOpacity(0.7),
|
||||
const Color.fromARGB(0, 0, 0, 0)
|
||||
],
|
||||
stops: const [
|
||||
0.0,
|
||||
0.6
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +118,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
_mediaItemSub?.cancel();
|
||||
//Fix bottom buttons
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color, statusBarColor: Colors.transparent));
|
||||
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color,
|
||||
statusBarColor: Colors.transparent));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -120,7 +131,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(gradient: settings.blurPlayerBackground ? null : _bgGradient),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
settings.blurPlayerBackground ? null : _bgGradient),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (settings.blurPlayerBackground)
|
||||
@@ -130,7 +143,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
image: DecorationImage(
|
||||
image: _blurImage ?? const NetworkImage(''),
|
||||
fit: BoxFit.fill,
|
||||
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.25), BlendMode.dstATop))),
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(0.25),
|
||||
BlendMode.dstATop))),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(color: Colors.transparent),
|
||||
@@ -138,7 +153,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: StreamZip([audioHandler.playbackState, audioHandler.mediaItem]),
|
||||
stream: StreamZip(
|
||||
[audioHandler.playbackState, audioHandler.mediaItem]),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
//When disconnected
|
||||
if (audioHandler.mediaItem.value == null) {
|
||||
@@ -214,26 +230,45 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: ScreenUtil().setSp(50),
|
||||
child: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!.length >= 22
|
||||
child: GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value!
|
||||
.displayTitle!
|
||||
.length >=
|
||||
22
|
||||
? Marquee(
|
||||
text: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
|
||||
text: GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value!
|
||||
.displayTitle!,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold),
|
||||
blankSpace: 32.0,
|
||||
startPadding: 10.0,
|
||||
accelerationDuration: const Duration(seconds: 1),
|
||||
pauseAfterRound: const Duration(seconds: 2),
|
||||
)
|
||||
: Text(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
|
||||
GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value!
|
||||
.displayTitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold),
|
||||
)),
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displaySubtitle ?? '',
|
||||
GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value!
|
||||
.displaySubtitle ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
@@ -266,8 +301,11 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||
semanticLabel: 'Download'.i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
|
||||
Track t = Track.fromMediaItem(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
if (await downloadManager.addOfflineTrack(t,
|
||||
private: false, isSingleton: true) !=
|
||||
false) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Downloads added!'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
@@ -328,26 +366,45 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: ScreenUtil().setSp(26),
|
||||
child: (GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '').length >= 26
|
||||
child: (GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displayTitle ??
|
||||
'')
|
||||
.length >=
|
||||
26
|
||||
? Marquee(
|
||||
text: GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
|
||||
text: GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displayTitle ??
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(22),
|
||||
fontWeight: FontWeight.bold),
|
||||
blankSpace: 32.0,
|
||||
startPadding: 10.0,
|
||||
accelerationDuration: const Duration(seconds: 1),
|
||||
pauseAfterRound: const Duration(seconds: 2),
|
||||
)
|
||||
: Text(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
|
||||
GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displayTitle ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(22),
|
||||
fontWeight: FontWeight.bold),
|
||||
)),
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ?? '',
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
@@ -374,10 +431,15 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||
semanticLabel: 'Download'.i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
|
||||
Track t = Track.fromMediaItem(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
if (await downloadManager.addOfflineTrack(t,
|
||||
private: false, isSingleton: true) !=
|
||||
false) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Downloads added!'.i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||
msg: 'Downloads added!'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -407,7 +469,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
||||
//Load data from native
|
||||
void _load() async {
|
||||
if (audioHandler.mediaItem.value == null) return;
|
||||
Map? data = await DownloadManager.platform.invokeMethod('getStreamInfo', {'id': audioHandler.mediaItem.value!.id});
|
||||
Map? data = await DownloadManager.platform.invokeMethod(
|
||||
'getStreamInfo', {'id': audioHandler.mediaItem.value!.id});
|
||||
//N/A
|
||||
if (data == null) {
|
||||
if (mounted) setState(() => value = '');
|
||||
@@ -449,7 +512,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
||||
return TextButton(
|
||||
child: Text(value),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QualitySettings()));
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const QualitySettings()));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -475,12 +539,15 @@ class LyricsIconButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Track track = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
Track track =
|
||||
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
|
||||
bool isEnabled = (track.lyrics?.id ?? '0') != '0';
|
||||
|
||||
return Opacity(
|
||||
opacity: isEnabled ? 1.0 : 0.7, // Full opacity for enabled, reduced for disabled
|
||||
opacity: isEnabled
|
||||
? 1.0
|
||||
: 0.7, // Full opacity for enabled, reduced for disabled
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
//Icons.lyrics,
|
||||
@@ -491,10 +558,11 @@ class LyricsIconButton extends StatelessWidget {
|
||||
onPressed: isEnabled
|
||||
? () async {
|
||||
//Fix bottom buttons
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent));
|
||||
|
||||
await Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) => LyricsScreen(trackId: track.id!)));
|
||||
await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => LyricsScreen(trackId: track.id!)));
|
||||
|
||||
if (afterOnPressed != null) {
|
||||
afterOnPressed!();
|
||||
@@ -519,16 +587,24 @@ class PlayerMenuButton extends StatelessWidget {
|
||||
semanticLabel: 'Options'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
Track t =
|
||||
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||
MenuSheet m = MenuSheet(navigateCallback: () {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] == null) {
|
||||
m.defaultTrackMenu(t, context: context, options: [m.sleepTimer(context), m.wakelock(context)]);
|
||||
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] ==
|
||||
null) {
|
||||
m.defaultTrackMenu(t,
|
||||
context: context,
|
||||
options: [m.sleepTimer(context), m.wakelock(context)]);
|
||||
} else {
|
||||
m.defaultShowEpisodeMenu(
|
||||
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'])),
|
||||
ShowEpisode.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!),
|
||||
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value!
|
||||
.extras?['show'])),
|
||||
ShowEpisode.fromMediaItem(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!),
|
||||
context: context,
|
||||
options: [m.sleepTimer(context), m.wakelock(context)]);
|
||||
}
|
||||
@@ -594,7 +670,8 @@ class PlaybackControls extends StatefulWidget {
|
||||
class _PlaybackControlsState extends State<PlaybackControls> {
|
||||
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
|
||||
Icon get libraryIcon {
|
||||
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
if (cache.checkTrackFavorite(
|
||||
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
return Icon(
|
||||
Icons.favorite,
|
||||
size: widget.iconSize * 0.44,
|
||||
@@ -636,15 +713,20 @@ class _PlaybackControlsState extends State<PlaybackControls> {
|
||||
onPressed: () async {
|
||||
cache.libraryTracks ??= [];
|
||||
|
||||
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
if (cache.checkTrackFavorite(
|
||||
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
//Remove from library
|
||||
setState(() => cache.libraryTracks?.remove(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI.removeFavorite(audioHandler.mediaItem.value!.id);
|
||||
setState(() => cache.libraryTracks
|
||||
?.remove(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI
|
||||
.removeFavorite(audioHandler.mediaItem.value!.id);
|
||||
await cache.save();
|
||||
} else {
|
||||
//Add
|
||||
setState(() => cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
||||
setState(() =>
|
||||
cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI
|
||||
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
||||
await cache.save();
|
||||
}
|
||||
},
|
||||
@@ -679,7 +761,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
||||
|
||||
_imageList = _getImageList(audioHandler.queue.value);
|
||||
|
||||
_currentItemAndQueueSub = Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
|
||||
_currentItemAndQueueSub =
|
||||
Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
|
||||
audioHandler.mediaItem,
|
||||
audioHandler.queue,
|
||||
(mediaItem, queue) {
|
||||
@@ -698,7 +781,9 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
List<ZoomableImage> _getImageList(List<MediaItem> queue) {
|
||||
return queue.map((item) => ZoomableImage(url: item.artUri?.toString() ?? '')).toList();
|
||||
return queue
|
||||
.map((item) => ZoomableImage(url: item.artUri?.toString() ?? ''))
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool _didQueueChange(List<MediaItem> newQueue) {
|
||||
@@ -718,7 +803,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
||||
|
||||
void _handleMediaItemChange(MediaItem? item) async {
|
||||
final targetItemId = item?.id ?? '';
|
||||
final targetPage = audioHandler.queue.value.indexWhere((item) => item.id == targetItemId);
|
||||
final targetPage =
|
||||
audioHandler.queue.value.indexWhere((item) => item.id == targetItemId);
|
||||
if (targetPage == -1) return;
|
||||
|
||||
// No need to animating to the same page
|
||||
@@ -799,7 +885,8 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||
final double? textWidth;
|
||||
final bool? short;
|
||||
final GlobalKey iconButtonKey = GlobalKey();
|
||||
PlayerScreenTopRow({super.key, this.textSize, this.iconSize, this.textWidth, this.short});
|
||||
PlayerScreenTopRow(
|
||||
{super.key, this.textSize, this.iconSize, this.textWidth, this.short});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -825,7 +912,9 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||
child: Text(
|
||||
(short ?? false)
|
||||
? (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? '')
|
||||
: 'Playing from:'.i18n + ' ' + (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
|
||||
: 'Playing from:'.i18n +
|
||||
' ' +
|
||||
(GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
@@ -844,11 +933,14 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||
splashRadius: iconSize ?? ScreenUtil().setWidth(52),
|
||||
onPressed: () async {
|
||||
//Fix bottom buttons (Not needed anymore?)
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
||||
|
||||
// Calculate the center of the icon
|
||||
final RenderBox buttonRenderBox = iconButtonKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final Offset buttonOffset = buttonRenderBox.localToGlobal(buttonRenderBox.size.center(Offset.zero));
|
||||
final RenderBox buttonRenderBox =
|
||||
iconButtonKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final Offset buttonOffset = buttonRenderBox
|
||||
.localToGlobal(buttonRenderBox.size.center(Offset.zero));
|
||||
//Navigate
|
||||
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
|
||||
await Navigator.of(context).push(CircularExpansionRoute(
|
||||
@@ -879,7 +971,8 @@ class _SeekBarState extends State<SeekBar> {
|
||||
|
||||
double get position {
|
||||
if (_seeking) return _pos;
|
||||
double p = audioHandler.playbackState.value.position.inMilliseconds.toDouble();
|
||||
double p =
|
||||
audioHandler.playbackState.value.position.inMilliseconds.toDouble();
|
||||
if (p > duration) return duration;
|
||||
return p;
|
||||
}
|
||||
@@ -904,17 +997,20 @@ class _SeekBarState extends State<SeekBar> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
_timeString(position),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
||||
),
|
||||
Text(
|
||||
_timeString(duration),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -924,7 +1020,8 @@ class _SeekBarState extends State<SeekBar> {
|
||||
child: Slider(
|
||||
focusNode: FocusNode(
|
||||
canRequestFocus: false,
|
||||
skipTraversal: true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||
skipTraversal:
|
||||
true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||
value: position,
|
||||
max: duration,
|
||||
onChangeStart: (double d) {
|
||||
@@ -1003,7 +1100,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final queueState = audioHandler.queueState;
|
||||
final shuffleModeEnabled = queueState.shuffleMode == AudioServiceShuffleMode.all;
|
||||
final shuffleModeEnabled =
|
||||
queueState.shuffleMode == AudioServiceShuffleMode.all;
|
||||
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar(
|
||||
@@ -1016,7 +1114,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
||||
//cons.shuffle,
|
||||
ReFreezerIcons.shuffle,
|
||||
semanticLabel: 'Shuffle'.i18n,
|
||||
color: shuffleModeEnabled ? Theme.of(context).primaryColor : null,
|
||||
color:
|
||||
shuffleModeEnabled ? Theme.of(context).primaryColor : null,
|
||||
),
|
||||
onPressed: () async {
|
||||
await audioHandler.toggleShuffle();
|
||||
@@ -1032,7 +1131,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
||||
),
|
||||
onPressed: () async {
|
||||
await audioHandler.clearQueue();
|
||||
mainNavigatorKey.currentState!.popUntil((route) => route.isFirst);
|
||||
mainNavigatorKey.currentState!
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1082,7 +1182,7 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
||||
await audioHandler.skipToQueueItem(index);
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
},
|
||||
key: Key(mediaItem.id),
|
||||
key: Key('${mediaItem.id}_$index'),
|
||||
trailing: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
|
||||
@@ -424,8 +424,6 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
},
|
||||
trailing: _removeHistoryItemWidget(i),
|
||||
);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -56,33 +56,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('General'.i18n),
|
||||
leading: const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const GeneralSettings())),
|
||||
leading:
|
||||
const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const GeneralSettings())),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download Settings'.i18n),
|
||||
leading: const LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadsSettings())),
|
||||
leading: const LeadingIcon(Icons.cloud_download,
|
||||
color: Color(0xffbe3266)),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const DownloadsSettings())),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Appearance'.i18n),
|
||||
leading: const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const AppearanceSettings())),
|
||||
leading:
|
||||
const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AppearanceSettings())),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Quality'.i18n),
|
||||
leading: const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const QualitySettings())),
|
||||
leading:
|
||||
const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const QualitySettings())),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Deezer'.i18n),
|
||||
leading: const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const DeezerSettings())),
|
||||
leading:
|
||||
const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DeezerSettings())),
|
||||
),
|
||||
//Language select
|
||||
ListTile(
|
||||
title: Text('Language'.i18n),
|
||||
leading: const LeadingIcon(Icons.language, color: Color(0xff009a85)),
|
||||
leading:
|
||||
const LeadingIcon(Icons.language, color: Color(0xff009a85)),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -94,8 +111,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: Text(l.name),
|
||||
subtitle: Text('${l.locale}-${l.country}'),
|
||||
onTap: () async {
|
||||
I18n.of(customNavigatorKey.currentContext!).locale = Locale(l.locale, l.country);
|
||||
setState(() => settings.language = '${l.locale}_${l.country}');
|
||||
I18n.of(customNavigatorKey.currentContext!).locale =
|
||||
Locale(l.locale, l.country);
|
||||
setState(() =>
|
||||
settings.language = '${l.locale}_${l.country}');
|
||||
await settings.save();
|
||||
// Close the SimpleDialog
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
@@ -107,12 +126,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ListTile(
|
||||
title: Text('Updates'.i18n),
|
||||
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const UpdaterScreen())),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => const UpdaterScreen())),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('About'.i18n),
|
||||
leading: const LeadingIcon(Icons.info, color: Colors.grey),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CreditsScreen())),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => const CreditsScreen())),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -138,7 +159,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Theme'.i18n),
|
||||
subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
|
||||
subtitle: Text('Currently'.i18n +
|
||||
': ${settings.theme.toString().split('.').last}'),
|
||||
leading: const Icon(Icons.color_lens),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
@@ -201,12 +223,29 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
},
|
||||
),
|
||||
leading: const Icon(Icons.android)),
|
||||
ListTile(
|
||||
title: Text('App icon'.i18n),
|
||||
leading: const Icon(Icons.app_settings_alt),
|
||||
subtitle: Text(settings.appIcon ?? 'DefaultIcon'),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => IconSelector(() {
|
||||
setState(() {});
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Font'.i18n),
|
||||
leading: const Icon(Icons.font_download),
|
||||
subtitle: Text(settings.font),
|
||||
onTap: () {
|
||||
showDialog(context: context, builder: (context) => FontSelector(() => Navigator.of(context).pop()));
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FontSelector(() => Navigator.of(context).pop()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -234,7 +273,9 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Visualizer'.i18n),
|
||||
subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n),
|
||||
subtitle: Text(
|
||||
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
|
||||
.i18n),
|
||||
leading: const Icon(Icons.equalizer),
|
||||
trailing: Switch(
|
||||
value: settings.lyricsVisualizer,
|
||||
@@ -320,7 +361,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
onPressed: () async {
|
||||
settings.displayMode = i;
|
||||
await settings.save();
|
||||
await FlutterDisplayMode.setPreferredMode(modes[i]);
|
||||
await FlutterDisplayMode.setPreferredMode(
|
||||
modes[i]);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -335,6 +377,65 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
}
|
||||
}
|
||||
|
||||
class IconSelector extends StatefulWidget {
|
||||
final Function callback;
|
||||
|
||||
const IconSelector(this.callback, {super.key});
|
||||
|
||||
@override
|
||||
_IconSelectorState createState() => _IconSelectorState();
|
||||
}
|
||||
|
||||
class _IconSelectorState extends State<IconSelector> {
|
||||
late String selectedIcon;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedIcon = settings.appIcon ?? 'DefaultIcon';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text('Select app icon'.i18n),
|
||||
// Add the warning message here
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0),
|
||||
child: Text(
|
||||
'Selecting a new icon will exit the app to apply the change!'
|
||||
.i18n, // Translatable string
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
...settings.availableIcons.map((String iconKey) {
|
||||
return SimpleDialogOption(
|
||||
onPressed: () async {
|
||||
await settings.updateAppIcon(iconKey);
|
||||
setState(() {
|
||||
selectedIcon = iconKey;
|
||||
});
|
||||
widget.callback();
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero, // Adjust padding if needed
|
||||
leading: Image.asset(
|
||||
'assets/icons/$iconKey.png',
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
title: Text(iconKey),
|
||||
trailing:
|
||||
selectedIcon == iconKey ? const Icon(Icons.check) : null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FontSelector extends StatefulWidget {
|
||||
final Function callback;
|
||||
|
||||
@@ -347,7 +448,9 @@ class FontSelector extends StatefulWidget {
|
||||
class _FontSelectorState extends State<FontSelector> {
|
||||
String query = '';
|
||||
List<String> get fonts {
|
||||
return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList();
|
||||
return settings.fonts
|
||||
.where((f) => f.toLowerCase().contains(query))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Font selected
|
||||
@@ -421,25 +524,29 @@ class _QualitySettingsState extends State<QualitySettings> {
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Mobile streaming'.i18n),
|
||||
leading: const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
|
||||
leading:
|
||||
const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
|
||||
),
|
||||
const QualityPicker('mobile'),
|
||||
const FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Wifi streaming'.i18n),
|
||||
leading: const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
|
||||
leading:
|
||||
const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
|
||||
),
|
||||
const QualityPicker('wifi'),
|
||||
const FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Offline'.i18n),
|
||||
leading: const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
|
||||
leading:
|
||||
const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
|
||||
),
|
||||
const QualityPicker('offline'),
|
||||
const FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('External downloads'.i18n),
|
||||
leading: const LeadingIcon(Icons.file_download, color: Color(0xff2ba766)),
|
||||
leading: const LeadingIcon(Icons.file_download,
|
||||
color: Color(0xff2ba766)),
|
||||
),
|
||||
const QualityPicker('download'),
|
||||
],
|
||||
@@ -608,7 +715,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Content language'.i18n),
|
||||
subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'),
|
||||
subtitle: Text('Not app language, used in headers. Now'.i18n +
|
||||
': ${settings.deezerLanguage}'),
|
||||
leading: const Icon(Icons.language),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
@@ -621,7 +729,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||
title: Text(ContentLanguage.all[i].name),
|
||||
subtitle: Text(ContentLanguage.all[i].code),
|
||||
onTap: () async {
|
||||
setState(() => settings.deezerLanguage = ContentLanguage.all[i].code);
|
||||
setState(() => settings.deezerLanguage =
|
||||
ContentLanguage.all[i].code);
|
||||
await settings.save();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -633,7 +742,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Content country'.i18n),
|
||||
subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'),
|
||||
subtitle: Text('Country used in headers. Now'.i18n +
|
||||
': ${settings.deezerCountry}'),
|
||||
leading: const Icon(Icons.vpn_lock),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
@@ -655,7 +765,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||
],
|
||||
),
|
||||
onValuePicked: (Country country) {
|
||||
setState(() => settings.deezerCountry = country.isoCode ?? 'us');
|
||||
setState(() =>
|
||||
settings.deezerCountry = country.isoCode ?? 'us');
|
||||
settings.save();
|
||||
},
|
||||
));
|
||||
@@ -663,7 +774,9 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Log tracks'.i18n),
|
||||
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
|
||||
subtitle: Text(
|
||||
'Send track listen logs to Deezer, enable it for features like Flow to work properly'
|
||||
.i18n),
|
||||
trailing: Switch(
|
||||
value: settings.logListen,
|
||||
onChanged: (bool v) {
|
||||
@@ -764,7 +877,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
||||
Text(
|
||||
'Valid variables are'.i18n +
|
||||
': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' +
|
||||
"If you want to use custom directory naming - use '/' as directory separator.".i18n,
|
||||
"If you want to use custom directory naming - use '/' as directory separator."
|
||||
.i18n,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
@@ -779,7 +893,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
||||
TextButton(
|
||||
child: Text('Reset'.i18n),
|
||||
onPressed: () {
|
||||
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
|
||||
_controller.value =
|
||||
_controller.value.copyWith(text: '%artist% - %title%');
|
||||
_new = '%artist% - %title%';
|
||||
},
|
||||
),
|
||||
@@ -808,7 +923,8 @@ class DownloadsSettings extends StatefulWidget {
|
||||
|
||||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
double _downloadThreads = settings.downloadThreads.toDouble();
|
||||
final TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator);
|
||||
final TextEditingController _artistSeparatorController =
|
||||
TextEditingController(text: settings.artistSeparator);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -852,7 +968,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return FilenameTemplateDialog(settings.downloadFilename, (f) async {
|
||||
return FilenameTemplateDialog(settings.downloadFilename,
|
||||
(f) async {
|
||||
setState(() => settings.downloadFilename = f);
|
||||
await settings.save();
|
||||
});
|
||||
@@ -861,13 +978,15 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Singleton naming'.i18n),
|
||||
subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'),
|
||||
subtitle:
|
||||
Text('Currently'.i18n + ': ${settings.singletonFilename}'),
|
||||
leading: const Icon(Icons.text_format),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return FilenameTemplateDialog(settings.singletonFilename, (f) async {
|
||||
return FilenameTemplateDialog(settings.singletonFilename,
|
||||
(f) async {
|
||||
setState(() => settings.singletonFilename = f);
|
||||
await settings.save();
|
||||
});
|
||||
@@ -877,7 +996,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'Download threads'.i18n + ': ${_downloadThreads.round().toString()}',
|
||||
'Download threads'.i18n +
|
||||
': ${_downloadThreads.round().toString()}',
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
@@ -897,14 +1017,17 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
await settings.save();
|
||||
|
||||
//Prevent null
|
||||
if (val > 8 && cache.threadsWarning != true && context.mounted) {
|
||||
if (val > 8 &&
|
||||
cache.threadsWarning != true &&
|
||||
context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Warning'.i18n),
|
||||
content: Text(
|
||||
'Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n),
|
||||
'Using too many concurrent downloads on older/weaker devices might cause crashes!'
|
||||
.i18n),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Dismiss'.i18n),
|
||||
@@ -922,8 +1045,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
ListTile(
|
||||
title: Text('Tags'.i18n),
|
||||
leading: const Icon(Icons.label),
|
||||
onTap: () =>
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TagSelectionScreen())),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const TagSelectionScreen())),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
@@ -1010,17 +1133,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
leading: const Icon(Icons.image)),
|
||||
ListTile(
|
||||
title: Text('Album cover resolution'.i18n),
|
||||
subtitle: Text("WARNING: Resolutions above 1200 aren't officially supported".i18n),
|
||||
subtitle: Text(
|
||||
"WARNING: Resolutions above 1200 aren't officially supported"
|
||||
.i18n),
|
||||
leading: const Icon(Icons.image),
|
||||
trailing: SizedBox(
|
||||
width: 75.0,
|
||||
child: DropdownButton<int>(
|
||||
value: settings.albumArtResolution,
|
||||
items: [400, 800, 1000, 1200, 1400, 1600, 1800]
|
||||
.map<DropdownMenuItem<int>>((int i) => DropdownMenuItem<int>(
|
||||
value: i,
|
||||
child: Text(i.toString()),
|
||||
))
|
||||
.map<DropdownMenuItem<int>>(
|
||||
(int i) => DropdownMenuItem<int>(
|
||||
value: i,
|
||||
child: Text(i.toString()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (int? n) async {
|
||||
setState(() {
|
||||
@@ -1031,7 +1157,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
))),
|
||||
ListTile(
|
||||
title: Text('Create .nomedia files'.i18n),
|
||||
subtitle: Text('To prevent gallery being filled with album art'.i18n),
|
||||
subtitle:
|
||||
Text('To prevent gallery being filled with album art'.i18n),
|
||||
trailing: Switch(
|
||||
value: settings.nomediaFiles,
|
||||
onChanged: (v) {
|
||||
@@ -1058,7 +1185,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
ListTile(
|
||||
title: Text('Download Log'.i18n),
|
||||
leading: const Icon(Icons.sticky_note_2),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadLogViewer())),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const DownloadLogViewer())),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -1159,7 +1287,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Error logging in, check your internet connections.'.i18n,
|
||||
msg:
|
||||
'Error logging in, check your internet connections.'
|
||||
.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
}
|
||||
@@ -1179,7 +1309,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Copy ARL'.i18n),
|
||||
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
|
||||
subtitle:
|
||||
Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
|
||||
leading: const Icon(Icons.lock),
|
||||
onTap: () async {
|
||||
await FlutterClipboard.copy(settings.arl ?? '');
|
||||
@@ -1190,7 +1321,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Enable equalizer'.i18n),
|
||||
subtitle: Text('Might enable some equalizer apps to work. Requires restart of ReFreezer'.i18n),
|
||||
subtitle: Text(
|
||||
'Might enable some equalizer apps to work. Requires restart of ReFreezer'
|
||||
.i18n),
|
||||
leading: const Icon(Icons.equalizer),
|
||||
trailing: Switch(
|
||||
value: settings.enableEqualizer,
|
||||
@@ -1202,7 +1335,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||
),
|
||||
ListTile(
|
||||
title: Text('LastFM'.i18n),
|
||||
subtitle: Text((settings.lastFMUsername != null) ? 'Log out'.i18n : 'Login to enable scrobbling.'.i18n),
|
||||
subtitle: Text((settings.lastFMUsername != null)
|
||||
? 'Log out'.i18n
|
||||
: 'Login to enable scrobbling.'.i18n),
|
||||
leading: const Icon(FontAwesome5.lastfm),
|
||||
onTap: () async {
|
||||
if (settings.lastFMUsername != null) {
|
||||
@@ -1241,8 +1376,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||
ListTile(
|
||||
title: Text('Application Log'.i18n),
|
||||
leading: const Icon(Icons.sticky_note_2),
|
||||
onTap: () =>
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ApplicationLogViewer())),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const ApplicationLogViewer())),
|
||||
),
|
||||
const FreezerDivider(),
|
||||
ListTile(
|
||||
@@ -1341,7 +1476,10 @@ class _LastFMLoginState extends State<LastFMLogin> {
|
||||
LastFM last;
|
||||
try {
|
||||
last = await LastFM.authenticate(
|
||||
apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: _username, password: _password);
|
||||
apiKey: Env.lastFmApiKey,
|
||||
apiSecret: Env.lastFmApiSecret,
|
||||
username: _username,
|
||||
password: _password);
|
||||
} catch (e) {
|
||||
Logger.root.severe('Error authorizing LastFM: $e');
|
||||
Fluttertoast.showToast(msg: 'Authorization error!'.i18n);
|
||||
@@ -1365,23 +1503,30 @@ class StorageInfo {
|
||||
final String appFilesDir;
|
||||
final int availableBytes;
|
||||
|
||||
StorageInfo({required this.rootDir, required this.appFilesDir, required this.availableBytes});
|
||||
StorageInfo(
|
||||
{required this.rootDir,
|
||||
required this.appFilesDir,
|
||||
required this.availableBytes});
|
||||
}
|
||||
|
||||
Future<List<StorageInfo>> getStorageInfo() async {
|
||||
final externalDirectories = await ExternalPath.getExternalStorageDirectories();
|
||||
final externalDirectories =
|
||||
await ExternalPath.getExternalStorageDirectories();
|
||||
|
||||
List<StorageInfo> storageInfoList = [];
|
||||
|
||||
if (externalDirectories.isNotEmpty) {
|
||||
for (var dir in externalDirectories) {
|
||||
var availableMegaBytes = (await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
|
||||
var availableMegaBytes =
|
||||
(await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
|
||||
|
||||
storageInfoList.add(
|
||||
StorageInfo(
|
||||
rootDir: dir,
|
||||
appFilesDir: dir,
|
||||
availableBytes: availableMegaBytes > 0 ? (availableMegaBytes * 1000000).floor() : 0,
|
||||
availableBytes: availableMegaBytes > 0
|
||||
? (availableMegaBytes * 1000000).floor()
|
||||
: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1453,14 +1598,17 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[CircularProgressIndicator()],
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
...List.generate(snapshot.data?.length ?? 0, (i) {
|
||||
...List.generate(snapshot.data?.length ?? 0,
|
||||
(i) {
|
||||
StorageInfo si = snapshot.data![i];
|
||||
return ListTile(
|
||||
title: Text(si.rootDir),
|
||||
@@ -1524,7 +1672,9 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_root == _path) {
|
||||
Fluttertoast.showToast(msg: 'Permission denied'.i18n, gravity: ToastGravity.BOTTOM);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Permission denied'.i18n,
|
||||
gravity: ToastGravity.BOTTOM);
|
||||
return;
|
||||
}
|
||||
_previous = _path;
|
||||
@@ -1618,8 +1768,10 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||
),
|
||||
const FreezerDivider(),
|
||||
const ListTile(
|
||||
title: Text('DJDoubleD', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Developer, tester, new icon & logo, some translations, ...'),
|
||||
title: Text('DJDoubleD',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(
|
||||
'Developer, tester, new icon & logo, some translations, ...'),
|
||||
),
|
||||
const FreezerDivider(),
|
||||
/*ListTile(
|
||||
@@ -1657,7 +1809,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||
ListTile(
|
||||
title: Text('Crowdin'.i18n),
|
||||
subtitle: Text('Help translating this app on Crowdin!'.i18n),
|
||||
leading: const Icon(ReFreezerIcons.crowdin, color: Color(0xffbdc1c6), size: 36.0),
|
||||
leading: const Icon(ReFreezerIcons.crowdin,
|
||||
color: Color(0xffbdc1c6), size: 36.0),
|
||||
onTap: () {
|
||||
launchUrlString('https://crowdin.com/project/refreezer');
|
||||
},
|
||||
@@ -1665,15 +1818,20 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: Text('Donate'.i18n),
|
||||
subtitle: Text('You should rather support your favorite artists, instead of this app!'.i18n),
|
||||
leading: const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
||||
subtitle: Text(
|
||||
'You should rather support your favorite artists, instead of this app!'
|
||||
.i18n),
|
||||
leading:
|
||||
const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Donate'.i18n),
|
||||
content: Text('No really, go support your favorite artists instead ;)'.i18n),
|
||||
content: Text(
|
||||
'No really, go support your favorite artists instead ;)'
|
||||
.i18n),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
@@ -1704,10 +1862,12 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
|
||||
Image.asset('assets/icon_legacy.png',
|
||||
width: 24, height: 24),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
'The original freezer development team'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
@@ -1720,7 +1880,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
|
||||
Image.asset('assets/icon_legacy.png',
|
||||
width: 24, height: 24),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -1739,7 +1900,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('Bas Curtiz'),
|
||||
subtitle: Text('Icon, logo, banner, design suggestions, tester'),
|
||||
subtitle:
|
||||
Text('Icon, logo, banner, design suggestions, tester'),
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('Tobs'),
|
||||
|
||||
25
lib/utils/app_icon_changer.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class AppIconChanger {
|
||||
static const MethodChannel _channel = MethodChannel('change_icon');
|
||||
|
||||
static Future<void> changeIcon(LauncherIcon icon) async {
|
||||
try {
|
||||
await _channel.invokeMethod('changeIcon', {'iconName': icon.key});
|
||||
} on PlatformException catch (e) {
|
||||
Logger.root.severe('Failed to change icon: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
static List<LauncherIcon> get availableIcons => LauncherIcon.values;
|
||||
}
|
||||
|
||||
enum LauncherIcon {
|
||||
defaultIcon('DefaultIcon'),
|
||||
catIcon('CatIcon'),
|
||||
deezerIcon('DeezerBlueIcon');
|
||||
|
||||
final String key;
|
||||
const LauncherIcon(this.key);
|
||||
}
|
||||
@@ -96,8 +96,6 @@ dependencies:
|
||||
move_to_background:
|
||||
path: ./move_to_background
|
||||
numberpicker: ^2.1.2
|
||||
# Awaiting release with fix for flutter 3.29.0:
|
||||
#open_filex: ^4.6.0
|
||||
open_filex: ^4.7.0
|
||||
package_info_plus: ^8.0.0
|
||||
palette_generator: ^0.3.3+3
|
||||
@@ -156,6 +154,9 @@ flutter:
|
||||
- assets/icon.png
|
||||
- assets/favorites_thumb.jpg
|
||||
- assets/browse_icon.png
|
||||
- assets/icons/DefaultIcon.png
|
||||
- assets/icons/CatIcon.png
|
||||
- assets/icons/DeezerBlueIcon.png
|
||||
|
||||
fonts:
|
||||
# - family: Montserrat
|
||||
|
||||
@@ -353,5 +353,7 @@
|
||||
"Permission denied, download canceled!": "Permission denied, download canceled!",
|
||||
"Crowdin": "Crowdin",
|
||||
"Help translating this app on Crowdin!": "Help translating this app on Crowdin!",
|
||||
"Allow screen to turn off": "Allow screen to turn off"
|
||||
"Allow screen to turn off": "Allow screen to turn off",
|
||||
"Selecting a new icon will exit the app to apply the change!":
|
||||
"Selecting a new icon will exit the app to apply the change!"
|
||||
}
|
||||
|
||||