8000 implemented GIF preview feature issue #136 by mmulet · Pull Request #329 · sindresorhus/Gifski · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

implemented GIF preview feature issue #136 #329

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 41 commits into from
Jun 25, 2025
Merged

Conversation

mmulet
Copy link
Contributor
@mmulet mmulet commented Mar 22, 2025

Hi!
I implemented the gif preview features as requested in issue #136
Please watch this short video for a demonstration:

Gifsky_preview_feature.mov

Fixes #136

Here is what the feature does:

  • adds the Show Preview Texture toggle
  • Renders the current frame right away
  • Starts rendering the whole thing for real.
  • Once it is all rendered you can preview the render with the play button
  • Allows for scrubbing to render one frame at the scrubbed position.
  • Implemented in SwiftUI and Combine
    Looking at the issue GIF preview feature #136 this should fulfill all of your major requirements.

I will submit this to issueHunt for the ongoing bounty, but any extra tips (you
can use GitHub Sponsor page) would be very welcome!


IssueHunt Summary

Referenced issues

This pull request has been submitted to:


See Gifski/PreviewGenerator for an overview
@mmulet mmulet mentioned this pull request Mar 25, 2025
@sindresorhus
Copy link
Owner
  • The spinner besides the preview is too big.
  • Don't need the "Preview" text on the video. The checkbox makes it clear enough.
  • The "Show PReview" checkbox doesn't look that good.

@sindresorhus
Copy link
Owner

I ended up with empty preview many times:

Screen Shot 2025-04-09 at 03 48 30 Screen Shot 2025-04-09 at 03 34 10

Test video:

60fps.-.short.mp4

@mmulet
Copy link
Contributor Author
mmulet commented Apr 13, 2025

Changes

As usually here is a new demo video:

Screen.Recording.2025-04-12.at.10.20.51.PM.mov
  • The spinner besides the preview is too big.
  • Don't need the "Preview" text on the video. The checkbox makes it clear enough.
  • Improve Visual for the "Show Preview" checkbox
  • FIX: I ended up with empty preview many times:
    • fixed
  • Don't hide PlayButton, just disable
  • Implement reusable getter/setter to hide/show the PlayButton on Trimmer
  • Preview Logic OUtside of trimming AVPlayer
    • All the logic has been moved out of trimmingAVplayer and moved into the PreviewViewState
  • Don't hard-wrap, playback rate
  • Instead of PeriodicTimeObserver, Add a AVPlayer extension that wraps addPeriodicTimeObserver in a asyncstream and use that instead. It would make it easier to use. It could even accept a Duration instead, for improved usability.
    • Moved the peridicTimerObserver to a new stream focued on when the trimer scrubs to a new time
  • /// Instead of /**
  • Use Combine. for playerRateObserer (m)
  • PreviewGenerator to Observable
  • Cleanup PreviewGenerator
    • Much better now, switched form my old-timey thread loop to an AsyncStream. It has been completely reworked

I found some more things that needed to be fixed while I was at it:

  • Fix not having enough frames for a preview
  • Fix trimmed video callback happening on loopback, bounce, or play button press
  • Fix video playback not syncing up with preview
  • Fix scrubbing preview not pausing the gif (have to pause the gif because there are no native gif pausing)

One last thing:

While testing the preview, I noticed a bug where it would flash the wrong frame. I confirmed that this is not a problem with preview, it is a problem with the export itself, if you convert the video it has the same results, the same bug, you can see it here:

BounceFlashBug.mov

@sindresorhus
Copy link
Owner
  • In preview, if I pause in the middle and resume, it starts from the beginning, not the paused position.
  • The "Show preview" button should be a toggle with a SF Symbol icon, just like with crop.
    • It should also be in the menu bar menu, like with crop.
  • When the drag the position handle (the red line), I often see the huge progress indicator show, then hide, then show. It flickers.
  • When it has converted the whole video to a GIF and I play and then pause, it still shows the huge progress indicator momentarily. It should not show.
  • If I click "Show preview", let it convert the whole video to GIF, then press "Hide preview", and then "Show preview" again, it re-renders the whole GIF. Nothing changed, so it should be able to reuse the existing GIF. A user may want to be able to quickly toggle back and forth to compare quality with the original.
  • The code still needs a lot of minor cleanup. Don't hard-wrap code comments.
  • Would be great if you could go over the code and try to simplify it. It's becoming quite complex.

