Implement App Icon selection

- added 2 additional app icons to choose from
- added translation string for settings screen
- smaller changes + formatting some touched files
This commit is contained in:
DJDoubleD
2025-06-11 20:03:47 +02:00
parent a6403ea638
commit 2bb8009bda
51 changed files with 982 additions and 226 deletions

View File

@@ -27,6 +27,7 @@
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"
android:required="false"
android:minSdkVersion="30"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-feature
android:name="android.software.leanback"
@@ -38,7 +39,10 @@
<application
android:name="${applicationName}"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher_round"
android:logo="@mipmap/ic_launcher"
android:banner="@mipmap/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="ReFreezer"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
@@ -59,7 +63,6 @@
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:banner="@mipmap/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:logo="@mipmap/ic_launcher"
android:windowSoftInputMode="adjustResize"
android:exported = "true">
@@ -76,10 +79,10 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!--<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />-->
</intent-filter>
<!-- Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -113,12 +116,70 @@
android:scheme="https"
android:host="www.deezer.page.link" />
</intent-filter>
<!-- New short domain -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="dzr.page.link" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="www.dzr.page.link" />
</intent-filter>
</activity>
<activity-alias
android:name="r.r.refreezer.DefaultIconActivity"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:targetActivity="r.r.refreezer.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name="r.r.refreezer.CatIconActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_cat"
android:roundIcon="@mipmap/ic_launcher_cat_round"
android:targetActivity="r.r.refreezer.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name="r.r.refreezer.DeezerBlueIconActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_deezer_blue"
android:roundIcon="@mipmap/ic_launcher_deezer_blue_round"
android:targetActivity="r.r.refreezer.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity-alias>
<!--
Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View 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;
}
}
}

View File

