-
Notifications
You must be signed in to change notification settings - Fork 45
Smilie Picker #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Smilie Picker #1213
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
fac63df
Implemented new smilie picker
JonET be89ca9
Update CI to use Xcode 16.0 on macOS 15
JonET e61d0ee
Add SmiliePicker files to Awful target
JonET c0df0ff
Add Swift concurrency flag to CI tests
JonET 3049059
Revert test.yml changes to match main branch
JonET 51f0762
Addressing PR feedbach
JonET f4b11c3
Replace fake ellipsis with real one
JonET 4e5c5ae
remove premature smilie optimization
JonET 29a4488
Fix bug where smiley picker wouldn't reload the theme
JonET 514adcc
Remove *.backup from .gitignore
JonET 0be5ede
Replace manual theme setting with .themed() modifier to enable automa…
JonET 1c8f835
Removed SmilieData wrapper and refactored to use Smilie directly.
JonET File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| // AnimatedImageView.swift | ||
| // | ||
| // Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app | ||
|
|
||
| import SwiftUI | ||
| import FLAnimatedImage | ||
|
|
||
| // Helper class to store weak references | ||
| private class Weak<T: AnyObject> { | ||
| weak var value: T? | ||
| init(_ value: T) { | ||
| self.value = value | ||
| } | ||
| } | ||
|
|
||
| // Simple LRU cache for animated images | ||
| private class AnimatedImageCache { | ||
| static let shared = AnimatedImageCache() | ||
|
|
||
| private var cache = NSCache<NSString, FLAnimatedImage>() | ||
|
|
||
| init() { | ||
| cache.countLimit = 50 // Cache up to 50 images | ||
| cache.totalCostLimit = 50 * 1024 * 1024 // ~50MB | ||
| } | ||
|
|
||
| func image(for key: String) -> FLAnimatedImage? { | ||
| cache.object(forKey: key as NSString) | ||
| } | ||
|
|
||
| func setImage(_ image: FLAnimatedImage, for key: String) { | ||
| let cost = image.data?.count ?? 0 | ||
| cache.setObject(image, forKey: key as NSString, cost: cost) | ||
| } | ||
| } | ||
|
|
||
| /// SwiftUI wrapper for FLAnimatedImageView to display animated GIFs | ||
| struct AnimatedImageView: UIViewRepresentable { | ||
| let data: Data | ||
| let imageID: String // Unique identifier for this image | ||
|
|
||
| class Coordinator { | ||
| var currentTask: Task<Void, Never>? | ||
| var currentImageID: String? | ||
|
|
||
| func cancelCurrentTask() { | ||
| currentTask?.cancel() | ||
| currentTask = nil | ||
| } | ||
| } | ||
|
|
||
| func makeCoordinator() -> Coordinator { | ||
| Coordinator() | ||
| } | ||
|
|
||
| func makeUIView(context: Context) -> FLAnimatedImageView { | ||
| let imageView = FLAnimatedImageView() | ||
| imageView.contentMode = .scaleAspectFit | ||
| // Use nearest neighbor scaling to preserve pixelated aesthetic | ||
| imageView.layer.magnificationFilter = .nearest | ||
| imageView.layer.minificationFilter = .nearest | ||
| return imageView | ||
| } | ||
|
|
||
| func updateUIView(_ uiView: FLAnimatedImageView, context: Context) { | ||
| // Check if we need to load this image (either first time or different image) | ||
| let needsLoad = context.coordinator.currentImageID != imageID || | ||
| (uiView.animatedImage == nil && uiView.image == nil && context.coordinator.currentTask == nil) | ||
|
|
||
| if needsLoad { | ||
| // Cancel any existing task if loading a different image | ||
| if context.coordinator.currentImageID != imageID { | ||
| context.coordinator.cancelCurrentTask() | ||
| } | ||
| context.coordinator.currentImageID = imageID | ||
|
|
||
| // Clear the current images immediately | ||
| uiView.animatedImage = nil | ||
| uiView.image = nil | ||
|
|
||
| // Store weak reference to avoid retain cycles | ||
| let weakView = Weak(uiView) | ||
|
|
||
| // Load the new animated image asynchronously | ||
| let task = Task { | ||
| // Check cache first | ||
| var animatedImage = AnimatedImageCache.shared.image(for: imageID) | ||
|
|
||
| // If not in cache, load it | ||
| if animatedImage == nil { | ||
| animatedImage = await Task.detached(priority: .userInitiated) { | ||
| if let image = FLAnimatedImage(animatedGIFData: data) { | ||
| AnimatedImageCache.shared.setImage(image, for: imageID) | ||
| return image | ||
| } | ||
| return nil | ||
| }.value | ||
| } | ||
|
|
||
| // Only update if this task hasn't been cancelled and we're still showing the same image | ||
| if !Task.isCancelled && context.coordinator.currentImageID == imageID { | ||
| await MainActor.run { | ||
| // Double-check we're still showing the same image after switching to main actor | ||
| guard context.coordinator.currentImageID == imageID else { | ||
| return | ||
| } | ||
|
|
||
| // Ensure the view is still valid | ||
| guard let strongView = weakView.value else { | ||
| return | ||
| } | ||
|
|
||
| if let animatedImage = animatedImage { | ||
| let frameCount = animatedImage.frameCount | ||
| strongView.animatedImage = animatedImage | ||
|
|
||
| // For single-frame GIFs, FLAnimatedImageView might not display properly | ||
| // So we also set the static image | ||
| if frameCount == 1, let staticImage = UIImage(data: data) { | ||
| strongView.image = staticImage | ||
| } else { | ||
| strongView.image = nil | ||
| // Start animating if needed | ||
| if !strongView.isAnimating && frameCount > 1 { | ||
| strongView.startAnimating() | ||
| } | ||
| } | ||
|
|
||
| // Force layout update | ||
| strongView.setNeedsLayout() | ||
| strongView.setNeedsDisplay() | ||
| } else { | ||
| // Log failure but keep the view empty rather than showing an error | ||
| print("AnimatedImageView: Failed to create FLAnimatedImage for \(imageID)") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| context.coordinator.currentTask = task | ||
| } | ||
| } | ||
|
|
||
| static func dismantleUIView(_ uiView: FLAnimatedImageView, coordinator: Coordinator) { | ||
| coordinator.cancelCurrentTask() | ||
| uiView.stopAnimating() | ||
| uiView.animatedImage = nil | ||
| uiView.image = nil | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.