@sindresorhus
Copy link
Owner

While testing the preview, I noticed a bug where it would flash the wrong frame. I confirmed that this is not a problem with preview, it is a problem with the export itself, if you convert the video it has the same results, the same bug, you can see it here:

Seems like a bug in the "Bounce" logic.

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
@mmulet
Copy link
Contributor Author
mmulet commented Apr 17, 2025
  • In preview, if I pause in the middle and resume, it starts from the beginning, not the paused position.
  • The preview outputs a GIF, which is directly passed to a NSImage and there are no GIF playback controls. All I can do is start at beginning or stop. The only solution to this is to make a custom GIF SwiftUI player, perhaps with CGAnimateImageDataWithBlock(_:_:_:). I will probably take this approach.
  • When it has converted the whole video to a GIF and I play and then pause, it still shows the huge progress indicator momentarily. It should not show.
  • When you Pause it takes a moment to generate the preview image at the current pause time. If I switch to a custom GIF player, this shouldn't be a problem anymore.

mmulet and others added 4 commits April 17, 2025 11:13
@sindresorhus
Copy link
Owner

The preview outputs a GIF, which is directly passed to a NSImage and there are no GIF playback controls. All I can do is start at beginning or stop. The only solution to this is to make a custom GIF SwiftUI player, perhaps with CGAnimateImageDataWithBlock(::_:). I will probably take this approach.

Wouldn't it be easier to make a video out of the GIF frames? Then you could reuse everything we already have since it's just a video.

@mmulet
Copy link
Contributor Author
mmulet commented Apr 17, 2025

The preview outputs a GIF, which is directly passed to a NSImage and there are no GIF playback controls. All I can do is start at beginning or stop. The only solution to this is to make a custom GIF SwiftUI player, perhaps with CGAnimateImageDataWithBlock(::_:). I will probably take this approach.

Wouldn't it be easier to make a video out of the GIF frames? Then you could reuse everything we already have since it's just a video.

I actually did try that earlier but I ran into 2 problems:

  1. Re-encoding the gif increased the time to preview by at least 2x.
  2. If I used a lossless format to preserve the preview exactly, the memory usage ballooned.
  • If I used a lossy format, how does the user know if a gif preview looks bad due to compression artifacts from the lossy preview or due to the my gif quality?

@mmulet
Copy link
Contributor Author
mmulet commented Apr 17, 2025

My video branch is here although it's not complete (doesn't have all the fixes from last time)

edit: It also doesn't actually run right now, it still has some bugs to be ironed out.

@mmulet
Copy link
Contributor Author
mmulet commented Apr 17, 2025

I'll just try both, starting with the video, and if I can't solve my problems, I'll move on to the custom GIF player.

@mmulet
Copy link
Contributor Author
mmulet commented Apr 19, 2025

I uploaded the commit with the video player, I would say it's in progress, but not finished.

Overall, I have mixed feelings about it.

On one hand, it does simplify things quite a bit, I was able to simplify most of the code into pure functions and I moved almost everything out of TrimmingAVPlayer, and there is no "PreviewState", "PreviewGenerator", or "LatestItemStream".

edit 04/21: I think I have a solution to all these problems. I will upload it in the next couple of days.

On the other, AVPlayerView is generally glitchy and hard to work with. I've made some good progress and squashed the major bugs, but I'm sure you will have no problem finding more.

