Merge pull request #31 from KometTeam/feature/app-icon-switcher

Feature/app icon switcher
This commit is contained in:
klockky
2026-06-03 18:07:04 +03:00
committed by GitHub
73 changed files with 599 additions and 199 deletions
+21 -1
View File
@@ -26,11 +26,31 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>
<activity-alias
android:name=".DefaultIcon"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="Komet"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</activity-alias>
<activity-alias
android:name=".MinimalIcon"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_minimal"
android:label="Komet"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<service
@@ -1,7 +1,9 @@
package ru.komet.app
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
@@ -20,11 +22,32 @@ import java.util.concurrent.atomic.AtomicBoolean
class MainActivity : FlutterActivity() {
private val channelName = "ru.komet.app/vpn_bypass"
private val iconAliases = listOf("DefaultIcon", "MinimalIcon")
private companion object {
const val LOG_TAG = "VpnBypass"
}
private fun applyIcon(name: String) {
val pm = packageManager
for (alias in iconAliases) {
val component = ComponentName(packageName, "$packageName.$alias")
val state = if (alias == name) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
pm.setComponentEnabledSetting(
component,
state,
PackageManager.DONT_KILL_APP,
)
}
Handler(Looper.getMainLooper()).postDelayed({
finishAndRemoveTask()
}, 250L)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
@@ -39,6 +62,28 @@ class MainActivity : FlutterActivity() {
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"ru.komet.app/app_icon",
).setMethodCallHandler { call, result ->
when (call.method) {
"setAppIcon" -> {
val name = call.argument<String>("name")
if (name == null || !iconAliases.contains(name)) {
result.error("INVALID_ICON", "Unknown icon: $name", null)
return@setMethodCallHandler
}
try {
applyIcon(name)
result.success(null)
} catch (e: Exception) {
result.error("APPLY_FAILED", e.message, null)
}
}
else -> result.notImplemented()
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"ru.komet.app/upload_service",
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_minimal_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_minimal_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
<color name="ic_launcher_minimal_background">#000000</color>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+10 -2
View File
@@ -15,6 +15,8 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png */; };
B1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png */; };
E835CC948F2F948DC3BBF405 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AE8CB2654009FE276CACA4B /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
@@ -62,6 +64,8 @@
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MinimalIcon@2x.png"; sourceTree = "<group>"; };
A1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MinimalIcon@3x.png"; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9D7B0FFE63E0BD5D67D7758D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
@@ -143,6 +147,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
A1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png */,
A1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
@@ -264,6 +270,8 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
B1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png in Resources */,
B1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -539,7 +547,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -596,7 +604,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
+37
View File
@@ -8,6 +8,43 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller = window?.rootViewController as? FlutterViewController
if let messenger = controller?.binaryMessenger {
let channel = FlutterMethodChannel(
name: "ru.komet.app/app_icon",
binaryMessenger: messenger
)
channel.setMethodCallHandler { (call, result) in
guard call.method == "setAppIcon" else {
result(FlutterMethodNotImplemented)
return
}
let args = call.arguments as? [String: Any]
let name = args?["name"] as? String
let iconName: String? = (name == "DefaultIcon") ? nil : name
if !UIApplication.shared.supportsAlternateIcons {
result(FlutterError(
code: "UNSUPPORTED",
message: "Alternate icons are not supported",
details: nil
))
return
}
UIApplication.shared.setAlternateIconName(iconName) { error in
if let error = error {
result(FlutterError(
code: "APPLY_FAILED",
message: error.localizedDescription,
details: nil
))
} else {
result(nil)
}
}
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

+15
View File
@@ -14,6 +14,21 @@
<string>6.0</string>
<key>CFBundleName</key>
<string>Komet</string>
<key>CFBundleIcons</key>
<dict>
<key>CFBundleAlternateIcons</key>
<dict>
<key>MinimalIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>MinimalIcon</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
</dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+53
View File
@@ -0,0 +1,53 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum AppIcon {
defaultIcon('default', 'Default', 'assets/komet_icon.png', 'DefaultIcon'),
minimal('minimal', 'Minimal', 'assets/meteor_icon.png', 'MinimalIcon');
final String id;
final String title;
final String previewAsset;
final String platformName;
const AppIcon(this.id, this.title, this.previewAsset, this.platformName);
}
class AppIconConfig {
static const prefKey = 'app_icon';
static const _channel = MethodChannel('ru.komet.app/app_icon');
static final ValueNotifier<AppIcon> current = ValueNotifier(
AppIcon.defaultIcon,
);
static bool get isSupported => Platform.isAndroid || Platform.isIOS;
static Future<void> load() async {
if (!isSupported) return;
final prefs = await SharedPreferences.getInstance();
final id = prefs.getString(prefKey);
current.value = _parse(id);
}
static Future<void> apply(AppIcon icon) async {
if (!isSupported) return;
if (current.value == icon) return;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(prefKey, icon.id);
current.value = icon;
await _channel.invokeMethod<void>('setAppIcon', {
'name': icon.platformName,
});
}
static AppIcon _parse(String? val) {
for (final icon in AppIcon.values) {
if (icon.id == val) return icon;
}
return AppIcon.defaultIcon;
}
}
@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/config/app_icon.dart';
import '../../../core/utils/haptics.dart';
import '../../widgets/custom_notification.dart';
class AppIconScreen extends StatefulWidget {
const AppIconScreen({super.key});
@override
State<AppIconScreen> createState() => _AppIconScreenState();
}
class _AppIconScreenState extends State<AppIconScreen> {
@override
void initState() {
super.initState();
AppIconConfig.load();
}
Future<void> _select(AppIcon icon) async {
if (!AppIconConfig.isSupported) {
showCustomNotification(
context,
'Смена иконки доступна только на Android и iOS',
);
return;
}
if (AppIconConfig.current.value == icon) return;
Haptics.selection();
try {
await AppIconConfig.apply(icon);
if (!mounted) return;
showCustomNotification(context, 'Иконка изменена на «${icon.title}»');
} catch (e) {
if (!mounted) return;
showCustomNotification(context, 'Не удалось сменить иконку: $e');
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBarM3E(
titleText: 'Иконка приложения',
backgroundColor: cs.surface,
),
body: SafeArea(
top: false,
child: ListView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 120),
children: [
Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Внешний вид иконки',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
AppIconConfig.isSupported
? 'На Android приложение закроется — лаунчер подхватит новую иконку. На iOS — мгновенно с системным диалогом.'
: 'Доступно только на Android и iOS',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
height: 1.35,
),
),
const SizedBox(height: 12),
ValueListenableBuilder<AppIcon>(
valueListenable: AppIconConfig.current,
builder: (context, current, _) {
return Column(
children: [
for (final icon in AppIcon.values)
_IconTile(
icon: icon,
selected: current == icon,
onTap: () => _select(icon),
),
],
);
},
),
],
),
),
),
],
),
),
);
}
}
class _IconTile extends StatelessWidget {
final AppIcon icon;
final bool selected;
final VoidCallback onTap;
const _IconTile({
required this.icon,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Image.asset(
icon.previewAsset,
width: 56,
height: 56,
fit: BoxFit.cover,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
icon.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
Icon(
selected
? Symbols.radio_button_checked
: Symbols.radio_button_unchecked,
color: selected ? cs.primary : cs.outline,
size: 22,
fill: selected ? 1 : 0,
),
],
),
),
),
);
}
}
@@ -3,6 +3,7 @@ import 'package:m3e_collection/m3e_collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/utils/haptics.dart';
import 'app_icon_screen.dart';
import 'appearance_screen.dart';
import 'font_settings_screen.dart';
import 'message_actions_screen.dart';
@@ -50,6 +51,12 @@ class CustomizationScreen extends StatelessWidget {
subtitle: 'Радиальное или список — для долгого нажатия на сообщение',
builder: _buildMessageActions,
),
_CustomizationCategory(
icon: Symbols.apps,
title: 'Иконка приложения',
subtitle: 'Default или Minimal — иконка на главном экране',
builder: _buildAppIcon,
),
];
static Widget _buildAppearance(BuildContext context) =>
@@ -64,6 +71,8 @@ class CustomizationScreen extends StatelessWidget {
static Widget _buildMessageActions(BuildContext context) =>
const MessageActionsScreen();
static Widget _buildAppIcon(BuildContext context) => const AppIconScreen();
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
@@ -1,68 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
"info": {
"version": 1,
"author": "xcode"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
"images": [
{
"size": "16x16",
"idiom": "mac",
"filename": "app_icon_16.png",
"scale": "1x"
},
{
"size": "16x16",
"idiom": "mac",
"filename": "app_icon_32.png",
"scale": "2x"
},
{
"size": "32x32",
"idiom": "mac",
"filename": "app_icon_32.png",
"scale": "1x"
},
{
"size": "32x32",
"idiom": "mac",
"filename": "app_icon_64.png",
"scale": "2x"
},
{
"size": "128x128",
"idiom": "mac",
"filename": "app_icon_128.png",
"scale": "1x"
},
{
"size": "128x128",
"idiom": "mac",
"filename": "app_icon_256.png",
"scale": "2x"
},
{
"size": "256x256",
"idiom": "mac",
"filename": "app_icon_256.png",
"scale": "1x"
},
{
"size": "256x256",
"idiom": "mac",
"filename": "app_icon_512.png",
"scale": "2x"
},
{
"size": "512x512",
"idiom": "mac",
"filename": "app_icon_512.png",
"scale": "1x"
},
{
"size": "512x512",
"idiom": "mac",
"filename": "app_icon_1024.png",
"scale": "2x"
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

+40 -8
View File
@@ -97,6 +97,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -310,6 +326,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -453,6 +477,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
@@ -569,10 +601,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mobile_scanner:
dependency: "direct main"
description:
@@ -950,10 +982,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
timezone:
dependency: "direct main"
description:
@@ -1006,10 +1038,10 @@ packages:
dependency: transitive
description:
name: video_player_android
sha256: "5d18d04084cc0cfc7afde39d0a308d4041e8ae6e9d5255bc086c263998dd1201"
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
url: "https://pub.dev"
source: hosted
version: "2.9.6"
version: "2.9.5"
video_player_avfoundation:
dependency: transitive
description:
@@ -1091,5 +1123,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
dart: ">=3.10.4 <4.0.0"
flutter: ">=3.38.4"
+19
View File
@@ -74,6 +74,23 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
flutter_launcher_icons:
image_path: "assets/komet_icon.png"
android: "ic_launcher"
ios: true
remove_alpha_ios: true
background_color_ios: "#000000"
adaptive_icon_background: "#000000"
adaptive_icon_foreground: "assets/komet.png"
windows:
generate: true
image_path: "assets/komet_icon.png"
icon_size: 256
macos:
generate: true
image_path: "assets/komet_icon.png"
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -88,6 +105,8 @@ flutter:
uses-material-design: true
assets:
- assets/komet.png
- assets/komet_icon.png
- assets/meteor_icon.png
# To add assets to your application, add an assets section, like this:
# assets:
+20
View File
@@ -0,0 +1,20 @@
import 'dart:io';
import 'package:image/image.dart' as img;
const _pairs = {
'assets/komet.png': 'assets/komet_icon.png',
'assets/meteor.png': 'assets/meteor_icon.png',
};
void main() {
for (final entry in _pairs.entries) {
final src = img.decodePng(File(entry.key).readAsBytesSync())!;
final size = src.width > src.height ? src.width : src.height;
final canvas = img.Image(width: size, height: size, numChannels: 4);
img.fill(canvas, color: img.ColorRgb8(0, 0, 0));
img.compositeImage(canvas, src,
dstX: (size - src.width) ~/ 2, dstY: (size - src.height) ~/ 2);
File(entry.value).writeAsBytesSync(img.encodePng(canvas));
stdout.writeln('wrote ${entry.value} (${size}x$size)');
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'dart:io';
import 'package:image/image.dart' as img;
const _densities = {
'mdpi': 108,
'hdpi': 162,
'xhdpi': 216,
'xxhdpi': 324,
'xxxhdpi': 432,
};
void main() {
final src = img.decodePng(File('assets/meteor.png').readAsBytesSync())!;
for (final entry in _densities.entries) {
final canvas =
img.Image(width: entry.value, height: entry.value, numChannels: 4);
final scaled = img.copyResize(src,
width: entry.value,
height: entry.value,
interpolation: img.Interpolation.cubic);
img.compositeImage(canvas, scaled);
final file = File(
'android/app/src/main/res/drawable-${entry.key}/ic_launcher_minimal_foreground.png');
file.writeAsBytesSync(img.encodePng(canvas));
stdout.writeln('wrote ${file.path} (${entry.value}x${entry.value})');
}
}
+25
View File
@@ -0,0 +1,25 @@
import 'dart:io';
import 'package:image/image.dart' as img;
const _densities = {
'mdpi': 48,
'hdpi': 72,
'xhdpi': 96,
'xxhdpi': 144,
'xxxhdpi': 192,
};
void main() {
final src = img.decodePng(File('assets/meteor_icon.png').readAsBytesSync())!;
for (final entry in _densities.entries) {
final resized = img.copyResize(src,
width: entry.value,
height: entry.value,
interpolation: img.Interpolation.average);
final dir =
Directory('android/app/src/main/res/mipmap-${entry.key}');
final file = File('${dir.path}/ic_launcher_minimal.png');
file.writeAsBytesSync(img.encodePng(resized));
stdout.writeln('wrote ${file.path} (${entry.value}x${entry.value})');
}
}
+19
View File
@@ -0,0 +1,19 @@
import 'dart:io';
import 'package:image/image.dart' as img;
const _sizes = {
'ios/Runner/MinimalIcon@2x.png': 120,
'ios/Runner/MinimalIcon@3x.png': 180,
};
void main() {
final src = img.decodePng(File('assets/meteor_icon.png').readAsBytesSync())!;
for (final entry in _sizes.entries) {
final resized = img.copyResize(src,
width: entry.value,
height: entry.value,
interpolation: img.Interpolation.cubic);
File(entry.key).writeAsBytesSync(img.encodePng(resized));
stdout.writeln('wrote ${entry.key} (${entry.value}x${entry.value})');
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 16 KiB