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"
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"
|
||||||
android:required="false"
|
android:required="false"
|
||||||
android:minSdkVersion="30"/>
|
android:minSdkVersion="30"/>
|
||||||
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
@@ -38,7 +39,10 @@
|
|||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:enableOnBackInvokedCallback="true"
|
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:label="ReFreezer"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
@@ -59,7 +63,6 @@
|
|||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:banner="@mipmap/ic_launcher"
|
android:banner="@mipmap/ic_launcher"
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:logo="@mipmap/ic_launcher"
|
android:logo="@mipmap/ic_launcher"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:exported = "true">
|
android:exported = "true">
|
||||||
@@ -76,10 +79,10 @@
|
|||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<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>
|
</intent-filter>
|
||||||
|
|
||||||
|
|
||||||
<!-- Deep Links -->
|
<!-- Deep Links -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -113,12 +116,70 @@
|
|||||||
android:scheme="https"
|
android:scheme="https"
|
||||||
android:host="www.deezer.page.link" />
|
android:host="www.deezer.page.link" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- New short domain -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<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>
|
</intent-filter>
|
||||||
</activity>
|
</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.
|
Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
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
|
@Override
|
||||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
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
|
//Flutter method channel
|
||||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(((call, result) -> {
|
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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<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>
|
</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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground" />
|
||||||
</adaptive-icon>
|
</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',
|
'Crowdin': 'Crowdin',
|
||||||
'Help translating this app on Crowdin!':
|
'Help translating this app on Crowdin!':
|
||||||
'Aidez-nous à traduire cette application sur 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': {
|
'he-IL': {
|
||||||
'Home': 'מסך הבית',
|
'Home': 'מסך הבית',
|
||||||
@@ -6850,7 +6852,9 @@ const crowdin = {
|
|||||||
'Toestemming geweigerd, download geannuleerd!',
|
'Toestemming geweigerd, download geannuleerd!',
|
||||||
'Help translating this app on Crowdin!':
|
'Help translating this app on Crowdin!':
|
||||||
'Help deze app te vertalen op 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': {
|
'pl-PL': {
|
||||||
'Home': 'Strona główna',
|
'Home': 'Strona główna',
|
||||||
|
|||||||
@@ -454,5 +454,9 @@ const language_en_us = {
|
|||||||
|
|
||||||
// 0.7.15
|
// 0.7.15
|
||||||
'Allow screen to turn off': 'Allow screen to turn off',
|
'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() {
|
AudioPlayerHandler() {
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
int? _prevAudioSession;
|
int? _prevAudioSession;
|
||||||
bool _equalizerOpen = false;
|
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 ...
|
// for some reason, dart can decide not to respect the 'await' due to weird task sceduling ...
|
||||||
final Completer<void> _playerInitializedCompleter = Completer<void>();
|
final Completer<void> _playerInitializedCompleter = Completer<void>();
|
||||||
@@ -70,7 +72,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
QueueSource? queueSource;
|
QueueSource? queueSource;
|
||||||
StreamSubscription? _queueStateSub;
|
StreamSubscription? _queueStateSub;
|
||||||
StreamSubscription? _mediaItemSub;
|
StreamSubscription? _mediaItemSub;
|
||||||
final BehaviorSubject<QueueState> _queueStateSubject = BehaviorSubject<QueueState>();
|
final BehaviorSubject<QueueState> _queueStateSubject =
|
||||||
|
BehaviorSubject<QueueState>();
|
||||||
Stream<QueueState> get queueStateStream => _queueStateSubject.stream;
|
Stream<QueueState> get queueStateStream => _queueStateSubject.stream;
|
||||||
QueueState get queueState => _queueStateSubject.value;
|
QueueState get queueState => _queueStateSubject.value;
|
||||||
int currentIndex = 0;
|
int currentIndex = 0;
|
||||||
@@ -85,13 +88,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
_player.sequenceStateStream
|
_player.sequenceStateStream
|
||||||
.map((state) {
|
.map((state) {
|
||||||
try {
|
try {
|
||||||
return state?.effectiveSequence.map((source) => source.tag as MediaItem).toList();
|
return state?.effectiveSequence
|
||||||
|
.map((source) => source.tag as MediaItem)
|
||||||
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is RangeError) {
|
if (e is RangeError) {
|
||||||
// This is caused by just_audio not updating the currentIndex first in the _broadcastSequence method.
|
// 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.
|
// Because in shufflemode it's out of range after removing items from the playlist.
|
||||||
// Might be fixed in future
|
// 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 to indicate that the queue could/should not be broadcasted
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -103,20 +109,25 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
.pipe(queue);
|
.pipe(queue);
|
||||||
|
|
||||||
// Update current QueueState
|
// Update current QueueState
|
||||||
_queueStateSub = Rx.combineLatest3<List<MediaItem>, PlaybackState, List<int>, QueueState>(
|
_queueStateSub = Rx.combineLatest3<List<MediaItem>, PlaybackState,
|
||||||
|
List<int>, QueueState>(
|
||||||
queue,
|
queue,
|
||||||
playbackState,
|
playbackState,
|
||||||
_player.shuffleIndicesStream.whereType<List<int>>(),
|
_player.shuffleIndicesStream.whereType<List<int>>(),
|
||||||
(queue, playbackState, shuffleIndices) => QueueState(
|
(queue, playbackState, shuffleIndices) => QueueState(
|
||||||
queue,
|
queue,
|
||||||
playbackState.queueIndex,
|
playbackState.queueIndex,
|
||||||
playbackState.shuffleMode == AudioServiceShuffleMode.all ? shuffleIndices : null,
|
playbackState.shuffleMode == AudioServiceShuffleMode.all
|
||||||
|
? shuffleIndices
|
||||||
|
: null,
|
||||||
playbackState.repeatMode,
|
playbackState.repeatMode,
|
||||||
playbackState.shuffleMode,
|
playbackState.shuffleMode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(state) => state.shuffleIndices == null || state.queue.length == state.shuffleIndices!.length,
|
(state) =>
|
||||||
|
state.shuffleIndices == null ||
|
||||||
|
state.queue.length == state.shuffleIndices!.length,
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.listen(_queueStateSubject.add);
|
.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,
|
// Broadcast media item changes after track or position in queue change,
|
||||||
// only emit value when different from previous item
|
// only emit value when different from previous item
|
||||||
_mediaItemSub = Rx.combineLatest3<int?, List<MediaItem>, bool, MediaItem?>(
|
_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
|
// Don't broadcast while shuffling to avoid intermediate MediaItem change
|
||||||
if (_rearranging) return null;
|
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.
|
// 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) {
|
_player.processingStateStream.listen((state) {
|
||||||
if (state == ProcessingState.completed && _player.playing) {
|
if (state == ProcessingState.completed && _player.playing) {
|
||||||
@@ -225,7 +240,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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
|
// Check if the mediaId is for Android Auto
|
||||||
if (mediaId.startsWith(AndroidAuto.prefix)) {
|
if (mediaId.startsWith(AndroidAuto.prefix)) {
|
||||||
// Forward the event to Android Auto
|
// Forward the event to Android Auto
|
||||||
@@ -238,7 +254,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_player.seek(
|
_player.seek(
|
||||||
Duration.zero,
|
Duration.zero,
|
||||||
index: _player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
|
index:
|
||||||
|
_player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Logger.root.severe('playFromMediaId: MediaItem not found');
|
Logger.root.severe('playFromMediaId: MediaItem not found');
|
||||||
@@ -252,11 +269,12 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
|
// Save queue before stopping player to save player details
|
||||||
|
Logger.root.info('saving queue');
|
||||||
|
_saveQueueToFile();
|
||||||
Logger.root.info('stopping player');
|
Logger.root.info('stopping player');
|
||||||
await _player.stop();
|
await _player.stop();
|
||||||
await super.stop();
|
await super.stop();
|
||||||
Logger.root.info('saving queue');
|
|
||||||
_saveQueueToFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -283,10 +301,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateQueue(List<MediaItem> newQueue) async {
|
Future<void> updateQueue(List<MediaItem> queue) async {
|
||||||
await _playlist.clear();
|
await _playlist.clear();
|
||||||
if (newQueue.isNotEmpty) {
|
if (queue.isNotEmpty) {
|
||||||
await _playlist.addAll(await _itemsToSources(newQueue));
|
await _playlist.addAll(await _itemsToSources(queue));
|
||||||
} else {
|
} else {
|
||||||
if (mediaItem.hasValue) {
|
if (mediaItem.hasValue) {
|
||||||
mediaItem.add(null);
|
mediaItem.add(null);
|
||||||
@@ -343,7 +361,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
|
|
||||||
_player.seek(
|
_player.seek(
|
||||||
Duration.zero,
|
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
|
// 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 {
|
Future<void> _startSession() async {
|
||||||
Logger.root.info('starting audio service...');
|
Logger.root.info('starting audio service...');
|
||||||
final session = await AudioSession.instance;
|
final session = await AudioSession.instance;
|
||||||
@@ -406,13 +452,15 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
_player = AudioPlayer();
|
_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.
|
/// Broadcasts the current state to all clients.
|
||||||
void _broadcastState(PlaybackEvent event) {
|
void _broadcastState(PlaybackEvent event) {
|
||||||
final playing = _player.playing;
|
final playing = _player.playing;
|
||||||
currentIndex = _getQueueIndex(_player.currentIndex ?? 0, shuffleModeEnabled: _player.shuffleModeEnabled);
|
currentIndex = _getQueueIndex(_player.currentIndex ?? 0,
|
||||||
|
shuffleModeEnabled: _player.shuffleModeEnabled);
|
||||||
|
|
||||||
playbackState.add(
|
playbackState.add(
|
||||||
playbackState.value.copyWith(
|
playbackState.value.copyWith(
|
||||||
@@ -421,9 +469,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
if (playing) MediaControl.pause else MediaControl.play,
|
if (playing) MediaControl.pause else MediaControl.play,
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
// Custom Stop
|
// 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],
|
androidCompactActionIndices: const [0, 1, 2],
|
||||||
processingState: const {
|
processingState: const {
|
||||||
ProcessingState.idle: AudioProcessingState.idle,
|
ProcessingState.idle: AudioProcessingState.idle,
|
||||||
@@ -478,7 +533,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
|
|
||||||
Future _getTrackUrl(MediaItem mediaItem) async {
|
Future _getTrackUrl(MediaItem mediaItem) async {
|
||||||
//Check if offline
|
//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));
|
File f = File(p.join(offlinePath, mediaItem.id));
|
||||||
if (await f.exists()) {
|
if (await f.exists()) {
|
||||||
//return f.path;
|
//return f.path;
|
||||||
@@ -495,14 +551,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
//This just returns fake url that contains metadata
|
//This just returns fake url that contains metadata
|
||||||
int quality = await getStreamQuality();
|
int quality = await getStreamQuality();
|
||||||
|
|
||||||
List? streamPlaybackDetails = jsonDecode(mediaItem.extras?['playbackDetails']);
|
List? streamPlaybackDetails =
|
||||||
|
jsonDecode(mediaItem.extras?['playbackDetails']);
|
||||||
String streamItemId = mediaItem.id;
|
String streamItemId = mediaItem.id;
|
||||||
|
|
||||||
//If Deezer provided a FALLBACK track, use the playbackDetails and id from the fallback track
|
//If Deezer provided a FALLBACK track, use the playbackDetails and id from the fallback track
|
||||||
//for streaming (original stream unavailable)
|
//for streaming (original stream unavailable)
|
||||||
if (mediaItem.extras?['fallbackId'] != null) {
|
if (mediaItem.extras?['fallbackId'] != null) {
|
||||||
streamItemId = mediaItem.extras?['fallbackId'];
|
streamItemId = mediaItem.extras?['fallbackId'];
|
||||||
streamPlaybackDetails = jsonDecode(mediaItem.extras?['playbackDetailsFallback']);
|
streamPlaybackDetails =
|
||||||
|
jsonDecode(mediaItem.extras?['playbackDetailsFallback']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((streamPlaybackDetails ?? []).length < 3) return null;
|
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
|
/// 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
|
//Set requested index
|
||||||
_requestedIndex = 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)
|
// Convert new queue to AudioSources playlist & add to just_audio (Concurrent approach)
|
||||||
await _playlist.addAll(await _itemsToSources(newQueue));
|
await _playlist.addAll(await _itemsToSources(newQueue));
|
||||||
|
|
||||||
|
// Wait for player to be ready before seeking
|
||||||
|
await _waitForPlayerReadiness();
|
||||||
|
|
||||||
//Seek to correct position & index
|
//Seek to correct position & index
|
||||||
try {
|
try {
|
||||||
await _player.seek(position, index: index);
|
await _player.seek(position, index: index);
|
||||||
@@ -542,7 +604,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Replace queue, play specified item index
|
//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)
|
// Pauze platback if playing (Player seems to crash on some devices otherwise)
|
||||||
await pause();
|
await pause();
|
||||||
//Set requested index
|
//Set requested index
|
||||||
@@ -582,7 +645,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
// Get current position
|
// Get current position
|
||||||
int pos = queue.value.length;
|
int pos = queue.value.length;
|
||||||
// Load 25 more tracks from playlist
|
// 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;
|
break;
|
||||||
default:
|
default:
|
||||||
Logger.root.info('Reached end of queue source: ${queueSource!.source}');
|
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
|
// Deduplicate tracks already in queue with the same id
|
||||||
List<String> queueIds = queue.value.map((mi) => mi.id).toList();
|
List<String> queueIds = queue.value.map((mi) => mi.id).toList();
|
||||||
tracks.removeWhere((track) => queueIds.contains(track.id));
|
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);
|
addQueueItems(extraTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _playbackError(err) {
|
void _playbackError(err) {
|
||||||
Logger.root.severe('Playback Error from audioservice: ${err.code}', 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;
|
return;
|
||||||
}
|
}
|
||||||
_onError(err, null);
|
_onError(err, null);
|
||||||
@@ -647,7 +714,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
}
|
}
|
||||||
Map data = {
|
Map data = {
|
||||||
'index': _player.currentIndex,
|
'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,
|
'position': _player.position.inMilliseconds,
|
||||||
'queueSource': (queueSource ?? QueueSource()).toJson(),
|
'queueSource': (queueSource ?? QueueSource()).toJson(),
|
||||||
'loopMode': LoopMode.values.indexOf(_player.loopMode)
|
'loopMode': LoopMode.values.indexOf(_player.loopMode)
|
||||||
@@ -674,20 +744,44 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
//Restore queue & playback info from path
|
//Restore queue & playback info from path
|
||||||
Future loadQueueFromFile() async {
|
Future loadQueueFromFile() async {
|
||||||
Logger.root.info('looking for saved queue file...');
|
Logger.root.info('looking for saved queue file...');
|
||||||
File f = File(await _getQueueFilePath());
|
try {
|
||||||
if (await f.exists()) {
|
File f = File(await _getQueueFilePath());
|
||||||
Logger.root.info('saved queue file found, loading...');
|
if (await f.exists()) {
|
||||||
Map<String, dynamic> json = jsonDecode(await f.readAsString());
|
Logger.root.info('saved queue file found, loading...');
|
||||||
List<MediaItem> savedQueue =
|
|
||||||
(json['queue'] ?? []).map<MediaItem>((mi) => (MediaItemConverter.mediaItemFromMap(mi))).toList();
|
try {
|
||||||
final int lastIndex = json['index'] ?? 0;
|
String fileContent = await f.readAsString();
|
||||||
final Duration lastPos = Duration(milliseconds: json['position'] ?? 0);
|
if (fileContent.isEmpty) {
|
||||||
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
|
Logger.root.warning('saved queue file is empty');
|
||||||
var repeatType = LoopMode.values[(json['loopMode'] ?? 0)];
|
return;
|
||||||
_player.setLoopMode(repeatType);
|
}
|
||||||
//Restore queue & Broadcast
|
|
||||||
await _loadQueueAtIndex(savedQueue, lastIndex, position: lastPos);
|
Map<String, dynamic> json = jsonDecode(fileContent);
|
||||||
Logger.root.info('saved queue loaded from file!');
|
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 ?? '';
|
String password = settings.lastFMPassword ?? '';
|
||||||
try {
|
try {
|
||||||
LastFM lastFM = await LastFM.authenticateWithPasswordHash(
|
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);
|
_scrobblenaut = Scrobblenaut(lastFM: lastFM);
|
||||||
_scrobblenautReady = true;
|
_scrobblenautReady = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -721,7 +818,9 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future toggleShuffle() async {
|
Future toggleShuffle() async {
|
||||||
await setShuffleMode(_player.shuffleModeEnabled ? AudioServiceShuffleMode.none : AudioServiceShuffleMode.all);
|
await setShuffleMode(_player.shuffleModeEnabled
|
||||||
|
? AudioServiceShuffleMode.none
|
||||||
|
: AudioServiceShuffleMode.all);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoopMode getLoopMode() {
|
LoopMode getLoopMode() {
|
||||||
@@ -749,53 +848,71 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
if (_player.playing) {
|
if (_player.playing) {
|
||||||
// Pauze playback if playing (Player seems to crash on some devices otherwise)
|
// Pauze playback if playing (Player seems to crash on some devices otherwise)
|
||||||
await pause();
|
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();
|
await _player.play();
|
||||||
} else {
|
} 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
|
//Play track from album
|
||||||
Future playFromAlbum(Album album, String trackId) async {
|
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
|
//Play mix by track
|
||||||
Future playMix(String trackId, String trackTitle) async {
|
Future playMix(String trackId, String trackTitle) async {
|
||||||
List<Track> tracks = await deezerAPI.playMix(trackId);
|
List<Track> tracks = await deezerAPI.playMix(trackId);
|
||||||
playFromTrackList(tracks, tracks[0].id ?? '',
|
playFromTrackList(
|
||||||
QueueSource(id: trackId, text: 'Mix based on'.i18n + ' $trackTitle', source: 'mix'));
|
tracks,
|
||||||
|
tracks[0].id ?? '',
|
||||||
|
QueueSource(
|
||||||
|
id: trackId,
|
||||||
|
text: 'Mix based on'.i18n + ' $trackTitle',
|
||||||
|
source: 'mix'));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Play from artist top tracks
|
//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(
|
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 {
|
Future playFromPlaylist(Playlist playlist, String trackId) async {
|
||||||
await playFromTrackList(
|
await playFromTrackList(playlist.tracks ?? [], trackId,
|
||||||
playlist.tracks ?? [], trackId, QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Play episode from show, load whole show as queue
|
//Play episode from show, load whole show as queue
|
||||||
Future playShowEpisode(Show show, List<ShowEpisode> episodes, {int index = 0}) async {
|
Future playShowEpisode(Show show, List<ShowEpisode> episodes,
|
||||||
QueueSource showQueueSource = QueueSource(id: show.id, text: show.name, source: 'show');
|
{int index = 0}) async {
|
||||||
|
QueueSource showQueueSource =
|
||||||
|
QueueSource(id: show.id, text: show.name, source: 'show');
|
||||||
//Generate media items
|
//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
|
//Load and play
|
||||||
await _loadQueueAndPlayAtIndex(showQueueSource, episodeQueue, index);
|
await _loadQueueAndPlayAtIndex(showQueueSource, episodeQueue, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load tracks as queue, play track id, set queue source
|
//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
|
//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
|
//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
|
//Load smart track list as queue, start from beginning
|
||||||
@@ -820,8 +937,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
QueueSource queueSource = QueueSource(
|
QueueSource queueSource = QueueSource(
|
||||||
id: stl.id,
|
id: stl.id,
|
||||||
source: (stl.id == 'flow') ? 'flow' : 'smarttracklist',
|
source: (stl.id == 'flow') ? 'flow' : 'smarttracklist',
|
||||||
text: stl.title ?? ((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
|
text: stl.title ??
|
||||||
await playFromTrackList(stl.tracks ?? [], stl.tracks?[0].id ?? '', queueSource);
|
((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
|
||||||
|
await playFromTrackList(
|
||||||
|
stl.tracks ?? [], stl.tracks?[0].id ?? '', queueSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Start visualizer
|
//Start visualizer
|
||||||
@@ -852,7 +971,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
class QueueState {
|
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 List<MediaItem> queue;
|
||||||
final int? queueIndex;
|
final int? queueIndex;
|
||||||
@@ -860,10 +980,15 @@ class QueueState {
|
|||||||
final AudioServiceRepeatMode repeatMode;
|
final AudioServiceRepeatMode repeatMode;
|
||||||
final AudioServiceShuffleMode shuffleMode;
|
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 hasPrevious =>
|
||||||
bool get hasNext => repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) + 1 < queue.length;
|
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:get_it/get_it.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import 'api/download.dart';
|
|||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
import 'service/audio_service.dart';
|
import 'service/audio_service.dart';
|
||||||
import 'ui/cached_image.dart';
|
import 'ui/cached_image.dart';
|
||||||
|
import 'utils/app_icon_changer.dart';
|
||||||
|
|
||||||
part 'settings.g.dart';
|
part 'settings.g.dart';
|
||||||
|
|
||||||
@@ -126,6 +128,9 @@ class Settings {
|
|||||||
bool useArtColor = false;
|
bool useArtColor = false;
|
||||||
StreamSubscription? _useArtColorSub;
|
StreamSubscription? _useArtColorSub;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: 'DefaultIcon')
|
||||||
|
String? appIcon;
|
||||||
|
|
||||||
//Deezer
|
//Deezer
|
||||||
@JsonKey(defaultValue: 'en')
|
@JsonKey(defaultValue: 'en')
|
||||||
late String deezerLanguage;
|
late String deezerLanguage;
|
||||||
@@ -171,11 +176,28 @@ class Settings {
|
|||||||
return ['Deezer', ...GoogleFonts.asMap().keys];
|
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
|
//JSON to forward into download service
|
||||||
Map getServiceSettings() {
|
Map getServiceSettings() {
|
||||||
return {'json': jsonEncode(toJson())};
|
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) {
|
void updateUseArtColor(bool v) {
|
||||||
useArtColor = v;
|
useArtColor = v;
|
||||||
if (v) {
|
if (v) {
|
||||||
@@ -403,7 +425,6 @@ class Settings {
|
|||||||
primaryColor: primaryColor,
|
primaryColor: primaryColor,
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: _sliderTheme,
|
||||||
scaffoldBackgroundColor: deezerBg,
|
scaffoldBackgroundColor: deezerBg,
|
||||||
dialogBackgroundColor: deezerBottom,
|
|
||||||
bottomSheetTheme:
|
bottomSheetTheme:
|
||||||
const BottomSheetThemeData(backgroundColor: deezerBottom),
|
const BottomSheetThemeData(backgroundColor: deezerBottom),
|
||||||
cardColor: deezerBg,
|
cardColor: deezerBg,
|
||||||
@@ -459,7 +480,7 @@ class Settings {
|
|||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom)),
|
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom), dialogTheme: DialogThemeData(backgroundColor: deezerBottom)),
|
||||||
Themes.Black: ThemeData(
|
Themes.Black: ThemeData(
|
||||||
useMaterial3: false,
|
useMaterial3: false,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
@@ -467,7 +488,6 @@ class Settings {
|
|||||||
fontFamily: _fontFamily,
|
fontFamily: _fontFamily,
|
||||||
primaryColor: primaryColor,
|
primaryColor: primaryColor,
|
||||||
scaffoldBackgroundColor: Colors.black,
|
scaffoldBackgroundColor: Colors.black,
|
||||||
dialogBackgroundColor: Colors.black,
|
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: _sliderTheme,
|
||||||
bottomSheetTheme: const BottomSheetThemeData(
|
bottomSheetTheme: const BottomSheetThemeData(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -524,7 +544,7 @@ class Settings {
|
|||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black))
|
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black), dialogTheme: DialogThemeData(backgroundColor: Colors.black))
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<String> getPath() async =>
|
Future<String> getPath() async =>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
|
|||||||
..primaryColor =
|
..primaryColor =
|
||||||
Settings._colorFromJson((json['primaryColor'] as num?)?.toInt())
|
Settings._colorFromJson((json['primaryColor'] as num?)?.toInt())
|
||||||
..useArtColor = json['useArtColor'] as bool? ?? false
|
..useArtColor = json['useArtColor'] as bool? ?? false
|
||||||
|
..appIcon = json['appIcon'] as String? ?? 'DefaultIcon'
|
||||||
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
|
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
|
||||||
..deezerCountry = json['deezerCountry'] as String? ?? 'US'
|
..deezerCountry = json['deezerCountry'] as String? ?? 'US'
|
||||||
..logListen = json['logListen'] as bool? ?? false
|
..logListen = json['logListen'] as bool? ?? false
|
||||||
@@ -121,6 +122,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
|||||||
'displayMode': instance.displayMode,
|
'displayMode': instance.displayMode,
|
||||||
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
||||||
'useArtColor': instance.useArtColor,
|
'useArtColor': instance.useArtColor,
|
||||||
|
'appIcon': instance.appIcon,
|
||||||
'deezerLanguage': instance.deezerLanguage,
|
'deezerLanguage': instance.deezerLanguage,
|
||||||
'deezerCountry': instance.deezerCountry,
|
'deezerCountry': instance.deezerCountry,
|
||||||
'logListen': instance.logListen,
|
'logListen': instance.logListen,
|
||||||
|
|||||||
@@ -62,21 +62,25 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
//BG Image
|
//BG Image
|
||||||
if (settings.blurPlayerBackground) {
|
if (settings.blurPlayerBackground) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_blurImage =
|
_blurImage = NetworkImage(
|
||||||
NetworkImage(audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri);
|
audioHandler.mediaItem.value?.extras?['thumb'] ??
|
||||||
|
audioHandler.mediaItem.value?.artUri);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Run in isolate
|
//Run in isolate
|
||||||
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(
|
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
|
||||||
audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri));
|
CachedNetworkImageProvider(
|
||||||
|
audioHandler.mediaItem.value?.extras?['thumb'] ??
|
||||||
|
audioHandler.mediaItem.value?.artUri));
|
||||||
|
|
||||||
//Update notification
|
//Update notification
|
||||||
if (settings.blurPlayerBackground) {
|
if (settings.blurPlayerBackground) {
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
statusBarColor: palette.dominantColor!.color.withOpacity(0.25),
|
statusBarColor: palette.dominantColor!.color.withOpacity(0.25),
|
||||||
systemNavigationBarColor:
|
systemNavigationBarColor: Color.alphaBlend(
|
||||||
Color.alphaBlend(palette.dominantColor!.color.withOpacity(0.25), scaffoldBackgroundColor)));
|
palette.dominantColor!.color.withOpacity(0.25),
|
||||||
|
scaffoldBackgroundColor)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Color gradient
|
//Color gradient
|
||||||
@@ -85,10 +89,16 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
statusBarColor: palette.dominantColor!.color.withOpacity(0.7),
|
statusBarColor: palette.dominantColor!.color.withOpacity(0.7),
|
||||||
));
|
));
|
||||||
setState(() => _bgGradient = LinearGradient(
|
setState(() => _bgGradient = LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [palette.dominantColor!.color.withOpacity(0.7), const Color.fromARGB(0, 0, 0, 0)],
|
colors: [
|
||||||
stops: const [0.0, 0.6]));
|
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();
|
_mediaItemSub?.cancel();
|
||||||
//Fix bottom buttons
|
//Fix bottom buttons
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color, statusBarColor: Colors.transparent));
|
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color,
|
||||||
|
statusBarColor: Colors.transparent));
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +131,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(gradient: settings.blurPlayerBackground ? null : _bgGradient),
|
decoration: BoxDecoration(
|
||||||
|
gradient:
|
||||||
|
settings.blurPlayerBackground ? null : _bgGradient),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (settings.blurPlayerBackground)
|
if (settings.blurPlayerBackground)
|
||||||
@@ -130,7 +143,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: _blurImage ?? const NetworkImage(''),
|
image: _blurImage ?? const NetworkImage(''),
|
||||||
fit: BoxFit.fill,
|
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(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||||
child: Container(color: Colors.transparent),
|
child: Container(color: Colors.transparent),
|
||||||
@@ -138,7 +153,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: StreamZip([audioHandler.playbackState, audioHandler.mediaItem]),
|
stream: StreamZip(
|
||||||
|
[audioHandler.playbackState, audioHandler.mediaItem]),
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
//When disconnected
|
//When disconnected
|
||||||
if (audioHandler.mediaItem.value == null) {
|
if (audioHandler.mediaItem.value == null) {
|
||||||
@@ -214,26 +230,45 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: ScreenUtil().setSp(50),
|
height: ScreenUtil().setSp(50),
|
||||||
child: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!.length >= 22
|
child: GetIt.I<AudioPlayerHandler>()
|
||||||
|
.mediaItem
|
||||||
|
.value!
|
||||||
|
.displayTitle!
|
||||||
|
.length >=
|
||||||
|
22
|
||||||
? Marquee(
|
? Marquee(
|
||||||
text: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
|
text: GetIt.I<AudioPlayerHandler>()
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
|
.mediaItem
|
||||||
|
.value!
|
||||||
|
.displayTitle!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: ScreenUtil().setSp(40),
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
blankSpace: 32.0,
|
blankSpace: 32.0,
|
||||||
startPadding: 10.0,
|
startPadding: 10.0,
|
||||||
accelerationDuration: const Duration(seconds: 1),
|
accelerationDuration: const Duration(seconds: 1),
|
||||||
pauseAfterRound: const Duration(seconds: 2),
|
pauseAfterRound: const Duration(seconds: 2),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
|
GetIt.I<AudioPlayerHandler>()
|
||||||
|
.mediaItem
|
||||||
|
.value!
|
||||||
|
.displayTitle!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: ScreenUtil().setSp(40),
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
)),
|
)),
|
||||||
Container(
|
Container(
|
||||||
height: 4,
|
height: 4,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displaySubtitle ?? '',
|
GetIt.I<AudioPlayerHandler>()
|
||||||
|
.mediaItem
|
||||||
|
.value!
|
||||||
|
.displaySubtitle ??
|
||||||
|
'',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
@@ -266,8 +301,11 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||||||
semanticLabel: 'Download'.i18n,
|
semanticLabel: 'Download'.i18n,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
Track t = Track.fromMediaItem(
|
||||||
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
|
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||||
|
if (await downloadManager.addOfflineTrack(t,
|
||||||
|
private: false, isSingleton: true) !=
|
||||||
|
false) {
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'Downloads added!'.i18n,
|
msg: 'Downloads added!'.i18n,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
@@ -328,26 +366,45 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: ScreenUtil().setSp(26),
|
height: ScreenUtil().setSp(26),
|
||||||
child: (GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '').length >= 26
|
child: (GetIt.I<AudioPlayerHandler>()
|
||||||
|
.mediaItem
|
||||||
|
.value
|
||||||
|
?.displayTitle ??
|
||||||
|
'')
|
||||||
|
.length >=
|
||||||
|
26
|
||||||
? Marquee(
|
? Marquee(
|
||||||
text: GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
|
text: GetIt.I<AudioPlayerHandler>()
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
|
.mediaItem
|
||||||
|
.value
|
||||||
|
?.displayTitle ??
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: ScreenUtil().setSp(22),
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
blankSpace: 32.0,
|
blankSpace: 32.0,
|
||||||
startPadding: 10.0,
|
startPadding: 10.0,
|
||||||
accelerationDuration: const Duration(seconds: 1),
|
accelerationDuration: const Duration(seconds: 1),
|
||||||
pauseAfterRound: const Duration(seconds: 2),
|
pauseAfterRound: const Duration(seconds: 2),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
|
GetIt.I<AudioPlayerHandler>()
|
||||||
|
.mediaItem
|
||||||
|
.value
|
||||||
|
?.displayTitle ??
|
||||||
|
'',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: ScreenUtil().setSp(22),
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
)),
|
)),
|
||||||
Container(
|
Container(
|
||||||
height: 4,
|
height: 4,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ?? '',
|
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ??
|
||||||
|
'',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
@@ -374,10 +431,15 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||||||
semanticLabel: 'Download'.i18n,
|
semanticLabel: 'Download'.i18n,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
Track t = Track.fromMediaItem(
|
||||||
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
|
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||||
|
if (await downloadManager.addOfflineTrack(t,
|
||||||
|
private: false, isSingleton: true) !=
|
||||||
|
false) {
|
||||||
Fluttertoast.showToast(
|
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
|
//Load data from native
|
||||||
void _load() async {
|
void _load() async {
|
||||||
if (audioHandler.mediaItem.value == null) return;
|
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
|
//N/A
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
if (mounted) setState(() => value = '');
|
if (mounted) setState(() => value = '');
|
||||||
@@ -449,7 +512,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
|||||||
return TextButton(
|
return TextButton(
|
||||||
child: Text(value),
|
child: Text(value),
|
||||||
onPressed: () {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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';
|
bool isEnabled = (track.lyrics?.id ?? '0') != '0';
|
||||||
|
|
||||||
return Opacity(
|
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(
|
child: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
//Icons.lyrics,
|
//Icons.lyrics,
|
||||||
@@ -491,10 +558,11 @@ class LyricsIconButton extends StatelessWidget {
|
|||||||
onPressed: isEnabled
|
onPressed: isEnabled
|
||||||
? () async {
|
? () async {
|
||||||
//Fix bottom buttons
|
//Fix bottom buttons
|
||||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent));
|
||||||
|
|
||||||
await Navigator.of(context)
|
await Navigator.of(context).push(MaterialPageRoute(
|
||||||
.push(MaterialPageRoute(builder: (context) => LyricsScreen(trackId: track.id!)));
|
builder: (context) => LyricsScreen(trackId: track.id!)));
|
||||||
|
|
||||||
if (afterOnPressed != null) {
|
if (afterOnPressed != null) {
|
||||||
afterOnPressed!();
|
afterOnPressed!();
|
||||||
@@ -519,16 +587,24 @@ class PlayerMenuButton extends StatelessWidget {
|
|||||||
semanticLabel: 'Options'.i18n,
|
semanticLabel: 'Options'.i18n,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
Track t =
|
||||||
|
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
|
||||||
MenuSheet m = MenuSheet(navigateCallback: () {
|
MenuSheet m = MenuSheet(navigateCallback: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
});
|
});
|
||||||
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] == null) {
|
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] ==
|
||||||
m.defaultTrackMenu(t, context: context, options: [m.sleepTimer(context), m.wakelock(context)]);
|
null) {
|
||||||
|
m.defaultTrackMenu(t,
|
||||||
|
context: context,
|
||||||
|
options: [m.sleepTimer(context), m.wakelock(context)]);
|
||||||
} else {
|
} else {
|
||||||
m.defaultShowEpisodeMenu(
|
m.defaultShowEpisodeMenu(
|
||||||
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'])),
|
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>()
|
||||||
ShowEpisode.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!),
|
.mediaItem
|
||||||
|
.value!
|
||||||
|
.extras?['show'])),
|
||||||
|
ShowEpisode.fromMediaItem(
|
||||||
|
GetIt.I<AudioPlayerHandler>().mediaItem.value!),
|
||||||
context: context,
|
context: context,
|
||||||
options: [m.sleepTimer(context), m.wakelock(context)]);
|
options: [m.sleepTimer(context), m.wakelock(context)]);
|
||||||
}
|
}
|
||||||
@@ -594,7 +670,8 @@ class PlaybackControls extends StatefulWidget {
|
|||||||
class _PlaybackControlsState extends State<PlaybackControls> {
|
class _PlaybackControlsState extends State<PlaybackControls> {
|
||||||
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
|
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
|
||||||
Icon get libraryIcon {
|
Icon get libraryIcon {
|
||||||
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
if (cache.checkTrackFavorite(
|
||||||
|
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.favorite,
|
Icons.favorite,
|
||||||
size: widget.iconSize * 0.44,
|
size: widget.iconSize * 0.44,
|
||||||
@@ -636,15 +713,20 @@ class _PlaybackControlsState extends State<PlaybackControls> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
cache.libraryTracks ??= [];
|
cache.libraryTracks ??= [];
|
||||||
|
|
||||||
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
if (cache.checkTrackFavorite(
|
||||||
|
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||||
//Remove from library
|
//Remove from library
|
||||||
setState(() => cache.libraryTracks?.remove(audioHandler.mediaItem.value!.id));
|
setState(() => cache.libraryTracks
|
||||||
await deezerAPI.removeFavorite(audioHandler.mediaItem.value!.id);
|
?.remove(audioHandler.mediaItem.value!.id));
|
||||||
|
await deezerAPI
|
||||||
|
.removeFavorite(audioHandler.mediaItem.value!.id);
|
||||||
await cache.save();
|
await cache.save();
|
||||||
} else {
|
} else {
|
||||||
//Add
|
//Add
|
||||||
setState(() => cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
|
setState(() =>
|
||||||
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
|
||||||
|
await deezerAPI
|
||||||
|
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
||||||
await cache.save();
|
await cache.save();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -679,7 +761,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
_imageList = _getImageList(audioHandler.queue.value);
|
_imageList = _getImageList(audioHandler.queue.value);
|
||||||
|
|
||||||
_currentItemAndQueueSub = Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
|
_currentItemAndQueueSub =
|
||||||
|
Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
|
||||||
audioHandler.mediaItem,
|
audioHandler.mediaItem,
|
||||||
audioHandler.queue,
|
audioHandler.queue,
|
||||||
(mediaItem, queue) {
|
(mediaItem, queue) {
|
||||||
@@ -698,7 +781,9 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<ZoomableImage> _getImageList(List<MediaItem> queue) {
|
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) {
|
bool _didQueueChange(List<MediaItem> newQueue) {
|
||||||
@@ -718,7 +803,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
void _handleMediaItemChange(MediaItem? item) async {
|
void _handleMediaItemChange(MediaItem? item) async {
|
||||||
final targetItemId = item?.id ?? '';
|
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;
|
if (targetPage == -1) return;
|
||||||
|
|
||||||
// No need to animating to the same page
|
// No need to animating to the same page
|
||||||
@@ -799,7 +885,8 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||||||
final double? textWidth;
|
final double? textWidth;
|
||||||
final bool? short;
|
final bool? short;
|
||||||
final GlobalKey iconButtonKey = GlobalKey();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -825,7 +912,9 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
(short ?? false)
|
(short ?? false)
|
||||||
? (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? '')
|
? (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? '')
|
||||||
: 'Playing from:'.i18n + ' ' + (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
|
: 'Playing from:'.i18n +
|
||||||
|
' ' +
|
||||||
|
(GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
@@ -844,11 +933,14 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||||||
splashRadius: iconSize ?? ScreenUtil().setWidth(52),
|
splashRadius: iconSize ?? ScreenUtil().setWidth(52),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
//Fix bottom buttons (Not needed anymore?)
|
//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
|
// Calculate the center of the icon
|
||||||
final RenderBox buttonRenderBox = iconButtonKey.currentContext!.findRenderObject() as RenderBox;
|
final RenderBox buttonRenderBox =
|
||||||
final Offset buttonOffset = buttonRenderBox.localToGlobal(buttonRenderBox.size.center(Offset.zero));
|
iconButtonKey.currentContext!.findRenderObject() as RenderBox;
|
||||||
|
final Offset buttonOffset = buttonRenderBox
|
||||||
|
.localToGlobal(buttonRenderBox.size.center(Offset.zero));
|
||||||
//Navigate
|
//Navigate
|
||||||
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
|
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
|
||||||
await Navigator.of(context).push(CircularExpansionRoute(
|
await Navigator.of(context).push(CircularExpansionRoute(
|
||||||
@@ -879,7 +971,8 @@ class _SeekBarState extends State<SeekBar> {
|
|||||||
|
|
||||||
double get position {
|
double get position {
|
||||||
if (_seeking) return _pos;
|
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;
|
if (p > duration) return duration;
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -904,17 +997,20 @@ class _SeekBarState extends State<SeekBar> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
_timeString(position),
|
_timeString(position),
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
style: TextStyle(
|
||||||
|
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_timeString(duration),
|
_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(
|
child: Slider(
|
||||||
focusNode: FocusNode(
|
focusNode: FocusNode(
|
||||||
canRequestFocus: false,
|
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,
|
value: position,
|
||||||
max: duration,
|
max: duration,
|
||||||
onChangeStart: (double d) {
|
onChangeStart: (double d) {
|
||||||
@@ -1003,7 +1100,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final queueState = audioHandler.queueState;
|
final queueState = audioHandler.queueState;
|
||||||
final shuffleModeEnabled = queueState.shuffleMode == AudioServiceShuffleMode.all;
|
final shuffleModeEnabled =
|
||||||
|
queueState.shuffleMode == AudioServiceShuffleMode.all;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: FreezerAppBar(
|
appBar: FreezerAppBar(
|
||||||
@@ -1016,7 +1114,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
|||||||
//cons.shuffle,
|
//cons.shuffle,
|
||||||
ReFreezerIcons.shuffle,
|
ReFreezerIcons.shuffle,
|
||||||
semanticLabel: 'Shuffle'.i18n,
|
semanticLabel: 'Shuffle'.i18n,
|
||||||
color: shuffleModeEnabled ? Theme.of(context).primaryColor : null,
|
color:
|
||||||
|
shuffleModeEnabled ? Theme.of(context).primaryColor : null,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await audioHandler.toggleShuffle();
|
await audioHandler.toggleShuffle();
|
||||||
@@ -1032,7 +1131,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await audioHandler.clearQueue();
|
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);
|
await audioHandler.skipToQueueItem(index);
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
key: Key(mediaItem.id),
|
key: Key('${mediaItem.id}_$index'),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
|
|||||||
@@ -424,8 +424,6 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
},
|
},
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
default:
|
|
||||||
return Container();
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -56,33 +56,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('General'.i18n),
|
title: Text('General'.i18n),
|
||||||
leading: const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
leading:
|
||||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const GeneralSettings())),
|
const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
||||||
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => const GeneralSettings())),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Download Settings'.i18n),
|
title: Text('Download Settings'.i18n),
|
||||||
leading: const LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
|
leading: const LeadingIcon(Icons.cloud_download,
|
||||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadsSettings())),
|
color: Color(0xffbe3266)),
|
||||||
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => const DownloadsSettings())),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Appearance'.i18n),
|
title: Text('Appearance'.i18n),
|
||||||
leading: const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
leading:
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const AppearanceSettings())),
|
const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const AppearanceSettings())),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Quality'.i18n),
|
title: Text('Quality'.i18n),
|
||||||
leading: const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
leading:
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const QualitySettings())),
|
const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const QualitySettings())),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Deezer'.i18n),
|
title: Text('Deezer'.i18n),
|
||||||
leading: const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
leading:
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const DeezerSettings())),
|
const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const DeezerSettings())),
|
||||||
),
|
),
|
||||||
//Language select
|
//Language select
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Language'.i18n),
|
title: Text('Language'.i18n),
|
||||||
leading: const LeadingIcon(Icons.language, color: Color(0xff009a85)),
|
leading:
|
||||||
|
const LeadingIcon(Icons.language, color: Color(0xff009a85)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -94,8 +111,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
title: Text(l.name),
|
title: Text(l.name),
|
||||||
subtitle: Text('${l.locale}-${l.country}'),
|
subtitle: Text('${l.locale}-${l.country}'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
I18n.of(customNavigatorKey.currentContext!).locale = Locale(l.locale, l.country);
|
I18n.of(customNavigatorKey.currentContext!).locale =
|
||||||
setState(() => settings.language = '${l.locale}_${l.country}');
|
Locale(l.locale, l.country);
|
||||||
|
setState(() =>
|
||||||
|
settings.language = '${l.locale}_${l.country}');
|
||||||
await settings.save();
|
await settings.save();
|
||||||
// Close the SimpleDialog
|
// Close the SimpleDialog
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
@@ -107,12 +126,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Updates'.i18n),
|
title: Text('Updates'.i18n),
|
||||||
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
|
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(
|
ListTile(
|
||||||
title: Text('About'.i18n),
|
title: Text('About'.i18n),
|
||||||
leading: const LeadingIcon(Icons.info, color: Colors.grey),
|
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>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Theme'.i18n),
|
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),
|
leading: const Icon(Icons.color_lens),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -201,12 +223,29 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
leading: const Icon(Icons.android)),
|
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(
|
ListTile(
|
||||||
title: Text('Font'.i18n),
|
title: Text('Font'.i18n),
|
||||||
leading: const Icon(Icons.font_download),
|
leading: const Icon(Icons.font_download),
|
||||||
subtitle: Text(settings.font),
|
subtitle: Text(settings.font),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(context: context, builder: (context) => FontSelector(() => Navigator.of(context).pop()));
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
FontSelector(() => Navigator.of(context).pop()));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -234,7 +273,9 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Visualizer'.i18n),
|
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),
|
leading: const Icon(Icons.equalizer),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
value: settings.lyricsVisualizer,
|
value: settings.lyricsVisualizer,
|
||||||
@@ -320,7 +361,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
settings.displayMode = i;
|
settings.displayMode = i;
|
||||||
await settings.save();
|
await settings.save();
|
||||||
await FlutterDisplayMode.setPreferredMode(modes[i]);
|
await FlutterDisplayMode.setPreferredMode(
|
||||||
|
modes[i]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
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 {
|
class FontSelector extends StatefulWidget {
|
||||||
final Function callback;
|
final Function callback;
|
||||||
|
|
||||||
@@ -347,7 +448,9 @@ class FontSelector extends StatefulWidget {
|
|||||||
class _FontSelectorState extends State<FontSelector> {
|
class _FontSelectorState extends State<FontSelector> {
|
||||||
String query = '';
|
String query = '';
|
||||||
List<String> get fonts {
|
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
|
//Font selected
|
||||||
@@ -421,25 +524,29 @@ class _QualitySettingsState extends State<QualitySettings> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Mobile streaming'.i18n),
|
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 QualityPicker('mobile'),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Wifi streaming'.i18n),
|
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 QualityPicker('wifi'),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Offline'.i18n),
|
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 QualityPicker('offline'),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('External downloads'.i18n),
|
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'),
|
const QualityPicker('download'),
|
||||||
],
|
],
|
||||||
@@ -608,7 +715,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Content language'.i18n),
|
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),
|
leading: const Icon(Icons.language),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -621,7 +729,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||||||
title: Text(ContentLanguage.all[i].name),
|
title: Text(ContentLanguage.all[i].name),
|
||||||
subtitle: Text(ContentLanguage.all[i].code),
|
subtitle: Text(ContentLanguage.all[i].code),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() => settings.deezerLanguage = ContentLanguage.all[i].code);
|
setState(() => settings.deezerLanguage =
|
||||||
|
ContentLanguage.all[i].code);
|
||||||
await settings.save();
|
await settings.save();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -633,7 +742,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Content country'.i18n),
|
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),
|
leading: const Icon(Icons.vpn_lock),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -655,7 +765,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onValuePicked: (Country country) {
|
onValuePicked: (Country country) {
|
||||||
setState(() => settings.deezerCountry = country.isoCode ?? 'us');
|
setState(() =>
|
||||||
|
settings.deezerCountry = country.isoCode ?? 'us');
|
||||||
settings.save();
|
settings.save();
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
@@ -663,7 +774,9 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Log tracks'.i18n),
|
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(
|
trailing: Switch(
|
||||||
value: settings.logListen,
|
value: settings.logListen,
|
||||||
onChanged: (bool v) {
|
onChanged: (bool v) {
|
||||||
@@ -764,7 +877,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
|||||||
Text(
|
Text(
|
||||||
'Valid variables are'.i18n +
|
'Valid variables are'.i18n +
|
||||||
': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' +
|
': %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(
|
style: const TextStyle(
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
),
|
),
|
||||||
@@ -779,7 +893,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Reset'.i18n),
|
child: Text('Reset'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
|
_controller.value =
|
||||||
|
_controller.value.copyWith(text: '%artist% - %title%');
|
||||||
_new = '%artist% - %title%';
|
_new = '%artist% - %title%';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -808,7 +923,8 @@ class DownloadsSettings extends StatefulWidget {
|
|||||||
|
|
||||||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||||
double _downloadThreads = settings.downloadThreads.toDouble();
|
double _downloadThreads = settings.downloadThreads.toDouble();
|
||||||
final TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator);
|
final TextEditingController _artistSeparatorController =
|
||||||
|
TextEditingController(text: settings.artistSeparator);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -852,7 +968,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return FilenameTemplateDialog(settings.downloadFilename, (f) async {
|
return FilenameTemplateDialog(settings.downloadFilename,
|
||||||
|
(f) async {
|
||||||
setState(() => settings.downloadFilename = f);
|
setState(() => settings.downloadFilename = f);
|
||||||
await settings.save();
|
await settings.save();
|
||||||
});
|
});
|
||||||
@@ -861,13 +978,15 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Singleton naming'.i18n),
|
title: Text('Singleton naming'.i18n),
|
||||||
subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'),
|
subtitle:
|
||||||
|
Text('Currently'.i18n + ': ${settings.singletonFilename}'),
|
||||||
leading: const Icon(Icons.text_format),
|
leading: const Icon(Icons.text_format),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return FilenameTemplateDialog(settings.singletonFilename, (f) async {
|
return FilenameTemplateDialog(settings.singletonFilename,
|
||||||
|
(f) async {
|
||||||
setState(() => settings.singletonFilename = f);
|
setState(() => settings.singletonFilename = f);
|
||||||
await settings.save();
|
await settings.save();
|
||||||
});
|
});
|
||||||
@@ -877,7 +996,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Download threads'.i18n + ': ${_downloadThreads.round().toString()}',
|
'Download threads'.i18n +
|
||||||
|
': ${_downloadThreads.round().toString()}',
|
||||||
style: const TextStyle(fontSize: 16.0),
|
style: const TextStyle(fontSize: 16.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -897,14 +1017,17 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
await settings.save();
|
await settings.save();
|
||||||
|
|
||||||
//Prevent null
|
//Prevent null
|
||||||
if (val > 8 && cache.threadsWarning != true && context.mounted) {
|
if (val > 8 &&
|
||||||
|
cache.threadsWarning != true &&
|
||||||
|
context.mounted) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Warning'.i18n),
|
title: Text('Warning'.i18n),
|
||||||
content: Text(
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Dismiss'.i18n),
|
child: Text('Dismiss'.i18n),
|
||||||
@@ -922,8 +1045,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Tags'.i18n),
|
title: Text('Tags'.i18n),
|
||||||
leading: const Icon(Icons.label),
|
leading: const Icon(Icons.label),
|
||||||
onTap: () =>
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TagSelectionScreen())),
|
builder: (context) => const TagSelectionScreen())),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Create folders for artist'.i18n),
|
title: Text('Create folders for artist'.i18n),
|
||||||
@@ -1010,17 +1133,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
leading: const Icon(Icons.image)),
|
leading: const Icon(Icons.image)),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Album cover resolution'.i18n),
|
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),
|
leading: const Icon(Icons.image),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
width: 75.0,
|
width: 75.0,
|
||||||
child: DropdownButton<int>(
|
child: DropdownButton<int>(
|
||||||
value: settings.albumArtResolution,
|
value: settings.albumArtResolution,
|
||||||
items: [400, 800, 1000, 1200, 1400, 1600, 1800]
|
items: [400, 800, 1000, 1200, 1400, 1600, 1800]
|
||||||
.map<DropdownMenuItem<int>>((int i) => DropdownMenuItem<int>(
|
.map<DropdownMenuItem<int>>(
|
||||||
value: i,
|
(int i) => DropdownMenuItem<int>(
|
||||||
child: Text(i.toString()),
|
value: i,
|
||||||
))
|
child: Text(i.toString()),
|
||||||
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (int? n) async {
|
onChanged: (int? n) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -1031,7 +1157,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
))),
|
))),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Create .nomedia files'.i18n),
|
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(
|
trailing: Switch(
|
||||||
value: settings.nomediaFiles,
|
value: settings.nomediaFiles,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
@@ -1058,7 +1185,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Download Log'.i18n),
|
title: Text('Download Log'.i18n),
|
||||||
leading: const Icon(Icons.sticky_note_2),
|
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);
|
setState(() => settings.offlineMode = false);
|
||||||
} else {
|
} else {
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'Error logging in, check your internet connections.'.i18n,
|
msg:
|
||||||
|
'Error logging in, check your internet connections.'
|
||||||
|
.i18n,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
toastLength: Toast.LENGTH_SHORT);
|
toastLength: Toast.LENGTH_SHORT);
|
||||||
}
|
}
|
||||||
@@ -1179,7 +1309,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Copy ARL'.i18n),
|
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),
|
leading: const Icon(Icons.lock),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await FlutterClipboard.copy(settings.arl ?? '');
|
await FlutterClipboard.copy(settings.arl ?? '');
|
||||||
@@ -1190,7 +1321,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Enable equalizer'.i18n),
|
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),
|
leading: const Icon(Icons.equalizer),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
value: settings.enableEqualizer,
|
value: settings.enableEqualizer,
|
||||||
@@ -1202,7 +1335,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('LastFM'.i18n),
|
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),
|
leading: const Icon(FontAwesome5.lastfm),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (settings.lastFMUsername != null) {
|
if (settings.lastFMUsername != null) {
|
||||||
@@ -1241,8 +1376,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Application Log'.i18n),
|
title: Text('Application Log'.i18n),
|
||||||
leading: const Icon(Icons.sticky_note_2),
|
leading: const Icon(Icons.sticky_note_2),
|
||||||
onTap: () =>
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ApplicationLogViewer())),
|
builder: (context) => const ApplicationLogViewer())),
|
||||||
),
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -1341,7 +1476,10 @@ class _LastFMLoginState extends State<LastFMLogin> {
|
|||||||
LastFM last;
|
LastFM last;
|
||||||
try {
|
try {
|
||||||
last = await LastFM.authenticate(
|
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) {
|
} catch (e) {
|
||||||
Logger.root.severe('Error authorizing LastFM: $e');
|
Logger.root.severe('Error authorizing LastFM: $e');
|
||||||
Fluttertoast.showToast(msg: 'Authorization error!'.i18n);
|
Fluttertoast.showToast(msg: 'Authorization error!'.i18n);
|
||||||
@@ -1365,23 +1503,30 @@ class StorageInfo {
|
|||||||
final String appFilesDir;
|
final String appFilesDir;
|
||||||
final int availableBytes;
|
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 {
|
Future<List<StorageInfo>> getStorageInfo() async {
|
||||||
final externalDirectories = await ExternalPath.getExternalStorageDirectories();
|
final externalDirectories =
|
||||||
|
await ExternalPath.getExternalStorageDirectories();
|
||||||
|
|
||||||
List<StorageInfo> storageInfoList = [];
|
List<StorageInfo> storageInfoList = [];
|
||||||
|
|
||||||
if (externalDirectories.isNotEmpty) {
|
if (externalDirectories.isNotEmpty) {
|
||||||
for (var dir in externalDirectories) {
|
for (var dir in externalDirectories) {
|
||||||
var availableMegaBytes = (await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
|
var availableMegaBytes =
|
||||||
|
(await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
|
||||||
|
|
||||||
storageInfoList.add(
|
storageInfoList.add(
|
||||||
StorageInfo(
|
StorageInfo(
|
||||||
rootDir: dir,
|
rootDir: dir,
|
||||||
appFilesDir: 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),
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[CircularProgressIndicator()],
|
children: <Widget>[
|
||||||
|
CircularProgressIndicator()
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
...List.generate(snapshot.data?.length ?? 0, (i) {
|
...List.generate(snapshot.data?.length ?? 0,
|
||||||
|
(i) {
|
||||||
StorageInfo si = snapshot.data![i];
|
StorageInfo si = snapshot.data![i];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(si.rootDir),
|
title: Text(si.rootDir),
|
||||||
@@ -1524,7 +1672,9 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_root == _path) {
|
if (_root == _path) {
|
||||||
Fluttertoast.showToast(msg: 'Permission denied'.i18n, gravity: ToastGravity.BOTTOM);
|
Fluttertoast.showToast(
|
||||||
|
msg: 'Permission denied'.i18n,
|
||||||
|
gravity: ToastGravity.BOTTOM);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_previous = _path;
|
_previous = _path;
|
||||||
@@ -1618,8 +1768,10 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||||||
),
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
const ListTile(
|
const ListTile(
|
||||||
title: Text('DJDoubleD', style: TextStyle(fontWeight: FontWeight.bold)),
|
title: Text('DJDoubleD',
|
||||||
subtitle: Text('Developer, tester, new icon & logo, some translations, ...'),
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
subtitle: Text(
|
||||||
|
'Developer, tester, new icon & logo, some translations, ...'),
|
||||||
),
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
/*ListTile(
|
/*ListTile(
|
||||||
@@ -1657,7 +1809,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Crowdin'.i18n),
|
title: Text('Crowdin'.i18n),
|
||||||
subtitle: Text('Help translating this app on 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: () {
|
onTap: () {
|
||||||
launchUrlString('https://crowdin.com/project/refreezer');
|
launchUrlString('https://crowdin.com/project/refreezer');
|
||||||
},
|
},
|
||||||
@@ -1665,15 +1818,20 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
title: Text('Donate'.i18n),
|
title: Text('Donate'.i18n),
|
||||||
subtitle: Text('You should rather support your favorite artists, instead of this app!'.i18n),
|
subtitle: Text(
|
||||||
leading: const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
'You should rather support your favorite artists, instead of this app!'
|
||||||
|
.i18n),
|
||||||
|
leading:
|
||||||
|
const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Donate'.i18n),
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('OK'),
|
child: const Text('OK'),
|
||||||
@@ -1704,10 +1862,12 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
|
Image.asset('assets/icon_legacy.png',
|
||||||
|
width: 24, height: 24),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'The original freezer development team'.i18n,
|
'The original freezer development team'.i18n,
|
||||||
textAlign: TextAlign.center,
|
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(
|
const ListTile(
|
||||||
title: Text('Bas Curtiz'),
|
title: Text('Bas Curtiz'),
|
||||||
subtitle: Text('Icon, logo, banner, design suggestions, tester'),
|
subtitle:
|
||||||
|
Text('Icon, logo, banner, design suggestions, tester'),
|
||||||
),
|
),
|
||||||
const ListTile(
|
const ListTile(
|
||||||
title: Text('Tobs'),
|
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:
|
move_to_background:
|
||||||
path: ./move_to_background
|
path: ./move_to_background
|
||||||
numberpicker: ^2.1.2
|
numberpicker: ^2.1.2
|
||||||
# Awaiting release with fix for flutter 3.29.0:
|
|
||||||
#open_filex: ^4.6.0
|
|
||||||
open_filex: ^4.7.0
|
open_filex: ^4.7.0
|
||||||
package_info_plus: ^8.0.0
|
package_info_plus: ^8.0.0
|
||||||
palette_generator: ^0.3.3+3
|
palette_generator: ^0.3.3+3
|
||||||
@@ -156,6 +154,9 @@ flutter:
|
|||||||
- assets/icon.png
|
- assets/icon.png
|
||||||
- assets/favorites_thumb.jpg
|
- assets/favorites_thumb.jpg
|
||||||
- assets/browse_icon.png
|
- assets/browse_icon.png
|
||||||
|
- assets/icons/DefaultIcon.png
|
||||||
|
- assets/icons/CatIcon.png
|
||||||
|
- assets/icons/DeezerBlueIcon.png
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
# - family: Montserrat
|
# - family: Montserrat
|
||||||
|
|||||||
@@ -353,5 +353,7 @@
|
|||||||
"Permission denied, download canceled!": "Permission denied, download canceled!",
|
"Permission denied, download canceled!": "Permission denied, download canceled!",
|
||||||
"Crowdin": "Crowdin",
|
"Crowdin": "Crowdin",
|
||||||
"Help translating this app on Crowdin!": "Help translating this app on 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!"
|
||||||
}
|
}
|
||||||
|
|||||||