For example:

  • When the preview generates, and we switch the AVAssets to the TrimmingAVPlayer, it causes a flash of (unrelated) video content to the screen. For the sake of toggling on and off the preview, I got around this by moving the preview view to a second track in a AVMutableComposition, and creating two separate AVMutableVideoComposition that you can switch at runtime. So that you can toggle the preview on/off, the asset stays the same, it's just changes videoComposition.

I had hoped I could do something similar for the EditScreen overall. Instead of swapping in and out AVAssets to the TrimmingAVPlayer, I could have one AVMutableComposition and I could swap in and out the tracks as you update the preview. But, I couldn't get this to work reliably, I had assumed you could just

track.removeTimeRange(...)
track.insertTimeRange(updatedPreview)

but I just end up with blank screen. And everything else I tried all needed an updated AVPlayerItem, which just got me back to where I started.

edit on 04/21:
Reading through the docs, I think the issue might be that AVPlayerItem expects an immutable snapshot of the composition. If I want to modify it, I should first make a copy pass that to the player Item


Last but not least, when you export a preview of a smaller size, it surrounds the entire rest of the video with a black background. This background, as far as I can tell, is baked into the AVPlayer. Nothing I have tried has gotten rid of it. There are only two solutions that I can think of:

  1. Create a video of the checkerboard pattern, calculated to the exact height and width needed to blend into the background and add another track below all the previous tracks, and place it there.
  2. Give up having the preview and the regular video in one AVMutableComposition, but then we have the AVAsset swapping problem mentioned above. (we would also have to introduce emptyRanges into the track so the trimmer view would match up between preview and normal mode, and I'm not sure if this would also cause a black box to appear over the background)

@sindresorhus
Copy link
Owner

I got the actor commit to crash too:

Thread 39: EXC_BAD_ACCESS (code=1, address=0xf000000001fc6b27)

Thread 39 Queue : com.apple.root.user-initiated-qos.cooperative (concurrent)
#0	0x0000000104bb73ac in std::__1::__hash_table<std::__1::__hash_value_type<long, qos_info_t>, std::__1::__unordered_map_hasher<long, std::__1::__hash_value_type<long, qos_info_t>, std::__1::hash<long>, std::__1::equal_to<long>, true>, std::__1::__unordered_map_equal<long, std::__1::__hash_value_type<long, qos_info_t>, std::__1::equal_to<long>, std::__1::hash<long>, true>, std::__1::allocator<std::__1::__hash_value_type<long, qos_info_t>>>::__do_rehash<true> ()
#1	0x0000000104bbb96c in std::__1::__hash_table<std::__1::__hash_value_type<long, qos_info_t>, std::__1::__unordered_map_hasher<long, std::__1::__hash_value_type<long, qos_info_t>, std::__1::hash<long>, std::__1::equal_to<long>, true>, std::__1::__unordered_map_equal<long, std::__1::__hash_value_type<long, qos_info_t>, std::__1::equal_to<long>, std::__1::hash<long>, true>, std::__1::allocator<std::__1::__hash_value_type<long, qos_info_t>>>::__emplace_unique_key_args<long, std::__1::piecewise_construct_t const&, std::__1::tuple<long const&>, std::__1::tuple<>> ()
#2	0x0000000104bba788 in createPrimitiveEntry ()
#3	0x0000000104bb5644 in interposed_dispatch_semaphore_create ()
#4	0x00000001b034ca4c in IOGPUNotificationQueueCreate ()
#5	0x00000001b0356f0c in IOGPUCommandQueueCreateWithQoS ()
#6	0x00000001b034367c in -[IOGPUMetalCommandQueue initWithDevice:descriptor:] ()
#7	0x0000000117ddc81c in -[AGXG16XFamilyCommandQueue initWithDevice:descriptor:] ()
#8	0x0000000117e0bae0 in -[AGXG16XFamilyDevice newCommandQueue] ()
#9	0x000000018eea9b9c in -[MTLDebugDevice newCommandQueue] ()
#10	0x000000019a70c3d4 in CIMetalCommandQueueCreate ()
#11	0x000000019a70c358 in +[CIContext(Internal) internalContextWithMTLDevice:options:] ()
#12	0x000000019a70c02c in -[CIContext initWithOptions:] ()
#13	0x0000000105fa1560 in @nonobjc CIContext.init() ()
#14	0x0000000105fa0ce4 in CIContext.__allocating_init() ()
#15	0x0000000105fa0534 in PreviewRenderer.renderPreview(previewImage:outputFrame:previewCheckerboardParams:) at /Users/sindresorhus/dev/oss/Gifski/Gifski/Preview/PreviewRenderer.swift:74
#16	0x0000000105f9fd44 in PreviewRenderer.renderPreview(previewFrame:outputFrame:previewCheckerboardParams:) at /Users/sindresorhus/dev/oss/Gifski/Gifski/Preview/PreviewRenderer.swift:50
#17	0x0000000105f9f764 in static PreviewRenderer.renderPreview(previewFrame:outputFrame:previewCheckerboardParams:) at /Users/sindresorhus/dev/oss/Gifski/Gifski/Preview/PreviewRenderer.swift:25
#18	0x0000000105fd1468 in PreviewVideoCompositor.RenderPreview.render(state:fullPreviewFrame:originalFrame:outputFrame:compositionTime:) at /Users/sindresorhus/dev/oss/Gifski/Gifski/Preview/PreviewVideoCompositor.swift:138
#19	0x0000000105fcf248 in closure #1 in PreviewVideoCompositor.startRequest(_:) at /Users/sindresorhus/dev/oss/Gifski/Gifski/Preview/PreviewVideoCompositor.swift:51
#20	0x0000000105fd77ac in partial apply for closure #1 in PreviewVideoCompositor.startRequest(_:) ()
#21	0x0000000105ea89f0 in thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) ()
#22	0x0000000105f6fc30 in partial apply for thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) ()

@sindresorhus
Copy link
Owner

So it seems like Metal is the way to go.

@sindresorhus
Copy link
Owner

Toggling edit crop regenerates preview even though nothing changed:

Screen.Shot.2025-05-30.at.16.46.45.mov

@sindresorhus
Copy link
Owner
  • I feel like the preview feature is still very complicated. I would love to see it refactored/simplified a bit. If there is a bug in the future, I'm honestly not sure where to start debugging. There are so many moving pieces. And I do realize it's a complicated task, but I think it's worth a shot.

  • Make sure all the Metal features work on all devices and do fallbacks if not: MTLDevice.supportsFeatureSet(_:) For example, I believe setMeshBytes does not work on Intel Macs.

  • Feed the files in the "Preview" folder to ChatGPT with this prompt (if you have it available, I would recommend the o4-mini-high model):

    Find real bugs and meaningful improvements. Ignore style and APIs you are not aware of. No nitpicking. Focus on logic flaws, missed edge cases, performance issues, unnecessary complexity, or cleaner structure. Be exhaustive.

Comment on lines 120 to 122
guard assetWriter.status != .failed else {
throw CreateAVAssetError.failedToWrite
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be:

Suggested change
guard assetWriter.status != .failed else {
throw CreateAVAssetError.failedToWrite
}
guard assetWriter.status == .completed else {
throw (assetWriter.status.error ?? CreateAVAssetError.failedToWrite)
}

currently it ignores .cancelled and .unknown.

}

guard let overlay else {
underTrimOverlayView = nil
// underTrimOverlayView = nil
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover?

let endRange = Set(numberOfFrames - numberOfFramesToPrecomputeAtTheEnd..<numberOfFrames)
let allFrames = beginningRange.union(endRange)

let dict = try await allFrames.asyncMap { frameIndex in
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no benefit of using asyncMap here. It's not concurrent, it just allows waiting for asynchronous operations. Also, concurrency may be problematic as I'm not sure if CGImageSource is thread-safe.

Suggested change
let dict = try await allFrames.asyncMap { frameIndex in
let dict = try await allFrames.map { frameIndex in

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could maybe also use some cancellation support:

try Task.checkCancellation()

let allFrames = beginningRange.union(endRange)

let dict = try await allFrames.asyncMap { frameIndex in
guard let image = CGImageSourceCreateImageAtIndex(imageSource, frameIndex, nil) else {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use imageSource.createImage

@sindresorhus
Copy link
Owner

Some possible alternatives

I could add a "loading" indicator watermark, so the user knows that the preview is still generating.
or
I could generate a few frames interspersed throughout the video, so the user can see a preview of the video while the full preview is generating. (This might interfere with the ability to click anywhere in the trimmer to seek to that exact position in the preview while the preview is generating. On the other hand, I could add an observer to the player rate and produce different behavior based on whether the player is playing or paused, so it might work out)

I don't think choppy output is very helpful (and sometimes it seems to even play an frame at wrong size). I would pause the playing, disable play button, and then prioritize generating the current frame the user is on as fast as possible. One frame is usually enough to see the initial quality so you can then choose some different sizes, etc.

@mmulet
Copy link
Contributor Author
mmulet commented May 30, 2025

I feel like the preview feature is still very complicated. I would love to see it refactored/simplified a bit. If there is a bug in the future, I'm honestly not sure where to start debugging. There are so many moving pieces. And I do realize it's a complicated task, but I think it's worth a shot.

I have a plan to simplify things quite a bit. I'm going to hold off on the suggested changes for now, and spend the next few days radically simplifying everything. Should be much better by then.

@mmulet
Copy link
Contributor Author
mmulet commented Jun 2, 2025
  • Radical simplification, see below
  • Set metal shader language version to 2.0, the old version supported by intel Macs. I don't have an Intel mac to test this on, however. I do use one advanced feature astc textures, but I put that behind a feature flag () with a fallback for older machines. (more about that in the simplification section)
  • Passes Swift 6 concurrency checks.

Find real bugs and meaningful improvements. Ignore style and APIs you are not aware of. No nitpicking. Focus on logic flaws, missed edge cases, performance issues, unnecessary complexity, or cleaner structure. Be exhaustive.

  • I have github copilot, so it has github copilot versions of the models, but I did this with:
    • openai gpt-4o
    • Gemini 2.5 Pro
    • Claude Sonnet 4
  • Pause preview when generating and try to get that single frame as fast as possible.
    • The only thing I can do to make it faster now would be to render at a smaller resolution, but that might not reflect the quality of the video.
    • Although if you want, I could do something like progressive image rendering where I very quickly convert a low res preview and then progressively increase the resolution as the frames are generated.

Radical simplification

The gist is that I removed the second track from the PreviewableComposition, and instead I pre-render the gif as textures. I didn't do this at first because I thought the texture size would be too big to be practicable. It turns out, that the the macs have a built in runtime support and encoder for astc textures, which allows compression down to about 2.00 bits per pixel. This makes the approach feasible. By using these compressed textures, I can avoid almost all of the weird AVPlayer quirks. This simplifies the code a lot.

Then, I went through each and every line of code and removed unnecessary parts, moving everything that could, made everything as reusable and generic as possible etc. Each file (other than Utilities) is now about 150 lines or less, and it is meant to be easy to read and understand.

Let's go through a summary of the major changes:

  1. createFullPreviewStream -> FullPreviewStream which handles creation of the FullPreview, the entire stream is now an actor which makes the state changes easy to reason about. Same as before only one generation will be in progress at a time, and each generation will be canceled if a new one is requested.

The actual conversion of the video to a FullPreview is simple, first convert to GIF, GIFGenerator.runProgressable then convert the animated GIF data to the textures one per frame (createAVAssetFromGIF is gone). Both tasks are done using the new ProgressableTask which is simply a Task that can report progress, along with a way to compose two together as just one task with one progress stream.

  1. FullPreviewGenerationEvent is the event emitted by the FullPreviewStream. It is now even simpler, and avoids unnecessary state like FullPreviewStatus because it is now directly Sendable.

Just as before, EditScreen subscribes the FullPreviewStream and updates the PreviewableComposition with the new FullPreview when it is available, now the logic is even simpler:

.task {
	for await event in fullPreviewStream.eventStream {
			fullPreviewState = event
	}
}

This new state hides all its internals, but it still can be used to display the proper UI (canShowPreview, progress, isGenerating, etc.) and to get frames for drawing with getPreviewFrame

  1. This brings us to the PreviewableComposition. It has been heavily simplified:
  • removes the preview track
  • no more need to updateFullPreviewTrack and no (AVPlayer quirks)

It just uses the standard code for adding a video compositor.

  1. After we have made our PreviewComposition we input into the TrimmingAVPlayer as before, but with handy helper functions like forceAVPlayerToRedraw and updatePreviewState, which do exactly what they say.

  2. Now we go to PreviewVideoCompositor which has been heavily simplified as well. Gone are the OutputCache, the PreBakedFrames. Render logic is now simple:

  • Show preview y/n
  • if no, just render the original video frame
  • if yes, render the preview frame, get it from the state
  1. The functionality of PreviewRenderer is mostly the same (draw the preview, with checkerboard pattern in the background). I did move to using Metal 2.0, the old version still compatible with Intel Macs. Plus, I moved it to an actor to future proof it for Swift 6. Despite it's similar function, I did completely refactor it to make the purpose of each part clear.

Still, here is a rundown of the PreviewRenderer:

  • Everything used to initialize the graphics context, is all put in PreviewRendererContext, this handles the initialization of all shared resource that will be used to render each and every frame.
  • Everything related to creating the textures is in SendableTexture, everything there is isolated to the PreviewRenderer actor and compiles with Swift 6 concurrency checks.
  • This handle conversion from CGImage and Data to MTLTexture as well as compression to astc textures.
  • PreviewRenderer itself is only concerned about the actual rendering of the frames.

Since you said you are unfamiliar with metal, I commented almost every line of code in PreviewRenderer.swift, PreviewRendererContext.swift, and compositePreview.metal. This should really help make it maintainable in the future.

That's it!

@mmulet
Copy link
Contributor Author
mmulet commented Jun 2, 2025
  • bug: synchronize preview with video after speed changes. (46fc0d0 fixes)

@sindresorhus
Copy link
Owner

Thanks for working more on this. The new version looks really good! It seems to run great and I was not able to find any bugs or crashes.

@sindresorhus
Copy link
Owner

Set metal shader language version to 2.0, the old version supported by intel Macs.

I think Intel Macs support the latest Metal version, just not all the features, so better to set it to latest and just feature detect for unsupported features.

@mmulet
Copy link
Contributor Author
mmulet commented Jun 7, 2025

Changes

  • feature check metal features
  • MTLDevice.supportsFeatureSet is deprecated, but we do have MTLDevice.supportFamily, and I do use that to check. I went through Metal Feature Set Tables manually, and I asked A.I., and we both agree that checks are correct. Everything we do except the astc compression is supported by the common1 family. Then, we also check for astc support, which is only available on apple2 and later, with a fallback for when it is not available.
  • fieldFont func -> fieldFont getter
  • move CropSettings out of Utilities
  • MTLPixelASTCCompressionError -> ASTCCompressionError
  • doc comment astc_ldr
  • SendableTexture don't capture self
  • The strong reference to self was necessary for actor isolation. The only way I could get around it was to not isolate the init for SendableTexture, but I made it fileprivate so we can manually check that each call to init is isolated.
  • CVPixelBufferError, CGImageSourceError -> Error,
  • CGImageSource.create: CFDictionary -> Dictionary
  • CGImageDestination.add(image:... -> CGImageDestination.addImage(...
  • Data.convertToTexture -> Data.convertToTexture()
  • doc Int.aligned
  • Actually removed Int.aligned because if you calculated the bytesPerRow of the ASTC image using blocksPerRow you don't have to align it because it already a multiple of ASTCBlockSize.
  • Add specific error codes for each CVReturn error in CVMetalTextureCache
  • CGPoint.clamped(within:) -> CGPoint.nearestPointInsideRectBounds(_:)
  • MTLCommandBuffer.commit with weak self
  • CVPixelBuffer.convertToGIF(...) out of Utilities.swift
  • GifGenerator extension in Utilities.swift to GIFGenerator.swift
  • reconfigure ASTCImage.bytesPerRow
  • renderEncoder.endEncoding() -> defer { renderEncoder.endEncoding() }

@sindresorhus
Copy link
Owner

Sorry about the delay. Been traveling.

@mmulet
Copy link
Contributor Author
mmulet commented Jun 21, 2025

Changes

  • PreviewRenderer.shared will now try to create a new PreviewRenderer if it has failed before.
  • Typed throws wherever possible
  • Honestly, mixed feelings about this. It makes some of the code more verbose, and most of the time we interact with apple provided APIs that throw untyped errors. So the entire functions error are still untyped. Furthermore, swift has no way to do union of errors other than the completely type erased any Error or creating an enum of the type (unless I missed something at WWDC last week), so we end up with typed errors like BaseAddressLockedError<LockedPlanesError<BaseAddressLockedError<LockedPlanesError<CopyError>>>>.

Sorry about the delay. Been traveling.

No worries.


UI Changes

This might still be a bit too early for this, but I was watching some of the WWDC videos last week, and as I'm sure you know, they are moving to the new "liquid glass" look. Do you think there is anything in GIFski preview? Beta 1 is usually a bit too buggy, so I haven't tried it out yet. But from the video, it looks like they are revamping the navigation bar among other changes.

@mmulet
Copy link
Contributor Author
mmulet commented Jun 22, 2025
  • Remove wrapped Errors
  • Use Error.error -> .error when appropriate

I removed the complicated typed error handling, I like it much better now 👍

@sindresorhus
Copy link
Owner

This might still be a bit too early for this, but I was watching some of the WWDC videos last week, and as I'm sure you know, they are moving to the new "liquid glass" look. Do you think there is anything in GIFski preview? Beta 1 is usually a bit too buggy, so I haven't tried it out yet. But from the video, it looks like they are revamping the navigation bar among other changes.

I haven't tried it yet, but it's likely some UI tweaks are needed. Although we get a lot for free by using SwiftUI. Let's continue in: #336

@sindresorhus sindresorhus merged commit 66a9097 into sindresorhus:main Jun 25, 2025
@sindresorhus
Copy link
Owner

This looks great now. Thank you so much for all your hard work on this. The end result is really good! 🙏

@sindresorhus
Copy link
Owner
sindresorhus commented Jun 25, 2025

If you're interested in tackling other issues, I'm happy to add bounties to them (not sure if you're doing it for the bounty, learnings, or both). No pressure though, I totally get it if you're feeling burnt out. Cropping and preview ended up being way more challenging than I expected.


In case you are interested, I think the priority now is:

@mmulet
Copy link
Contributor Author
mmulet commented Jun 25, 2025

If you're interested in tackling other issues, I'm happy to add bounties to them.

Yeah, let's do it! Obviously, I'm not going to get rich doing this (I probably made less than $1/hr on these 2 issues), but bounties really help motivate me to see the issues through completion (even if they take 3 months longer than anticipated 😅).
(Also, if you ever feel like bumping up the size of the bounties on the longer ones, I wouldn’t complain).

No pressure though, I totally get it if you're feeling burnt out. Cropping and preview ended up being way more challenging than I expected.

I'm not feeling burnt out at all. Let's go!

In case you are interested, I think the priority now is:

Definitely interested.

Honestly, the hardest part of bug/issue bounty hunting is just finding issues that people are willing to fund and actually respond to. So many PRs end up ignored or forgotten. You’ve been great to work with so far, so keep them coming.

If you’ve got issues on other repos you’re funding, I’m happy to check those out too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GIF preview feature
2 participants
0