@@ -63,6 +63,13 @@ public class MainActivity extends AudioServiceActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
// Initialize ChangeIconPlugin
ChangeIconPlugin changeIconPlugin = new ChangeIconPlugin(this);
changeIconPlugin.initWith(flutterEngine.getDartExecutor().getBinaryMessenger());
changeIconPlugin.tryFixLauncherIconIfNeeded();
//Flutter method channel
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(((call, result) -> {

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground"/>
</adaptive-icon>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_mono_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_cat_background">#1C1C14</color>
</resources>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/icons/CatIcon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -3888,7 +3888,9 @@ const crowdin = {
'Crowdin': 'Crowdin',
'Help translating this app on Crowdin!':
'Aidez-nous à traduire cette application sur Crowdin !',
'Allow screen to turn off': "Permettre à l'écran de s'éteindre"
'Allow screen to turn off': "Permettre à l'écran de s'éteindre",
'Selecting a new icon will exit the app to apply the change!':
"Sélectionner une nouvelle icône quittera l'application pour appliquer la modification !"
},
'he-IL': {
'Home': 'מסך הבית',
@@ -6850,7 +6852,9 @@ const crowdin = {
'Toestemming geweigerd, download geannuleerd!',
'Help translating this app on Crowdin!':
'Help deze app te vertalen op Crowdin!',
'Allow screen to turn off': 'Scherm uitschakelen toestaan'
'Allow screen to turn off': 'Scherm uitschakelen toestaan',
'Selecting a new icon will exit the app to apply the change!':
'Het selecteren van een nieuw icoon zal de app afsluiten om de wijziging toe te passen!'
},
'pl-PL': {
'Home': 'Strona główna',

View File

@@ -454,5 +454,9 @@ const language_en_us = {
// 0.7.15
'Allow screen to turn off': 'Allow screen to turn off',
// 0.7.16
'Selecting a new icon will exit the app to apply the change!':
'Selecting a new icon will exit the app to apply the change!',
}
};

View File

@@ -39,7 +39,8 @@ Future<AudioPlayerHandler> initAudioService() async {
);
}
class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
class AudioPlayerHandler extends BaseAudioHandler
with QueueHandler, SeekHandler {
AudioPlayerHandler() {
_init();
}
@@ -48,7 +49,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
int? _prevAudioSession;
bool _equalizerOpen = false;
final AndroidAuto _androidAuto = AndroidAuto(); // Create an instance of AndroidAuto
final AndroidAuto _androidAuto =
AndroidAuto(); // Create an instance of AndroidAuto
// for some reason, dart can decide not to respect the 'await' due to weird task sceduling ...
final Completer<void> _playerInitializedCompleter = Completer<void>();
@@ -70,7 +72,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
QueueSource? queueSource;
StreamSubscription? _queueStateSub;
StreamSubscription? _mediaItemSub;
final BehaviorSubject<QueueState> _queueStateSubject = BehaviorSubject<QueueState>();
final BehaviorSubject<QueueState> _queueStateSubject =
BehaviorSubject<QueueState>();
Stream<QueueState> get queueStateStream => _queueStateSubject.stream;
QueueState get queueState => _queueStateSubject.value;
int currentIndex = 0;
@@ -85,13 +88,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
_player.sequenceStateStream
.map((state) {
try {
return state?.effectiveSequence.map((source) => source.tag as MediaItem).toList();
return state?.effectiveSequence
.map((source) => source.tag as MediaItem)
.toList();
} catch (e) {
if (e is RangeError) {
// This is caused by just_audio not updating the currentIndex first in the _broadcastSequence method.
// Because in shufflemode it's out of range after removing items from the playlist.
// Might be fixed in future
Logger.root.severe('RangeError occurred while accessing effectiveSequence: $e');
Logger.root.severe(
'RangeError occurred while accessing effectiveSequence: $e');
// Return null to indicate that the queue could/should not be broadcasted
return null;
}
@@ -103,20 +109,25 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
.pipe(queue);
// Update current QueueState
_queueStateSub = Rx.combineLatest3<List<MediaItem>, PlaybackState, List<int>, QueueState>(
_queueStateSub = Rx.combineLatest3<List<MediaItem>, PlaybackState,
List<int>, QueueState>(
queue,
playbackState,
_player.shuffleIndicesStream.whereType<List<int>>(),
(queue, playbackState, shuffleIndices) => QueueState(
queue,
playbackState.queueIndex,
playbackState.shuffleMode == AudioServiceShuffleMode.all ? shuffleIndices : null,
playbackState.shuffleMode == AudioServiceShuffleMode.all
? shuffleIndices
: null,
playbackState.repeatMode,
playbackState.shuffleMode,
),
)
.where(
(state) => state.shuffleIndices == null || state.queue.length == state.shuffleIndices!.length,
(state) =>
state.shuffleIndices == null ||
state.queue.length == state.shuffleIndices!.length,
)
.distinct()
.listen(_queueStateSubject.add);
@@ -124,7 +135,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
// Broadcast media item changes after track or position in queue change,
// only emit value when different from previous item
_mediaItemSub = Rx.combineLatest3<int?, List<MediaItem>, bool, MediaItem?>(
_player.currentIndexStream, queue, _player.shuffleModeEnabledStream, (index, queue, shuffleModeEnabled) {
_player.currentIndexStream, queue, _player.shuffleModeEnabledStream,
(index, queue, shuffleModeEnabled) {
// Don't broadcast while shuffling to avoid intermediate MediaItem change
if (_rearranging) return null;
@@ -155,11 +167,14 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
});
// Propagate all events from the audio player to AudioService clients.
_player.playbackEventStream.listen(_broadcastState, onError: _playbackError);
_player.playbackEventStream
.listen(_broadcastState, onError: _playbackError);
_player.shuffleModeEnabledStream.listen((enabled) => _broadcastState(_player.playbackEvent));
_player.shuffleModeEnabledStream
.listen((enabled) => _broadcastState(_player.playbackEvent));
_player.loopModeStream.listen((mode) => _broadcastState(_player.playbackEvent));
_player.loopModeStream
.listen((mode) => _broadcastState(_player.playbackEvent));
_player.processingStateStream.listen((state) {
if (state == ProcessingState.completed && _player.playing) {
@@ -225,7 +240,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
@override
Future<void> playFromMediaId(String mediaId, [Map<String, dynamic>? extras]) async {
Future<void> playFromMediaId(String mediaId,
[Map<String, dynamic>? extras]) async {
// Check if the mediaId is for Android Auto
if (mediaId.startsWith(AndroidAuto.prefix)) {
// Forward the event to Android Auto
@@ -238,7 +254,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
if (index != -1) {
_player.seek(
Duration.zero,
index: _player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
index:
_player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
);
} else {
Logger.root.severe('playFromMediaId: MediaItem not found');
@@ -252,11 +269,12 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
@override
Future<void> stop() async {
// Save queue before stopping player to save player details
Logger.root.info('saving queue');
_saveQueueToFile();
Logger.root.info('stopping player');
await _player.stop();
await super.stop();
Logger.root.info('saving queue');
_saveQueueToFile();
}
@override
@@ -283,10 +301,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
@override
Future<void> updateQueue(List<MediaItem> newQueue) async {
Future<void> updateQueue(List<MediaItem> queue) async {
await _playlist.clear();
if (newQueue.isNotEmpty) {
await _playlist.addAll(await _itemsToSources(newQueue));
if (queue.isNotEmpty) {
await _playlist.addAll(await _itemsToSources(queue));
} else {
if (mediaItem.hasValue) {
mediaItem.add(null);
@@ -343,7 +361,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
_player.seek(
Duration.zero,
index: _player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
index:
_player.shuffleModeEnabled ? _player.shuffleIndices![index] : index,
);
}
@@ -391,6 +410,33 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
// Start internal methods native to AudioHandler
//----------------------------------------------
/// Wait for the player to be in a ready state before performing operations
Future<void> _waitForPlayerReadiness() async {
if (_player.processingState == ProcessingState.ready) {
return;
}
Completer<void> readyCompleter = Completer<void>();
late StreamSubscription subscription;
subscription = _player.processingStateStream.listen((state) {
if (state == ProcessingState.ready) {
if (!readyCompleter.isCompleted) {
readyCompleter.complete();
}
subscription.cancel();
}
});
return readyCompleter.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
subscription.cancel();
Logger.root.warning('Timed out waiting for player to be ready');
},
);
}
Future<void> _startSession() async {
Logger.root.info('starting audio service...');
final session = await AudioSession.instance;
@@ -406,13 +452,15 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
_player = AudioPlayer();
}
_loadEmptyPlaylist().then((_) => Logger.root.info('audio player initialized!'));
_loadEmptyPlaylist()
.then((_) => Logger.root.info('audio player initialized!'));
}
/// Broadcasts the current state to all clients.
void _broadcastState(PlaybackEvent event) {
final playing = _player.playing;
currentIndex = _getQueueIndex(_player.currentIndex ?? 0, shuffleModeEnabled: _player.shuffleModeEnabled);
currentIndex = _getQueueIndex(_player.currentIndex ?? 0,
shuffleModeEnabled: _player.shuffleModeEnabled);
playbackState.add(
playbackState.value.copyWith(
@@ -421,9 +469,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
// Custom Stop
const MediaControl(androidIcon: 'drawable/ic_action_stop', label: 'stop', action: MediaAction.stop),
const MediaControl(
androidIcon: 'drawable/ic_action_stop',
label: 'stop',
action: MediaAction.stop),
],
systemActions: const {MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward},
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward
},
androidCompactActionIndices: const [0, 1, 2],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
@@ -478,7 +533,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
Future _getTrackUrl(MediaItem mediaItem) async {
//Check if offline
String offlinePath = p.join((await getExternalStorageDirectory())!.path, 'offline/');
String offlinePath =
p.join((await getExternalStorageDirectory())!.path, 'offline/');
File f = File(p.join(offlinePath, mediaItem.id));
if (await f.exists()) {
//return f.path;
@@ -495,14 +551,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
//This just returns fake url that contains metadata
int quality = await getStreamQuality();
List? streamPlaybackDetails = jsonDecode(mediaItem.extras?['playbackDetails']);
List? streamPlaybackDetails =
jsonDecode(mediaItem.extras?['playbackDetails']);
String streamItemId = mediaItem.id;
//If Deezer provided a FALLBACK track, use the playbackDetails and id from the fallback track
//for streaming (original stream unavailable)
if (mediaItem.extras?['fallbackId'] != null) {
streamItemId = mediaItem.extras?['fallbackId'];
streamPlaybackDetails = jsonDecode(mediaItem.extras?['playbackDetailsFallback']);
streamPlaybackDetails =
jsonDecode(mediaItem.extras?['playbackDetailsFallback']);
}
if ((streamPlaybackDetails ?? []).length < 3) return null;
@@ -522,7 +580,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
/// Load new queue of MediaItems to just_audio & seek to given index & position
Future _loadQueueAtIndex(List<MediaItem> newQueue, int index, {Duration position = Duration.zero}) async {
Future _loadQueueAtIndex(List<MediaItem> newQueue, int index,
{Duration position = Duration.zero}) async {
//Set requested index
_requestedIndex = index;
@@ -532,6 +591,9 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
// Convert new queue to AudioSources playlist & add to just_audio (Concurrent approach)
await _playlist.addAll(await _itemsToSources(newQueue));
// Wait for player to be ready before seeking
await _waitForPlayerReadiness();
//Seek to correct position & index
try {
await _player.seek(position, index: index);
@@ -542,7 +604,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
//Replace queue, play specified item index
Future _loadQueueAndPlayAtIndex(QueueSource newQueueSource, List<MediaItem> newQueue, int index) async {
Future _loadQueueAndPlayAtIndex(
QueueSource newQueueSource, List<MediaItem> newQueue, int index) async {
// Pauze platback if playing (Player seems to crash on some devices otherwise)
await pause();
//Set requested index
@@ -582,7 +645,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
// Get current position
int pos = queue.value.length;
// Load 25 more tracks from playlist
tracks = await deezerAPI.playlistTracksPage(queueSource!.id!, pos, nb: 25);
tracks =
await deezerAPI.playlistTracksPage(queueSource!.id!, pos, nb: 25);
break;
default:
Logger.root.info('Reached end of queue source: ${queueSource!.source}');
@@ -592,13 +656,16 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
// Deduplicate tracks already in queue with the same id
List<String> queueIds = queue.value.map((mi) => mi.id).toList();
tracks.removeWhere((track) => queueIds.contains(track.id));
List<MediaItem> extraTracks = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
List<MediaItem> extraTracks =
tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
addQueueItems(extraTracks);
}
void _playbackError(err) {
Logger.root.severe('Playback Error from audioservice: ${err.code}', err);
if (err is PlatformException && err.code == 'abort' && err.message == 'Connection aborted') {
if (err is PlatformException &&
err.code == 'abort' &&
err.message == 'Connection aborted') {
return;
}
_onError(err, null);
@@ -647,7 +714,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
Map data = {
'index': _player.currentIndex,
'queue': queue.value.map<Map<String, dynamic>>((mi) => MediaItemConverter.mediaItemToMap(mi)).toList(),
'queue': queue.value
.map<Map<String, dynamic>>(
(mi) => MediaItemConverter.mediaItemToMap(mi))
.toList(),
'position': _player.position.inMilliseconds,
'queueSource': (queueSource ?? QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_player.loopMode)
@@ -674,20 +744,44 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
//Restore queue & playback info from path
Future loadQueueFromFile() async {
Logger.root.info('looking for saved queue file...');
File f = File(await _getQueueFilePath());
if (await f.exists()) {
Logger.root.info('saved queue file found, loading...');
Map<String, dynamic> json = jsonDecode(await f.readAsString());
List<MediaItem> savedQueue =
(json['queue'] ?? []).map<MediaItem>((mi) => (MediaItemConverter.mediaItemFromMap(mi))).toList();
final int lastIndex = json['index'] ?? 0;
final Duration lastPos = Duration(milliseconds: json['position'] ?? 0);
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
var repeatType = LoopMode.values[(json['loopMode'] ?? 0)];
_player.setLoopMode(repeatType);
//Restore queue & Broadcast
await _loadQueueAtIndex(savedQueue, lastIndex, position: lastPos);
Logger.root.info('saved queue loaded from file!');
try {
File f = File(await _getQueueFilePath());
if (await f.exists()) {
Logger.root.info('saved queue file found, loading...');
try {
String fileContent = await f.readAsString();
if (fileContent.isEmpty) {
Logger.root.warning('saved queue file is empty');
return;
}
Map<String, dynamic> json = jsonDecode(fileContent);
List<MediaItem> savedQueue = (json['queue'] ?? [])
.map<MediaItem>((mi) => (MediaItemConverter.mediaItemFromMap(mi)))
.toList();
final int lastIndex = json['index'] ?? 0;
final Duration lastPos =
Duration(milliseconds: json['position'] ?? 0);
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
var repeatType = LoopMode.values[(json['loopMode'] ?? 0)];
await _player.setLoopMode(repeatType);
// Restore queue & Broadcast
await _loadQueueAtIndex(savedQueue, lastIndex, position: lastPos);
Logger.root.info('saved queue loaded from file!');
} catch (e) {
Logger.root.severe('Error parsing queue file: $e');
// Delete corrupted file to prevent future errors
await f.delete();
await _loadEmptyPlaylist();
}
}
} catch (e, st) {
Logger.root.severe('Error loading queue from file', e, st);
await _loadEmptyPlaylist();
}
}
@@ -706,7 +800,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
String password = settings.lastFMPassword ?? '';
try {
LastFM lastFM = await LastFM.authenticateWithPasswordHash(
apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: username, passwordHash: password);
apiKey: Env.lastFmApiKey,
apiSecret: Env.lastFmApiSecret,
username: username,
passwordHash: password);
_scrobblenaut = Scrobblenaut(lastFM: lastFM);
_scrobblenautReady = true;
} catch (e) {
@@ -721,7 +818,9 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
Future toggleShuffle() async {
await setShuffleMode(_player.shuffleModeEnabled ? AudioServiceShuffleMode.none : AudioServiceShuffleMode.all);
await setShuffleMode(_player.shuffleModeEnabled
? AudioServiceShuffleMode.none
: AudioServiceShuffleMode.all);
}
LoopMode getLoopMode() {
@@ -749,53 +848,71 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
if (_player.playing) {
// Pauze playback if playing (Player seems to crash on some devices otherwise)
await pause();
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0, position: _player.position);
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0,
position: _player.position);
await _player.play();
} else {
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0, position: _player.position);
await _loadQueueAtIndex(queue.value, queueState.queueIndex ?? 0,
position: _player.position);
}
}
//Play track from album
Future playFromAlbum(Album album, String trackId) async {
await playFromTrackList(album.tracks ?? [], trackId, QueueSource(id: album.id, text: album.title, source: 'album'));
await playFromTrackList(album.tracks ?? [], trackId,
QueueSource(id: album.id, text: album.title, source: 'album'));
}
//Play mix by track
Future playMix(String trackId, String trackTitle) async {
List<Track> tracks = await deezerAPI.playMix(trackId);
playFromTrackList(tracks, tracks[0].id ?? '',
QueueSource(id: trackId, text: 'Mix based on'.i18n + ' $trackTitle', source: 'mix'));
playFromTrackList(
tracks,
tracks[0].id ?? '',
QueueSource(
id: trackId,
text: 'Mix based on'.i18n + ' $trackTitle',
source: 'mix'));
}
//Play from artist top tracks
Future playFromTopTracks(List<Track> tracks, String trackId, Artist artist) async {
Future playFromTopTracks(
List<Track> tracks, String trackId, Artist artist) async {
await playFromTrackList(
tracks, trackId, QueueSource(id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
tracks,
trackId,
QueueSource(
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
}
Future playFromPlaylist(Playlist playlist, String trackId) async {
await playFromTrackList(
playlist.tracks ?? [], trackId, QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
await playFromTrackList(playlist.tracks ?? [], trackId,
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
}
//Play episode from show, load whole show as queue
Future playShowEpisode(Show show, List<ShowEpisode> episodes, {int index = 0}) async {
QueueSource showQueueSource = QueueSource(id: show.id, text: show.name, source: 'show');
Future playShowEpisode(Show show, List<ShowEpisode> episodes,
{int index = 0}) async {
QueueSource showQueueSource =
QueueSource(id: show.id, text: show.name, source: 'show');
//Generate media items
List<MediaItem> episodeQueue = episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
List<MediaItem> episodeQueue =
episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
//Load and play
await _loadQueueAndPlayAtIndex(showQueueSource, episodeQueue, index);
}
//Load tracks as queue, play track id, set queue source
Future playFromTrackList(List<Track> tracks, String trackId, QueueSource trackQueueSource) async {
Future playFromTrackList(
List<Track> tracks, String trackId, QueueSource trackQueueSource) async {
//Generate media items
List<MediaItem> trackQueue = tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
List<MediaItem> trackQueue =
tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
//Load and play
await _loadQueueAndPlayAtIndex(trackQueueSource, trackQueue, trackQueue.indexWhere((m) => m.id == trackId));
await _loadQueueAndPlayAtIndex(trackQueueSource, trackQueue,
trackQueue.indexWhere((m) => m.id == trackId));
}
//Load smart track list as queue, start from beginning
@@ -820,8 +937,10 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
QueueSource queueSource = QueueSource(
id: stl.id,
source: (stl.id == 'flow') ? 'flow' : 'smarttracklist',
text: stl.title ?? ((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
await playFromTrackList(stl.tracks ?? [], stl.tracks?[0].id ?? '', queueSource);
text: stl.title ??
((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
await playFromTrackList(
stl.tracks ?? [], stl.tracks?[0].id ?? '', queueSource);
}
//Start visualizer
@@ -852,7 +971,8 @@ class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler
}
class QueueState {
static const QueueState empty = QueueState([], 0, [], AudioServiceRepeatMode.none, AudioServiceShuffleMode.none);
static const QueueState empty = QueueState(
[], 0, [], AudioServiceRepeatMode.none, AudioServiceShuffleMode.none);
final List<MediaItem> queue;
final int? queueIndex;
@@ -860,10 +980,15 @@ class QueueState {
final AudioServiceRepeatMode repeatMode;
final AudioServiceShuffleMode shuffleMode;
const QueueState(this.queue, this.queueIndex, this.shuffleIndices, this.repeatMode, this.shuffleMode);
const QueueState(this.queue, this.queueIndex, this.shuffleIndices,
this.repeatMode, this.shuffleMode);
bool get hasPrevious => repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) > 0;
bool get hasNext => repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) + 1 < queue.length;
bool get hasPrevious =>
repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) > 0;
bool get hasNext =>
repeatMode != AudioServiceRepeatMode.none ||
(queueIndex ?? 0) + 1 < queue.length;
List<int> get indices => shuffleIndices ?? List.generate(queue.length, (i) => i);
List<int> get indices =>
shuffleIndices ?? List.generate(queue.length, (i) => i);
}

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@@ -15,6 +16,7 @@ import 'api/download.dart';
import 'main.dart';
import 'service/audio_service.dart';
import 'ui/cached_image.dart';
import 'utils/app_icon_changer.dart';
part 'settings.g.dart';
@@ -126,6 +128,9 @@ class Settings {
bool useArtColor = false;
StreamSubscription? _useArtColorSub;
@JsonKey(defaultValue: 'DefaultIcon')
String? appIcon;
//Deezer
@JsonKey(defaultValue: 'en')
late String deezerLanguage;
@@ -171,11 +176,28 @@ class Settings {
return ['Deezer', ...GoogleFonts.asMap().keys];
}
// Get all available app icons
List<String> get availableIcons {
return AppIconChanger.availableIcons.map((icon) => icon.key).toList();
}
//JSON to forward into download service
Map getServiceSettings() {
return {'json': jsonEncode(toJson())};
}
Future<void> updateAppIcon(String iconKey) async {
try {
LauncherIcon icon =
LauncherIcon.values.firstWhere((e) => e.key == iconKey);
await AppIconChanger.changeIcon(icon);
appIcon = iconKey;
await save();
} catch (e) {
Logger.root.severe('Error updating app icon: $e');
}
}
void updateUseArtColor(bool v) {
useArtColor = v;
if (v) {
@@ -403,7 +425,6 @@ class Settings {
primaryColor: primaryColor,
sliderTheme: _sliderTheme,
scaffoldBackgroundColor: deezerBg,
dialogBackgroundColor: deezerBottom,
bottomSheetTheme:
const BottomSheetThemeData(backgroundColor: deezerBottom),
cardColor: deezerBg,
@@ -459,7 +480,7 @@ class Settings {
return null;
}),
),
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom)),
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom), dialogTheme: DialogThemeData(backgroundColor: deezerBottom)),
Themes.Black: ThemeData(
useMaterial3: false,
brightness: Brightness.dark,
@@ -467,7 +488,6 @@ class Settings {
fontFamily: _fontFamily,
primaryColor: primaryColor,
scaffoldBackgroundColor: Colors.black,
dialogBackgroundColor: Colors.black,
sliderTheme: _sliderTheme,
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.black,
@@ -524,7 +544,7 @@ class Settings {
return null;
}),
),
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black))
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black), dialogTheme: DialogThemeData(backgroundColor: Colors.black))
};
Future<String> getPath() async =>

View File

@@ -74,6 +74,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
..primaryColor =
Settings._colorFromJson((json['primaryColor'] as num?)?.toInt())
..useArtColor = json['useArtColor'] as bool? ?? false
..appIcon = json['appIcon'] as String? ?? 'DefaultIcon'
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
..deezerCountry = json['deezerCountry'] as String? ?? 'US'
..logListen = json['logListen'] as bool? ?? false
@@ -121,6 +122,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'displayMode': instance.displayMode,
'primaryColor': Settings._colorToJson(instance.primaryColor),
'useArtColor': instance.useArtColor,
'appIcon': instance.appIcon,
'deezerLanguage': instance.deezerLanguage,
'deezerCountry': instance.deezerCountry,
'logListen': instance.logListen,

View File

@@ -62,21 +62,25 @@ class _PlayerScreenState extends State<PlayerScreen> {
//BG Image
if (settings.blurPlayerBackground) {
setState(() {
_blurImage =
NetworkImage(audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri);
_blurImage = NetworkImage(
audioHandler.mediaItem.value?.extras?['thumb'] ??
audioHandler.mediaItem.value?.artUri);
});
}
//Run in isolate
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(
audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri));
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider(
audioHandler.mediaItem.value?.extras?['thumb'] ??
audioHandler.mediaItem.value?.artUri));
//Update notification
if (settings.blurPlayerBackground) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: palette.dominantColor!.color.withOpacity(0.25),
systemNavigationBarColor:
Color.alphaBlend(palette.dominantColor!.color.withOpacity(0.25), scaffoldBackgroundColor)));
systemNavigationBarColor: Color.alphaBlend(
palette.dominantColor!.color.withOpacity(0.25),
scaffoldBackgroundColor)));
}
//Color gradient
@@ -85,10 +89,16 @@ class _PlayerScreenState extends State<PlayerScreen> {
statusBarColor: palette.dominantColor!.color.withOpacity(0.7),
));
setState(() => _bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [palette.dominantColor!.color.withOpacity(0.7), const Color.fromARGB(0, 0, 0, 0)],
stops: const [0.0, 0.6]));
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
palette.dominantColor!.color.withOpacity(0.7),
const Color.fromARGB(0, 0, 0, 0)
],
stops: const [
0.0,
0.6
]));
}
}
@@ -108,7 +118,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
_mediaItemSub?.cancel();
//Fix bottom buttons
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color, statusBarColor: Colors.transparent));
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color,
statusBarColor: Colors.transparent));
super.dispose();
}
@@ -120,7 +131,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
return Scaffold(
body: SafeArea(
child: Container(
decoration: BoxDecoration(gradient: settings.blurPlayerBackground ? null : _bgGradient),
decoration: BoxDecoration(
gradient:
settings.blurPlayerBackground ? null : _bgGradient),
child: Stack(
children: [
if (settings.blurPlayerBackground)
@@ -130,7 +143,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
image: DecorationImage(
image: _blurImage ?? const NetworkImage(''),
fit: BoxFit.fill,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.25), BlendMode.dstATop))),
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(0.25),
BlendMode.dstATop))),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(color: Colors.transparent),
@@ -138,7 +153,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
),
),
StreamBuilder(
stream: StreamZip([audioHandler.playbackState, audioHandler.mediaItem]),
stream: StreamZip(
[audioHandler.playbackState, audioHandler.mediaItem]),
builder: (BuildContext context, AsyncSnapshot snapshot) {
//When disconnected
if (audioHandler.mediaItem.value == null) {
@@ -214,26 +230,45 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
children: <Widget>[
SizedBox(
height: ScreenUtil().setSp(50),
child: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!.length >= 22
child: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displayTitle!
.length >=
22
? Marquee(
text: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
text: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displayTitle!,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: const Duration(seconds: 1),
pauseAfterRound: const Duration(seconds: 2),
)
: Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displayTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold),
)),
Container(
height: 4,
),
Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displaySubtitle ?? '',
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displaySubtitle ??
'',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
@@ -266,8 +301,11 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
semanticLabel: 'Download'.i18n,
),
onPressed: () async {
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
Track t = Track.fromMediaItem(
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t,
private: false, isSingleton: true) !=
false) {
Fluttertoast.showToast(
msg: 'Downloads added!'.i18n,
gravity: ToastGravity.BOTTOM,
@@ -328,26 +366,45 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
children: <Widget>[
SizedBox(
height: ScreenUtil().setSp(26),
child: (GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '').length >= 26
child: (GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'')
.length >=
26
? Marquee(
text: GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
text: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'',
style: TextStyle(
fontSize: ScreenUtil().setSp(22),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: const Duration(seconds: 1),
pauseAfterRound: const Duration(seconds: 2),
)
: Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: ScreenUtil().setSp(22),
fontWeight: FontWeight.bold),
)),
Container(
height: 4,
),
Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ?? '',
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ??
'',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
@@ -374,10 +431,15 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
semanticLabel: 'Download'.i18n,
),
onPressed: () async {
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
Track t = Track.fromMediaItem(
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t,
private: false, isSingleton: true) !=
false) {
Fluttertoast.showToast(
msg: 'Downloads added!'.i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
msg: 'Downloads added!'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
}
},
),
@@ -407,7 +469,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
//Load data from native
void _load() async {
if (audioHandler.mediaItem.value == null) return;
Map? data = await DownloadManager.platform.invokeMethod('getStreamInfo', {'id': audioHandler.mediaItem.value!.id});
Map? data = await DownloadManager.platform.invokeMethod(
'getStreamInfo', {'id': audioHandler.mediaItem.value!.id});
//N/A
if (data == null) {
if (mounted) setState(() => value = '');
@@ -449,7 +512,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
return TextButton(
child: Text(value),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QualitySettings()));
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const QualitySettings()));
},
);
}
@@ -475,12 +539,15 @@ class LyricsIconButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
Track track = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
Track track =
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
bool isEnabled = (track.lyrics?.id ?? '0') != '0';
return Opacity(
opacity: isEnabled ? 1.0 : 0.7, // Full opacity for enabled, reduced for disabled
opacity: isEnabled
? 1.0
: 0.7, // Full opacity for enabled, reduced for disabled
child: IconButton(
icon: Icon(
//Icons.lyrics,
@@ -491,10 +558,11 @@ class LyricsIconButton extends StatelessWidget {
onPressed: isEnabled
? () async {
//Fix bottom buttons
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent));
await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => LyricsScreen(trackId: track.id!)));
await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => LyricsScreen(trackId: track.id!)));
if (afterOnPressed != null) {
afterOnPressed!();
@@ -519,16 +587,24 @@ class PlayerMenuButton extends StatelessWidget {
semanticLabel: 'Options'.i18n,
),
onPressed: () {
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
Track t =
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
MenuSheet m = MenuSheet(navigateCallback: () {
Navigator.of(context).pop();
});
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] == null) {
m.defaultTrackMenu(t, context: context, options: [m.sleepTimer(context), m.wakelock(context)]);
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] ==
null) {
m.defaultTrackMenu(t,
context: context,
options: [m.sleepTimer(context), m.wakelock(context)]);
} else {
m.defaultShowEpisodeMenu(
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'])),
ShowEpisode.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!),
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.extras?['show'])),
ShowEpisode.fromMediaItem(
GetIt.I<AudioPlayerHandler>().mediaItem.value!),
context: context,
options: [m.sleepTimer(context), m.wakelock(context)]);
}
@@ -594,7 +670,8 @@ class PlaybackControls extends StatefulWidget {
class _PlaybackControlsState extends State<PlaybackControls> {
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
Icon get libraryIcon {
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
return Icon(
Icons.favorite,
size: widget.iconSize * 0.44,
@@ -636,15 +713,20 @@ class _PlaybackControlsState extends State<PlaybackControls> {
onPressed: () async {
cache.libraryTracks ??= [];
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
//Remove from library
setState(() => cache.libraryTracks?.remove(audioHandler.mediaItem.value!.id));
await deezerAPI.removeFavorite(audioHandler.mediaItem.value!.id);
setState(() => cache.libraryTracks
?.remove(audioHandler.mediaItem.value!.id));
await deezerAPI
.removeFavorite(audioHandler.mediaItem.value!.id);
await cache.save();
} else {
//Add
setState(() => cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
setState(() =>
cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
await deezerAPI
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
await cache.save();
}
},
@@ -679,7 +761,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
_imageList = _getImageList(audioHandler.queue.value);
_currentItemAndQueueSub = Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
_currentItemAndQueueSub =
Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
audioHandler.mediaItem,
audioHandler.queue,
(mediaItem, queue) {
@@ -698,7 +781,9 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
}
List<ZoomableImage> _getImageList(List<MediaItem> queue) {
return queue.map((item) => ZoomableImage(url: item.artUri?.toString() ?? '')).toList();
return queue
.map((item) => ZoomableImage(url: item.artUri?.toString() ?? ''))
.toList();
}
bool _didQueueChange(List<MediaItem> newQueue) {
@@ -718,7 +803,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
void _handleMediaItemChange(MediaItem? item) async {
final targetItemId = item?.id ?? '';
final targetPage = audioHandler.queue.value.indexWhere((item) => item.id == targetItemId);
final targetPage =
audioHandler.queue.value.indexWhere((item) => item.id == targetItemId);
if (targetPage == -1) return;
// No need to animating to the same page
@@ -799,7 +885,8 @@ class PlayerScreenTopRow extends StatelessWidget {
final double? textWidth;
final bool? short;
final GlobalKey iconButtonKey = GlobalKey();
PlayerScreenTopRow({super.key, this.textSize, this.iconSize, this.textWidth, this.short});
PlayerScreenTopRow(
{super.key, this.textSize, this.iconSize, this.textWidth, this.short});
@override
Widget build(BuildContext context) {
@@ -825,7 +912,9 @@ class PlayerScreenTopRow extends StatelessWidget {
child: Text(
(short ?? false)
? (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? '')
: 'Playing from:'.i18n + ' ' + (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
: 'Playing from:'.i18n +
' ' +
(GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
@@ -844,11 +933,14 @@ class PlayerScreenTopRow extends StatelessWidget {
splashRadius: iconSize ?? ScreenUtil().setWidth(52),
onPressed: () async {
//Fix bottom buttons (Not needed anymore?)
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
// Calculate the center of the icon
final RenderBox buttonRenderBox = iconButtonKey.currentContext!.findRenderObject() as RenderBox;
final Offset buttonOffset = buttonRenderBox.localToGlobal(buttonRenderBox.size.center(Offset.zero));
final RenderBox buttonRenderBox =
iconButtonKey.currentContext!.findRenderObject() as RenderBox;
final Offset buttonOffset = buttonRenderBox
.localToGlobal(buttonRenderBox.size.center(Offset.zero));
//Navigate
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
await Navigator.of(context).push(CircularExpansionRoute(
@@ -879,7 +971,8 @@ class _SeekBarState extends State<SeekBar> {
double get position {
if (_seeking) return _pos;
double p = audioHandler.playbackState.value.position.inMilliseconds.toDouble();
double p =
audioHandler.playbackState.value.position.inMilliseconds.toDouble();
if (p > duration) return duration;
return p;
}
@@ -904,17 +997,20 @@ class _SeekBarState extends State<SeekBar> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
padding:
const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
_timeString(position),
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
style: TextStyle(
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
),
Text(
_timeString(duration),
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
style: TextStyle(
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
)
],
),
@@ -924,7 +1020,8 @@ class _SeekBarState extends State<SeekBar> {
child: Slider(
focusNode: FocusNode(
canRequestFocus: false,
skipTraversal: true), // Don't focus on Slider - it doesn't work (and not needed)
skipTraversal:
true), // Don't focus on Slider - it doesn't work (and not needed)
value: position,
max: duration,
onChangeStart: (double d) {
@@ -1003,7 +1100,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final queueState = audioHandler.queueState;
final shuffleModeEnabled = queueState.shuffleMode == AudioServiceShuffleMode.all;
final shuffleModeEnabled =
queueState.shuffleMode == AudioServiceShuffleMode.all;
return Scaffold(
appBar: FreezerAppBar(
@@ -1016,7 +1114,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
//cons.shuffle,
ReFreezerIcons.shuffle,
semanticLabel: 'Shuffle'.i18n,
color: shuffleModeEnabled ? Theme.of(context).primaryColor : null,
color:
shuffleModeEnabled ? Theme.of(context).primaryColor : null,
),
onPressed: () async {
await audioHandler.toggleShuffle();
@@ -1032,7 +1131,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
),
onPressed: () async {
await audioHandler.clearQueue();
mainNavigatorKey.currentState!.popUntil((route) => route.isFirst);
mainNavigatorKey.currentState!
.popUntil((route) => route.isFirst);
},
),
)
@@ -1082,7 +1182,7 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
await audioHandler.skipToQueueItem(index);
if (context.mounted) Navigator.of(context).pop();
},
key: Key(mediaItem.id),
key: Key('${mediaItem.id}_$index'),
trailing: IconButton(
icon: Icon(
Icons.close,

View File

@@ -424,8 +424,6 @@ class _SearchScreenState extends State<SearchScreen> {
},
trailing: _removeHistoryItemWidget(i),
);
default:
return Container();
}
}),

View File

@@ -56,33 +56,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: <Widget>[
ListTile(
title: Text('General'.i18n),
leading: const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const GeneralSettings())),
leading:
const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const GeneralSettings())),
),
ListTile(
title: Text('Download Settings'.i18n),
leading: const LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadsSettings())),
leading: const LeadingIcon(Icons.cloud_download,
color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const DownloadsSettings())),
),
ListTile(
title: Text('Appearance'.i18n),
leading: const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const AppearanceSettings())),
leading:
const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AppearanceSettings())),
),
ListTile(
title: Text('Quality'.i18n),
leading: const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const QualitySettings())),
leading:
const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const QualitySettings())),
),
ListTile(
title: Text('Deezer'.i18n),
leading: const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const DeezerSettings())),
leading:
const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeezerSettings())),
),
//Language select
ListTile(
title: Text('Language'.i18n),
leading: const LeadingIcon(Icons.language, color: Color(0xff009a85)),
leading:
const LeadingIcon(Icons.language, color: Color(0xff009a85)),
onTap: () {
showDialog(
context: context,
@@ -94,8 +111,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: Text(l.name),
subtitle: Text('${l.locale}-${l.country}'),
onTap: () async {
I18n.of(customNavigatorKey.currentContext!).locale = Locale(l.locale, l.country);
setState(() => settings.language = '${l.locale}_${l.country}');
I18n.of(customNavigatorKey.currentContext!).locale =
Locale(l.locale, l.country);
setState(() =>
settings.language = '${l.locale}_${l.country}');
await settings.save();
// Close the SimpleDialog
if (context.mounted) Navigator.of(context).pop();
@@ -107,12 +126,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
ListTile(
title: Text('Updates'.i18n),
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const UpdaterScreen())),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => const UpdaterScreen())),
),
ListTile(
title: Text('About'.i18n),
leading: const LeadingIcon(Icons.info, color: Colors.grey),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CreditsScreen())),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => const CreditsScreen())),
),
],
),
@@ -138,7 +159,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
children: <Widget>[
ListTile(
title: Text('Theme'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
subtitle: Text('Currently'.i18n +
': ${settings.theme.toString().split('.').last}'),
leading: const Icon(Icons.color_lens),
onTap: () {
showDialog(
@@ -201,12 +223,29 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
leading: const Icon(Icons.android)),
ListTile(
title: Text('App icon'.i18n),
leading: const Icon(Icons.app_settings_alt),
subtitle: Text(settings.appIcon ?? 'DefaultIcon'),
onTap: () {
showDialog(
context: context,
builder: (context) => IconSelector(() {
setState(() {});
Navigator.of(context).pop();
}),
);
},
),
ListTile(
title: Text('Font'.i18n),
leading: const Icon(Icons.font_download),
subtitle: Text(settings.font),
onTap: () {
showDialog(context: context, builder: (context) => FontSelector(() => Navigator.of(context).pop()));
showDialog(
context: context,
builder: (context) =>
FontSelector(() => Navigator.of(context).pop()));
},
),
ListTile(
@@ -234,7 +273,9 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
),
ListTile(
title: Text('Visualizer'.i18n),
subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n),
subtitle: Text(
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
.i18n),
leading: const Icon(Icons.equalizer),
trailing: Switch(
value: settings.lyricsVisualizer,
@@ -320,7 +361,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
onPressed: () async {
settings.displayMode = i;
await settings.save();
await FlutterDisplayMode.setPreferredMode(modes[i]);
await FlutterDisplayMode.setPreferredMode(
modes[i]);
if (context.mounted) {
Navigator.of(context).pop();
}
@@ -335,6 +377,65 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}
}
class IconSelector extends StatefulWidget {
final Function callback;
const IconSelector(this.callback, {super.key});
@override
_IconSelectorState createState() => _IconSelectorState();
}
class _IconSelectorState extends State<IconSelector> {
late String selectedIcon;
@override
void initState() {
super.initState();
selectedIcon = settings.appIcon ?? 'DefaultIcon';
}
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text('Select app icon'.i18n),
// Add the warning message here
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0),
child: Text(
'Selecting a new icon will exit the app to apply the change!'
.i18n, // Translatable string
style: TextStyle(fontWeight: FontWeight.bold),
),
),
...settings.availableIcons.map((String iconKey) {
return SimpleDialogOption(
onPressed: () async {
await settings.updateAppIcon(iconKey);
setState(() {
selectedIcon = iconKey;
});
widget.callback();
},
child: ListTile(
contentPadding: EdgeInsets.zero, // Adjust padding if needed
leading: Image.asset(
'assets/icons/$iconKey.png',
width: 32,
height: 32,
),
title: Text(iconKey),
trailing:
selectedIcon == iconKey ? const Icon(Icons.check) : null,
),
);
}),
],
);
}
}
class FontSelector extends StatefulWidget {
final Function callback;
@@ -347,7 +448,9 @@ class FontSelector extends StatefulWidget {
class _FontSelectorState extends State<FontSelector> {
String query = '';
List<String> get fonts {
return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList();
return settings.fonts
.where((f) => f.toLowerCase().contains(query))
.toList();
}
//Font selected
@@ -421,25 +524,29 @@ class _QualitySettingsState extends State<QualitySettings> {
children: <Widget>[
ListTile(
title: Text('Mobile streaming'.i18n),
leading: const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
leading:
const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
),
const QualityPicker('mobile'),
const FreezerDivider(),
ListTile(
title: Text('Wifi streaming'.i18n),
leading: const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
leading:
const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
),
const QualityPicker('wifi'),
const FreezerDivider(),
ListTile(
title: Text('Offline'.i18n),
leading: const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
leading:
const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
),
const QualityPicker('offline'),
const FreezerDivider(),
ListTile(
title: Text('External downloads'.i18n),
leading: const LeadingIcon(Icons.file_download, color: Color(0xff2ba766)),
leading: const LeadingIcon(Icons.file_download,
color: Color(0xff2ba766)),
),
const QualityPicker('download'),
],
@@ -608,7 +715,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
children: <Widget>[
ListTile(
title: Text('Content language'.i18n),
subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'),
subtitle: Text('Not app language, used in headers. Now'.i18n +
': ${settings.deezerLanguage}'),
leading: const Icon(Icons.language),
onTap: () {
showDialog(
@@ -621,7 +729,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
title: Text(ContentLanguage.all[i].name),
subtitle: Text(ContentLanguage.all[i].code),
onTap: () async {
setState(() => settings.deezerLanguage = ContentLanguage.all[i].code);
setState(() => settings.deezerLanguage =
ContentLanguage.all[i].code);
await settings.save();
if (context.mounted) {
Navigator.of(context).pop();
@@ -633,7 +742,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
),
ListTile(
title: Text('Content country'.i18n),
subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'),
subtitle: Text('Country used in headers. Now'.i18n +
': ${settings.deezerCountry}'),
leading: const Icon(Icons.vpn_lock),
onTap: () {
showDialog(
@@ -655,7 +765,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
],
),
onValuePicked: (Country country) {
setState(() => settings.deezerCountry = country.isoCode ?? 'us');
setState(() =>
settings.deezerCountry = country.isoCode ?? 'us');
settings.save();
},
));
@@ -663,7 +774,9 @@ class _DeezerSettingsState extends State<DeezerSettings> {
),
ListTile(
title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
subtitle: Text(
'Send track listen logs to Deezer, enable it for features like Flow to work properly'
.i18n),
trailing: Switch(
value: settings.logListen,
onChanged: (bool v) {
@@ -764,7 +877,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
Text(
'Valid variables are'.i18n +
': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' +
"If you want to use custom directory naming - use '/' as directory separator.".i18n,
"If you want to use custom directory naming - use '/' as directory separator."
.i18n,
style: const TextStyle(
fontSize: 12.0,
),
@@ -779,7 +893,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
TextButton(
child: Text('Reset'.i18n),
onPressed: () {
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
_controller.value =
_controller.value.copyWith(text: '%artist% - %title%');
_new = '%artist% - %title%';
},
),
@@ -808,7 +923,8 @@ class DownloadsSettings extends StatefulWidget {
class _DownloadsSettingsState extends State<DownloadsSettings> {
double _downloadThreads = settings.downloadThreads.toDouble();
final TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator);
final TextEditingController _artistSeparatorController =
TextEditingController(text: settings.artistSeparator);
@override
Widget build(BuildContext context) {
@@ -852,7 +968,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
showDialog(
context: context,
builder: (context) {
return FilenameTemplateDialog(settings.downloadFilename, (f) async {
return FilenameTemplateDialog(settings.downloadFilename,
(f) async {
setState(() => settings.downloadFilename = f);
await settings.save();
});
@@ -861,13 +978,15 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
),
ListTile(
title: Text('Singleton naming'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'),
subtitle:
Text('Currently'.i18n + ': ${settings.singletonFilename}'),
leading: const Icon(Icons.text_format),
onTap: () {
showDialog(
context: context,
builder: (context) {
return FilenameTemplateDialog(settings.singletonFilename, (f) async {
return FilenameTemplateDialog(settings.singletonFilename,
(f) async {
setState(() => settings.singletonFilename = f);
await settings.save();
});
@@ -877,7 +996,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Download threads'.i18n + ': ${_downloadThreads.round().toString()}',
'Download threads'.i18n +
': ${_downloadThreads.round().toString()}',
style: const TextStyle(fontSize: 16.0),
),
),
@@ -897,14 +1017,17 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
await settings.save();
//Prevent null
if (val > 8 && cache.threadsWarning != true && context.mounted) {
if (val > 8 &&
cache.threadsWarning != true &&
context.mounted) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Warning'.i18n),
content: Text(
'Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n),
'Using too many concurrent downloads on older/weaker devices might cause crashes!'
.i18n),
actions: [
TextButton(
child: Text('Dismiss'.i18n),
@@ -922,8 +1045,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
ListTile(
title: Text('Tags'.i18n),
leading: const Icon(Icons.label),
onTap: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TagSelectionScreen())),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const TagSelectionScreen())),
),
ListTile(
title: Text('Create folders for artist'.i18n),
@@ -1010,17 +1133,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
leading: const Icon(Icons.image)),
ListTile(
title: Text('Album cover resolution'.i18n),
subtitle: Text("WARNING: Resolutions above 1200 aren't officially supported".i18n),
subtitle: Text(
"WARNING: Resolutions above 1200 aren't officially supported"
.i18n),
leading: const Icon(Icons.image),
trailing: SizedBox(
width: 75.0,
child: DropdownButton<int>(
value: settings.albumArtResolution,
items: [400, 800, 1000, 1200, 1400, 1600, 1800]
.map<DropdownMenuItem<int>>((int i) => DropdownMenuItem<int>(
value: i,
child: Text(i.toString()),
))
.map<DropdownMenuItem<int>>(
(int i) => DropdownMenuItem<int>(
value: i,
child: Text(i.toString()),
))
.toList(),
onChanged: (int? n) async {
setState(() {
@@ -1031,7 +1157,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
))),
ListTile(
title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n),
subtitle:
Text('To prevent gallery being filled with album art'.i18n),
trailing: Switch(
value: settings.nomediaFiles,
onChanged: (v) {
@@ -1058,7 +1185,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
ListTile(
title: Text('Download Log'.i18n),
leading: const Icon(Icons.sticky_note_2),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadLogViewer())),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const DownloadLogViewer())),
)
],
),
@@ -1159,7 +1287,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
setState(() => settings.offlineMode = false);
} else {
Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.'.i18n,
msg:
'Error logging in, check your internet connections.'
.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
}
@@ -1179,7 +1309,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
ListTile(
title: Text('Copy ARL'.i18n),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
subtitle:
Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
leading: const Icon(Icons.lock),
onTap: () async {
await FlutterClipboard.copy(settings.arl ?? '');
@@ -1190,7 +1321,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
ListTile(
title: Text('Enable equalizer'.i18n),
subtitle: Text('Might enable some equalizer apps to work. Requires restart of ReFreezer'.i18n),
subtitle: Text(
'Might enable some equalizer apps to work. Requires restart of ReFreezer'
.i18n),
leading: const Icon(Icons.equalizer),
trailing: Switch(
value: settings.enableEqualizer,
@@ -1202,7 +1335,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
ListTile(
title: Text('LastFM'.i18n),
subtitle: Text((settings.lastFMUsername != null) ? 'Log out'.i18n : 'Login to enable scrobbling.'.i18n),
subtitle: Text((settings.lastFMUsername != null)
? 'Log out'.i18n
: 'Login to enable scrobbling.'.i18n),
leading: const Icon(FontAwesome5.lastfm),
onTap: () async {
if (settings.lastFMUsername != null) {
@@ -1241,8 +1376,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
ListTile(
title: Text('Application Log'.i18n),
leading: const Icon(Icons.sticky_note_2),
onTap: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ApplicationLogViewer())),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const ApplicationLogViewer())),
),
const FreezerDivider(),
ListTile(
@@ -1341,7 +1476,10 @@ class _LastFMLoginState extends State<LastFMLogin> {
LastFM last;
try {
last = await LastFM.authenticate(
apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: _username, password: _password);
apiKey: Env.lastFmApiKey,
apiSecret: Env.lastFmApiSecret,
username: _username,
password: _password);
} catch (e) {
Logger.root.severe('Error authorizing LastFM: $e');
Fluttertoast.showToast(msg: 'Authorization error!'.i18n);
@@ -1365,23 +1503,30 @@ class StorageInfo {
final String appFilesDir;
final int availableBytes;
StorageInfo({required this.rootDir, required this.appFilesDir, required this.availableBytes});
StorageInfo(
{required this.rootDir,
required this.appFilesDir,
required this.availableBytes});
}
Future<List<StorageInfo>> getStorageInfo() async {
final externalDirectories = await ExternalPath.getExternalStorageDirectories();
final externalDirectories =
await ExternalPath.getExternalStorageDirectories();
List<StorageInfo> storageInfoList = [];
if (externalDirectories.isNotEmpty) {
for (var dir in externalDirectories) {
var availableMegaBytes = (await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
var availableMegaBytes =
(await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
storageInfoList.add(
StorageInfo(
rootDir: dir,
appFilesDir: dir,
availableBytes: availableMegaBytes > 0 ? (availableMegaBytes * 1000000).floor() : 0,
availableBytes: availableMegaBytes > 0
? (availableMegaBytes * 1000000).floor()
: 0,
),
);
}
@@ -1453,14 +1598,17 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
children: <Widget>[
CircularProgressIndicator()
],
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
...List.generate(snapshot.data?.length ?? 0, (i) {
...List.generate(snapshot.data?.length ?? 0,
(i) {
StorageInfo si = snapshot.data![i];
return ListTile(
title: Text(si.rootDir),
@@ -1524,7 +1672,9 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
onTap: () {
setState(() {
if (_root == _path) {
Fluttertoast.showToast(msg: 'Permission denied'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Permission denied'.i18n,
gravity: ToastGravity.BOTTOM);
return;
}
_previous = _path;
@@ -1618,8 +1768,10 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
const FreezerDivider(),
const ListTile(
title: Text('DJDoubleD', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('Developer, tester, new icon & logo, some translations, ...'),
title: Text('DJDoubleD',
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
'Developer, tester, new icon & logo, some translations, ...'),
),
const FreezerDivider(),
/*ListTile(
@@ -1657,7 +1809,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
ListTile(
title: Text('Crowdin'.i18n),
subtitle: Text('Help translating this app on Crowdin!'.i18n),
leading: const Icon(ReFreezerIcons.crowdin, color: Color(0xffbdc1c6), size: 36.0),
leading: const Icon(ReFreezerIcons.crowdin,
color: Color(0xffbdc1c6), size: 36.0),
onTap: () {
launchUrlString('https://crowdin.com/project/refreezer');
},
@@ -1665,15 +1818,20 @@ class _CreditsScreenState extends State<CreditsScreen> {
ListTile(
isThreeLine: true,
title: Text('Donate'.i18n),
subtitle: Text('You should rather support your favorite artists, instead of this app!'.i18n),
leading: const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
subtitle: Text(
'You should rather support your favorite artists, instead of this app!'
.i18n),
leading:
const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Donate'.i18n),
content: Text('No really, go support your favorite artists instead ;)'.i18n),
content: Text(
'No really, go support your favorite artists instead ;)'
.i18n),
actions: [
TextButton(
child: const Text('OK'),
@@ -1704,10 +1862,12 @@ class _CreditsScreenState extends State<CreditsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
Image.asset('assets/icon_legacy.png',
width: 24, height: 24),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'The original freezer development team'.i18n,
textAlign: TextAlign.center,
@@ -1720,7 +1880,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
),
),
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
Image.asset('assets/icon_legacy.png',
width: 24, height: 24),
],
),
],
@@ -1739,7 +1900,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
const ListTile(
title: Text('Bas Curtiz'),
subtitle: Text('Icon, logo, banner, design suggestions, tester'),
subtitle:
Text('Icon, logo, banner, design suggestions, tester'),
),
const ListTile(
title: Text('Tobs'),

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

View File

@@ -96,8 +96,6 @@ dependencies:
move_to_background:
path: ./move_to_background
numberpicker: ^2.1.2
# Awaiting release with fix for flutter 3.29.0:
#open_filex: ^4.6.0
open_filex: ^4.7.0
package_info_plus: ^8.0.0
palette_generator: ^0.3.3+3
@@ -156,6 +154,9 @@ flutter:
- assets/icon.png
- assets/favorites_thumb.jpg
- assets/browse_icon.png
- assets/icons/DefaultIcon.png
- assets/icons/CatIcon.png
- assets/icons/DeezerBlueIcon.png
fonts:
# - family: Montserrat

View File

@@ -353,5 +353,7 @@
"Permission denied, download canceled!": "Permission denied, download canceled!",
"Crowdin": "Crowdin",
"Help translating this app on Crowdin!": "Help translating this app on Crowdin!",
"Allow screen to turn off": "Allow screen to turn off"
"Allow screen to turn off": "Allow screen to turn off",
"Selecting a new icon will exit the app to apply the change!":
"Selecting a new icon will exit the app to apply the change!"
}