wip: iOS refactor — iMessage extension, sticker browser, app icon assets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
146
PLAN.md
Normal file
146
PLAN.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Plan: standalone iOS app + invisible iMessage sticker extension
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
- **iOS app** on the home screen: paste Telegram URL, download packs, preview stickers, toggle individual stickers on/off
|
||||||
|
- **iMessage extension** is invisible — no custom UI. It serves stickers via `MSStickerBrowserViewController` so they appear in the system sticker picker (emoji keyboard → stickers tab on iOS 17+)
|
||||||
|
- Data flows from the iOS app → shared App Group container → extension reads it
|
||||||
|
|
||||||
|
## Why the 404 happens
|
||||||
|
|
||||||
|
The backend always calls `manager.getPack()` (Telegram Bot API) before checking its cache. If the bot token is invalid or Telegram is unreachable, it returns `None` → 404, even when sticker files exist on disk. This is a **backend bug** — fix separately. The iOS URL construction is correct.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ iOS App (home screen) │ │ iMessage Extension │
|
||||||
|
│ │ │ (no custom UI) │
|
||||||
|
│ - Paste URL │ │ │
|
||||||
|
│ - Download packs │ ──► │ MSStickerBrowserVC │
|
||||||
|
│ - Preview stickers │ │ reads shared container │
|
||||||
|
│ - Toggle on/off │ │ → stickers appear in │
|
||||||
|
│ - Delete packs │ │ system sticker picker │
|
||||||
|
└─────────────────────────┘ └──────────────────────────┘
|
||||||
|
│ │
|
||||||
|
└──────── App Group ───────────────┘
|
||||||
|
UserDefaults(suiteName:)
|
||||||
|
shared Caches/stickers/
|
||||||
|
```
|
||||||
|
|
||||||
|
## File layout after changes
|
||||||
|
|
||||||
|
```
|
||||||
|
StickerCloner/
|
||||||
|
├── Shared/ ← NEW synced folder, both targets
|
||||||
|
│ ├── Models.swift moved + updated
|
||||||
|
│ └── StickerStore.swift moved + rewritten (no Messages import)
|
||||||
|
│
|
||||||
|
├── StickerCloner/ ← iOS app target (auto-synced)
|
||||||
|
│ ├── Assets.xcassets/ existing
|
||||||
|
│ ├── StickerClonerApp.swift NEW — SwiftUI @main entry
|
||||||
|
│ ├── PackListView.swift NEW — list of packs + URL input
|
||||||
|
│ ├── PackDetailView.swift NEW — sticker grid with toggles
|
||||||
|
│ └── StickerClonerAPI.swift moved from extension (only app needs it)
|
||||||
|
│
|
||||||
|
├── StickerCloner MessagesExtension/ ← extension target (auto-synced)
|
||||||
|
│ ├── Assets.xcassets/ existing
|
||||||
|
│ ├── Base.lproj/MainInterface.storyboard existing
|
||||||
|
│ ├── Info.plist existing
|
||||||
|
│ ├── MessagesViewController.swift rewritten (minimal)
|
||||||
|
│ └── StickerBrowserViewController.swift rewritten (reads shared data)
|
||||||
|
│
|
||||||
|
└── StickerCloner.xcodeproj/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project file (pbxproj) changes
|
||||||
|
|
||||||
|
1. **Change main target product type**: `com.apple.product-type.application.messages` → `com.apple.product-type.application`
|
||||||
|
2. **Add Sources build phase** to main target (empty — synced groups handle inclusion)
|
||||||
|
3. **Add `Shared/` PBXFileSystemSynchronizedRootGroup** to both targets' `fileSystemSynchronizedGroups`
|
||||||
|
4. **Add App Group entitlements** via `CODE_SIGN_ENTITLEMENTS` build setting on both targets
|
||||||
|
|
||||||
|
## Data model changes
|
||||||
|
|
||||||
|
### Models.swift (Shared/)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// API types (unchanged)
|
||||||
|
struct StickerSetResponse: Codable { ... }
|
||||||
|
struct StickerResponse: Codable { ... }
|
||||||
|
|
||||||
|
// Local persistence — now tracks enabled stickers
|
||||||
|
struct SavedPack: Codable {
|
||||||
|
let name: String
|
||||||
|
let title: String
|
||||||
|
let stickerIds: [String] // all sticker IDs in order
|
||||||
|
var enabledStickerIds: Set<String> // subset the user wants
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### StickerStore.swift (Shared/)
|
||||||
|
|
||||||
|
- Uses `UserDefaults(suiteName: "group.de.felixfoertsch.StickerCloner")` instead of `.standard`
|
||||||
|
- Uses `FileManager.containerURL(forSecurityApplicationGroupIdentifier:)` instead of caches directory
|
||||||
|
- `stickerFileURLs(for pack:)` returns `[URL]` (not MSSticker — no Messages dependency)
|
||||||
|
- Returns only enabled stickers
|
||||||
|
|
||||||
|
### StickerClonerAPI.swift (iOS app only)
|
||||||
|
|
||||||
|
Stays in the app target — the extension never calls the API. Unchanged except move location.
|
||||||
|
|
||||||
|
## iOS App views
|
||||||
|
|
||||||
|
### StickerClonerApp.swift
|
||||||
|
- `@main` SwiftUI app
|
||||||
|
- Single `PackListView` as root in NavigationStack
|
||||||
|
|
||||||
|
### PackListView.swift
|
||||||
|
- Text field + "Add" button to paste Telegram URL or pack name
|
||||||
|
- List of saved packs with swipe-to-delete
|
||||||
|
- Navigation link to `PackDetailView` per pack
|
||||||
|
- Loading/error states during download
|
||||||
|
|
||||||
|
### PackDetailView.swift
|
||||||
|
- Grid of sticker thumbnails (loaded from local PNG files)
|
||||||
|
- Tap to toggle enabled/disabled (dimmed + checkmark overlay)
|
||||||
|
- "Select All" / "Deselect All" toolbar buttons
|
||||||
|
- Pack title in navigation bar
|
||||||
|
|
||||||
|
## Extension changes
|
||||||
|
|
||||||
|
### MessagesViewController.swift
|
||||||
|
- Hosts `StickerBrowserViewController` as child, fills entire view
|
||||||
|
- No expanded mode handling, no SwiftUI views
|
||||||
|
- Reloads stickers on `willBecomeActive`
|
||||||
|
|
||||||
|
### StickerBrowserViewController.swift
|
||||||
|
- Loads enabled sticker file URLs from `StickerStore`
|
||||||
|
- Creates `MSSticker` from each URL
|
||||||
|
- Standard `MSStickerBrowserViewController` data source
|
||||||
|
|
||||||
|
## Entitlements
|
||||||
|
|
||||||
|
Both targets get identical entitlements:
|
||||||
|
```xml
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.de.felixfoertsch.StickerCloner</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
1. `convert to standalone iOS app, add shared folder, app group entitlements`
|
||||||
|
2. `add iOS app ui: pack list, pack detail with sticker filtering`
|
||||||
|
3. `simplify iMessage extension to read-only sticker browser`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Build in Xcode → no compile errors for both targets
|
||||||
|
2. iOS app appears on home screen
|
||||||
|
3. Paste pack URL → downloads (once backend 404 is fixed)
|
||||||
|
4. Tap pack → see sticker grid, toggle individual stickers
|
||||||
|
5. Open Messages → stickers appear in sticker picker
|
||||||
|
6. Only enabled stickers shown
|
||||||
|
7. Kill app, reopen → state persists via App Group
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
final class StickerStore {
|
final class StickerStore {
|
||||||
static let shared = StickerStore()
|
static let shared = StickerStore()
|
||||||
@@ -6,6 +7,7 @@ final class StickerStore {
|
|||||||
private let packsKey = "savedPacks"
|
private let packsKey = "savedPacks"
|
||||||
private let suiteName = "group.de.felixfoertsch.StickerCloner"
|
private let suiteName = "group.de.felixfoertsch.StickerCloner"
|
||||||
private let fileManager = FileManager.default
|
private let fileManager = FileManager.default
|
||||||
|
private let thumbnailCache = NSCache<NSString, UIImage>()
|
||||||
|
|
||||||
private lazy var defaults: UserDefaults = {
|
private lazy var defaults: UserDefaults = {
|
||||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
@@ -66,6 +68,7 @@ final class StickerStore {
|
|||||||
var packs = savedPacks
|
var packs = savedPacks
|
||||||
packs.removeAll { $0.name == name }
|
packs.removeAll { $0.name == name }
|
||||||
savedPacks = packs
|
savedPacks = packs
|
||||||
|
thumbnailCache.removeObject(forKey: name as NSString)
|
||||||
|
|
||||||
let packDir = packDirectory(name: name)
|
let packDir = packDirectory(name: name)
|
||||||
try? fileManager.removeItem(at: packDir)
|
try? fileManager.removeItem(at: packDir)
|
||||||
@@ -89,6 +92,14 @@ final class StickerStore {
|
|||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stickerFileURLs(forPack name: String) -> [URL] {
|
||||||
|
let packDir = packDirectory(name: name)
|
||||||
|
guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return [] }
|
||||||
|
return files
|
||||||
|
.filter { $0.pathExtension == "png" }
|
||||||
|
.sorted { $0.lastPathComponent < $1.lastPathComponent }
|
||||||
|
}
|
||||||
|
|
||||||
func firstStickerURL(forPack name: String) -> URL? {
|
func firstStickerURL(forPack name: String) -> URL? {
|
||||||
let packDir = packDirectory(name: name)
|
let packDir = packDirectory(name: name)
|
||||||
guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return nil }
|
guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return nil }
|
||||||
@@ -98,6 +109,16 @@ final class StickerStore {
|
|||||||
.first
|
.first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func thumbnail(forPack name: String) -> UIImage? {
|
||||||
|
let key = name as NSString
|
||||||
|
if let cached = thumbnailCache.object(forKey: key) { return cached }
|
||||||
|
guard let url = firstStickerURL(forPack: name),
|
||||||
|
let data = try? Data(contentsOf: url),
|
||||||
|
let image = UIImage(data: data) else { return nil }
|
||||||
|
thumbnailCache.setObject(image, forKey: key)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Migration from old sandbox
|
// MARK: - Migration from old sandbox
|
||||||
|
|
||||||
private func migrateIfNeeded() {
|
private func migrateIfNeeded() {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ final class MessagesViewController: MSMessagesAppViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
addStickerBrowser()
|
addStickerBrowser()
|
||||||
stickerBrowser.reloadStickers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func willBecomeActive(with conversation: MSConversation) {
|
override func willBecomeActive(with conversation: MSConversation) {
|
||||||
|
|||||||
@@ -2,7 +2,23 @@ import Messages
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class StickerBrowserViewController: MSStickerBrowserViewController {
|
final class StickerBrowserViewController: MSStickerBrowserViewController {
|
||||||
private var stickers: [MSSticker] = []
|
private struct Pack {
|
||||||
|
let savedPack: SavedPack
|
||||||
|
let thumbnail: UIImage?
|
||||||
|
let stickers: [MSSticker]
|
||||||
|
}
|
||||||
|
|
||||||
|
private var packs: [Pack] = []
|
||||||
|
private var selectedPackIndex: Int? // nil = all packs
|
||||||
|
private var displayedStickers: [MSSticker] = []
|
||||||
|
|
||||||
|
private var tabCollectionView: UICollectionView!
|
||||||
|
|
||||||
|
private let tabSize: CGFloat = 36
|
||||||
|
private let tabSpacing: CGFloat = 6
|
||||||
|
private let tabInset: CGFloat = 8
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(stickerSize: .small)
|
super.init(stickerSize: .small)
|
||||||
@@ -13,24 +29,196 @@ final class StickerBrowserViewController: MSStickerBrowserViewController {
|
|||||||
fatalError("init(coder:) is not supported")
|
fatalError("init(coder:) is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupTabBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
func reloadStickers() {
|
func reloadStickers() {
|
||||||
let urls = StickerStore.shared.stickerFileURLs()
|
packs = StickerStore.shared.savedPacks
|
||||||
stickers = urls.compactMap { url in
|
.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
|
||||||
try? MSSticker(
|
.compactMap { pack in
|
||||||
contentsOfFileURL: url,
|
let urls = StickerStore.shared.stickerFileURLs(forPack: pack.name)
|
||||||
localizedDescription: url.deletingLastPathComponent().lastPathComponent
|
let stickers = urls.compactMap { url in
|
||||||
)
|
try? MSSticker(contentsOfFileURL: url, localizedDescription: pack.title)
|
||||||
|
}
|
||||||
|
guard !stickers.isEmpty else { return nil }
|
||||||
|
let thumbnail = StickerStore.shared.thumbnail(forPack: pack.name)
|
||||||
|
return Pack(savedPack: pack, thumbnail: thumbnail, stickers: stickers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let index = selectedPackIndex, index >= packs.count {
|
||||||
|
selectedPackIndex = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplayedStickers()
|
||||||
|
tabCollectionView?.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateDisplayedStickers() {
|
||||||
|
if let index = selectedPackIndex {
|
||||||
|
displayedStickers = packs[index].stickers
|
||||||
|
} else {
|
||||||
|
displayedStickers = packs.flatMap(\.stickers)
|
||||||
}
|
}
|
||||||
stickerBrowserView.reloadData()
|
stickerBrowserView.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Tab bar
|
||||||
|
|
||||||
|
private func setupTabBar() {
|
||||||
|
let tabBarHeight = tabSize + tabInset * 2
|
||||||
|
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
layout.minimumInteritemSpacing = tabSpacing
|
||||||
|
layout.minimumLineSpacing = tabSpacing
|
||||||
|
layout.sectionInset = UIEdgeInsets(top: 0, left: tabInset, bottom: 0, right: tabInset)
|
||||||
|
layout.itemSize = CGSize(width: tabSize, height: tabSize)
|
||||||
|
|
||||||
|
tabCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
tabCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tabCollectionView.backgroundColor = .secondarySystemBackground
|
||||||
|
tabCollectionView.showsHorizontalScrollIndicator = false
|
||||||
|
tabCollectionView.dataSource = self
|
||||||
|
tabCollectionView.delegate = self
|
||||||
|
tabCollectionView.register(TabCell.self, forCellWithReuseIdentifier: TabCell.reuseID)
|
||||||
|
|
||||||
|
let separator = UIView()
|
||||||
|
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
separator.backgroundColor = .separator
|
||||||
|
|
||||||
|
view.addSubview(tabCollectionView)
|
||||||
|
view.addSubview(separator)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tabCollectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tabCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tabCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tabCollectionView.heightAnchor.constraint(equalToConstant: tabBarHeight),
|
||||||
|
|
||||||
|
separator.topAnchor.constraint(equalTo: tabCollectionView.bottomAnchor),
|
||||||
|
separator.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
separator.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
separator.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale),
|
||||||
|
])
|
||||||
|
|
||||||
|
additionalSafeAreaInsets = UIEdgeInsets(top: tabBarHeight, left: 0, bottom: 0, right: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectPack(at index: Int?) {
|
||||||
|
guard selectedPackIndex != index else { return }
|
||||||
|
selectedPackIndex = index
|
||||||
|
updateDisplayedStickers()
|
||||||
|
tabCollectionView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - MSStickerBrowserViewDataSource
|
// MARK: - MSStickerBrowserViewDataSource
|
||||||
|
|
||||||
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
|
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
|
||||||
stickers.count
|
displayedStickers.count
|
||||||
}
|
}
|
||||||
|
|
||||||
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
|
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
|
||||||
stickers[index]
|
displayedStickers[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UICollectionViewDataSource
|
||||||
|
|
||||||
|
extension StickerBrowserViewController: UICollectionViewDataSource {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
packs.count + 1 // +1 for "All" tab
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCell.reuseID, for: indexPath) as! TabCell
|
||||||
|
if indexPath.item == 0 {
|
||||||
|
let isSelected = selectedPackIndex == nil
|
||||||
|
cell.configureAsAll(isSelected: isSelected)
|
||||||
|
} else {
|
||||||
|
let pack = packs[indexPath.item - 1]
|
||||||
|
let isSelected = selectedPackIndex == indexPath.item - 1
|
||||||
|
cell.configure(image: pack.thumbnail, isSelected: isSelected)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UICollectionViewDelegate
|
||||||
|
|
||||||
|
extension StickerBrowserViewController: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
if indexPath.item == 0 {
|
||||||
|
selectPack(at: nil)
|
||||||
|
} else {
|
||||||
|
selectPack(at: indexPath.item - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TabCell
|
||||||
|
|
||||||
|
private final class TabCell: UICollectionViewCell {
|
||||||
|
static let reuseID = "TabCell"
|
||||||
|
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
private let label = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
contentView.layer.cornerRadius = 8
|
||||||
|
contentView.clipsToBounds = true
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(imageView)
|
||||||
|
|
||||||
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.isHidden = true
|
||||||
|
contentView.addSubview(label)
|
||||||
|
|
||||||
|
let inset: CGFloat = 4
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset),
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset),
|
||||||
|
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset),
|
||||||
|
|
||||||
|
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||||
|
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(image: UIImage?, isSelected: Bool) {
|
||||||
|
imageView.image = image
|
||||||
|
imageView.isHidden = false
|
||||||
|
label.isHidden = true
|
||||||
|
contentView.backgroundColor = isSelected ? .systemGray5 : .clear
|
||||||
|
imageView.alpha = isSelected ? 1.0 : 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureAsAll(isSelected: Bool) {
|
||||||
|
imageView.isHidden = true
|
||||||
|
label.isHidden = false
|
||||||
|
label.text = "All"
|
||||||
|
label.textColor = isSelected ? .label : .secondaryLabel
|
||||||
|
contentView.backgroundColor = isSelected ? .systemGray5 : .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
imageView.image = nil
|
||||||
|
imageView.isHidden = false
|
||||||
|
label.isHidden = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,13 +73,6 @@
|
|||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
3FD2D01B2F57098E00B76B50 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
3FD2CFFD2F57098D00B76B50 /* Frameworks */ = {
|
3FD2CFFD2F57098D00B76B50 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -88,6 +81,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3FD2D01B2F57098E00B76B50 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -226,14 +226,14 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
3FD2D01A2F57098E00B76B50 /* Sources */ = {
|
3FD2CFFC2F57098D00B76B50 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
3FD2CFFC2F57098D00B76B50 /* Sources */ = {
|
3FD2D01A2F57098E00B76B50 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -255,6 +255,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon";
|
||||||
|
ASSETCATALOG_COMPILER_STICKER_ICON_NAME = "iMessage App Icon";
|
||||||
CODE_SIGN_ENTITLEMENTS = "StickerCloner MessagesExtension/StickerClonerMessagesExtension.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "StickerCloner MessagesExtension/StickerClonerMessagesExtension.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -285,6 +286,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon";
|
||||||
|
ASSETCATALOG_COMPILER_STICKER_ICON_NAME = "iMessage App Icon";
|
||||||
CODE_SIGN_ENTITLEMENTS = "StickerCloner MessagesExtension/StickerClonerMessagesExtension.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "StickerCloner MessagesExtension/StickerClonerMessagesExtension.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|||||||
@@ -41,13 +41,30 @@ struct AddPackView: View {
|
|||||||
Text("Paste a Telegram sticker link or pack name.")
|
Text("Paste a Telegram sticker link or pack name.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !packs.isEmpty {
|
if packs.isEmpty {
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
urlText = "https://t.me/addstickers/MrBat"
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "star")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("MrBat")
|
||||||
|
Text("Tap to try this pack")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Suggestions")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Section("Saved Packs") {
|
Section("Saved Packs") {
|
||||||
ForEach(packs, id: \.name) { pack in
|
ForEach(packs, id: \.name) { pack in
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
if let url = StickerStore.shared.firstStickerURL(forPack: pack.name),
|
if let uiImage = StickerStore.shared.thumbnail(forPack: pack.name) {
|
||||||
let data = try? Data(contentsOf: url),
|
|
||||||
let uiImage = UIImage(data: data) {
|
|
||||||
Image(uiImage: uiImage)
|
Image(uiImage: uiImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
|
|||||||
Reference in New Issue
Block a user