mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-08 08:23:54 +02:00
[release] RyukGram v1.2.2
- Profile Analyzer (beta) — follower/following scans with mutuals, non-followbacks, new/lost trackers, and profile change history; searchable lists with batch follow/unfollow - Theme settings — force dark mode, Full OLED, OLED chat theme, and keyboard theme picker - Confirm story like - Confirm story emoji reaction - Swipe down to dismiss media viewer - Manually add users to story/chat exclusion lists by username - Keep stories visually seen locally - Auto-scroll reels mode - Quality picker: audio-only and raw photo download rows - Clear cache button with optional auto-clear interval - Spanish, Russian, Korean, Arabic, and Chinese (Traditional) translations - About page with version, credits, and links - Release notes popup on first launch of a new version - Anonymous live viewing - Toggle live comments - Disappearing DM media overlay — action button, mark-as-viewed eye, and audio toggle - Hide RyukGram UI on screenshots, screen recordings, and mirroring - Open link from clipboard — long-press the search tab - Messages-only mode: optional "Hide tab bar" sub-toggle - Fake profile stats — verified badge and follower/following/post counts on your own profile - Language switcher + import/export localization from Debug - Reveal poll/slider vote counts and quiz answers on stories and reels before interacting - Force legacy Quiz sticker back into the story composer tray - Advanced experimental features menu — toggle hidden IG experiments (QuickSnap, Homecoming, Prism, Direct Notes reply types) with apply-on-restart batching and a crash-loop auto-reset - Shortcut to Advanced experimental features from the General experimental features section - Push notifications render with rich previews on sideload again - IG 426 compatibility across story audio toggle, like confirmation, seen-on-like, live comments, notes audio download - Call confirm split into separate voice-call and video-call toggles - Messages-only mode: tab swiping disabled - Settings quick-access broken in non-English languages - Story seen-receipt block restored on IG v426 - Block selected mode no longer marks listed stories as seen - Hide explore posts grid works again on recent IG versions - Hide suggested stories no longer breaks profile highlights - Hide trending searches now also hides the category chip bar - Story eye long-press menu opens next to the button - Disable video autoplay: tap-to-play now works on videos inside carousels - Disable vanish mode swipe fixed on IG 426 - "Confirm shh mode" renamed to "Confirm vanish mode" across all languages - Confirm sticker interaction split into separate story and highlight toggles - Shared link embed presets: added eeinstagram.com and vxinstagram.com - Downloaded media filenames follow `@username_context_timestamp` - Reels pause mode: optional tap-to-mute on photo reels - Backup & Restore — scope picker with live preview for Settings / Excluded lists / Analyzer data - Profile Analyzer: filter by Not verified - Settings header: tap to open a sheet with GitHub and Telegram channel links - Thanks to Furamako for the Spanish translation - Thanks to [ZomkaDEV](https://github.com/ZomkaDEV) for the Russian translation - Thanks to [@ch1tmdgus](https://github.com/ch1tmdgus) (N4C) for the Korean translation - Thanks to [@bruuhim](https://github.com/bruuhim) for the Arabic translation - Thanks to [@jaydenjcpy](https://github.com/jaydenjcpy) for the Chinese (Traditional) translation - Thanks to [@darthplagueiswise](https://github.com/darthplagueiswise) (Radan) for the experimental flag feature set - Thanks to [@asdfzxcvbn](https://github.com/asdfzxcvbn) for [zxPluginsInject](https://github.com/asdfzxcvbn/zxPluginsInject) and [ipapatch](https://github.com/asdfzxcvbn/ipapatch) - Preserved unsent messages can't be removed via "Delete for you"; pull-to-refresh clears them (warning available in settings) - "Delete for you" detection uses a ~2s window after the local action — a real unsend landing in that window may be missed (rare) - With Liquid Glass buttons + Hide UI on capture both on, the DM eye leaves an empty glass bubble in captures
This commit is contained in:
@@ -118,11 +118,16 @@ const void *kSCIDismissKey = &kSCIDismissKey;
|
||||
id media = provider(sender);
|
||||
if (media == (id)kCFNull) return;
|
||||
|
||||
SCIActionContext tapCtx = (SCIActionContext)ctxNum.integerValue;
|
||||
NSString *tapCtxLabel = [SCIMediaActions contextLabelForContext:tapCtx];
|
||||
|
||||
if ([tap isEqualToString:@"expand"]) {
|
||||
[SCIMediaActions expandMedia:media fromView:sender caption:nil];
|
||||
} else if ([tap isEqualToString:@"download_share"]) {
|
||||
[SCIMediaActions setCurrentFilenameStem:[SCIMediaActions filenameStemForMedia:media contextLabel:tapCtxLabel]];
|
||||
[SCIMediaActions downloadAndShareMedia:media];
|
||||
} else if ([tap isEqualToString:@"download_photos"]) {
|
||||
[SCIMediaActions setCurrentFilenameStem:[SCIMediaActions filenameStemForMedia:media contextLabel:tapCtxLabel]];
|
||||
[SCIMediaActions downloadAndSaveMedia:media];
|
||||
} else if ([tap isEqualToString:@"copy_link"]) {
|
||||
[SCIMediaActions copyURLForMedia:media];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "../InstagramHeaders.h"
|
||||
#import "../Downloader/Download.h"
|
||||
#import "SCIActionMenu.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -16,6 +17,18 @@ typedef NS_ENUM(NSInteger, SCIActionContext) {
|
||||
|
||||
@interface SCIMediaActions : NSObject
|
||||
|
||||
// MARK: - Filename naming
|
||||
|
||||
// `@username_context_yyyyMMdd_HHmmss` (sanitized). UUID fallback on failure.
|
||||
+ (NSString *)filenameStemForMedia:(nullable id)media contextLabel:(NSString *)ctxLabel;
|
||||
|
||||
// "feed" / "reels" / "stories".
|
||||
+ (NSString *)contextLabelForContext:(SCIActionContext)ctx;
|
||||
|
||||
// Stem read by the download + mux write sites to name output files.
|
||||
+ (nullable NSString *)currentFilenameStem;
|
||||
+ (void)setCurrentFilenameStem:(nullable NSString *)stem;
|
||||
|
||||
// MARK: - Media extraction
|
||||
|
||||
/// Return the post's caption string. Tries selectors first, falls back to
|
||||
@@ -28,6 +41,16 @@ typedef NS_ENUM(NSInteger, SCIActionContext) {
|
||||
/// Ordered children of a carousel IGMedia. Empty array for non-carousels.
|
||||
+ (NSArray *)carouselChildrenForMedia:(id)media;
|
||||
|
||||
/// YES if the media has an audio track (`has_audio` fieldCache == 1).
|
||||
+ (BOOL)mediaHasAudio:(id)media;
|
||||
|
||||
/// Download the raw photo URL, skipping any video route.
|
||||
+ (void)downloadPhotoOnlyForMedia:(id)media action:(DownloadAction)action;
|
||||
|
||||
/// Extract the audio-only track from the DASH manifest via FFmpeg. Photos
|
||||
/// library can't hold audio, so both actions end at the share sheet.
|
||||
+ (void)downloadAudioOnlyForMedia:(id)media action:(DownloadAction)action;
|
||||
|
||||
/// Best URL for a single (non-carousel) media item. Prefers video URL, falls
|
||||
/// back to photo URL. Returns nil if nothing extractable.
|
||||
+ (nullable NSURL *)bestURLForMedia:(id)media;
|
||||
|
||||
@@ -21,6 +21,57 @@ static SCIDownloadDelegate *sciActiveDownloadDelegate = nil;
|
||||
extern void sciToggleStoryAudio(void);
|
||||
extern BOOL sciIsStoryAudioEnabled(void);
|
||||
|
||||
// MARK: - Filename naming
|
||||
|
||||
static NSString *sciCurrentFilenameStem = nil;
|
||||
|
||||
static NSString *sciSanitizeFilenameComponent(NSString *s) {
|
||||
if (!s.length) return @"";
|
||||
NSMutableCharacterSet *bad = [NSMutableCharacterSet alphanumericCharacterSet];
|
||||
[bad addCharactersInString:@"._-"];
|
||||
NSCharacterSet *drop = bad.invertedSet;
|
||||
NSArray *parts = [s componentsSeparatedByCharactersInSet:drop];
|
||||
NSString *out = [parts componentsJoinedByString:@""];
|
||||
if (out.length > 30) out = [out substringToIndex:30];
|
||||
return out;
|
||||
}
|
||||
|
||||
// IGAPIStorableObject's backing dict.
|
||||
static NSDictionary *sciMediaFieldCache(id obj) {
|
||||
if (!obj) return nil;
|
||||
static Ivar fcIvar = NULL;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
Class c = NSClassFromString(@"IGAPIStorableObject");
|
||||
if (c) fcIvar = class_getInstanceVariable(c, "_fieldCache");
|
||||
});
|
||||
if (!fcIvar) return nil;
|
||||
id v = object_getIvar(obj, fcIvar);
|
||||
return [v isKindOfClass:[NSDictionary class]] ? v : nil;
|
||||
}
|
||||
|
||||
static NSString *sciUsernameForMedia(id media) {
|
||||
if (!media) return nil;
|
||||
@try {
|
||||
id user = nil;
|
||||
@try { user = [media valueForKey:@"user"]; } @catch (__unused id e) {}
|
||||
if (!user) {
|
||||
NSDictionary *fc = sciMediaFieldCache(media);
|
||||
user = fc[@"user"];
|
||||
}
|
||||
if (!user) return nil;
|
||||
NSString *u = nil;
|
||||
@try { u = [user valueForKey:@"username"]; } @catch (__unused id e) {}
|
||||
if (![u isKindOfClass:[NSString class]] || !u.length) {
|
||||
NSDictionary *ufc = sciMediaFieldCache(user);
|
||||
id v = ufc[@"username"];
|
||||
if ([v isKindOfClass:[NSString class]]) u = v;
|
||||
else if ([user isKindOfClass:[NSDictionary class]]) u = ((NSDictionary *)user)[@"username"];
|
||||
}
|
||||
return [u isKindOfClass:[NSString class]] ? u : nil;
|
||||
} @catch (__unused id e) { return nil; }
|
||||
}
|
||||
|
||||
// Match keys used in the settings-entry title map for openSettingsForContext:
|
||||
static NSString *sciSettingsTitleForContext(SCIActionContext ctx) {
|
||||
switch (ctx) {
|
||||
@@ -74,6 +125,38 @@ static void sciConfirmThen(NSString *title, void(^block)(void)) {
|
||||
|
||||
@implementation SCIMediaActions
|
||||
|
||||
+ (NSString *)contextLabelForContext:(SCIActionContext)ctx {
|
||||
switch (ctx) {
|
||||
case SCIActionContextFeed: return @"feed";
|
||||
case SCIActionContextReels: return @"reels";
|
||||
case SCIActionContextStories: return @"stories";
|
||||
}
|
||||
return @"media";
|
||||
}
|
||||
|
||||
+ (NSString *)filenameStemForMedia:(id)media contextLabel:(NSString *)ctxLabel {
|
||||
@try {
|
||||
NSString *user = sciSanitizeFilenameComponent(sciUsernameForMedia(media));
|
||||
NSString *userPart = user.length ? [@"@" stringByAppendingString:user] : @"media";
|
||||
NSString *ctxPart = sciSanitizeFilenameComponent(ctxLabel);
|
||||
if (!ctxPart.length) ctxPart = @"media";
|
||||
static NSDateFormatter *fmt = nil;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
fmt = [NSDateFormatter new];
|
||||
fmt.dateFormat = @"yyyyMMdd_HHmmss";
|
||||
fmt.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||||
});
|
||||
NSString *ts = [fmt stringFromDate:[NSDate date]];
|
||||
return [NSString stringWithFormat:@"%@_%@_%@", userPart, ctxPart, ts];
|
||||
} @catch (__unused id e) {
|
||||
return [[NSUUID UUID] UUIDString];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)currentFilenameStem { return sciCurrentFilenameStem; }
|
||||
+ (void)setCurrentFilenameStem:(NSString *)stem { sciCurrentFilenameStem = [stem copy]; }
|
||||
|
||||
// MARK: - Media extraction
|
||||
|
||||
+ (NSString *)captionForMedia:(id)media {
|
||||
@@ -209,6 +292,96 @@ static void sciConfirmThen(NSString *title, void(^block)(void)) {
|
||||
return @[];
|
||||
}
|
||||
|
||||
+ (BOOL)mediaHasAudio:(id)media {
|
||||
if (!media) return NO;
|
||||
// fieldCache on media (old IG path).
|
||||
id v = sciFieldCache(media, @"has_audio");
|
||||
if ([v respondsToSelector:@selector(boolValue)] && [v boolValue]) return YES;
|
||||
|
||||
// IGVideo.isAudioDetected — positive signal only; NO often means "IG
|
||||
// hasn't decoded the manifest yet" for stories, not actually silent.
|
||||
@try {
|
||||
id video = nil;
|
||||
if ([media respondsToSelector:@selector(video)])
|
||||
video = ((id(*)(id, SEL))objc_msgSend)(media, @selector(video));
|
||||
if (video && [video respondsToSelector:@selector(isAudioDetected)]) {
|
||||
if (((BOOL(*)(id, SEL))objc_msgSend)(video, @selector(isAudioDetected))) return YES;
|
||||
}
|
||||
} @catch (__unused id e) {}
|
||||
|
||||
// Stories often carry audio but don't surface it in fieldCache. If any
|
||||
// of these music/audio hints are present, treat as audio-bearing.
|
||||
for (NSString *key in @[@"music_metadata", @"story_music_stickers",
|
||||
@"is_story_image_with_music", @"story_sound_on",
|
||||
@"spotify_stickers", @"story_music_lyric_stickers"]) {
|
||||
id val = sciFieldCache(media, key);
|
||||
if (val && ![val isKindOfClass:[NSNull class]]) {
|
||||
if ([val respondsToSelector:@selector(boolValue)] && [val boolValue]) return YES;
|
||||
if ([val isKindOfClass:[NSArray class]] && [(NSArray *)val count]) return YES;
|
||||
if ([val isKindOfClass:[NSDictionary class]] && [(NSDictionary *)val count]) return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: if a DASH manifest exists, assume audio is present.
|
||||
return [SCIDashParser dashManifestForMedia:media].length > 0;
|
||||
}
|
||||
|
||||
+ (void)downloadPhotoOnlyForMedia:(id)media action:(DownloadAction)action {
|
||||
NSURL *url = [self hdPhotoURLForMedia:media];
|
||||
if (!url) url = [SCIUtils getPhotoUrlForMedia:(IGMedia *)media];
|
||||
if (!url) url = [self fieldCachePhotoURLForMedia:media];
|
||||
if (!url) { [SCIUtils showErrorHUDWithDescription:SCILocalized(@"Could not extract photo URL")]; return; }
|
||||
NSString *ext = [[url lastPathComponent] pathExtension];
|
||||
if (!ext.length) ext = @"jpg";
|
||||
sciActiveDownloadDelegate = sciMakeDownloader(action, NO);
|
||||
[sciActiveDownloadDelegate downloadFileWithURL:url fileExtension:ext hudLabel:nil];
|
||||
}
|
||||
|
||||
// Photos library can't hold audio — save action falls back to share sheet.
|
||||
+ (void)downloadAudioOnlyForMedia:(id)media action:(DownloadAction)action {
|
||||
NSString *manifest = [SCIDashParser dashManifestForMedia:media];
|
||||
if (!manifest.length) {
|
||||
[SCIUtils showErrorHUDWithDescription:SCILocalized(@"No audio stream available")];
|
||||
return;
|
||||
}
|
||||
NSArray *reps = [SCIDashParser parseManifest:manifest];
|
||||
SCIDashRepresentation *audio = [SCIDashParser bestAudioFromRepresentations:reps];
|
||||
if (!audio.url) {
|
||||
[SCIUtils showErrorHUDWithDescription:SCILocalized(@"No audio track found")];
|
||||
return;
|
||||
}
|
||||
if (![SCIFFmpeg isAvailable]) {
|
||||
[SCIUtils showErrorHUDWithDescription:SCILocalized(@"FFmpeg not available")];
|
||||
return;
|
||||
}
|
||||
|
||||
SCIDownloadPillView *pill = [SCIDownloadPillView shared];
|
||||
NSString *ticket = [pill beginTicketWithTitle:SCILocalized(@"Downloading audio...")
|
||||
onCancel:^{ [SCIFFmpeg cancelAll]; }];
|
||||
|
||||
NSString *audioStem = [self currentFilenameStem] ?: [[NSUUID UUID] UUIDString];
|
||||
NSString *outPath = [NSTemporaryDirectory() stringByAppendingPathComponent:
|
||||
[NSString stringWithFormat:@"%@.m4a", audioStem]];
|
||||
NSString *cmd = [NSString stringWithFormat:@"-i \"%@\" -vn -c:a copy -y \"%@\"",
|
||||
audio.url.absoluteString, outPath];
|
||||
[SCIFFmpeg executeCommand:cmd completion:^(BOOL success, NSString *output) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!success) {
|
||||
[pill finishTicket:ticket errorMessage:SCILocalized(@"Audio extract failed")];
|
||||
return;
|
||||
}
|
||||
[pill finishTicket:ticket successMessage:SCILocalized(@"Audio ready")];
|
||||
NSURL *fileURL = [NSURL fileURLWithPath:outPath];
|
||||
switch (action) {
|
||||
case quickLook: [SCIUtils showQuickLookVC:@[fileURL]]; break;
|
||||
case share:
|
||||
case saveToPhotos:
|
||||
default: [SCIUtils showShareVC:fileURL]; break;
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSURL *)bestURLForMedia:(id)media {
|
||||
if (!media) return nil;
|
||||
|
||||
@@ -328,6 +501,7 @@ static void sciConfirmThen(NSString *title, void(^block)(void)) {
|
||||
// Try enhanced HD path via reusable quality picker
|
||||
BOOL handled = [SCIQualityPicker pickQualityForMedia:media
|
||||
fromView:sourceView
|
||||
action:action
|
||||
picked:^(SCIDashRepresentation *video, SCIDashRepresentation *audio) {
|
||||
[self downloadDASHVideo:video audio:audio action:action];
|
||||
}
|
||||
@@ -621,13 +795,18 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
NSMutableArray<NSURL *> *files = [NSMutableArray array];
|
||||
NSLock *lock = [NSLock new];
|
||||
__block NSUInteger completed = 0;
|
||||
NSString *bulkStem = [self currentFilenameStem];
|
||||
|
||||
NSUInteger __idx = 0;
|
||||
for (NSURL *url in urls) {
|
||||
if (cancelled) break;
|
||||
dispatch_group_enter(group);
|
||||
NSString *ext = [[url lastPathComponent] pathExtension];
|
||||
NSString *name = bulkStem
|
||||
? [NSString stringWithFormat:@"%@_%lu", bulkStem, (unsigned long)(++__idx)]
|
||||
: [[NSUUID UUID] UUIDString];
|
||||
NSString *tmp = [NSTemporaryDirectory() stringByAppendingPathComponent:
|
||||
[NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString],
|
||||
[NSString stringWithFormat:@"%@.%@", name,
|
||||
ext.length ? ext : @"jpg"]];
|
||||
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession]
|
||||
downloadTaskWithURL:url completionHandler:^(NSURL *loc, NSURLResponse *resp, NSError *err) {
|
||||
@@ -763,6 +942,12 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
fromView:(UIView *)sourceView {
|
||||
NSMutableArray<SCIAction *> *out = [NSMutableArray array];
|
||||
|
||||
NSString *ctxLabel = [self contextLabelForContext:ctx];
|
||||
// Stamp the filename stem before a download fires.
|
||||
void (^stampStemForMedia)(id) = ^(id m) {
|
||||
[SCIMediaActions setCurrentFilenameStem:[SCIMediaActions filenameStemForMedia:m contextLabel:ctxLabel]];
|
||||
};
|
||||
|
||||
// Resolve parent media for carousel detection + bulk actions.
|
||||
id parentMedia = media;
|
||||
if (media && ![self isCarouselMedia:media]) {
|
||||
@@ -946,9 +1131,11 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
[SCIMediaActions copyAllURLsForMedia:bulkMedia];
|
||||
}],
|
||||
[SCIAction actionWithTitle:SCILocalized(@"Download and share all") icon:@"square.and.arrow.up.on.square" handler:^{
|
||||
stampStemForMedia(bulkMedia);
|
||||
[SCIMediaActions downloadAllAndShareMedia:bulkMedia];
|
||||
}],
|
||||
[SCIAction actionWithTitle:SCILocalized(@"Download all to Photos") icon:@"square.and.arrow.down.on.square" handler:^{
|
||||
stampStemForMedia(bulkMedia);
|
||||
[SCIMediaActions downloadAllAndSaveMedia:bulkMedia];
|
||||
}],
|
||||
];
|
||||
@@ -1068,6 +1255,7 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
if (u) [urls addObject:u];
|
||||
}
|
||||
if (!urls.count) return;
|
||||
stampStemForMedia(capturedMedias.firstObject);
|
||||
[SCIMediaActions bulkDownloadURLs:urls title:SCILocalized(@"Download all stories and share?") done:^(NSArray<NSURL *> *files) {
|
||||
if (!files.count) return;
|
||||
UIViewController *top = topMostController();
|
||||
@@ -1083,6 +1271,7 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
if (u) [urls addObject:u];
|
||||
}
|
||||
if (!urls.count) return;
|
||||
stampStemForMedia(capturedMedias.firstObject);
|
||||
[SCIMediaActions bulkDownloadURLs:urls title:SCILocalized(@"Save all stories to Photos?") done:^(NSArray<NSURL *> *files) {
|
||||
[SCIMediaActions bulkSaveFiles:files];
|
||||
}];
|
||||
@@ -1105,11 +1294,13 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
[out addObject:[SCIAction actionWithTitle:SCILocalized(@"Download and share")
|
||||
icon:@"square.and.arrow.up"
|
||||
handler:^{
|
||||
stampStemForMedia(media);
|
||||
[SCIMediaActions downloadAndShareMedia:media];
|
||||
}]];
|
||||
[out addObject:[SCIAction actionWithTitle:SCILocalized(@"Download to Photos")
|
||||
icon:@"square.and.arrow.down"
|
||||
handler:^{
|
||||
stampStemForMedia(media);
|
||||
[SCIMediaActions downloadAndSaveMedia:media];
|
||||
}]];
|
||||
|
||||
@@ -1138,13 +1329,18 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
NSMutableArray<NSURL *> *files = [NSMutableArray array];
|
||||
NSLock *lock = [NSLock new];
|
||||
__block NSUInteger completed = 0;
|
||||
NSString *bulkStem2 = [self currentFilenameStem];
|
||||
|
||||
NSUInteger __idx2 = 0;
|
||||
for (NSURL *url in urls) {
|
||||
if (cancelled) break;
|
||||
dispatch_group_enter(group);
|
||||
NSString *ext = [[url lastPathComponent] pathExtension];
|
||||
NSString *name = bulkStem2
|
||||
? [NSString stringWithFormat:@"%@_%lu", bulkStem2, (unsigned long)(++__idx2)]
|
||||
: [[NSUUID UUID] UUIDString];
|
||||
NSString *tmp = [NSTemporaryDirectory() stringByAppendingPathComponent:
|
||||
[NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString],
|
||||
[NSString stringWithFormat:@"%@.%@", name,
|
||||
ext.length ? ext : @"jpg"]];
|
||||
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession]
|
||||
downloadTaskWithURL:url completionHandler:^(NSURL *loc, NSURLResponse *resp, NSError *err) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#import "SCIMediaViewer.h"
|
||||
#import "../Utils.h"
|
||||
#import "../SCIImageCache.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <AVKit/AVKit.h>
|
||||
|
||||
@@ -57,15 +58,10 @@
|
||||
[self.view addSubview:self.spinner];
|
||||
[self.spinner startAnimating];
|
||||
|
||||
NSURL *url = [self.photoURL copy];
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSData *data = [NSData dataWithContentsOfURL:url];
|
||||
UIImage *img = data ? [UIImage imageWithData:data] : nil;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.spinner stopAnimating];
|
||||
if (img) self.imageView.image = img;
|
||||
});
|
||||
});
|
||||
[SCIImageCache loadImageFromURL:self.photoURL completion:^(UIImage *img) {
|
||||
[self.spinner stopAnimating];
|
||||
if (img) self.imageView.image = img;
|
||||
}];
|
||||
|
||||
// Double-tap to zoom
|
||||
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
|
||||
|
||||
Reference in New Issue
Block a user