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

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

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

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

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

View File

@@ -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!',
} }
}; };

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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