App Badge Selector

This commit is contained in:
Kylmakalle 2025-07-10 01:23:15 +03:00
parent b55782a5d1
commit 0a0a0f9eae
27 changed files with 372 additions and 42 deletions

View File

@ -0,0 +1,5 @@
filegroup(
name = "SGAppBadgeAssets",
srcs = glob(["Images.xcassets/**"]),
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Day@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Ducky@3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Night@3-1.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Pro@3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Sky@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Sparkling@3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Titanium@3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -12,7 +12,7 @@ import SwiftSignalKit
import TelegramUIPreferences
@available(iOS 13.0, *)
public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager, openUrl: @escaping (String, Bool) -> Void /* url, forceExternal */, paymentsEnabled: Bool, canBuyInBeta: Bool, openAppStorePage: @escaping () -> Void, proSupportUrl: String?) -> ViewController {
// let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let theme = defaultDarkColorPresentationTheme
@ -44,7 +44,7 @@ private let innerShadowWidth: CGFloat = 15.0
private let accentColorHex: String = "F1552E"
@available(iOS 13.0, *)
struct BackgroundView: View {
var body: some View {
@ -96,7 +96,7 @@ struct BackgroundView: View {
}
@available(iOS 13.0, *)
struct SGPayWallFeatureDetails: View {
let dismissAction: () -> Void
@ -225,7 +225,7 @@ struct SGPayWallFeatureDetails: View {
}
@available(iOS 13.0, *)
struct SGProFeatureView: View {
let feature: SGProFeature
@ -267,7 +267,7 @@ enum SGProFeatureId: Hashable {
}
@available(iOS 13.0, *)
struct SGProFeature: Identifiable {
let id: SGProFeatureId
@ -316,7 +316,7 @@ struct SGProFeature: Identifiable {
}
@available(iOS 13.0, *)
struct SGPayWallView: View {
@Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat
@Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout?
@ -362,11 +362,11 @@ struct SGPayWallView: View {
private var features: [SGProFeature] {
return [
SGProFeature(id: .backup, title: "PayWall.SessionBackup.Title".i18n(lang), subtitle: "PayWall.SessionBackup.Notice".i18n(lang), description: "PayWall.SessionBackup.Description".i18n(lang)),
SGProFeature(id: .filter, title: "PayWall.MessageFilter.Title".i18n(lang), subtitle: "PayWall.MessageFilter.Notice".i18n(lang), description: "PayWall.MessageFilter.Description".i18n(lang)),
SGProFeature(id: .notifications, title: "PayWall.Notifications.Title".i18n(lang), subtitle: "PayWall.Notifications.Notice".i18n(lang), description: "PayWall.Notifications.Description".i18n(lang)),
SGProFeature(id: .toolbar, title: "PayWall.InputToolbar.Title".i18n(lang), subtitle: "PayWall.InputToolbar.Notice".i18n(lang), description: "PayWall.InputToolbar.Description".i18n(lang)),
SGProFeature(id: .icons, title: "PayWall.AppIcons.Title".i18n(lang), subtitle: "PayWall.AppIcons.Notice".i18n(lang), description: nil)
SGProFeature(id: .filter, title: "PayWall.MessageFilter.Title".i18n(lang), subtitle: "PayWall.MessageFilter.Notice".i18n(lang), description: "PayWall.MessageFilter.Description".i18n(lang)),
SGProFeature(id: .icons, title: "PayWall.AppIcons.Title".i18n(lang), subtitle: "PayWall.AppIcons.Notice".i18n(lang), description: nil),
SGProFeature(id: .backup, title: "PayWall.SessionBackup.Title".i18n(lang), subtitle: "PayWall.SessionBackup.Notice".i18n(lang), description: "PayWall.SessionBackup.Description".i18n(lang)),
SGProFeature(id: .notifications, title: "PayWall.Notifications.Title".i18n(lang), subtitle: "PayWall.Notifications.Notice".i18n(lang), description: "PayWall.Notifications.Description".i18n(lang)),
]
}
@ -727,7 +727,7 @@ struct SGPayWallView: View {
}
@available(iOS 13.0, *)
struct FeatureIcon: View {
let icon: String
let iconColor: Color
@ -764,7 +764,7 @@ struct FeatureIcon: View {
}
@available(iOS 13.0, *)
struct FeatureRow<IconContent: View>: View {
let icon: IconContent
let title: String
@ -810,7 +810,7 @@ struct FeatureRow<IconContent: View>: View {
// Confetti
@available(iOS 13.0, *)
struct ConfettiType {
let color: Color
let shape: ConfettiShape
@ -824,7 +824,7 @@ struct ConfettiType {
}
}
@available(iOS 13.0, *)
enum ConfettiShape: CaseIterable {
case circle
case triangle
@ -849,7 +849,7 @@ enum ConfettiShape: CaseIterable {
}
}
@available(iOS 13.0, *)
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
@ -861,7 +861,7 @@ struct Triangle: Shape {
}
}
@available(iOS 13.0, *)
public struct SlimRectangle: Shape {
public func path(in rect: CGRect) -> Path {
var path = Path()
@ -875,7 +875,7 @@ public struct SlimRectangle: Shape {
}
}
@available(iOS 13.0, *)
public struct RoundedCross: Shape {
public func path(in rect: CGRect) -> Path {
var path = Path()
@ -896,7 +896,7 @@ public struct RoundedCross: Shape {
}
}
@available(iOS 13.0, *)
struct ConfettiModifier: ViewModifier {
@Binding var isActive: Bool
let duration: Double
@ -917,7 +917,7 @@ struct ConfettiModifier: ViewModifier {
}
}
@available(iOS 13.0, *)
struct ConfettiPiece: View {
let confettiType: ConfettiType
let duration: Double
@ -943,7 +943,7 @@ struct ConfettiPiece: View {
}
}
@available(iOS 13.0, *)
struct FallingModifier: ViewModifier {
let distance: CGFloat
let duration: Double
@ -961,7 +961,7 @@ struct FallingModifier: ViewModifier {
}
}
@available(iOS 13.0, *)
struct MoveModifier: ViewModifier {
let offset: CGSize
let duration: Double
@ -985,7 +985,7 @@ struct MoveModifier: ViewModifier {
}
// Extension to make it easier to use
@available(iOS 13.0, *)
extension View {
func confetti(isActive: Binding<Bool>, duration: Double = 2.0) -> some View {
modifier(ConfettiModifier(isActive: isActive, duration: duration))

View File

@ -16,10 +16,12 @@ swift_library(
"//Swiftgram/SGLogging:SGLogging",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGStrings:SGStrings",
"//Swiftgram/SGAPI:SGAPI",
"//Swiftgram/SGAPI:SGAPI",
"//Swiftgram/SGAPIToken:SGAPIToken",
"//Swiftgram/SGSwiftUI:SGSwiftUI",
#
"//submodules/SettingsUI:SettingsUI",
#
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",

View File

@ -0,0 +1,149 @@
import Foundation
import SwiftUI
import SGSwiftUI
import SGStrings
import SGSimpleSettings
import LegacyUI
import Display
import TelegramPresentationData
import AccountContext
struct AppBadge: Identifiable, Hashable {
let id: UUID = .init()
let displayName: String
let assetName: String
}
func getAvailableAppBadges() -> [AppBadge] {
var appBadges: [AppBadge] = [
.init(displayName: "Default", assetName: "Components/AppBadge"),
.init(displayName: "Sky", assetName: "SkyAppBadge"),
.init(displayName: "Night", assetName: "NightAppBadge"),
.init(displayName: "Titanium", assetName: "TitaniumAppBadge"),
.init(displayName: "Pro", assetName: "ProAppBadge"),
.init(displayName: "Day", assetName: "DayAppBadge"),
]
if SGSimpleSettings.shared.duckyAppIconAvailable {
appBadges.append(.init(displayName: "Ducky", assetName: "DuckyAppBadge"))
}
appBadges += [
.init(displayName: "Sparkling", assetName: "SparklingAppBadge"),
]
return appBadges
}
@available(iOS 14.0, *)
struct AppBadgeSettingsView: View {
weak var wrapperController: LegacyController?
let context: AccountContext
@Environment(\.colorScheme) var colorScheme
@Environment(\.lang) var lang: String
@State var selectedBadge: AppBadge
let availableAppBadges: [AppBadge] = getAvailableAppBadges()
private enum Layout {
static let cardCorner: CGFloat = 12
static let imageHeight: CGFloat = 56
static let columnSpacing: CGFloat = 16
static let horizontalPadding: CGFloat = 20
}
private var columns: [SwiftUI.GridItem] {
Array(repeating: GridItem(.flexible(), spacing: Layout.columnSpacing), count: 2)
}
init(wrapperController: LegacyController?, context: AccountContext) {
self.wrapperController = wrapperController
self.context = context
for badge in self.availableAppBadges {
if badge.assetName == SGSimpleSettings.shared.customAppBadge {
self._selectedBadge = State(initialValue: badge)
return
}
}
self._selectedBadge = State(initialValue: self.availableAppBadges.first!)
}
private func onSelectBadge(_ badge: AppBadge) {
self.selectedBadge = badge
let image = UIImage(bundleImageName: selectedBadge.assetName) ?? UIImage(bundleImageName: "Components/AppBadge")
if self.context.sharedContext.immediateSGStatus.status > 1 {
DispatchQueue.main.async {
SGSimpleSettings.shared.customAppBadge = selectedBadge.assetName
self.context.sharedContext.mainWindow?.badgeView.image = image
}
}
}
var body: some View {
ScrollView {
LazyVGrid(columns: columns, alignment: .center, spacing: Layout.columnSpacing) {
ForEach(availableAppBadges) { badge in
Button {
onSelectBadge(badge)
} label: {
VStack(spacing: 8) {
Image(badge.assetName)
.resizable()
.scaledToFit()
.frame(height: Layout.imageHeight)
.accessibilityHidden(true)
Text(badge.displayName)
.font(.footnote)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(colorScheme == .dark ? .secondarySystemBackground : .systemBackground))
.cornerRadius(Layout.cardCorner)
.overlay(
RoundedRectangle(cornerRadius: Layout.cardCorner)
.stroke(selectedBadge == badge ? Color.accentColor : Color.clear, lineWidth: 2)
)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, Layout.horizontalPadding)
.padding(.vertical, 24)
}
.background(Color(colorScheme == .light ? .secondarySystemBackground : .systemBackground).ignoresSafeArea())
}
}
@available(iOS 14.0, *)
public func sgAppBadgeSettingsController(context: AccountContext, presentationData: PresentationData? = nil) -> ViewController {
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let strings = presentationData?.strings ?? defaultPresentationStrings
let legacyController = LegacySwiftUIController(
presentation: .navigation,
theme: theme,
strings: strings
)
legacyController.statusBar.statusBarStyle = theme.rootController
.statusBarStyle.style
legacyController.title = "AppBadge.Title".i18n(strings.baseLanguageCode)
let swiftUIView = SGSwiftUIView<AppBadgeSettingsView>(
legacyController: legacyController,
manageSafeArea: true,
content: {
AppBadgeSettingsView(wrapperController: legacyController, context: context)
}
)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}

View File

@ -11,6 +11,7 @@ import SwiftSignalKit
import TelegramPresentationData
import PresentationDataUtils
import TelegramUIPreferences
import SettingsUI
// Optional
import SGSimpleSettings
@ -19,6 +20,7 @@ import SGLogging
private enum SGProControllerSection: Int32, SGItemListSection {
case base
case appearance
case notifications
case footer
}
@ -26,6 +28,8 @@ private enum SGProControllerSection: Int32, SGItemListSection {
private enum SGProDisclosureLink: String {
case sessionBackupManager
case messageFilter
case appIcons
case appBages
}
private enum SGProToggles: String {
@ -56,6 +60,10 @@ private func SGProControllerEntries(presentationData: PresentationData) -> [SGPr
entries.append(.header(id: id.count, section: .notifications, text: presentationData.strings.Notifications_Title.uppercased(), badge: nil))
entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .pinnedMessageNotifications, text: "Notifications.PinnedMessages.Title".i18n(lang), value: "Notifications.PinnedMessages.value.\(SGSimpleSettings.shared.pinnedMessageNotifications)".i18n(lang), enabled: true))
entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .mentionsAndRepliesNotifications, text: "Notifications.MentionsAndReplies.Title".i18n(lang), value: "Notifications.MentionsAndReplies.value.\(SGSimpleSettings.shared.mentionsAndRepliesNotifications)".i18n(lang), enabled: true))
entries.append(.header(id: id.count, section: .appearance, text: presentationData.strings.Appearance_Title.uppercased(), badge: nil))
entries.append(.disclosure(id: id.count, section: .appearance, link: .appIcons, text: presentationData.strings.Appearance_AppIcon))
entries.append(.disclosure(id: id.count, section: .appearance, link: .appBages, text: "AppBadge.Title".i18n(lang)))
entries.append(.notice(id: id.count, section: .appearance, text: "AppBadge.Notice".i18n(lang)))
#if DEBUG
entries.append(.action(id: id.count, section: .footer, actionType: .resetIAP, text: "Reset Pro", kind: .destructive))
@ -124,14 +132,14 @@ public func sgProController(context: AccountContext) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch (link) {
case .sessionBackupManager:
if #available(iOS 13.0, *) {
pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData))
} else {
presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil)
}
pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData))
case .messageFilter:
if #available(iOS 13.0, *) {
pushControllerImpl?(sgMessageFilterController(presentationData: presentationData))
pushControllerImpl?(sgMessageFilterController(presentationData: presentationData))
case .appIcons:
pushControllerImpl?(themeSettingsController(context: context, focusOnItemTag: .icon))
case .appBages:
if #available(iOS 14.0, *) {
pushControllerImpl?(sgAppBadgeSettingsController(context: context, presentationData: presentationData))
} else {
presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil)
}

View File

@ -43,7 +43,8 @@ public class SGSimpleSettings {
{ let _ = self.startTelescopeWithRearCam },
{ let _ = self.hideRecordingButton },
{ let _ = self.inputToolbar },
{ let _ = self.dismissedSGSuggestions }
{ let _ = self.dismissedSGSuggestions },
{ let _ = self.customAppBadge }
]
tasks.forEach { task in
@ -134,6 +135,7 @@ public class SGSimpleSettings {
case duckyAppIconAvailable
case transcriptionBackend
case translationBackend
case customAppBadge
}
public enum DownloadSpeedBoostValues: String, CaseIterable {
@ -262,7 +264,8 @@ public class SGSimpleSettings {
Keys.dismissedSGSuggestions.rawValue: [],
Keys.duckyAppIconAvailable.rawValue: true,
Keys.transcriptionBackend.rawValue: TranscriptionBackend.default.rawValue,
Keys.translationBackend.rawValue: TranslationBackend.default.rawValue
Keys.translationBackend.rawValue: TranslationBackend.default.rawValue,
Keys.customAppBadge.rawValue: "",
]
public static let groupDefaultValues: [String: Any] = [
@ -485,6 +488,9 @@ public class SGSimpleSettings {
@UserDefault(key: Keys.translationBackend.rawValue)
public var translationBackend: String
@UserDefault(key: Keys.customAppBadge.rawValue)
public var customAppBadge: String
}
extension SGSimpleSettings {

View File

@ -62,6 +62,8 @@ public class UserDefault<T> /*where T: AllowedUserDefaultTypes*/ {
return (userDefaults.bool(forKey: key) as! T)
case is String.Type:
return (userDefaults.string(forKey: key) as! T)
case is Int64.Type:
return (Int64(exactly: userDefaults.integer(forKey: key)) as! T)
case is Int32.Type:
return (Int32(exactly: userDefaults.integer(forKey: key)) as! T)
case is Int.Type:

View File

@ -224,8 +224,8 @@
"PayWall.InputToolbar.Notice" = "Bold, Italic, Links? Formatting with just a single tap.";
"PayWall.InputToolbar.Description" = "Apply and clear Formatting or insert new lines like a Pro.";
"PayWall.AppIcons.Title" = "Unique App Icons";
"PayWall.AppIcons.Notice" = "Customize Swiftgram look on your home screen.";
"PayWall.AppIcons.Title" = "Unique App Icons and Badges";
"PayWall.AppIcons.Notice" = "Customize Swiftgram look on your home screen and screenshots.";
"PayWall.About.Title" = "About Swiftgram Pro";
"PayWall.About.Notice" = "Free version of Swiftgram provides dozens of features and improvements over Telegram app. Innovating and keeping Swiftgram in sync with monthly Telegram updates is a huge effort that requires a lot of time and expensive hardware.\n\nSwiftgram is an open-source app that respects your privacy and doesn't bother you with ads. Subscribing to Swiftgram Pro you get access to exclusive features and support an independent developer.";
@ -258,3 +258,6 @@
"PayWall.ValidationError" = "Validation Error";
"PayWall.ValidationError.TryAgain" = "Something went wrong during purchase validation. No worries! Try to Restore Purchases a bit later.";
"PayWall.ValidationError.Expired" = "Your subscription expired. Subscribe again to regain access to Pro features.";
"AppBadge.Title" = "App Badge";
"AppBadge.Notice" = "Customize App Badge shown on screenshots";

View File

@ -346,7 +346,7 @@ objc_library(
],
)
SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets", "//Swiftgram/SGPayWall:SGPayWallAssets"]
SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets", "//Swiftgram/SGPayWall:SGPayWallAssets", "//Swiftgram/SGAppBadgeAssets:SGAppBadgeAssets"]
swift_library(
name = "Lib",

View File

@ -5,7 +5,8 @@ sgsrc = [
]
sgdeps = [
"//submodules/Utils/DeviceModel"
"//submodules/Utils/DeviceModel",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
]
swift_library(

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import SGSimpleSettings
private struct WindowLayout: Equatable {
let size: CGSize
@ -339,7 +340,11 @@ public class Window1 {
public init(hostView: WindowHostView, statusBarHost: StatusBarHost?) {
self.hostView = hostView
self.badgeView = UIImageView()
if SGSimpleSettings.shared.status > 1, let image = UIImage(bundleImageName: SGSimpleSettings.shared.customAppBadge) {
self.badgeView.image = image
} else {
self.badgeView.image = UIImage(bundleImageName: "Components/AppBadge")
}
self.badgeView.isHidden = true
self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle

View File

@ -4102,12 +4102,8 @@ extension SharedAccountContextImpl {
public func makeSGUpdateIOSController() -> ViewController {
let presentationData = self.currentPresentationData.with { $0 }
let controller = UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Common.UpdateOS".i18n(presentationData.strings.baseLanguageCode), timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
)
let controller = textAlertController(sharedContext: self, title: nil, text: "Common.UpdateOS".i18n(presentationData.strings.baseLanguageCode), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})])
return controller
}
}