mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-06 15:33:53 +02:00
feat: Confirm story like
feat: Confirm story emoji reaction feat: Spanish translation feat: Language switcher + import/export localization from Debug feat: Swipe down to dismiss media viewer feat: Manually add users to story/chat exclusion lists by username feat: Keep stories visually seen locally — split mode (grey ring locally, seen receipt still blockedon server) feat: Auto-scroll reels — IG default or RyukGram mode, keeps advancing after swiping back (#3) fix: Messages-only mode — tab swiping disabled fix: Settings quick-access broken in non-English languages fix: Story seen-receipt block restored on IG v425+ (Sundial uploader), per-owner, both "Block all" and "Block selected" modes fix: Block selected mode no longer marks listed stories as seen imp: Story-interaction pipeline unifies confirm + seen/advance side effects
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#import "../Utils.h"
|
||||
#import "../Downloader/Download.h"
|
||||
#import "../PhotoAlbum.h"
|
||||
#import "../Features/StoriesAndMessages/SCIExcludedStoryUsers.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
#import <Photos/Photos.h>
|
||||
@@ -23,9 +24,9 @@ extern BOOL sciIsStoryAudioEnabled(void);
|
||||
// Match keys used in the settings-entry title map for openSettingsForContext:
|
||||
static NSString *sciSettingsTitleForContext(SCIActionContext ctx) {
|
||||
switch (ctx) {
|
||||
case SCIActionContextFeed: return @"Feed";
|
||||
case SCIActionContextReels: return @"Reels";
|
||||
case SCIActionContextStories: return @"Stories";
|
||||
case SCIActionContextFeed: return SCILocalized(@"Feed");
|
||||
case SCIActionContextReels: return SCILocalized(@"Reels");
|
||||
case SCIActionContextStories: return SCILocalized(@"Stories");
|
||||
}
|
||||
return @"General";
|
||||
}
|
||||
@@ -888,6 +889,36 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
|
||||
}
|
||||
}
|
||||
|
||||
// Story user list management (add/remove from exclusion list).
|
||||
if (ctx == SCIActionContextStories && [SCIUtils getBoolPref:@"enable_story_user_exclusions"]) {
|
||||
extern NSDictionary *sciOwnerInfoForView(UIView *);
|
||||
extern void sciRefreshAllVisibleOverlays(UIViewController *);
|
||||
extern __weak UIViewController *sciActiveStoryViewerVC;
|
||||
NSDictionary *ownerInfo = sourceView ? sciOwnerInfoForView(sourceView) : nil;
|
||||
NSString *ownerPK = ownerInfo[@"pk"];
|
||||
if (ownerPK.length) {
|
||||
BOOL inList = [SCIExcludedStoryUsers isInList:ownerPK];
|
||||
BOOL bs = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
NSString *addLabel = bs ? SCILocalized(@"Add to block list") : SCILocalized(@"Exclude from seen");
|
||||
NSString *removeLabel = bs ? SCILocalized(@"Remove from block list") : SCILocalized(@"Remove from exclude list");
|
||||
NSString *title = inList ? removeLabel : addLabel;
|
||||
NSString *icon = inList ? @"eye.fill" : @"eye.slash";
|
||||
NSString *capturedPK = [ownerPK copy];
|
||||
NSString *capturedUser = [ownerInfo[@"username"] ?: @"" copy];
|
||||
NSString *capturedName = [ownerInfo[@"fullName"] ?: @"" copy];
|
||||
[out addObject:[SCIAction actionWithTitle:title icon:icon handler:^{
|
||||
if (inList) {
|
||||
[SCIExcludedStoryUsers removePK:capturedPK];
|
||||
[SCIUtils showToastForDuration:2.0 title:bs ? SCILocalized(@"Unblocked") : SCILocalized(@"Removed from list")];
|
||||
} else {
|
||||
[SCIExcludedStoryUsers addOrUpdateEntry:@{@"pk": capturedPK, @"username": capturedUser, @"fullName": capturedName}];
|
||||
[SCIUtils showToastForDuration:2.0 title:bs ? SCILocalized(@"Added to block list") : SCILocalized(@"Added to exclude list")];
|
||||
}
|
||||
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx != SCIActionContextStories) {
|
||||
// Caption lives on the parent media (not on carousel children).
|
||||
[out addObject:[SCIAction actionWithTitle:SCILocalized(@"Copy caption")
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
#pragma mark - Container VC (PageViewController-based)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@interface _SCIMediaViewerContainerVC : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
|
||||
@interface _SCIMediaViewerContainerVC : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIGestureRecognizerDelegate>
|
||||
@property (nonatomic, strong) NSArray<SCIMediaViewerItem *> *items;
|
||||
@property (nonatomic, assign) NSUInteger currentIndex;
|
||||
@property (nonatomic, strong) UIPageViewController *pageVC;
|
||||
@@ -238,18 +238,16 @@
|
||||
[self.captionLabel.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:-8],
|
||||
]];
|
||||
|
||||
// Swipe down to dismiss
|
||||
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissPan:)];
|
||||
pan.delegate = (id<UIGestureRecognizerDelegate>)self;
|
||||
[self.view addGestureRecognizer:pan];
|
||||
|
||||
// Single tap toggles chrome
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleChrome)];
|
||||
tap.cancelsTouchesInView = NO;
|
||||
[self.pageVC.view addGestureRecognizer:tap];
|
||||
|
||||
// For photos, let double-tap zoom work without triggering single-tap
|
||||
for (UIGestureRecognizer *gr in self.pageVC.view.gestureRecognizers) {
|
||||
if ([gr isKindOfClass:[UITapGestureRecognizer class]] && ((UITapGestureRecognizer *)gr).numberOfTapsRequired == 1) {
|
||||
// Already have our tap
|
||||
}
|
||||
}
|
||||
|
||||
[self updateChrome];
|
||||
}
|
||||
|
||||
@@ -290,6 +288,45 @@
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gr {
|
||||
if (![gr isKindOfClass:[UIPanGestureRecognizer class]]) return YES;
|
||||
CGPoint v = [gr velocityInView:self.view];
|
||||
return fabs(v.y) > fabs(v.x) && v.y > 0;
|
||||
}
|
||||
|
||||
- (void)handleDismissPan:(UIPanGestureRecognizer *)gr {
|
||||
CGFloat ty = [gr translationInView:self.view].y;
|
||||
CGFloat h = self.view.bounds.size.height;
|
||||
CGFloat progress = fmin(fmax(ty / h, 0), 1);
|
||||
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
self.view.transform = CGAffineTransformMakeTranslation(0, ty);
|
||||
self.view.alpha = 1.0 - progress * 0.5;
|
||||
break;
|
||||
}
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled: {
|
||||
CGFloat vy = [gr velocityInView:self.view].y;
|
||||
if (progress > 0.25 || vy > 800) {
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
self.view.transform = CGAffineTransformMakeTranslation(0, h);
|
||||
self.view.alpha = 0;
|
||||
} completion:^(BOOL finished) {
|
||||
[self dismissViewControllerAnimated:NO completion:nil];
|
||||
}];
|
||||
} else {
|
||||
[UIView animateWithDuration:0.25 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:0 animations:^{
|
||||
self.view.transform = CGAffineTransformIdentity;
|
||||
self.view.alpha = 1;
|
||||
} completion:nil];
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeTapped {
|
||||
// Pause any playing video
|
||||
UIViewController *current = self.pageVC.viewControllers.firstObject;
|
||||
@@ -424,7 +461,7 @@
|
||||
_SCIMediaViewerContainerVC *vc = [[_SCIMediaViewerContainerVC alloc] init];
|
||||
vc.items = items;
|
||||
vc.currentIndex = index;
|
||||
vc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
vc.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
||||
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
|
||||
[topMostController() presentViewController:vc animated:YES completion:nil];
|
||||
});
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
#define CONFIRMPOSTLIKE(orig) \
|
||||
if ([SCIUtils getBoolPref:@"like_confirm"]) \
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
else return orig;
|
||||
|
||||
// Confirmation handlers
|
||||
|
||||
#define CONFIRMPOSTLIKE(orig) \
|
||||
if ([SCIUtils getBoolPref:@"like_confirm"]) { \
|
||||
NSLog(@"[SCInsta] Confirm post like triggered"); \
|
||||
\
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
} \
|
||||
else { \
|
||||
return orig; \
|
||||
} \
|
||||
|
||||
#define CONFIRMREELSLIKE(orig) \
|
||||
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) { \
|
||||
NSLog(@"[SCInsta] Confirm reels like triggered"); \
|
||||
\
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
} \
|
||||
else { \
|
||||
return orig; \
|
||||
} \
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
#define CONFIRMREELSLIKE(orig) \
|
||||
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) \
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
else return orig;
|
||||
|
||||
// Liking posts
|
||||
%hook IGUFIButtonBarView
|
||||
@@ -96,53 +80,9 @@
|
||||
}
|
||||
%end
|
||||
|
||||
// Liking stories
|
||||
%hook IGStoryFullscreenDefaultFooterView
|
||||
- (void)_handleLikeTapped {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)_likeTapped {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)inputView:(id)arg1 didTapLikeButton:(id)arg2 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
// Story like/emoji confirm handled by SCIStoryInteractionPipeline.
|
||||
|
||||
// For some stupid reason they removed the "liketapped" methods on newer Instagram versions
|
||||
// Now we have to do a shitty workaround instead :(
|
||||
// Works 99% of the time, but sometimes clicks get through directly to the like button (somehow)
|
||||
- (void)layoutSubviews {
|
||||
%orig;
|
||||
|
||||
if (![SCIUtils getBoolPref:@"like_confirm"]) return;
|
||||
|
||||
UIButton *likeButton = [self valueForKey:@"likeButton"];
|
||||
if (!likeButton) return;
|
||||
|
||||
// 129115 = L(12) I(9) K(11) E(5)
|
||||
static NSInteger kOverlayTag = 129115;
|
||||
if ([likeButton viewWithTag:kOverlayTag]) return;
|
||||
|
||||
UIButton *overlay = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
overlay.tag = kOverlayTag;
|
||||
overlay.frame = likeButton.bounds;
|
||||
overlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[overlay addTarget:self action:@selector(overlayTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[likeButton addSubview:overlay];
|
||||
}
|
||||
|
||||
%new - (void)overlayTapped:(UIButton *)overlay {
|
||||
UIButton *likeButton = (UIButton *)overlay.superview;
|
||||
|
||||
[SCIUtils showConfirmation:^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[likeButton sendActionsForControlEvents:UIControlEventTouchUpInside];
|
||||
});
|
||||
}];
|
||||
}
|
||||
%end
|
||||
|
||||
// DM like button (seems to be hidden)
|
||||
// DM like button
|
||||
%hook IGDirectThreadViewController
|
||||
- (void)_didTapLikeButton {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
|
||||
@@ -92,12 +92,11 @@ NSArray *filterSurfacesArray(NSArray *surfaces) {
|
||||
}
|
||||
|
||||
- (BOOL)isTabSwipingEnabled {
|
||||
|
||||
// Swipe lands on stripped tabs in messages-only.
|
||||
if ([SCIUtils getBoolPref:@"messages_only"]) return NO;
|
||||
if ([[SCIUtils getStringPref:@"swipe_nav_tabs"] isEqualToString:@"enabled"]) return YES;
|
||||
else if ([[SCIUtils getStringPref:@"swipe_nav_tabs"] isEqualToString:@"disabled"]) return NO;
|
||||
|
||||
return %orig;
|
||||
|
||||
}
|
||||
- (void)setIsTabSwipingEnabled:(BOOL)arg1 {
|
||||
return;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
%orig;
|
||||
|
||||
BOOL msgOnly = [SCIUtils getBoolPref:@"messages_only"];
|
||||
NSString *target = msgOnly ? SCILocalized(@"direct-inbox-tab") : SCILocalized(@"mainfeed-tab");
|
||||
NSString *target = msgOnly ? @"direct-inbox-tab" : @"mainfeed-tab";
|
||||
if (![self.accessibilityIdentifier isEqualToString:target]) return;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"settings_shortcut"]) {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Auto-scroll reels. Modes:
|
||||
// * ig — flip IG's own auto-scroll gates; covers video + photo reels
|
||||
// * custom — same flag flip (photos) + per-cell loopCount trigger calling
|
||||
// WantsScrollToNextItem each loop (videos keep advancing after back-swipe)
|
||||
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
|
||||
static const void *kSCILoopCountKey = &kSCILoopCountKey;
|
||||
static BOOL sciAdvanceInFlight = NO;
|
||||
|
||||
static inline NSString *sciMode(void) {
|
||||
NSString *m = [SCIUtils getStringPref:@"auto_scroll_reels_mode"];
|
||||
return m.length ? m : @"off";
|
||||
}
|
||||
static inline BOOL sciModeOn(void) { return ![sciMode() isEqualToString:@"off"]; }
|
||||
static inline BOOL sciModeCustom(void) { return [sciMode() isEqualToString:@"custom"]; }
|
||||
|
||||
static UIViewController *sciFindFeedVCFromView(UIView *view) {
|
||||
UIResponder *r = view;
|
||||
while (r) {
|
||||
if ([r isKindOfClass:[UIViewController class]] &&
|
||||
[NSStringFromClass([r class]) isEqualToString:@"IGSundialFeedViewController"])
|
||||
return (UIViewController *)r;
|
||||
r = [r nextResponder];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
%hook IGSundialFeedViewController
|
||||
- (BOOL)shouldForceEnableAutoScroll {
|
||||
if (sciModeOn()) return YES;
|
||||
return %orig;
|
||||
}
|
||||
- (BOOL)autoAdvanceToNextItem {
|
||||
if (sciModeOn()) return YES;
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGSundialViewerVideoCell
|
||||
- (void)videoView:(id)v didUpdatePlaybackStatus:(id)status {
|
||||
%orig;
|
||||
if (!sciModeCustom() || !status) return;
|
||||
SEL loopSel = @selector(loopCount);
|
||||
if (![status respondsToSelector:loopSel]) return;
|
||||
|
||||
long long cur = ((long long(*)(id, SEL))objc_msgSend)(status, loopSel);
|
||||
NSNumber *prev = objc_getAssociatedObject(self, kSCILoopCountKey);
|
||||
objc_setAssociatedObject(self, kSCILoopCountKey, @(cur), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
if (!prev || cur <= prev.longLongValue || sciAdvanceInFlight) return;
|
||||
|
||||
UIViewController *feedVC = sciFindFeedVCFromView((UIView *)self);
|
||||
if (!feedVC || !feedVC.viewIfLoaded.window) return;
|
||||
|
||||
sciAdvanceInFlight = YES;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
SEL wants = @selector(sundialViewerInteractionCoordinatorWantsScrollToNextItemAnimated:);
|
||||
if ([feedVC respondsToSelector:wants])
|
||||
((void(*)(id, SEL, BOOL))objc_msgSend)(feedVC, wants, YES);
|
||||
sciAdvanceInFlight = NO;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)prepareForReuse {
|
||||
objc_setAssociatedObject(self, kSCILoopCountKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
@@ -1,5 +1,11 @@
|
||||
// Story seen receipt blocking + visual seen state blocking
|
||||
// Story seen-receipt blocking. Legacy + Sundial uploads are Swift-dispatched
|
||||
// via a `networker` ivar — we cache the uploaders at init and nil the ivar
|
||||
// while the active owner is blocked. `keep_seen_visual_local` ON runs orig
|
||||
// (local stores update, server blocked). OFF skips orig (full block).
|
||||
|
||||
#import "StoryHelpers.h"
|
||||
#import "SCIStoryInteractionPipeline.h"
|
||||
#import "SCIExcludedStoryUsers.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
#import <substrate.h>
|
||||
@@ -12,6 +18,8 @@ NSMutableSet *sciAllowedSeenPKs = nil;
|
||||
extern BOOL sciIsCurrentStoryOwnerExcluded(void);
|
||||
extern BOOL sciIsObjectStoryOwnerExcluded(id obj);
|
||||
|
||||
static void sciStateRestore(void); // fwd — used by VC hook above its definition
|
||||
|
||||
static BOOL sciStorySeenToggleBypass(void) {
|
||||
return [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"] && sciStorySeenToggleEnabled;
|
||||
}
|
||||
@@ -28,41 +36,48 @@ static BOOL sciIsPKAllowed(id media) {
|
||||
if (!media || !sciAllowedSeenPKs || sciAllowedSeenPKs.count == 0) return NO;
|
||||
id pk = sciCall(media, @selector(pk));
|
||||
if (!pk) return NO;
|
||||
return [sciAllowedSeenPKs containsObject:[NSString stringWithFormat:@"%@", pk]];
|
||||
NSString *pkStr = [NSString stringWithFormat:@"%@", pk];
|
||||
if (![sciAllowedSeenPKs containsObject:pkStr]) return NO;
|
||||
if ([SCIExcludedStoryUsers isFeatureEnabled] && ![SCIExcludedStoryUsers isUserPKExcluded:pkStr])
|
||||
return NO;
|
||||
return YES;
|
||||
}
|
||||
|
||||
static BOOL sciShouldBlockSeenNetwork() {
|
||||
// ============ Feature gates ============
|
||||
|
||||
static BOOL sciShouldBlockSeenNetwork(void) {
|
||||
if (sciSeenBypassActive) return NO;
|
||||
if (sciStorySeenToggleBypass()) return NO;
|
||||
if (sciIsCurrentStoryOwnerExcluded()) return NO;
|
||||
return [SCIUtils getBoolPref:@"no_seen_receipt"];
|
||||
}
|
||||
|
||||
static BOOL sciShouldBlockSeenVisual() {
|
||||
static BOOL sciShouldBlockSeenVisual(void) {
|
||||
if (sciSeenBypassActive) return NO;
|
||||
if (sciStorySeenToggleBypass()) return NO;
|
||||
if (sciIsCurrentStoryOwnerExcluded()) return NO;
|
||||
return [SCIUtils getBoolPref:@"no_seen_receipt"] && [SCIUtils getBoolPref:@"no_seen_visual"];
|
||||
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) return NO;
|
||||
return ![SCIUtils getBoolPref:@"keep_seen_visual_local"];
|
||||
}
|
||||
|
||||
// Per-instance gating for tray/item/ring hooks where the "current" story
|
||||
// VC may not be the owner of the model in question.
|
||||
// Per-instance gate — tray/item/ring models may not match the active VC.
|
||||
static BOOL sciShouldBlockSeenVisualForObj(id obj) {
|
||||
if (sciSeenBypassActive) return NO;
|
||||
if (sciStorySeenToggleBypass()) return NO;
|
||||
if (![SCIUtils getBoolPref:@"no_seen_receipt"] || ![SCIUtils getBoolPref:@"no_seen_visual"]) return NO;
|
||||
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) return NO;
|
||||
if ([SCIUtils getBoolPref:@"keep_seen_visual_local"]) return NO;
|
||||
if (sciIsObjectStoryOwnerExcluded(obj)) return NO;
|
||||
return YES;
|
||||
}
|
||||
|
||||
// network seen blocking
|
||||
// ============ Legacy network-upload hooks (pre-Sundial fallback) ============
|
||||
%hook IGStorySeenStateUploader
|
||||
- (void)uploadSeenStateWithMedia:(id)arg1 {
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)uploadSeenState {
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !(sciAllowedSeenPKs && sciAllowedSeenPKs.count > 0)) return;
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)_uploadSeenState:(id)arg1 {
|
||||
@@ -73,16 +88,16 @@ static BOOL sciShouldBlockSeenVisualForObj(id obj) {
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||
%orig;
|
||||
}
|
||||
- (id)networker { return %orig; }
|
||||
%end
|
||||
|
||||
// visual seen blocking + story auto-advance
|
||||
// ============ Visual-seen hooks + auto-advance ============
|
||||
|
||||
%hook IGStoryFullscreenSectionController
|
||||
- (void)markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
|
||||
- (void)_markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
|
||||
- (void)storySeenStateDidChange:(id)arg1 { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
- (void)markCurrentItemAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenNetwork()) return; %orig; }
|
||||
- (void)storyPlayerMediaViewDidPlayToEnd:(id)arg1 {
|
||||
if (!sciAdvanceBypassActive && [SCIUtils getBoolPref:@"stop_story_auto_advance"]) return;
|
||||
%orig;
|
||||
@@ -93,13 +108,6 @@ static BOOL sciShouldBlockSeenVisualForObj(id obj) {
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGStoryViewerViewController
|
||||
- (void)fullscreenSectionController:(id)arg1 didMarkItemAsSeen:(id)arg2 {
|
||||
if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg2)) return;
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGStoryTrayViewModel
|
||||
- (void)markAsSeen { if (sciShouldBlockSeenVisualForObj(self)) return; %orig; }
|
||||
- (void)setHasUnseenMedia:(BOOL)arg1 { if (sciShouldBlockSeenVisualForObj(self)) { %orig(YES); return; } %orig; }
|
||||
@@ -119,9 +127,7 @@ static BOOL sciShouldBlockSeenVisualForObj(id obj) {
|
||||
- (void)updateRingForSeenState:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||
%end
|
||||
|
||||
// ============ STORY LIKE HOOKS ============
|
||||
// Hooks all known like entry points to trigger mark-seen and auto-advance on like.
|
||||
// Uses sciMarkSeenTapped: from OverlayButtons.xm for the actual seen flow.
|
||||
// ============ Active story VC tracking ============
|
||||
|
||||
__weak UIViewController *sciActiveStoryVC = nil;
|
||||
|
||||
@@ -132,95 +138,171 @@ __weak UIViewController *sciActiveStoryVC = nil;
|
||||
}
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
if (sciActiveStoryVC == (UIViewController *)self) sciActiveStoryVC = nil;
|
||||
sciStateRestore();
|
||||
%orig;
|
||||
}
|
||||
- (void)fullscreenSectionController:(id)arg1 didMarkItemAsSeen:(id)arg2 {
|
||||
if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg2)) return;
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
|
||||
static UIView *sciFindStoryOverlayView(UIViewController *vc) {
|
||||
if (!vc) return nil;
|
||||
Class targetCls = NSClassFromString(@"IGStoryFullscreenOverlayView");
|
||||
if (!targetCls) return nil;
|
||||
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
|
||||
while (stack.count > 0) {
|
||||
UIView *v = stack.lastObject;
|
||||
[stack removeLastObject];
|
||||
if ([v isKindOfClass:targetCls]) return v;
|
||||
for (UIView *sub in v.subviews) [stack addObject:sub];
|
||||
}
|
||||
return nil;
|
||||
// ============ Networker-ivar swap (v425+ split-mode) ============
|
||||
|
||||
static __weak id sciLegacyUploader = nil; // IGStorySeenStateUploader
|
||||
static __weak id sciSundialManager = nil; // IGSundialSeenStateManager
|
||||
|
||||
static id (*orig_pendingStoreInit)(id, SEL, id, id, id, BOOL);
|
||||
static id new_pendingStoreInit(id self, SEL _cmd, id sessionPK, id uploader, id fileMgr, BOOL bgTask) {
|
||||
if (uploader) sciLegacyUploader = uploader;
|
||||
return orig_pendingStoreInit(self, _cmd, sessionPK, uploader, fileMgr, bgTask);
|
||||
}
|
||||
|
||||
static void sciMarkActiveStorySeen(void) {
|
||||
if (![SCIUtils getBoolPref:@"seen_on_story_like"]) return;
|
||||
UIView *overlay = sciFindStoryOverlayView(sciActiveStoryVC);
|
||||
if (!overlay) return;
|
||||
SEL sel = NSSelectorFromString(@"sciMarkSeenTapped:");
|
||||
if ([overlay respondsToSelector:sel])
|
||||
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
|
||||
static id (*orig_sundialMgrInit)(id, SEL, id, id, id, id);
|
||||
static id new_sundialMgrInit(id self, SEL _cmd, id networker, id diskMgr, id launcherSet, id announcer) {
|
||||
id res = orig_sundialMgrInit(self, _cmd, networker, diskMgr, launcherSet, announcer);
|
||||
if (res) sciSundialManager = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Dedup guard — multiple hooks fire for the same like event
|
||||
static uint64_t sciLastLikeAdvanceTime = 0;
|
||||
|
||||
static void sciAdvanceOnStoryLike(void) {
|
||||
if (![SCIUtils getBoolPref:@"advance_on_story_like"]) return;
|
||||
UIViewController *storyVC = sciActiveStoryVC;
|
||||
if (!storyVC) return;
|
||||
id sectionCtrl = sciFindSectionController(storyVC);
|
||||
if (!sectionCtrl) return;
|
||||
|
||||
uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
if (now - sciLastLikeAdvanceTime < 500000000ULL) return;
|
||||
sciLastLikeAdvanceTime = now;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
sciAdvanceBypassActive = YES;
|
||||
SEL advSel = NSSelectorFromString(@"advanceToNextItemWithNavigationAction:");
|
||||
if ([sectionCtrl respondsToSelector:advSel])
|
||||
((void(*)(id, SEL, NSInteger))objc_msgSend)(sectionCtrl, advSel, 1);
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
id sc2 = storyVC ? sciFindSectionController(storyVC) : nil;
|
||||
if (sc2) {
|
||||
SEL resumeSel = NSSelectorFromString(@"tryResumePlaybackWithReason:");
|
||||
if ([sc2 respondsToSelector:resumeSel])
|
||||
((void(*)(id, SEL, NSInteger))objc_msgSend)(sc2, resumeSel, 0);
|
||||
// Swap each cached uploader's networker ivar; saved dict is used to restore.
|
||||
static NSDictionary *sciSwapNetworkers(id newNetworker) {
|
||||
NSMutableDictionary *saved = [NSMutableDictionary dictionary];
|
||||
@try {
|
||||
id legacy = sciLegacyUploader;
|
||||
if (legacy) {
|
||||
Ivar iv = class_getInstanceVariable([legacy class], "_networker");
|
||||
if (iv) {
|
||||
id old = object_getIvar(legacy, iv);
|
||||
if (old) saved[@"legacy"] = old;
|
||||
object_setIvar(legacy, iv, newNetworker);
|
||||
}
|
||||
sciAdvanceBypassActive = NO;
|
||||
});
|
||||
});
|
||||
}
|
||||
id mgr = sciSundialManager;
|
||||
if (mgr) {
|
||||
for (NSString *ivName in @[@"seenStateUploader", @"seenStateUploaderDeprecated"]) {
|
||||
Ivar mgrIv = class_getInstanceVariable([mgr class], [ivName UTF8String]);
|
||||
if (!mgrIv) continue;
|
||||
id up = object_getIvar(mgr, mgrIv);
|
||||
if (!up) continue;
|
||||
Ivar netIv = class_getInstanceVariable([up class], "networker");
|
||||
if (!netIv) continue;
|
||||
id oldNet = object_getIvar(up, netIv);
|
||||
if (oldNet) saved[ivName] = oldNet;
|
||||
object_setIvar(up, netIv, newNetworker);
|
||||
}
|
||||
}
|
||||
} @catch (__unused id e) {}
|
||||
return saved;
|
||||
}
|
||||
|
||||
static void sciOnStoryLike(void) {
|
||||
sciMarkActiveStorySeen();
|
||||
sciAdvanceOnStoryLike();
|
||||
static void sciRestoreNetworkers(NSDictionary *saved) {
|
||||
@try {
|
||||
id legacy = sciLegacyUploader;
|
||||
if (legacy && saved[@"legacy"]) {
|
||||
Ivar iv = class_getInstanceVariable([legacy class], "_networker");
|
||||
if (iv) object_setIvar(legacy, iv, saved[@"legacy"]);
|
||||
}
|
||||
id mgr = sciSundialManager;
|
||||
if (mgr) {
|
||||
for (NSString *ivName in @[@"seenStateUploader", @"seenStateUploaderDeprecated"]) {
|
||||
if (!saved[ivName]) continue;
|
||||
Ivar mgrIv = class_getInstanceVariable([mgr class], [ivName UTF8String]);
|
||||
if (!mgrIv) continue;
|
||||
id up = object_getIvar(mgr, mgrIv);
|
||||
if (!up) continue;
|
||||
Ivar netIv = class_getInstanceVariable([up class], "networker");
|
||||
if (netIv) object_setIvar(up, netIv, saved[ivName]);
|
||||
}
|
||||
}
|
||||
} @catch (__unused id e) {}
|
||||
}
|
||||
|
||||
// Idempotent block/restore. Guard prevents double-swap clobbering the saved originals.
|
||||
static BOOL sciNetBlocked = NO;
|
||||
static NSDictionary *sciNetSaved = nil;
|
||||
|
||||
static void sciStateBlock(void) {
|
||||
if (sciNetBlocked) return;
|
||||
sciNetSaved = sciSwapNetworkers(nil);
|
||||
sciNetBlocked = YES;
|
||||
}
|
||||
|
||||
static void sciStateRestore(void) {
|
||||
if (!sciNetBlocked) return;
|
||||
sciRestoreNetworkers(sciNetSaved);
|
||||
sciNetSaved = nil;
|
||||
sciNetBlocked = NO;
|
||||
}
|
||||
|
||||
static NSString *sciExtractOwnerPKFromItem(id item) {
|
||||
NSString *pk = nil;
|
||||
@try {
|
||||
id reelPk = [item respondsToSelector:@selector(reelPk)] ? [item performSelector:@selector(reelPk)] : nil;
|
||||
if (reelPk) pk = [reelPk description];
|
||||
if (!pk) {
|
||||
id media = [item respondsToSelector:@selector(media)] ? [item performSelector:@selector(media)] : item;
|
||||
id user = [media respondsToSelector:@selector(user)] ? [media performSelector:@selector(user)] : nil;
|
||||
if (!user) user = [media respondsToSelector:@selector(owner)] ? [media performSelector:@selector(owner)] : nil;
|
||||
if (user) {
|
||||
Ivar pkIvar = NULL;
|
||||
for (Class c = [user class]; c && !pkIvar; c = class_getSuperclass(c))
|
||||
pkIvar = class_getInstanceVariable(c, "_pk");
|
||||
if (pkIvar) pk = [object_getIvar(user, pkIvar) description];
|
||||
}
|
||||
}
|
||||
} @catch (__unused id e) {}
|
||||
return pk;
|
||||
}
|
||||
|
||||
// Mark-seen delegate: restore on non-blocked owners, block + run orig on
|
||||
// blocked owners when split-mode is on, skip orig when it's off.
|
||||
static void (*orig_delegateMarkSeen)(id, SEL, id, id);
|
||||
static void new_delegateMarkSeen(id self, SEL _cmd, id ctrl, id item) {
|
||||
if (sciSeenBypassActive) { sciStateRestore(); orig_delegateMarkSeen(self, _cmd, ctrl, item); return; }
|
||||
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) { sciStateRestore(); orig_delegateMarkSeen(self, _cmd, ctrl, item); return; }
|
||||
|
||||
NSString *ownerPK = sciExtractOwnerPKFromItem(item);
|
||||
BOOL shouldBlock;
|
||||
if ([SCIExcludedStoryUsers isFeatureEnabled])
|
||||
shouldBlock = ownerPK.length && ![SCIExcludedStoryUsers isUserPKExcluded:ownerPK];
|
||||
else
|
||||
shouldBlock = YES;
|
||||
|
||||
if (!shouldBlock) {
|
||||
sciStateRestore();
|
||||
orig_delegateMarkSeen(self, _cmd, ctrl, item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (![SCIUtils getBoolPref:@"keep_seen_visual_local"]) {
|
||||
sciStateRestore();
|
||||
return;
|
||||
}
|
||||
|
||||
sciStateBlock();
|
||||
@try { orig_delegateMarkSeen(self, _cmd, ctrl, item); }
|
||||
@catch (__unused id e) { sciStateRestore(); }
|
||||
}
|
||||
|
||||
// ============ Like → mark-seen side effects ============
|
||||
|
||||
static void (*orig_didLikeSundial)(id, SEL, id);
|
||||
static void new_didLikeSundial(id self, SEL _cmd, id pk) {
|
||||
orig_didLikeSundial(self, _cmd, pk);
|
||||
sciOnStoryLike();
|
||||
sciStoryInteractionSideEffects(SCIStoryInteractionLike);
|
||||
}
|
||||
|
||||
static void (*orig_overlaySetIsLiked)(id, SEL, BOOL, BOOL);
|
||||
static void new_overlaySetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL animated) {
|
||||
orig_overlaySetIsLiked(self, _cmd, isLiked, animated);
|
||||
if (isLiked) sciOnStoryLike();
|
||||
}
|
||||
|
||||
// IGUFIButton selected state: YES = heart filled (liked), NO = empty (not liked).
|
||||
// handleStoryLikeTapWithButton: is a toggle — check state before orig to determine direction.
|
||||
static void (*orig_handleLikeTap)(id, SEL, id);
|
||||
static void new_handleLikeTap(id self, SEL _cmd, id button) {
|
||||
BOOL isLike = [button isKindOfClass:[UIButton class]] && [(UIButton *)button isSelected];
|
||||
orig_handleLikeTap(self, _cmd, button);
|
||||
if (isLike) sciOnStoryLike();
|
||||
if (isLiked) sciStoryInteractionSideEffects(SCIStoryInteractionLike);
|
||||
}
|
||||
|
||||
static void (*orig_likeButtonSetIsLiked)(id, SEL, BOOL, BOOL);
|
||||
static void new_likeButtonSetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL animated) {
|
||||
orig_likeButtonSetIsLiked(self, _cmd, isLiked, animated);
|
||||
if (isLiked) sciOnStoryLike();
|
||||
if (isLiked) sciStoryInteractionSideEffects(SCIStoryInteractionLike);
|
||||
}
|
||||
|
||||
%ctor {
|
||||
@@ -229,23 +311,39 @@ static void new_likeButtonSetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL anima
|
||||
SEL didLike = NSSelectorFromString(@"didLikeSundialWithMediaPK:");
|
||||
if (class_getInstanceMethod(overlayCtl, didLike))
|
||||
MSHookMessageEx(overlayCtl, didLike, (IMP)new_didLikeSundial, (IMP *)&orig_didLikeSundial);
|
||||
|
||||
SEL setLiked = @selector(setIsLiked:animated:);
|
||||
if (class_getInstanceMethod(overlayCtl, setLiked))
|
||||
MSHookMessageEx(overlayCtl, setLiked, (IMP)new_overlaySetIsLiked, (IMP *)&orig_overlaySetIsLiked);
|
||||
}
|
||||
|
||||
Class likesImpl = NSClassFromString(@"IGStoryLikesInteractionControllingImpl");
|
||||
if (likesImpl) {
|
||||
SEL handleTap = NSSelectorFromString(@"handleStoryLikeTapWithButton:");
|
||||
if (class_getInstanceMethod(likesImpl, handleTap))
|
||||
MSHookMessageEx(likesImpl, handleTap, (IMP)new_handleLikeTap, (IMP *)&orig_handleLikeTap);
|
||||
}
|
||||
|
||||
Class likeBtn = NSClassFromString(@"IGSundialViewerUFI.IGSundialLikeButton");
|
||||
if (likeBtn) {
|
||||
SEL setLiked = @selector(setIsLiked:animated:);
|
||||
if (class_getInstanceMethod(likeBtn, setLiked))
|
||||
MSHookMessageEx(likeBtn, setLiked, (IMP)new_likeButtonSetIsLiked, (IMP *)&orig_likeButtonSetIsLiked);
|
||||
}
|
||||
|
||||
Class pending = NSClassFromString(@"IGStoryPendingSeenStateStore");
|
||||
SEL pendingSel = NSSelectorFromString(@"initWithUserSessionPK:uploader:fileManager:uploadInBackgroundTask:");
|
||||
if (pending && class_getInstanceMethod(pending, pendingSel))
|
||||
MSHookMessageEx(pending, pendingSel, (IMP)new_pendingStoreInit, (IMP *)&orig_pendingStoreInit);
|
||||
|
||||
Class sundialMgr = NSClassFromString(@"_TtC23IGSundialSeenStateSwift25IGSundialSeenStateManager");
|
||||
SEL mgrSel = NSSelectorFromString(@"initWithNetworker:diskManager:launcherSet:seenStateManagerAnnouncer:");
|
||||
if (sundialMgr && class_getInstanceMethod(sundialMgr, mgrSel))
|
||||
MSHookMessageEx(sundialMgr, mgrSel, (IMP)new_sundialMgrInit, (IMP *)&orig_sundialMgrInit);
|
||||
|
||||
// Mark-as-seen delegate; extras are forward-compat candidates.
|
||||
for (NSString *clsName in @[
|
||||
@"IGStoryViewerViewController",
|
||||
@"IGStoryViewerUpdater",
|
||||
@"IGStoryFullscreenViewModel",
|
||||
@"IGStoriesManager",
|
||||
]) {
|
||||
Class cls = NSClassFromString(clsName);
|
||||
if (!cls) continue;
|
||||
SEL delegateSel = NSSelectorFromString(@"fullscreenSectionController:didMarkItemAsSeen:");
|
||||
if (class_getInstanceMethod(cls, delegateSel))
|
||||
MSHookMessageEx(cls, delegateSel, (IMP)new_delegateMarkSeen, (IMP *)&orig_delegateMarkSeen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +139,14 @@ NSDictionary *sciOwnerInfoForView(UIView *view) {
|
||||
|
||||
BOOL sciIsCurrentStoryOwnerExcluded(void) {
|
||||
NSDictionary *info = sciCurrentStoryOwnerInfo();
|
||||
if (!info) return NO;
|
||||
// Unknown owner: block_selected → don't block; block_all → block.
|
||||
if (!info) return [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
return [SCIExcludedStoryUsers isUserPKExcluded:info[@"pk"]];
|
||||
}
|
||||
|
||||
BOOL sciIsObjectStoryOwnerExcluded(id obj) {
|
||||
NSDictionary *info = sciOwnerInfoFromObject(obj);
|
||||
if (!info) return NO;
|
||||
if (!info) return [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
return [SCIExcludedStoryUsers isUserPKExcluded:info[@"pk"]];
|
||||
}
|
||||
|
||||
|
||||
@@ -314,50 +314,31 @@ static void sciResumeStoryPlayback(UIView *sourceView) {
|
||||
[btn setImage:[UIImage systemImageNamed:icon withConfiguration:cfg] forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
// Rebuilds the eye button (tag 1339) based on current owner + prefs. Idempotent.
|
||||
// Rebuilds the eye button (tag 1339). Visible only when the story is
|
||||
// actively blocked for this owner. List management lives in the hold menu
|
||||
// and the ellipsis action menu.
|
||||
%new - (void)sciRefreshSeenButton {
|
||||
BOOL seenBlockingOn = [SCIUtils getBoolPref:@"no_seen_receipt"];
|
||||
BOOL storyBlockSelected = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
// In block_selected mode, show the eye for list management even if global toggle is off
|
||||
if (!seenBlockingOn && !storyBlockSelected) return;
|
||||
// Skip for DM visual messages inside an excluded thread
|
||||
NSString *activeTid = [SCIExcludedThreads activeThreadId];
|
||||
if (activeTid && [SCIExcludedThreads isInList:activeTid] && ![SCIExcludedThreads isBlockSelectedMode]) return;
|
||||
if (!seenBlockingOn) return;
|
||||
|
||||
NSDictionary *ownerInfo = sciOwnerInfoForView(self);
|
||||
NSString *ownerPK = ownerInfo[@"pk"] ?: @"";
|
||||
BOOL ownerInList = ownerPK.length && [SCIExcludedStoryUsers isInList:ownerPK];
|
||||
// block_all + in list: show remove icon (excluded user, behaves normally)
|
||||
// block_selected + in list: show normal eye (blocked user, needs mark-seen)
|
||||
// block_selected + not in list: show add icon
|
||||
BOOL showExcludeIcon = ownerInList && !storyBlockSelected;
|
||||
BOOL showAddIcon = storyBlockSelected && !ownerInList;
|
||||
BOOL listBtnPref = [SCIUtils getBoolPref:@"story_excluded_show_unexclude_eye"];
|
||||
BOOL hideForListedOwner = (showExcludeIcon || showAddIcon) && !listBtnPref;
|
||||
BOOL toggleMode = [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"];
|
||||
BOOL excluded = ownerPK.length && [SCIExcludedStoryUsers isUserPKExcluded:ownerPK];
|
||||
UIButton *existing = (UIButton *)[self viewWithTag:1339];
|
||||
|
||||
// Not blocked → no eye button.
|
||||
if (excluded) { [existing removeFromSuperview]; return; }
|
||||
|
||||
BOOL toggleMode = [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"];
|
||||
NSString *symName;
|
||||
UIColor *tint;
|
||||
if (showExcludeIcon) {
|
||||
// block_all + in list: remove-from-exclude icon
|
||||
symName = @"eye.slash.fill"; tint = SCIUtils.SCIColor_Primary;
|
||||
} else if (storyBlockSelected && !ownerInList) {
|
||||
// block_selected + not in list: add-to-block icon
|
||||
symName = @"eye.slash"; tint = [UIColor whiteColor];
|
||||
} else if (toggleMode) {
|
||||
if (toggleMode) {
|
||||
symName = sciStorySeenToggleEnabled ? @"eye.fill" : @"eye";
|
||||
tint = sciStorySeenToggleEnabled ? SCIUtils.SCIColor_Primary : [UIColor whiteColor];
|
||||
} else {
|
||||
symName = @"eye"; tint = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
UIButton *existing = (UIButton *)[self viewWithTag:1339];
|
||||
|
||||
if (hideForListedOwner) {
|
||||
[existing removeFromSuperview];
|
||||
return;
|
||||
}
|
||||
|
||||
UIImageSymbolConfiguration *cfg = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold];
|
||||
|
||||
if (existing) {
|
||||
@@ -444,57 +425,6 @@ static void sciResumeStoryPlayback(UIView *sourceView) {
|
||||
// ============ Seen button tap ============
|
||||
|
||||
%new - (void)sciSeenButtonTapped:(UIButton *)sender {
|
||||
NSDictionary *ownerInfo = sciOwnerInfoForView(self);
|
||||
NSString *ownerPK = ownerInfo[@"pk"];
|
||||
BOOL inList = ownerPK && [SCIExcludedStoryUsers isInList:ownerPK];
|
||||
BOOL bs = [SCIExcludedStoryUsers isBlockSelectedMode];
|
||||
|
||||
// Block selected + not in list: tap to ADD to block list (with confirmation)
|
||||
if (bs && !inList && ownerPK) {
|
||||
UIViewController *host = [SCIUtils nearestViewControllerForView:self];
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:SCILocalized(@"Add to block list?")
|
||||
message:[NSString stringWithFormat:SCILocalized(@"Story seen receipts will be blocked for @%@."), ownerInfo[@"username"] ?: @""]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Add") style:UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedStoryUsers addOrUpdateEntry:@{
|
||||
@"pk": ownerPK,
|
||||
@"username": ownerInfo[@"username"] ?: @"",
|
||||
@"fullName": ownerInfo[@"fullName"] ?: @""
|
||||
}];
|
||||
[SCIUtils showToastForDuration:2.0 title:SCILocalized(@"Added to block list")];
|
||||
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
|
||||
[host presentViewController:alert animated:YES completion:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// Block selected + in list: blocked story, tap = mark seen (long-press to remove)
|
||||
if (bs && inList) {
|
||||
((void(*)(id, SEL, id))objc_msgSend)(self, @selector(sciMarkSeenTapped:), sender);
|
||||
return;
|
||||
}
|
||||
|
||||
// Block all + in list: tap to remove from exclude list
|
||||
if (inList) {
|
||||
UIViewController *host = [SCIUtils nearestViewControllerForView:self];
|
||||
NSString *alertTitle = bs ? SCILocalized(@"Remove from block list?") : SCILocalized(@"Un-exclude story seen?");
|
||||
NSString *alertMsg = bs ? [NSString stringWithFormat:@"@%@ will no longer have seen receipts blocked.", ownerInfo[@"username"] ?: @""]
|
||||
: [NSString stringWithFormat:@"@%@ will resume normal story-seen blocking.", ownerInfo[@"username"] ?: @""];
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:alertTitle message:alertMsg preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:bs ? SCILocalized(@"Unblock") : SCILocalized(@"Un-exclude") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *_) {
|
||||
[SCIExcludedStoryUsers removePK:ownerPK];
|
||||
[SCIUtils showToastForDuration:2.0 title:bs ? SCILocalized(@"Unblocked") : SCILocalized(@"Un-excluded")];
|
||||
if (bs) sciTriggerStoryMarkSeen(sciActiveStoryViewerVC);
|
||||
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
|
||||
[host presentViewController:alert animated:YES completion:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle mode
|
||||
if ([[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"]) {
|
||||
sciStorySeenToggleEnabled = !sciStorySeenToggleEnabled;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Story interaction pipeline — confirm gate + seen/advance per policy table.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, SCIStoryInteraction) {
|
||||
SCIStoryInteractionLike,
|
||||
SCIStoryInteractionEmojiReaction,
|
||||
SCIStoryInteractionTextReply,
|
||||
};
|
||||
|
||||
void sciStoryInteraction(SCIStoryInteraction type,
|
||||
void (^action)(void),
|
||||
void (^_Nullable uiRevert)(void),
|
||||
void (^_Nullable uiReapply)(void));
|
||||
|
||||
// Side-effects only (seen/advance). No confirm, no action.
|
||||
void sciStoryInteractionSideEffects(SCIStoryInteraction type);
|
||||
@@ -0,0 +1,131 @@
|
||||
#import "SCIStoryInteractionPipeline.h"
|
||||
#import "StoryHelpers.h"
|
||||
#import "../../Utils.h"
|
||||
#import <objc/message.h>
|
||||
#import <mach/mach_time.h>
|
||||
|
||||
extern __weak UIViewController *sciActiveStoryVC;
|
||||
extern BOOL sciAdvanceBypassActive;
|
||||
|
||||
#pragma mark - Policy table
|
||||
|
||||
typedef struct {
|
||||
NSString *confirmPref;
|
||||
NSString *seenPref;
|
||||
NSString *advancePref;
|
||||
NSTimeInterval advanceDelay;
|
||||
} SCIStoryPolicy;
|
||||
|
||||
static SCIStoryPolicy sciPolicyForType(SCIStoryInteraction type) {
|
||||
switch (type) {
|
||||
case SCIStoryInteractionLike:
|
||||
return (SCIStoryPolicy){
|
||||
@"story_like_confirm",
|
||||
@"seen_on_story_like",
|
||||
@"advance_on_story_like",
|
||||
0.3
|
||||
};
|
||||
case SCIStoryInteractionEmojiReaction:
|
||||
return (SCIStoryPolicy){
|
||||
@"emoji_reaction_confirm",
|
||||
@"seen_on_story_reply",
|
||||
@"advance_on_story_reply",
|
||||
0.4
|
||||
};
|
||||
case SCIStoryInteractionTextReply:
|
||||
return (SCIStoryPolicy){
|
||||
nil,
|
||||
@"seen_on_story_reply",
|
||||
@"advance_on_story_reply",
|
||||
0.4
|
||||
};
|
||||
}
|
||||
return (SCIStoryPolicy){ nil, nil, nil, 0.3 };
|
||||
}
|
||||
|
||||
#pragma mark - Side effects
|
||||
|
||||
static UIView *sciFindOverlay(UIViewController *vc) {
|
||||
if (!vc) return nil;
|
||||
Class cls = NSClassFromString(@"IGStoryFullscreenOverlayView");
|
||||
if (!cls) return nil;
|
||||
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
|
||||
while (stack.count) {
|
||||
UIView *v = stack.lastObject;
|
||||
[stack removeLastObject];
|
||||
if ([v isKindOfClass:cls]) return v;
|
||||
for (UIView *s in v.subviews) [stack addObject:s];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static void sciMarkSeen(NSString *prefKey) {
|
||||
if (!prefKey || ![SCIUtils getBoolPref:prefKey]) return;
|
||||
UIView *overlay = sciFindOverlay(sciActiveStoryVC);
|
||||
if (!overlay) return;
|
||||
SEL sel = NSSelectorFromString(@"sciMarkSeenTapped:");
|
||||
if ([overlay respondsToSelector:sel])
|
||||
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
|
||||
}
|
||||
|
||||
static uint64_t sciLastAdvanceTime = 0;
|
||||
|
||||
static void sciAdvance(NSString *prefKey, NSTimeInterval delay) {
|
||||
if (!prefKey || ![SCIUtils getBoolPref:prefKey]) return;
|
||||
UIViewController *vc = sciActiveStoryVC;
|
||||
if (!vc) return;
|
||||
id ctrl = sciFindSectionController(vc);
|
||||
if (!ctrl) return;
|
||||
|
||||
uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
if (now - sciLastAdvanceTime < 500000000ULL) return;
|
||||
sciLastAdvanceTime = now;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
sciAdvanceBypassActive = YES;
|
||||
SEL advSel = NSSelectorFromString(@"advanceToNextItemWithNavigationAction:");
|
||||
if ([ctrl respondsToSelector:advSel])
|
||||
((void(*)(id, SEL, NSInteger))objc_msgSend)(ctrl, advSel, 1);
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
id c2 = vc ? sciFindSectionController(vc) : nil;
|
||||
if (c2) {
|
||||
SEL resumeSel = NSSelectorFromString(@"tryResumePlaybackWithReason:");
|
||||
if ([c2 respondsToSelector:resumeSel])
|
||||
((void(*)(id, SEL, NSInteger))objc_msgSend)(c2, resumeSel, 0);
|
||||
}
|
||||
sciAdvanceBypassActive = NO;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void sciFireSideEffects(SCIStoryPolicy policy) {
|
||||
sciMarkSeen(policy.seenPref);
|
||||
sciAdvance(policy.advancePref, policy.advanceDelay);
|
||||
}
|
||||
|
||||
#pragma mark - Pipeline
|
||||
|
||||
void sciStoryInteraction(SCIStoryInteraction type,
|
||||
void (^action)(void),
|
||||
void (^_Nullable uiRevert)(void),
|
||||
void (^_Nullable uiReapply)(void)) {
|
||||
SCIStoryPolicy policy = sciPolicyForType(type);
|
||||
|
||||
if (policy.confirmPref && [SCIUtils getBoolPref:policy.confirmPref]) {
|
||||
if (uiRevert) uiRevert();
|
||||
[SCIUtils showConfirmation:^{
|
||||
if (uiReapply) uiReapply();
|
||||
if (action) action();
|
||||
sciFireSideEffects(policy);
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
if (action) action();
|
||||
sciFireSideEffects(policy);
|
||||
}
|
||||
|
||||
void sciStoryInteractionSideEffects(SCIStoryInteraction type) {
|
||||
sciFireSideEffects(sciPolicyForType(type));
|
||||
}
|
||||
@@ -197,7 +197,7 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
|
||||
if (w.isKeyWindow) { win = w; break; }
|
||||
}
|
||||
}
|
||||
[SCIUtils showSettingsVC:win atTopLevelEntry:@"Messages"];
|
||||
[SCIUtils showSettingsVC:win atTopLevelEntry:SCILocalized(@"Messages")];
|
||||
}];
|
||||
[items addObject:openSettings];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Mark seen + advance when replying or reacting to a story.
|
||||
// Story reply + emoji reaction hooks. Routes through the interaction pipeline.
|
||||
|
||||
#import "SCIStoryInteractionPipeline.h"
|
||||
#import "../../Utils.h"
|
||||
#import "StoryHelpers.h"
|
||||
#import <objc/message.h>
|
||||
@@ -9,106 +10,43 @@
|
||||
extern __weak UIViewController *sciActiveStoryVC;
|
||||
extern BOOL sciAdvanceBypassActive;
|
||||
|
||||
static UIView *sciFindOverlayForStoryVC(UIViewController *vc) {
|
||||
if (!vc) return nil;
|
||||
Class overlayCls = NSClassFromString(@"IGStoryFullscreenOverlayView");
|
||||
if (!overlayCls) return nil;
|
||||
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
|
||||
while (stack.count) {
|
||||
UIView *v = stack.lastObject;
|
||||
[stack removeLastObject];
|
||||
if ([v isKindOfClass:overlayCls]) return v;
|
||||
for (UIView *s in v.subviews) [stack addObject:s];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static void sciMarkSeenOnReply(void) {
|
||||
if (![SCIUtils getBoolPref:@"seen_on_story_reply"]) return;
|
||||
UIView *overlay = sciFindOverlayForStoryVC(sciActiveStoryVC);
|
||||
if (!overlay) return;
|
||||
SEL sel = @selector(sciMarkSeenTapped:);
|
||||
if ([overlay respondsToSelector:sel])
|
||||
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
|
||||
}
|
||||
|
||||
static uint64_t sciLastReplyAdvanceTime = 0;
|
||||
|
||||
static void sciAdvanceOnReply(void) {
|
||||
if (![SCIUtils getBoolPref:@"advance_on_story_reply"]) return;
|
||||
UIViewController *storyVC = sciActiveStoryVC;
|
||||
if (!storyVC) return;
|
||||
id sectionCtrl = sciFindSectionController(storyVC);
|
||||
if (!sectionCtrl) return;
|
||||
|
||||
// Dedup across multiple hooks firing for the same event
|
||||
uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
if (now - sciLastReplyAdvanceTime < 500000000ULL) return;
|
||||
sciLastReplyAdvanceTime = now;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
sciAdvanceBypassActive = YES;
|
||||
SEL advSel = NSSelectorFromString(@"advanceToNextItemWithNavigationAction:");
|
||||
if ([sectionCtrl respondsToSelector:advSel])
|
||||
((void(*)(id, SEL, NSInteger))objc_msgSend)(sectionCtrl, advSel, 1);
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
id sc2 = storyVC ? sciFindSectionController(storyVC) : nil;
|
||||
if (sc2) {
|
||||
SEL resumeSel = NSSelectorFromString(@"tryResumePlaybackWithReason:");
|
||||
if ([sc2 respondsToSelector:resumeSel])
|
||||
((void(*)(id, SEL, NSInteger))objc_msgSend)(sc2, resumeSel, 0);
|
||||
}
|
||||
sciAdvanceBypassActive = NO;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void sciOnStoryReply(void) {
|
||||
sciMarkSeenOnReply();
|
||||
sciAdvanceOnReply();
|
||||
}
|
||||
|
||||
// Text reply — IGDirectComposer is shared with DMs, gate by active story VC.
|
||||
%hook IGDirectComposer
|
||||
- (void)_didTapSend:(id)arg {
|
||||
%orig;
|
||||
if (sciActiveStoryVC) sciOnStoryReply();
|
||||
if (sciActiveStoryVC) sciStoryInteraction(SCIStoryInteractionTextReply, nil, nil, nil);
|
||||
}
|
||||
- (void)_send {
|
||||
%orig;
|
||||
if (sciActiveStoryVC) sciOnStoryReply();
|
||||
if (sciActiveStoryVC) sciStoryInteraction(SCIStoryInteractionTextReply, nil, nil, nil);
|
||||
}
|
||||
%end
|
||||
|
||||
// Composer emoji reaction buttons (forwarded to the Swift footer delegate)
|
||||
// Composer emoji reaction buttons
|
||||
static void (*orig_footerEmojiQuick)(id, SEL, id, id);
|
||||
static void new_footerEmojiQuick(id self, SEL _cmd, id inputView, id btn) {
|
||||
orig_footerEmojiQuick(self, _cmd, inputView, btn);
|
||||
sciOnStoryReply();
|
||||
sciStoryInteraction(SCIStoryInteractionEmojiReaction,
|
||||
^{ orig_footerEmojiQuick(self, _cmd, inputView, btn); }, nil, nil);
|
||||
}
|
||||
|
||||
static void (*orig_footerEmojiReaction)(id, SEL, id, id);
|
||||
static void new_footerEmojiReaction(id self, SEL _cmd, id inputView, id btn) {
|
||||
orig_footerEmojiReaction(self, _cmd, inputView, btn);
|
||||
sciOnStoryReply();
|
||||
sciStoryInteraction(SCIStoryInteractionEmojiReaction,
|
||||
^{ orig_footerEmojiReaction(self, _cmd, inputView, btn); }, nil, nil);
|
||||
}
|
||||
|
||||
// Swipe-up quick reactions tray
|
||||
// Swipe-up quick reactions. qrCtrl → qrDelegate internally, gate only qrCtrl.
|
||||
static void (*orig_qrCtrlDidTapEmoji)(id, SEL, id, id, id);
|
||||
static void new_qrCtrlDidTapEmoji(id self, SEL _cmd, id view, id sourceBtn, id emoji) {
|
||||
orig_qrCtrlDidTapEmoji(self, _cmd, view, sourceBtn, emoji);
|
||||
sciOnStoryReply();
|
||||
sciStoryInteraction(SCIStoryInteractionEmojiReaction,
|
||||
^{ orig_qrCtrlDidTapEmoji(self, _cmd, view, sourceBtn, emoji); }, nil, nil);
|
||||
}
|
||||
|
||||
static void (*orig_qrDelegateDidTapEmoji)(id, SEL, id, id, id);
|
||||
static void new_qrDelegateDidTapEmoji(id self, SEL _cmd, id ctrl, id sourceBtn, id emoji) {
|
||||
orig_qrDelegateDidTapEmoji(self, _cmd, ctrl, sourceBtn, emoji);
|
||||
sciOnStoryReply();
|
||||
}
|
||||
|
||||
// Swift classes aren't guaranteed to be registered at %ctor time — install
|
||||
// lazily on first overlay appearance as a fallback.
|
||||
static void sciInstallReplyHooks(void) {
|
||||
static BOOL installed = NO;
|
||||
if (installed) return;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Story like button hook. Routes through the interaction pipeline.
|
||||
|
||||
#import "SCIStoryInteractionPipeline.h"
|
||||
#import "../../Utils.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
#import <substrate.h>
|
||||
|
||||
static void (*orig_sciStoryLikeTap)(id, SEL, id);
|
||||
static void new_sciStoryLikeTap(id self, SEL _cmd, id button) {
|
||||
BOOL isSelected = [button isKindOfClass:[UIButton class]] ? [(UIButton *)button isSelected] : NO;
|
||||
if (!isSelected) { orig_sciStoryLikeTap(self, _cmd, button); return; }
|
||||
|
||||
UIButton *btn = (UIButton *)button;
|
||||
SEL setLiked = NSSelectorFromString(@"setIsLiked:animated:");
|
||||
|
||||
sciStoryInteraction(SCIStoryInteractionLike,
|
||||
^{ orig_sciStoryLikeTap(self, _cmd, button); },
|
||||
^{
|
||||
[UIView performWithoutAnimation:^{
|
||||
[btn setSelected:NO];
|
||||
if ([btn respondsToSelector:setLiked])
|
||||
((void(*)(id, SEL, BOOL, BOOL))objc_msgSend)(btn, setLiked, NO, NO);
|
||||
}];
|
||||
},
|
||||
^{
|
||||
[btn setSelected:YES];
|
||||
if ([btn respondsToSelector:setLiked])
|
||||
((void(*)(id, SEL, BOOL, BOOL))objc_msgSend)(btn, setLiked, YES, YES);
|
||||
});
|
||||
}
|
||||
|
||||
%ctor {
|
||||
Class cls = NSClassFromString(@"IGStoryLikesInteractionControllingImpl");
|
||||
if (!cls) return;
|
||||
SEL sel = NSSelectorFromString(@"handleStoryLikeTapWithButton:");
|
||||
if (!class_getInstanceMethod(cls, sel)) return;
|
||||
MSHookMessageEx(cls, sel, (IMP)new_sciStoryLikeTap, (IMP *)&orig_sciStoryLikeTap);
|
||||
}
|
||||
@@ -229,6 +229,10 @@
|
||||
|
||||
"Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below." = "Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below.";
|
||||
"Always show progress scrubber" = "Always show progress scrubber";
|
||||
"Auto-scroll reels" = "Auto-scroll reels";
|
||||
"IG default: native behavior. RyukGram: re-advances after swiping back." = "IG default: native behavior. RyukGram: re-advances after swiping back.";
|
||||
"IG default" = "IG default";
|
||||
"RyukGram" = "RyukGram";
|
||||
"Change what happens when you tap on a reel" = "Change what happens when you tap on a reel";
|
||||
"Confirm reel refresh" = "Confirm reel refresh";
|
||||
"Disable auto-unmuting reels" = "Disable auto-unmuting reels";
|
||||
@@ -315,7 +319,7 @@
|
||||
"Hides the functionality to create/send instants" = "Hides the functionality to create/send instants";
|
||||
"Hides the notification for others when you view their story" = "Hides the notification for others when you view their story";
|
||||
"Inserts a button next to the seen/eye button on story overlays" = "Inserts a button next to the seen/eye button on story overlays";
|
||||
"Keep stories visually unseen" = "Keep stories visually unseen";
|
||||
"Keep stories visually seen locally" = "Keep stories visually seen locally";
|
||||
"Liking a story automatically advances to the next one after a short delay" = "Liking a story automatically advances to the next one after a short delay";
|
||||
"Manage list" = "Manage list";
|
||||
"Manage list (%lu)" = "Manage list (%lu)";
|
||||
@@ -327,7 +331,7 @@
|
||||
"Master toggle. When off, the list is ignored" = "Master toggle. When off, the list is ignored";
|
||||
"Other" = "Other";
|
||||
"Playback" = "Playback";
|
||||
"Prevents stories from visually marking as seen in the tray (keeps colorful ring)" = "Prevents stories from visually marking as seen in the tray (keeps colorful ring)";
|
||||
"Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server" = "Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server";
|
||||
"Quick list button in stories" = "Quick list button in stories";
|
||||
"Search, sort, swipe to remove" = "Search, sort, swipe to remove";
|
||||
"Seen receipts" = "Seen receipts";
|
||||
@@ -466,13 +470,18 @@
|
||||
"Confirm changing theme" = "Confirm changing theme";
|
||||
"Confirm follow" = "Confirm follow";
|
||||
"Confirm follow requests" = "Confirm follow requests";
|
||||
"Confirm like: Posts/Stories" = "Confirm like: Posts/Stories";
|
||||
"Confirm like: Posts" = "Confirm like: Posts";
|
||||
"Confirm like: Reels" = "Confirm like: Reels";
|
||||
"Confirm posting comment" = "Confirm posting comment";
|
||||
"Confirm repost" = "Confirm repost";
|
||||
"Confirm shh mode" = "Confirm shh mode";
|
||||
"Confirm sticker interaction" = "Confirm sticker interaction";
|
||||
"Confirm story emoji reaction" = "Confirm story emoji reaction";
|
||||
"Confirm story like" = "Confirm story like";
|
||||
"Confirm unfollow" = "Confirm unfollow";
|
||||
"Shows an alert before sending an emoji reaction on a story" = "Shows an alert before sending an emoji reaction on a story";
|
||||
"Shows an alert when you click the like button on posts to confirm the like" = "Shows an alert when you click the like button on posts to confirm the like";
|
||||
"Shows an alert when you click the like button on stories to confirm the like" = "Shows an alert when you click the like button on stories to confirm the like";
|
||||
"Confirm voice messages" = "Confirm voice messages";
|
||||
"Shows an alert to confirm before sending a voice message" = "Shows an alert to confirm before sending a voice message";
|
||||
"Shows an alert to confirm before toggling disappearing messages" = "Shows an alert to confirm before toggling disappearing messages";
|
||||
@@ -739,15 +748,21 @@
|
||||
"Set location" = "Set location";
|
||||
"Settings…" = "Settings…";
|
||||
"Type emoji..." = "Type emoji...";
|
||||
"direct-inbox-tab" = "direct-inbox-tab";
|
||||
"mainfeed-tab" = "mainfeed-tab";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// SETTINGS VIEWS & DIALOGS //
|
||||
// Excluded-lists managers, backup/restore flows, in-picker labels. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Add chat" = "Add chat";
|
||||
"Add custom domain" = "Add custom domain";
|
||||
"Add to list?" = "Add to list?";
|
||||
"Add user" = "Add user";
|
||||
"Could not resolve user ID" = "Could not resolve user ID";
|
||||
"Enter username" = "Enter username";
|
||||
"Enter username of the DM thread" = "Enter username of the DM thread";
|
||||
"No DM thread found with @%@" = "No DM thread found with @%@";
|
||||
"User '%@' not found" = "User '%@' not found";
|
||||
"Add preset…" = "Add preset…";
|
||||
"All RyukGram settings will be reset to defaults and the imported values applied. The app will need to restart for some changes to take effect." = "All RyukGram settings will be reset to defaults and the imported values applied. The app will need to restart for some changes to take effect.";
|
||||
"Apply" = "Apply";
|
||||
@@ -902,4 +917,10 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Navigation Cell" = "Navigation Cell";
|
||||
"Localization" = "Localization";
|
||||
"Update localization file" = "Update localization file";
|
||||
"Import a .strings file for a language" = "Import a .strings file for a language";
|
||||
"Import a .strings file to update a translation. Pick a language, select the file, restart." = "Import a .strings file to update a translation. Pick a language, select the file, restart.";
|
||||
"Export English strings" = "Export English strings";
|
||||
"Share the base English .strings file for translating" = "Share the base English .strings file for translating";
|
||||
|
||||
|
||||
@@ -0,0 +1,930 @@
|
||||
// Spanish translation by Furamako (@furamako)
|
||||
//
|
||||
/*
|
||||
* RyukGram — Localizable.strings (Spanish)
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Every user-facing string in RyukGram goes through the macro
|
||||
* SCILocalized(@"English text here")
|
||||
* in the Objective-C source. The argument is BOTH the lookup key and the
|
||||
* English fallback, so if a translation is missing the user still sees
|
||||
* clean English — nothing ever breaks.
|
||||
*
|
||||
*
|
||||
* HOW TO ADD A NEW LANGUAGE
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* 1. Copy this file into a new folder named after the language code:
|
||||
* src/Localization/Resources/<code>.lproj/Localizable.strings
|
||||
* e.g. ar.lproj (Arabic)
|
||||
* es.lproj (Spanish)
|
||||
* fr.lproj (French)
|
||||
* 2. Translate the RIGHT-hand side of every `"key" = "value";` line.
|
||||
* Do NOT touch the left-hand side — that is the lookup key and must
|
||||
* stay identical to the English version, otherwise the app will never
|
||||
* find your translation.
|
||||
* 3. Keep every format specifier (%@, %lu, %d, %lld, %1$@, …) exactly
|
||||
* as-is, in the same order. If you need to reorder them, switch to
|
||||
* positional specifiers (%1$@ %2$lu).
|
||||
* 4. Keep embedded quotes escaped with a backslash: \" — and newlines
|
||||
* as \n.
|
||||
* 5. Open a pull request at https://github.com/faroukbmiled/RyukGram/pulls
|
||||
* so we can ship the language in the next release.
|
||||
*
|
||||
*
|
||||
* HOW TO ADD A NEW STRING IN CODE
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Just wrap the English text with SCILocalized(...) in the .m / .x / .xm
|
||||
* file — the helper resolves to the English text automatically when no
|
||||
* translation exists. Then add the same English text as BOTH the key and
|
||||
* the value inside the matching section below, e.g.
|
||||
*
|
||||
* "Download all items" = "Download all items";
|
||||
*
|
||||
* Translators copy that line into their own .lproj and translate only the
|
||||
* right-hand side.
|
||||
*
|
||||
*
|
||||
* FILE FORMAT NOTES
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* - UTF-8, LF line endings.
|
||||
* - Slash-star block comments and double-slash line comments both work.
|
||||
* - DO NOT nest one slash-star block comment inside another — the
|
||||
* parser will close the outer block at the first inner close marker
|
||||
* and every lookup in the file will silently fail.
|
||||
* - Keys and values are both quoted; every line ends with a semicolon.
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// CHROME — TOP BAR, LANGUAGE PICKER, FIRST-RUN //
|
||||
// Shown on the root Settings screen: title, search bar, the globe language //
|
||||
// menu, and the one-time welcome alert. These use dotted keys (settings.*) //
|
||||
// and are hand-authored rather than extracted from English source. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"settings.firstrun.message" = "Para el futuro: Mantener pulsadas las tres líneas en la parte superior derecha en la página de perfil, para volver a abrir la configuración de RyukGram";
|
||||
"settings.firstrun.ok" = "Entiendo!";
|
||||
"settings.firstrun.title" = "Información de configuración de RyukGram";
|
||||
"settings.language.system" = "Por defecto del sistema";
|
||||
"settings.language.title" = "Idioma";
|
||||
/* [ADDED_BY_DEV] */ "settings.language.english_only" = "Por el momento, RyukGram solo está disponible en Inglés. ¡Las traducciones son bienvenidas!";
|
||||
/* [ADDED_BY_DEV] */ "settings.language.help_translate" = "Ayudar a traducir";
|
||||
/* [ADDED_BY_DEV] */ "settings.language.ok" = "OK";
|
||||
"settings.results.many" = "%lu resultados";
|
||||
"settings.results.none" = "Sin resultados";
|
||||
"settings.results.one" = "%lu resultado";
|
||||
"settings.search.placeholder" = "Configuración de búsqueda";
|
||||
"settings.title" = "Configuración de RyukGram";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// GENERAL //
|
||||
// Settings → General tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Adds a copy option to the comment long-press menu" = "Añade la opción de copiar en el menú que aparece al mantener pulsado un comentario";
|
||||
"Adds a download option for GIF comments" = "Añade la opción de descargar los GIF en comentarios";
|
||||
"Browser" = "Navegador";
|
||||
"Comments" = "Comentarios";
|
||||
"Copy comment text" = "Copiar texto del comentario";
|
||||
"Copy description" = "Copiar descripción";
|
||||
"Copy description text fields by long-pressing on them" = "Copia la descripción al mantenerla pulsada";
|
||||
"Date format" = "Formato de fecha";
|
||||
"Disable app haptics" = "Deshabilitar respuesta háptica de la aplicación";
|
||||
"Disables haptics/vibrations within the app" = "Deshabilita la respuesta háptica y vibraciones dentro de la aplicación";
|
||||
"Do not save recent searches" = "No guardar búsquedas recientes";
|
||||
/* [ADDED_BY_DEV] */ "Search bars will no longer save your recent searches" = "Las barras de búsqueda ya no guardarán tus búsquedas recientes";
|
||||
"Download GIF comments" = "Descargar GIF en comentarios";
|
||||
"Embed domain" = "Dominio embebido";
|
||||
"Embed domain: %@" = "Dominio embebido: %@";
|
||||
"Enable liquid glass buttons" = "Habilitar botones Liquid Glass";
|
||||
"Enable liquid glass surfaces" = "Habilitar superficies Liquid Glass";
|
||||
"Enable teen app icons" = "Habilitar íconos para adolescentes";
|
||||
"Enables experimental liquid glass buttons" = "Habilita botones experimentales Liquid Glass";
|
||||
"Enables liquid glass tab bar, floating navigation, and other UI elements" = "Habilita Liquid Glass en la barra de pestañas, navegación flotante y otros elementos de la interfaz";
|
||||
"Experimental features" = "Funciones experimentales";
|
||||
"Focus/distractions" = "Concentración/Distracciones";
|
||||
"General" = "General";
|
||||
"Hide Meta AI" = "Ocultar Meta AI";
|
||||
"Hide ads" = "Ocultar anuncios";
|
||||
"Hide explore posts grid" = "Ocultar la cuadrícula de publicaciones";
|
||||
"Hide friends map" = "Ocultar el mapa de amigos";
|
||||
"Hide metrics" = "Ocultar métricas";
|
||||
"Hide notes tray" = "Ocultar bandeja de notas";
|
||||
"Hide trending searches" = "Ocultar búsquedas en tendencia";
|
||||
"Hides all suggested users for you to follow, outside your feed" = "Oculta 'Sugerencias para ti' en tu Feed (Inicio)";
|
||||
"Hides like/comment/share counts on posts and reels" = "Oculta el contador de me gusta, comentarios y compartidos en publicaciones y reels";
|
||||
"Hides the friends map icon in the notes tray" = "Oculta el ícono de mapa de amigos en la bandeja de notas";
|
||||
"Hides the grid of suggested posts on the explore/search tab" = "Oculta la cuadrícula de publicaciones sugeridas en la pestaña de exploración/búsqueda";
|
||||
"Hides the meta ai buttons/functionality within the app" = "Oculta los botones y funcionalidad de Meta AI dentro de la aplicación";
|
||||
"Hides the notes tray in the DM inbox" = "Oculta la bandeja de notas en la pestaña Mensajes";
|
||||
"Hides the suggested broadcast channels in direct messages" = "Oculta los canales sugeridos en mensajes";
|
||||
"Hides the trending searches under the explore search bar" = "Oculta las búsquedas en tendencia debajo de la barra de búsqueda";
|
||||
"Hold down on the Instagram logo to change the app icon" = "Mantén pulsado el logo de Instagram para cambiar el ícono de la aplicación";
|
||||
"Long press on the eyedropper tool in stories to customize the text color more precisely" = "Mantener pulsada la herramienta de selección de color en historias para seleccionar el color del texto de manera más precisa";
|
||||
"No suggested chats" = "Ocultar conversaciones sugeridas";
|
||||
"No suggested users" = "Ocultar usuarios sugeridos";
|
||||
"Notes" = "Notas";
|
||||
"Open links in external browser" = "Abrir enlaces en navegador externo";
|
||||
"Opens links in Safari instead of Instagram's in-app browser" = "Abrir enlaces en Safari en vez del navegador interno de Instagram";
|
||||
"Removes Instagram tracking wrappers (l.instagram.com) and UTM/fbclid params from URLs" = "Elimina intermediarios de rastreo de Instagram (l.instagram.com) y los parámetros UTM/fbclid de los enlaces";
|
||||
"Removes all ads from the Instagram app" = "Elimina todos los anuncios de la aplicación de Instagram";
|
||||
"Removes igsh, utm_source, and other tracking parameters from shared links" = "Elimina igsh, utm_source, y otros parámetros de rastreo de los enlaces compartidos";
|
||||
"Replace IG's relative timestamps (\"3d ago\") with a custom format. Toggle which surfaces it applies to inside the picker." = "Reemplaza las marcas de tiempo relativas de Instagram (\"Hace 3d\") con un formato personalizado. Escoge sobre cuales superficies se aplica dentro del selector.";
|
||||
"Replace domain in shared links" = "Reemplazar dominio en enlaces compartidos";
|
||||
"Rewrites copied/shared links to use an embed-friendly domain for previews in Discord, Telegram, etc." = "Reescribe enlaces copiados/compartidos para utilizar un dominio compatible con vistas previas embebidas en Discord, Telegram, etc.";
|
||||
"Sharing" = "Compartir";
|
||||
"Strip tracking from links" = "Eliminar rastreo de los enlaces";
|
||||
"Strip tracking params" = "Eliminar parámetros de rastreo";
|
||||
"These features rely on hidden Instagram flags and may not work on all accounts or versions.\nExperimental flags research by @euoradan (Radan)." = "Estas funciones se basan en opciones ocultas de Instagram y es posible que no funcionen en todas las cuentas o versiones.\nInvestigación sobre opciones experimentales por @euoradan (Radan).";
|
||||
"Use detailed color picker" = "Usar selector de color detallado";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// DATE FORMAT //
|
||||
// Settings → Date format tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Alternate" = "Alternativo";
|
||||
"Always ask" = "Siempre preguntar";
|
||||
"Balanced" = "Balanceada";
|
||||
"Block all" = "Bloquear todo";
|
||||
"Block selected" = "Bloquear seleccionado";
|
||||
"Button" = "Botón";
|
||||
"Classic" = "Clásico";
|
||||
"Date format — %@" = "Formato de fecha — %@";
|
||||
"Default" = "Predeterminado";
|
||||
"Disabled" = "Desactivado";
|
||||
"Download and share" = "Descargar y compartir";
|
||||
"Download to Photos" = "Descargar a Fotos";
|
||||
"Enabled" = "Activado";
|
||||
"Expand" = "Ampliar";
|
||||
"Explore" = "Explorar";
|
||||
"Fast" = "Rápida";
|
||||
"Feed" = "Feed (Inicio)";
|
||||
"High" = "Alta";
|
||||
"Inbox" = "Bandeja de entrada";
|
||||
"Low" = "Baja";
|
||||
"Max" = "Máxima";
|
||||
"Medium" = "Media";
|
||||
"Mute/Unmute" = "Silencio/Sonido";
|
||||
"Open menu" = "Abrir menú";
|
||||
"Pause/Play" = "Pausar/Reproducir";
|
||||
"Profile" = "Perfil";
|
||||
"Quality" = "Calidad";
|
||||
"Reels" = "Reels";
|
||||
"Requires restart" = "Requiere reiniciar";
|
||||
"Save to Photos" = "Guardar en Fotos";
|
||||
"Share sheet" = "Menú de compartir";
|
||||
"Standard" = "Estándar";
|
||||
"Toggle" = "Interruptor";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// FEED //
|
||||
// Settings → Feed tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Action button" = "Botón de acción";
|
||||
"Adds 'View profile picture' and 'View cover' to story tray long-press menus" = "Añade las opciones 'Ver foto de perfil' y 'Ver portada' al menú que aparece al mantener pulsado en la bandeja de historias";
|
||||
"Adds a RyukGram action button under each feed post with download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below." = "Añade un botón de acción de RyukGram debajo de cada publicación en el Feed (Inicio), con las opciones descargar, compartir, copiar, ampliar y repost. Tocar abre el menú de forma predeterminada.\nArriba puedes cambiar el comportamiento al tocar.";
|
||||
"Controls when and how the feed refreshes. Background refresh occurs when returning to the app after ~10 minutes. Home button refresh occurs when tapping the Home tab while already on it." = "Controla cuando y como se actualiza el Feed (Inicio). La actualización en segundo plano ocurre cuando vuelves a la aplicación después de unos 10 minutos. La actualización al pulsar el botón de Inicio se produce al tocar el botón mientras te encuentras en el Feed (Inicio)";
|
||||
"Default tap action" = "Acción al tocar";
|
||||
"Disable background refresh" = "Deshabilitar actualización en segundo plano";
|
||||
"Disable home button refresh" = "Deshabilitar actualización con botón de Inicio";
|
||||
"Disable home button scroll" = "Deshabilitar desplazamiento con botón de Inicio";
|
||||
"Disable video autoplay" = "Deshabilitar reproducción automática de video";
|
||||
"Hide" = "Ocultar";
|
||||
"Hide entire feed" = "Ocultar todo el Feed (Inicio)";
|
||||
"Hide repost button" = "Ocultar botón de repost";
|
||||
"Hide stories tray" = "Ocultar bandeja de historias";
|
||||
"Hide suggested stories" = "Ocultar historias sugeridas";
|
||||
"Hides suggested accounts" = "Oculta las cuentas sugeridas";
|
||||
"Hides suggested reels" = "Oculta los reels sugeridos";
|
||||
"Hides suggested threads posts" = "Oculta los hilos sugeridos de Threads";
|
||||
"Hides the repost button on feed posts" = "Oculta el botón de repost en las publicaciones del Feed (Inicio)";
|
||||
"Hides the story tray at the top" = "Oculta la bandeja de historias en la parte superior";
|
||||
"Inserts a button row below like/comment/share on each post" = "Inserta un botón en la fila de los botones me gusta, comentar y compartir en cada publicación";
|
||||
"Long press on media to expand in full-screen viewer" = "Mantener pulsado el contenido multimedia para ver en pantalla completa";
|
||||
"Media" = "Contenido multimedia";
|
||||
"Media zoom" = "Ampliar contenido multimedia";
|
||||
"No suggested for you" = "Sin 'Sugerencias para ti'";
|
||||
"No suggested posts" = "Sin 'Publicaciones sugeridas'";
|
||||
"No suggested reels" = "Sin 'Reels sugeridos'";
|
||||
"No suggested threads" = "Sin'Hilos sugeridos'";
|
||||
"Prevents feed from reloading when returning from background" = "Evita que el Feed (Inicio) se actualice cuando se regrese de segundo plano";
|
||||
"Prevents videos from playing automatically" = "Evita que los videos se reproduzcan automáticamente";
|
||||
"Refresh" = "Actualizar";
|
||||
"Removes all content from your home feed" = "Elimina todo el contenido de tu Feed (Inicio)";
|
||||
"Removes suggested accounts from the stories tray" = "Elimina las cuentas sugeridas de la bandeja de historias";
|
||||
"Removes suggested posts" = "Elimina las publicaciones sugeridas";
|
||||
"Scroll to top without refreshing when tapping Home" = "Desplazarse hacia arriba sin actualizar al pulsar el botón de Inicio";
|
||||
"Show action button" = "Mostrar botón de acción";
|
||||
"Stories tray" = "Bandeja de historias";
|
||||
"Tapping Home does nothing when already on feed" = "Pulsar botón de Inicio no hace nada cuando te encuentres en la pestaña de Feed (Inicio)";
|
||||
"Tray long-press actions" = "Acciones al mantener pulsado en la bandeja";
|
||||
"What happens on a single tap. Long-press always opens the full menu" = "Lo que ocurre con un solo toque. Mantener pulsado siempre abre el menú completo";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// REELS //
|
||||
// Settings → Reels tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below." = "Añade un botón de acción de RyukGram sobre la barra lateral del reel con las opciones ver portada, descargar, compartir, copiar, ampliar y repost. Tocar abre el menú de forma predeterminada.\nArriba puedes cambiar el comportamiento al tocar.";
|
||||
"Always show progress scrubber" = "Siempre mostrar el indicador de progreso";
|
||||
"Auto-scroll reels" = "Desplazamiento automático de reels";
|
||||
"IG default: native behavior. RyukGram: re-advances after swiping back." = "IG por defecto: comportamiento nativo. RyukGram: vuelve a avanzar después de deslizar hacia atrás.";
|
||||
"IG default" = "IG por defecto";
|
||||
"RyukGram" = "RyukGram";
|
||||
"Change what happens when you tap on a reel" = "Cambia lo que ocurre cuando tocas en un reel";
|
||||
"Confirm reel refresh" = "Confirmar actualización de reels";
|
||||
"Disable auto-unmuting reels" = "Deshabilitar el reactivado automático del sonido en los reels";
|
||||
"Disable scrolling reels" = "Deshabilitar desplazamiento en reels";
|
||||
"Disable tab button refresh" = "Deshabilitar actualización con el botón de la pestaña";
|
||||
"Doom scrolling limit" = "Límite de doom scrolling";
|
||||
"Forces the progress bar to appear on every reel" = "Fuerza la barra de progreso a aparecer en todos los reels";
|
||||
"Hide reels header" = "Ocultar el encabezado de los reels";
|
||||
"Hides the repost button on the reels sidebar" = "Oculta el botón repost en la barra lateral de los reels";
|
||||
"Hides the top navigation bar when watching reels" = "Oculta la barra de navegación superior al ver reels";
|
||||
"Hiding" = "Ocultar";
|
||||
"Limits" = "Límites";
|
||||
"Limits the amount of reels available to scroll at any given time, and prevents refreshing" = "Limita la cantidad de reels disponibles para desplazar en cualquier momento, y evita que se actualice";
|
||||
"Only loads %@ %@" = "Solo cargar %@ %@";
|
||||
"Places a button above the like/comment/share column on each reel" = "Coloca un botón sobre la columna de botones me gusta, comentar y compartir en cada reel";
|
||||
"Prevent doom scrolling" = "Evitar doom scrolling";
|
||||
"Prevents reels from being scrolled to the next video" = "Evita que los reels se desplacen al siguiente video";
|
||||
"Prevents reels from unmuting when the volume/silent button is pressed" = "Evita que los reels dejen de estar silenciados cuando se presionan los botones de volumen o silencio";
|
||||
"Shows an alert when you trigger a reels refresh" = "Muestra una alerta al solicitar una actualización de reels";
|
||||
"Shows buttons to reveal and auto-fill the password on locked reels" = "Muestra botones para revelar y auto-completar la contraseña en reels bloqueados";
|
||||
"Tap Controls" = "Controles táctiles";
|
||||
"Tapping the Reels tab while on reels does nothing" = "Pulsar el botón de reels no hace nada cuando te encuentres en la pestaña de Reels";
|
||||
"Unlock password-locked reels" = "Desbloquea reels bloqueados por contraseña";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// PROFILE //
|
||||
// Settings → Profile tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Adds a button next to the burger menu on profiles to copy username, name or bio" = "Añade un botón junto al menú de hamburguesa (☰) en los perfiles para copiar nombre de usuario, nombre o presentación";
|
||||
"Adds a view option to the highlight long-press menu to open the cover in full-screen" = "Añade una opción de visualización en el menú que aparece al mantener pulsado sobre la historia destacada para abrir la portada en pantalla completa";
|
||||
"Copy note on long press" = "Copia la nota al mantener pulsado";
|
||||
"Follow indicator" = "Indicador de seguido";
|
||||
"Long press a profile picture to open it in full-screen with zoom, share, and save" = "Mantener pulsado en una foto de perfil para abrirla en pantalla completa para ampliar, compartir y guardar";
|
||||
"Long press the note bubble on a profile to copy the text" = "Mantén pulsado la burbuja de una nota en un perfil para copiar el texto";
|
||||
"Long press to download directly (ignored when zoom is on)" = "Mantén pulsado para descargar directamente (Se ignora cuando la foto está ampliada)";
|
||||
"Long-press gestures on profile elements — kept separate from the per-feature action buttons." = "Los gestos al mantener pulsado en los elementos del perfil, se mantienen separados de los botones de acción específicos de cada función.";
|
||||
"Profile copy button" = "Botón de copiar perfil";
|
||||
"Save profile picture" = "Guardar foto de perfil";
|
||||
"Shows whether the profile user follows you" = "Muestra si el usuario del perfil te sigue";
|
||||
"View highlight cover" = "Ver portada de la historia destacada";
|
||||
"Zoom profile photo" = "Ampliar foto de perfil";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// SAVING & DOWNLOADS //
|
||||
// Settings → Saving tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Confirm before download" = "Confirmar antes de descargar";
|
||||
"Deprecated. The RyukGram action button (configured per feature in Feed/Reels/Stories) is the new way to download media. Enable this master toggle only if you prefer the old multi-finger long-press directly on the media." = "Obsoleto. El botón de acción de RyukGram (configurado por cada por cada tipo de contenido en Feed (Inicio)/Reels/Historias) es la nueva forma de descargar contenido multimedia. Habilita este control general solo si prefieres el antiguo método de mantener pulsado con varios dedos directamente sobre el contenido multimedia.";
|
||||
"Downloads" = "Descargas";
|
||||
"Downloads with %@ %@" = "Descargar con %@ %@";
|
||||
"Enable long-press gesture" = "Habilitar gesto de mantener pulsado";
|
||||
"Finger count for long-press" = "Cantidad de dedos a mantener pulsados";
|
||||
"Legacy long-press gesture" = "Gesto antiguo de mantener pulsado";
|
||||
"Long-press hold time" = "Tiempo a mantener pulsado";
|
||||
"Master toggle for the deprecated gesture workflow (off by default)" = "Control general para el gesto antiguo (Desactivado por defecto)";
|
||||
"Press finger(s) for %@ %@" = "Pulsar dedo(s) por %@ %@";
|
||||
"Route saves into a dedicated album in Photos instead of the camera roll root" = "Guarda en un álbum dedicado en Fotos, en vez de la Fototeca";
|
||||
"Save action" = "Acción después de guardar";
|
||||
"Save to RyukGram album" = "Guardar en álbum RyukGram";
|
||||
"Saving" = "Descargar";
|
||||
"Show a confirmation dialog before starting a download" = "Muestra un diálogo de confirmación antes de iniciar una descarga";
|
||||
"What happens after the gesture downloads" = "Lo que ocurre después de que termina la descarga";
|
||||
"When \"Save to RyukGram album\" is on, downloads and share-sheet \"Save to Photos\" picks are routed into a dedicated \"RyukGram\" album in your Photos library." = "Cuando \"Guardar en álbum RyukGram\" está activado, las descargas y elementos seleccionados en \"Guardar en Fotos\" se dirigen a un álbum llamado \"RyukGram\" en tu Fototeca.";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// STORIES //
|
||||
// Settings → Stories tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Adds a RyukGram action button next to the eye button on stories with download/share/copy/expand/repost/view-mentions entries. Tap opens the menu by default; change the tap behavior below." = "Añade un botón de acción de RyukGram junto al botón con forma de ojo (👁) en las historias con las opciones descargar, compartir, copiar, ampliar, repost y ver menciones. Tocar abre el menú de forma predeterminada.\nArriba puedes cambiar el comportamiento al tocar.";
|
||||
"Adds a speaker button to the story overlay to unmute/mute audio. Also available in the 3-dot menu" = "Añade un botón de altavoz a la superposición de las historias para alternar el sonido. También disponible en el menú de los 3 puntos";
|
||||
"Advance on story like" = "Avanzar historia al dar me gusta";
|
||||
"Advance on story reply" = "Avanzar historia al responder";
|
||||
"Advance when marking as seen" = "Avanzar historia cuando se marque como vista";
|
||||
"Audio" = "Sonido";
|
||||
"Block all: all stories blocked — listed users are exceptions.\nBlock selected: only listed users are blocked — everything else is normal.\nBoth lists are saved independently." = "Bloquear todo: Todas las historias bloqueadas — Usuarios en la lista son excepciones.\nBloquear seleccionadas: Solo los usuarios en la lista son bloqueados — Todo lo demás permanece normal.\nAmbas listas son guardadas independientemente.";
|
||||
"Blocking mode" = "Modo de bloqueo";
|
||||
"Button = single-tap mark seen. Toggle = tap toggles story read receipts on/off (eye fills blue when on)" = "Botón = Un toque marca como vista. Interruptor = Tocar el interruptor alterna el aviso de visualización de las historias (👁 se vuelve azul cuando se activa)";
|
||||
"Disable instants creation" = "Deshabilitar creación de instantáneas";
|
||||
"Disable story seen receipt" = "Deshabilitar aviso de visualización de historias";
|
||||
"Enable story user list" = "Habilitar lista de usuarios para historias";
|
||||
"Hides the functionality to create/send instants" = "Oculta la funcionalidad de crear/enviar instantáneas";
|
||||
"Hides the notification for others when you view their story" = "Oculta la notificación a los demás cuando ves sus historias";
|
||||
"Inserts a button next to the seen/eye button on story overlays" = "Inserta un botón junto al botón con forma de ojo (👁) en la superposición de las historias";
|
||||
"Keep stories visually seen locally" = "Marcar historias como vistas localmente";
|
||||
"Liking a story automatically advances to the next one after a short delay" = "Darle me gusta a una historia avanza automáticamente a la siguiente después de un breve período";
|
||||
"Manage list" = "Gestionar lista";
|
||||
"Manage list (%lu)" = "Gestionar lista (%lu)";
|
||||
"Manual seen button mode" = "Modo visualización manual";
|
||||
"Mark seen on story like" = "Marcar visualización de historia al darle me gusta";
|
||||
"Mark seen on story reply" = "Marcar visualización de historia al responder";
|
||||
"Marks a story as seen the moment you tap the heart, even with seen blocking on" = "Marca una historia como vista en el momento en que tocas el corazón, incluso con el bloqueo de aviso de visualización activado";
|
||||
"Marks a story as seen when you send a reply or emoji reaction, even with seen blocking on" = "Marca una historia como vista cuando envías una respuesta o una reacción con emoji, incluso con el bloqueo de aviso de visualización activado";
|
||||
"Master toggle. When off, the list is ignored" = "Control general. Cuando está desactivado, la lista es ignorada";
|
||||
"Other" = "Otros";
|
||||
"Playback" = "Reproducción";
|
||||
"Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server" = "Marca las historias como vistas localmente (círculo gris) mientras sigue bloqueando el recibo de visto en el servidor";
|
||||
"Quick list button in stories" = "Botón de lista rápida en historias";
|
||||
"Search, sort, swipe to remove" = "Buscar y ordenar. Desliza para eliminar";
|
||||
"Seen receipts" = "Confirmación de visualización";
|
||||
"Sending a reply or emoji reaction automatically advances to the next story" = "Enviar una respuesta o una reacción con emoji automáticamente avanza a la siguiente historia";
|
||||
"Show mentioned users in eye button and story menu" = "Mostrar usuarios mencionados en el botón con forma de ojo (👁) y el menú de la historia";
|
||||
"Shows an eye button on stories to add/remove users from the list. Off = use the 3-dot menu or long-press only" = "Muestra un botón con forma de ojo (👁) en las historias para añadir o eliminar usuarios de la lista. Desactivado = Usar el menú de los 3 puntos o solo mantener pulsado";
|
||||
"Stop story auto-advance" = "Detener avance automático de las historias";
|
||||
"Stories" = "Historias";
|
||||
"Stories won't auto-skip to the next one when the timer ends. Tap to advance manually" = "Historias no avanzan automáticamente a la siguiente cuando el temporizador termina. Toca para avanzar manualmente";
|
||||
"Story audio toggle" = "Alternar sonido de la historia";
|
||||
"Story user list" = "Lista de usuarios para historias";
|
||||
"Tapping the eye button to mark a story as seen advances to the next story automatically" = "Tocar el botón con forma de ojo para marcar una historia como vista avanza a la siguiente historia automáticamente";
|
||||
"View story mentions" = "Ver mencionados en la historia";
|
||||
"Which stories get seen-receipt blocking" = "Cuales historias tienen bloqueado el aviso de visualización";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// MESSAGES — READ RECEIPTS //
|
||||
// Settings → Read receipts tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Adds a button to DM threads to mark messages as seen" = "Añade un botón en las conversaciones para marcar los mensajes como vistos";
|
||||
"Auto mark seen on interact" = "Marcar automáticamente como visto al interactuar";
|
||||
"Auto mark seen on typing" = "Marcar automáticamente como visto al escribir";
|
||||
"Control when messages are marked as seen" = "Controla cuando los mensajes son marcados como vistos";
|
||||
"How the seen button behaves" = "Como el botón de visto se comporta";
|
||||
"Manually mark messages as seen" = "Marcar manualmente los mensajes como vistos";
|
||||
"Marks messages as seen when you send any message" = "Marca los mensajes como vistos cuando envías cualquier mensaje";
|
||||
"Marks messages as seen when you start typing" = "Marca los mensajes como vistos cuando comienzas a escribir";
|
||||
"Read receipt mode" = "Modo de confirmación de lectura";
|
||||
"Read receipts" = "Confirmación de lectura";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// MESSAGES — KEEP DELETED //
|
||||
// Settings → Keep deleted messages tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Activity" = "Actividad";
|
||||
"Adds a 'Download' option to the long-press menu on voice messages to save them as M4A audio" = "Añade una opción 'Descargar' al menú que aparece al mantener pulsado sobre los mensajes de voz para guardarlos como archivos M4A";
|
||||
"Adds a 'Send File' option to the plus menu in DMs. Supported file types may be limited by Instagram" = "Añade una opción 'Enviar archivo' al menú ⊕ en las conversaciones.\nTipos de archivos soportados puede estar limitado por Instagram";
|
||||
"Adds an 'Audio File' option to the plus menu in DMs to send audio files as voice messages" = "Añade una opción 'Archivo de audio' al menú ⊕ en las conversaciones para enviar archivos de audio como mensajes de voz";
|
||||
"Adds copy text, download GIF/audio to the note long-press menu" = "Añade las opciones copiar texto, descargar GIF/Audio al menú que aparece al mantener pulsada una nota";
|
||||
"Block all: all chats blocked — listed chats are exceptions.\nBlock selected: only listed chats are blocked — everything else is normal.\nBoth lists are saved independently. Long-press a chat in the inbox to add or remove." = "Bloquear todo: Todas las conversaciones bloqueadas — Conversaciones en la lista son excepciones.\nBloquear seleccionadas: Solo las conversaciones en la lista son bloqueadas — Todo lo demás permanece normal.\nAmbas listas son guardadas independientemente. Mantén pulsada una conversación en la bandeja de entrada para añadir o eliminar";
|
||||
"Block keep-deleted for excluded chats" = "Bloquea mantener mensajes eliminados para conversaciones excluidas";
|
||||
"Block keep-deleted for unlisted chats" = "Bloquear mantener eliminados para conversaciones sin listar";
|
||||
"Chat list" = "Lista de conversaciones";
|
||||
"Confirmation dialog before clearing preserved messages" = "Muestra un diálogo de confirmación antes de borrar mensajes guardados";
|
||||
"Copies note text directly on long press without opening the menu" = "Copia el texto de la nota directamente al mantener pulsado sin abrir el menú";
|
||||
"Copy text on hold" = "Copiar texto al mantener pulsado";
|
||||
"Custom emojis and background/text colors" = "Emojis, color de fondo y texto personalizado";
|
||||
"Custom note themes" = "Tema de notas personalizado";
|
||||
"Disable disappearing mode swipe" = "Deshabilitar deslizamiento para mensajes temporales";
|
||||
"Disable screenshot detection" = "Deshabilitar detección de capturas de pantalla";
|
||||
"Disable typing status" = "Deshabilitar estado de escritura";
|
||||
"Disable view-once limitations" = "Deshabilitar limitaciones de ver una vez";
|
||||
"Download voice messages" = "Descargar mensajes de voz";
|
||||
"Enable chat list" = "Habilitar lista de conversaciones";
|
||||
"Enable note theming" = "Habilitar temas en notas";
|
||||
"Enables the notes theme picker" = "Habilita el selector de temas para notas";
|
||||
"Files" = "Archivos";
|
||||
"Full last active date" = "Fecha de última vez completa";
|
||||
"Hide reels blend button" = "Ocultar el botón de blend";
|
||||
"Hide video call button" = "Ocultar botón de video llamada";
|
||||
"Hide voice call button" = "Ocultar botón de llamada";
|
||||
"Hides the blend button in DMs" = "Elimina el botón de blend en las conversaciones";
|
||||
"Hides typing indicator from others" = "Oculta el indicador de escribiendo para los demás";
|
||||
"Indicate unsent messages" = "Indicar eliminación de mensaje";
|
||||
"Keep deleted messages" = "Mantener mensajes eliminados";
|
||||
"Makes view-once messages behave like normal visual messages (loopable/pauseable)" = "Hace que los mensajes para ver una vez se comporten como mensajes visuales normales (Búcle/Pausa)";
|
||||
"Note actions" = "Acciones en notas";
|
||||
"Preserve messages that others unsend" = "Guardar los mensajes que los demás eliminen";
|
||||
"Preserves messages that others unsend" = "Guarda los mensajes que los demás eliminen";
|
||||
"Prevents accidental swipe-up activation of disappearing mode" = "Evita la activación accidental de los mensajes temporales al deslizar hacia arriba";
|
||||
"Quick list button in chats" = "Botón de lista rápida en conversaciones";
|
||||
"Removes the audio call button from DM thread header" = "Elimina el botón de llamada en las conversaciones";
|
||||
"Removes the screenshot-prevention features for visual messages in DMs" = "Elimina las funciones que impiden hacer capturas de pantalla para mensajes visuales en las conversaciones";
|
||||
"Removes the video call button from DM thread header" = "Elimina el botón de video llamada en las conversaciones";
|
||||
"Replay visual messages without expiring. Toggle in the eye button menu, or as a standalone button when the eye button is disabled" = "Reproducir mensajes visuales sin expiración. Alterna en el menú del botón ojo (👁), o como un botón por separado cuando el botón ojo (👁) está deshabilitado";
|
||||
"Search, sort, swipe to remove or toggle keep-deleted" = "Buscar y ordenar. Desliza para eliminar o alterna mantener eliminados";
|
||||
"Send audio as file" = "Enviar audio como archivo";
|
||||
"Send files (experimental)" = "Enviar archivos (Experimental)";
|
||||
"Show full date instead of \"Active 2h ago\"" = "Muestra la fecha completa en vez de \"Activo hace 2 horas\"";
|
||||
"Shows a button in DM threads to add/remove chats from the list. Long-press for more options" = "Muestra un botón en la pestaña Mensajes para añadir o eliminar conversaciones de la lista. Mantén pulsado para más opciones";
|
||||
"Shows a notification pill when a message is unsent" = "Muestra una notificación cuando se elimine un mensaje";
|
||||
"Shows an \"Unsent\" label on preserved messages" = "Muestra una etiqueta \"Mensaje eliminado\" en mensajes guardados";
|
||||
"Unlimited replay of visual messages" = "Reproducción ilimitada de mensajes visuales";
|
||||
"Unsent message notification" = "Notificación de eliminación de mensaje";
|
||||
"Visual messages" = "Mensajes visuales";
|
||||
"Voice messages" = "Mensajes de voz";
|
||||
"Warn before clearing on refresh" = "Mostrar un aviso antes de actualizar";
|
||||
"Which chats get read-receipt blocking" = "Cuales conversaciones tienen bloqueada la confirmación de lectura";
|
||||
"⚠️ Pull-to-refresh in the DMs tab clears all preserved messages. Enable the warning below to get a confirmation dialog." = "⚠️ Deslizar para actualizar en la pestaña Mensajes borra todos los mensajes guardados. Activa la advertencia que aparece arriba para que se muestre diálogo de confirmación.";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// MESSAGES //
|
||||
// Settings → Messages tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Messages" = "Mensajes";
|
||||
"Threads" = "Hilos";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// NAVIGATION //
|
||||
// Settings → Navigation tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Hide create tab" = "Ocultar pestaña Crear";
|
||||
"Hide explore tab" = "Ocultar pestaña Explorar";
|
||||
"Hide feed tab" = "Ocultar pestaña Feed (Inicio)";
|
||||
"Hide messages tab" = "Ocultar pestaña Mensajes";
|
||||
"Hide reels tab" = "Ocultar pestaña Reels";
|
||||
"Hides every tab except DM inbox + profile and forces launch into the inbox. Settings shortcut moves to long-press on the inbox tab." = "Oculta todas las pestañas excepto Mensajes y Perfil. Inicia la aplicación en la pestaña Mensajes. El acceso rápido a la configuración se cambia a mantener pulsada la pestaña Mensajes.";
|
||||
"Hides the create tab on the bottom navigation bar" = "Oculta la pestaña Crear en la barra de navegación inferior";
|
||||
"Hides the direct messages tab on the bottom navigation bar" = "Oculta la pestaña Mensajes en la barra de navegación inferior";
|
||||
"Hides the explore/search tab on the bottom navigation bar" = "Oculta la pestaña Explorar (Busca) en la barra de navegación inferior";
|
||||
"Hides the feed/home tab on the bottom navigation bar" = "Oculta la pestaña Feed (Inicio) en la barra de navegación inferior";
|
||||
"Hides the reels tab on the bottom navigation bar" = "Oculta la pestaña Reels en la barra de navegación inferior";
|
||||
"Hiding tabs" = "Ocultar pestañas";
|
||||
"Icon order" = "Orden de los íconos";
|
||||
"Launch tab" = "Pestaña inicial";
|
||||
"Lets you swipe to switch between navigation bar tabs" = "Permite deslizar para cambiar entre las pestañas de la barra de navegación";
|
||||
"Messages only" = "Únicamente Mensajes";
|
||||
"Messages-only mode" = "Modo mensajería";
|
||||
"Navigation" = "Navegación";
|
||||
"Swipe between tabs" = "Deslizar entre pestañas";
|
||||
"Tab the app opens to. Ignored when Messages-only is on" = "Pestaña en la que inicia la aplicación. Se ignora cuando el modo mensajería está activado";
|
||||
"The order of the icons on the bottom navigation bar" = "Orden de los íconos en la barra de navegación inferior";
|
||||
"Turn IG into a DM-only client" = "Convierte Instagram en una aplicación de mensajería instantánea";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// CONFIRM ACTIONS //
|
||||
// Settings → Confirm actions tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Confirm actions" = "Confirmar acciones";
|
||||
"Confirm call" = "Confirmar llamada";
|
||||
"Confirm changing theme" = "Confirmar cambiar el tema";
|
||||
"Confirm follow" = "Confirmar seguir";
|
||||
"Confirm follow requests" = "Confirmar solicitud de seguimiento";
|
||||
/* [ADDED_BY_DEV] */ "Confirm like: Posts" = "Confirmar me gusta en publicaciones";
|
||||
/* [ADDED_BY_DEV] */ "Confirm story like" = "Confirmar me gusta en historias";
|
||||
/* [ADDED_BY_DEV] */ "Confirm story emoji reaction" = "Confirmar reacción con emojis en historias";
|
||||
"Confirm like: Reels" = "Confirmar me gusta en reels";
|
||||
"Confirm posting comment" = "Confirmar publicar comentario";
|
||||
"Confirm repost" = "Confirmar repost";
|
||||
"Confirm shh mode" = "Confirmar mensajes temporales";
|
||||
"Confirm sticker interaction" = "Confirma interacción con stickers";
|
||||
"Confirm unfollow" = "Confirmar dejar de seguir";
|
||||
"Confirm voice messages" = "Confirmar mensaje de voz";
|
||||
"Shows an alert to confirm before sending a voice message" = "Muestra una alerta para confirmar antes de enviar un mensaje de voz";
|
||||
"Shows an alert to confirm before toggling disappearing messages" = "Muestra una alerta para confirmar antes de activar los mensajes temporales";
|
||||
"Shows an alert when you accept/decline a follow request" = "Muestra una alerta cuando aceptas o rechazas una solicitud de seguimiento";
|
||||
"Shows an alert when you change a chat theme to confirm" = "Muestra una alerta para confirmar cuando cambias el tema en una conversación";
|
||||
"Shows an alert when you click a sticker on someone's story to confirm the action" = "Muestra una alerta para confirmar la acción cuando tocas un sticker en la historia de alguien";
|
||||
"Shows an alert when you click the audio/video call button to confirm before calling" = "Muestra una alerta cuando tocas los botones de llamada y video llamada, antes de llamar";
|
||||
"Shows an alert when you click the follow button to confirm the follow" = "Muestra una alerta para confirmar cuando tocas el botón de seguir";
|
||||
/* [ADDED_BY_DEV] */ "Shows an alert when you click the like button on posts to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en publicaciones";
|
||||
"Shows an alert when you click the like button on reels to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en reels";
|
||||
/* [ADDED_BY_DEV] */ "Shows an alert when you click the like button on stories to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en historias";
|
||||
/* [ADDED_BY_DEV] */ "Shows an alert before sending an emoji reaction on a story" = "Muestra una alerta antes de enviar una reacción con emojis en una historia";
|
||||
"Shows an alert when you click the post comment button to confirm" = "Muestra una alerta para confirmar cuando tocas el botón de publicar comentario";
|
||||
"Shows an alert when you click the repost button to confirm before resposting" = "Muestra una alerta para confirmar cuando tocas el botón de repost";
|
||||
"Shows an alert when you click the unfollow button to confirm" = "Muestra una alerta para confirmar cuando tocas el botón de dejar de seguir";
|
||||
"Shows an alert when you click the like button on posts or stories to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en publicaciones o historias";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// BACKUP & RESTORE //
|
||||
// Settings → Backup & Restore tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Backup & Restore" = "Copia de seguridad & restauración";
|
||||
"Export settings" = "Exportar configuración";
|
||||
"Export your RyukGram settings to a JSON file and import them later. Importing resets all settings to defaults before applying the imported values, and shows a preview before anything changes." = "Exporta tu configuración de RyukGram a un archivo JSON para importarlos mas tarde. Al importar, se restaurarán los valores predeterminados antes de aplicar la nueva configuración. Podrás ver una vista previa antes de confirmar los cambios.";
|
||||
"Import settings" = "Importar configuración";
|
||||
"Load settings from a JSON file" = "Cargar configuración desde un archivo JSON";
|
||||
"Reset to defaults" = "Restablecer los valores predeterminados";
|
||||
"Revert every RyukGram preference" = "Restablecer todas las preferencias de RyukGram";
|
||||
"Save settings as a JSON file" = "Guarda configuración como un archivo JSON";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// EXPERIMENTAL //
|
||||
// Settings → Experimental tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Experimental" = "Experimental";
|
||||
"These features are unstable and cause the Instagram app to crash unexpectedly.\n\nUse at your own risk!" = "Estas funciones son inestables y provocan que la aplicación de Instagram se cierre inesperadamente.\n\n¡Úsalas bajo tu propia responsabilidad!";
|
||||
"Warning" = "Advertencia";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// ADVANCED //
|
||||
// Settings → Advanced tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Advanced" = "Avanzado";
|
||||
"Automatically opens settings when the app launches" = "Abre la configuración automáticamente cuando se inicia la aplicación";
|
||||
"Disable safe mode" = "Deshabilitar modo seguro";
|
||||
"Enable tweak settings quick-access" = "Habilitar acceso rápido a la configuración del Tweak";
|
||||
"Hold on the home tab to open RyukGram settings" = "Mantén pulsada la pestaña Feed (Inicio) para abrir la configuración de RyukGram";
|
||||
"Instagram" = "Instagram";
|
||||
"Pause playback when opening settings" = "Pausa la reproducción al abrir la configuración";
|
||||
"Pauses any playing video/audio when settings opens" = "Pausa cualquier reproducción de video o audio cuando se abre la configuración";
|
||||
"Prevents Instagram from resetting settings after crashes (at your own risk)" = "Evita que Instagram restablezca la configuración después de un cierre inesperado\n(¡Bajo tu propia responsabilidad!)";
|
||||
"Reset onboarding state" = "Restablecer estado onboarding"; // Verify onboarding - Verificar onboarding
|
||||
"Settings" = "Configuración";
|
||||
"Show tweak settings on app launch" = "Muestra la configuración del Tweak cuando se inicia la aplicación";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// DEBUG //
|
||||
// Settings → Debug tab //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Button Cell" = "Celda de botón";
|
||||
"Change the value on the right" = "Cambia el valor a la derecha";
|
||||
"Debug" = "Debug";
|
||||
"Enable FLEX gesture" = "Habilitar gesto FLEX";
|
||||
"Hold 5 fingers on the screen to open FLEX" = "Mantén pulsados 5 dedos en la pantalla para abrir FLEX";
|
||||
"I have %@%@" = "Tengo %@%@";
|
||||
"Link Cell" = "Celda de enlace";
|
||||
"Menu Cell" = "Celda de menú";
|
||||
"Open FLEX on app focus" = "Abrir FLEX al enfocar la aplicación";
|
||||
"Open FLEX on app launch" = "Abrir FLEX al iniciar la aplicación";
|
||||
"Opens FLEX when the app is focused" = "Abre FLEX cuando la aplicación es enfocada";
|
||||
"Opens FLEX when the app launches" = "Abre FLEX cuando la aplicación se inicia";
|
||||
"Static Cell" = "Celda estática";
|
||||
"Stepper cell" = "Celda de paso";
|
||||
"Switch Cell" = "Celda interruptor";
|
||||
"Switch Cell (Restart)" = "Cambiar celda (Reinicio)";
|
||||
"Tap the switch" = "Toca el interruptor";
|
||||
"Using icon" = "Usar ícono";
|
||||
"Using image" = "Usar imagen";
|
||||
"_ Example" = "_ Ejemplo";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// DOWNLOADS & MEDIA ACTIONS //
|
||||
// Action button menus, download/share/copy toasts, quality picker pills. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"%@ settings" = "Configuración de %@";
|
||||
"Cancelled" = "Cancelado";
|
||||
"Copied %lu URLs" = "Copiados %lu enlaces";
|
||||
"Copied caption" = "Descripción copiada";
|
||||
"Copied download URL" = "Enlace de descarga copiado";
|
||||
"Copy all URLs" = "Copiar todos los enlaces";
|
||||
"Copy caption" = "Copiar descripción";
|
||||
"Copy download URL" = "Copiar enlace de descarga";
|
||||
"Could not extract any URLs" = "No se logró extraer ningún enlace";
|
||||
"Could not extract media URL" = "No se logró extraer enlace de medios";
|
||||
"Could not extract photo URL" = "No se logró extraer enlace de fotos";
|
||||
"Could not extract video URL" = "No se logró extraer enlace de videos";
|
||||
"Done" = "Finalizado";
|
||||
"Download all (%lu)" = "Descargar todos (%lu)";
|
||||
"Download all stories and share?" = "¿Descargar todas las historias y compartir?";
|
||||
"Download all to Photos" = "Descargar todo en Fotos";
|
||||
"Download and share all" = "Descargar y compartir todo";
|
||||
"Download and share?" = "¿Descargar y compartir?";
|
||||
"Download failed" = "Descarga fallida";
|
||||
"Downloaded %lu items" = "Descargando %lu elementos";
|
||||
"Downloading %@..." = "Descargando %@...";
|
||||
"Downloading..." = "Descargando...";
|
||||
"Failed to save" = "Error al guardar";
|
||||
"HD download complete" = "Descarga HD completada";
|
||||
"Mute audio" = "Silenciar sonido";
|
||||
"No URLs" = "Sin enlaces";
|
||||
"No URLs found" = "Sin enlaces encontrados";
|
||||
"No caption on this post" = "No hay descripción en esta publicación";
|
||||
"No carousel children" = "Sin carrusel hijo";
|
||||
"No cover image" = "Sin imagen de portada";
|
||||
"No files downloaded" = "No se descargaron archivos";
|
||||
"No media" = "Sin medios";
|
||||
"No media URL" = "Sin enlace de medios";
|
||||
"No media to expand" = "Sin medios para ampliar";
|
||||
"No media to show" = "Sin medios para mostrar";
|
||||
"No video URL" = "Sin enlace de video";
|
||||
"Not a carousel" = "No es un carrusel";
|
||||
"Nothing to save" = "Nada para guardar";
|
||||
"Nothing to share" = "Nada para compartir";
|
||||
"Opening creator..." = "Abriendo creador...";
|
||||
"Photo library access denied" = "Acceso a la Fototeca denegado";
|
||||
"Photos access denied" = "Acceso a Fotos denegado";
|
||||
"Preparing repost..." = "Preparando repost...";
|
||||
"Repost" = "Repost";
|
||||
"Repost unavailable" = "Repost no disponible";
|
||||
"Save all stories to Photos?" = "¿Guardar todas las historias en Fotos?";
|
||||
"Save failed" = "Error al guardar";
|
||||
"Save to Photos?" = "¿Guardar en Fotos?";
|
||||
"Saved %lu items" = "Guardados %lu elementos";
|
||||
"Saved to Photos" = "Guardado en Fotos";
|
||||
"Saved to RyukGram" = "Guardado en RyukGram";
|
||||
"Tap to cancel" = "Toca para cancelar";
|
||||
"Unmute audio" = "Activar sonido";
|
||||
"View cover" = "Ver portada";
|
||||
"View mentions" = "Ver menciones";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// STORIES & MESSAGES (FEATURES) //
|
||||
// Buttons, menu entries, toasts and alerts shown while watching stories or //
|
||||
// inside DM threads. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"A message was unsent" = "Se anuló el envío de un mensaje";
|
||||
"Add" = "Añadir";
|
||||
"Add to block list" = "Añadir a la lista de bloqueo";
|
||||
"Add to block list?" = "¿Añadir a la lista de bloqueo?";
|
||||
"Added to block list" = "Añadido a la lista de bloqueo";
|
||||
"Audio not loaded yet. Play the message first and try again." = "Audio aún no cargado. Reproduce el mensaje primero y vuelve a intentar.";
|
||||
"Audio sent" = "Audio enviado";
|
||||
"Audio/Video from Files" = "Audio/Video desde Archivos";
|
||||
"Blocked" = "Bloqueado";
|
||||
"Cancel" = "Cancelar";
|
||||
"Clear preserved messages?" = "¿Borrar mensajes guardados?";
|
||||
"Converting..." = "Convirtiendo...";
|
||||
"Copy text" = "Copiar texto";
|
||||
"Could not find media" = "No se logró encontrar medios";
|
||||
"Could not find story media" = "No se logró encontrar medios para la historia";
|
||||
"Could not get audio data. Try again after refreshing the chat." = "No se encontró datos de audio. Intenta nuevamente luego de actualizar la conversación.";
|
||||
"Could not get video URL" = "No se encontró enlace al video";
|
||||
"Disable read receipts" = "Deshabilitar confirmación de lectura";
|
||||
"Done!" = "¡Finalizado!";
|
||||
"Download audio" = "Descargar audio";
|
||||
"Downloading audio..." = "Descargando audio...";
|
||||
"Enable read receipts" = "Habilitar confirmación de lectura";
|
||||
"Error: %@" = "Error: %@";
|
||||
"Exclude chat" = "Excluir chat";
|
||||
"Exclude story seen" = "Excluir de visualización de historia";
|
||||
"Excluded" = "Excluído";
|
||||
"Extracting audio..." = "Extrayendo audio...";
|
||||
"Failed to encode GIF" = "No se logró codificar el GIF";
|
||||
"File sending not supported" = "Enviar archivos no soportado";
|
||||
"Follow" = "Seguir";
|
||||
"Following" = "Siguiendo";
|
||||
"Mark messages as seen" = "Marcar mensajes como vistos";
|
||||
"Mark seen" = "Marcar como vista";
|
||||
"Marked as seen" = "Marcado como visto";
|
||||
"Marked as viewed" = "Marcado como visto";
|
||||
"Marked messages as seen" = "Mensajes marcados como vistos";
|
||||
"Mentions" = "Menciones";
|
||||
"Message sender not found" = "No se encontró remitente del mensaje";
|
||||
"Messages settings" = "Configuración de Mensajes";
|
||||
"Mute story audio" = "Silenciar sonido de historia";
|
||||
"No audio URL found. Try again after refreshing the chat." = "No se encontró enlace de audio. Intenta nuevamente luego de actualizar la conversación.";
|
||||
"No mentions in this story" = "Sin menciones en esta historia";
|
||||
"No thread key" = "Sin llave";
|
||||
"No voice send method found" = "No se encontró método para envío de voz";
|
||||
"Note not found" = "Nota no encontrada";
|
||||
"Note text copied" = "Texto de la nota copiado";
|
||||
"Open GitHub" = "Abrir GitHub";
|
||||
"Read receipts disabled" = "Confirmación de lectura DESACTIVADA";
|
||||
"Read receipts enabled" = "Confirmación de lectura ACTIVADA";
|
||||
"Read receipts will be blocked for this chat." = "Confirmación de lectura estará bloqueado para esta conversación.";
|
||||
"Read receipts will no longer be blocked for this chat." = "Confirmación de lectura ya no estará bloqueado para este chat.";
|
||||
"Remove" = "Eliminar";
|
||||
"Remove from block list" = "Eliminar de la lista de bloqueo";
|
||||
"Remove from block list?" = "¿Eliminar de la lista de bloqueo?";
|
||||
"Removed" = "Eliminado";
|
||||
"Save GIF" = "Guardar GIF";
|
||||
"Selection too short (min 0.5s)" = "Selección demasiado corta (min 0.5s)";
|
||||
"Send Audio" = "Enviar audio";
|
||||
"Send anyway" = "Enviar de todos modos";
|
||||
"Send failed: %@" = "Envío fallido: %@";
|
||||
"Send service not found" = "Servicio de envío no encontrado";
|
||||
"Share" = "Compartir";
|
||||
"Story read receipts disabled" = "Aviso de visualización de historia DESACTIVADO";
|
||||
"Story read receipts enabled" = "Aviso de visualización de historia ACTIVADO";
|
||||
"Story seen receipts will be blocked for @%@." = "Aviso de visualización de historia será bloqueado para @%@.";
|
||||
"This chat will resume normal read-receipt behavior." = "Este chat volverá a funcionar con el sistema habitual de confirmaciones de lectura.";
|
||||
"Total: %@" = "Total: %@";
|
||||
"Un-exclude" = "No excluir";
|
||||
"Un-exclude chat" = "No excluir conversación";
|
||||
"Un-exclude chat?" = "¿No excluir conversación?";
|
||||
"Un-exclude story seen" = "No excluir de visualización de la historia";
|
||||
"Un-exclude story seen?" = "¿No excluir de visualización de la historia?";
|
||||
"Un-excluded" = "No excluído";
|
||||
"Unblock" = "Desbloquear";
|
||||
"Unblocked" = "Desbloqueado";
|
||||
"Unlimited replay enabled" = "Reproducción ilimitada ACTIVADA";
|
||||
"Unmute story audio" = "Activar sonido de la historia";
|
||||
"Unsent" = "Mensaje eliminado";
|
||||
"Upload Audio" = "Subir Audio";
|
||||
"VC not found" = "VC no encontrado"; // Verify - Verificar
|
||||
"Video from Library" = "Video desde Fototeca";
|
||||
"Visual messages will expire" = "Mensajes visuales expirarán";
|
||||
"Visual messages: expiring" = "Mensajes visuales: Expirando";
|
||||
"Visual messages: unlimited replay" = "Mensajes visuales: Reproducción ilimitada";
|
||||
"Will sync when leaving stories" = "Se sincronizará al salir de las historias";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// GENERAL FEATURES //
|
||||
// Strings inside per-feature overlays: fake location, color picker, notes //
|
||||
// customization, profile copy, etc. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Add location" = "Añadir ubicación";
|
||||
"Add preset" = "Añadir ajuste preestablecido";
|
||||
"Change location" = "Cambiar ubicación";
|
||||
"Click the Apply button after this to see the emoji" = "Toca el botón Aplicar después de esto para ver el emoji";
|
||||
"Copied text to clipboard" = "Texto copiado al portapapeles";
|
||||
"Copy" = "Copiar";
|
||||
"Copy all" = "Copiar todo";
|
||||
"Copy bio" = "Copiar presentación";
|
||||
"Copy from profile" = "Copiar desde perfil";
|
||||
"Copy name" = "Copiar nombre";
|
||||
"Could not find cover image" = "No se encontró imagen de portada";
|
||||
"Current: %@" = "Actual: %@";
|
||||
"Disable" = "Desactivado";
|
||||
"Download GIF" = "Descargar GIF";
|
||||
"Enable" = "Activado";
|
||||
"Enter Emoji Text" = "Introduce Texto con Emoji";
|
||||
"Fake location" = "Ubicación falsa";
|
||||
"Name" = "Nombre";
|
||||
"Nothing to copy" = "Nada para copiar";
|
||||
"Save" = "Guardar";
|
||||
"Save preset" = "Guardar ajuste preestablecido";
|
||||
"Saved locations" = "Ubicaciones guardadas";
|
||||
"Select color" = "Escoger color";
|
||||
"Set location" = "Establecer ubicación";
|
||||
"Settings…" = "Configuración…";
|
||||
"Type emoji..." = "Introduce emoji...";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// SETTINGS VIEWS & DIALOGS //
|
||||
// Excluded-lists managers, backup/restore flows, in-picker labels. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Add chat" = "Añadir conversación";
|
||||
"Add custom domain" = "Añadir dominio personalizado";
|
||||
"Add preset…" = "Añadir ajuste preestablecido";
|
||||
"Add to list?" = "¿Añadir a la lista?";
|
||||
"Add user" = "Añadir usuario";
|
||||
"Could not resolve user ID" = "No se pudo resolver el ID del usuario";
|
||||
"Enter username" = "Introducir nombre de usuario";
|
||||
"Enter username of the DM thread" = "Introducir nombre de usuario de la conversación";
|
||||
"No DM thread found with @%@" = "No se encontró conversación con @%@";
|
||||
"User '%@' not found" = "Usuario '%@' no encontrado";
|
||||
"All RyukGram settings will be reset to defaults and the imported values applied. The app will need to restart for some changes to take effect." = "Todas las configuraciones de RyukGram se restablecerán a los valores predeterminados y se aplicarán los valores importados. Será necesario reiniciar la aplicación para que algunos cambios surtan efecto.";
|
||||
"Apply" = "Aplicar";
|
||||
"Apply imported settings?" = "¿Aplicar configuración importada?";
|
||||
"Apply to" = "Aplicar a";
|
||||
"Chats" = "Conversaciones";
|
||||
"Could not read file." = "No se logró leer el archivo.";
|
||||
"Could not write temporary file." = "No se logró escribir el archivo temporal.";
|
||||
"Current location" = "Ubicación actual";
|
||||
"Custom" = "Personalizada";
|
||||
"Date Format" = "Formato de Fecha";
|
||||
"Delete" = "Eliminar";
|
||||
"Done editing" = "Finalizar edición";
|
||||
"Edit values" = "Editar valores";
|
||||
"Enable fake location" = "Habilitar ubicación falsa";
|
||||
"Every RyukGram preference will revert to its built-in default. This can't be undone." = "Todas las preferencias de RyukGram volverán a los valores predeterminados. Esto no se puede deshacer.";
|
||||
"Excluded chats" = "Conversaciones excluídas";
|
||||
"Excluded users" = "Usuarios excluídos";
|
||||
"File is not a valid RyukGram settings export." = "Archivo no es una exportación válida de la configuración de RyukGram.";
|
||||
"Follow default" = "Seguir predeterminada";
|
||||
"Force OFF (allow unsends)" = "Forzar DESACTIVADO (Permite anular envío)";
|
||||
"Force ON (preserve unsends)" = "Forzar ACTIVADO (Mantiene eliminados)";
|
||||
"Form view" = "Vista de forma";
|
||||
"Format" = "Formato";
|
||||
"Import failed" = "Importación fallida";
|
||||
"Import preview" = "Previsualizar importación";
|
||||
"Included chats" = "Conversaciones incluidas";
|
||||
"Included users" = "Usuarios incluidos";
|
||||
"KD: ON" = "KD: ACTIVADO";
|
||||
"KD: default" = "ME: Predeterminado";
|
||||
"Keep-deleted" = "Mantener eliminados";
|
||||
"Keep-deleted override" = "Anular mantener eliminados";
|
||||
"Off" = "DESACTIVADO";
|
||||
"On" = "ACTIVADO";
|
||||
"Presets" = "Preajustes";
|
||||
"Raw JSON view" = "Ver JSON sin formato";
|
||||
"Remove Selected" = "Eliminar Seleccionados";
|
||||
"Remove from list" = "Eliminar de la lista";
|
||||
"Reset" = "Restablecer";
|
||||
"Reset all settings?" = "¿Restablecer todas las configuraciones?";
|
||||
"Saved presets are reusable. Tap a preset to make it the active location." = "Los ajustes preestablecidos guardados se pueden reutilizar. Toca un ajuste preestablecido para convertirlo en la ubicación activa.";
|
||||
"Search address or place" = "Buscar dirección o lugar";
|
||||
"Search by name or username" = "Buscar por nombre o nombre de usuario";
|
||||
"Search by username or name" = "Buscar por nombre de usuario o nombre";
|
||||
"Search settings" = "Buscar en configuración";
|
||||
"Select" = "Seleccionar";
|
||||
"Select location on map" = "Seleccionar ubicación en el mapa";
|
||||
"Set current location" = "Establecer ubicación actual";
|
||||
"Set keep-deleted override" = "Establecer anulación de mantener eliminados";
|
||||
"Settings exported" = "Configuración exportada";
|
||||
"Settings imported" = "Configuración importada";
|
||||
"Show seconds" = "Mostrar segundos";
|
||||
"Sort by" = "Ordenar por";
|
||||
"Story users" = "Usuarios de historias";
|
||||
"Toggle each NSDate formatter IG uses. Different surfaces (feed, comments, stories, DMs) go through different methods — enable the ones you want the custom format applied to." = "Alterna cada formato NSDate que utiliza Instagram. Las distintas secciones (Feed (Inicio), comentarios, historias, mensajes) utilizan métodos diferentes: activa aquellos a los que quieras aplicar el formato personalizado.";
|
||||
"Use this location" = "Usar esta ubicación";
|
||||
"When on, all CoreLocation requests inside Instagram return the location below." = "Cuando está activada, todas las solicitudes de CoreLocation dentro de Instagram devuelven la ubicación que se indica a continuación.";
|
||||
"Show map button" = "Botón de mostrar mapa";
|
||||
"When on, all CoreLocation requests inside Instagram return the location below. Toggle the map button to show or hide the quick toggle on the Friends Map view." = "Cuando está activado, todas las solicitudes de CoreLocation dentro de Instagram devuelven la ubicación que se muestra a continuación. Pulsa el botón del mapa para mostrar u ocultar el control rápido en la vista mapa de amigos.";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// REELS (FEATURES) //
|
||||
// Strings from Reels. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Copied!" = "¡Copiado!";
|
||||
"No password found" = "No se encontró contraseña";
|
||||
"No text field found" = "No se encontró campo de texto";
|
||||
"Password" = "Contraseña";
|
||||
"Refresh Reels?" = "¿Actualizar Reels?";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// PROFILE (FEATURES) //
|
||||
// Strings from Profile. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Doesn't follow you" = "No te sigue";
|
||||
"Follows you" = "Te sigue";
|
||||
"Note copied" = "Nota copiada";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// CONFIRM DIALOGS (IN-FEATURE) //
|
||||
// Strings from Confirm dialogs. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Unfollow?" = "¿Dejar de seguir?";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// MISC //
|
||||
// Anything that didn't fit a named section. Usually short labels. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"720p • progressive • fastest" = "720p • Progresivo • Más rápido";
|
||||
"Are you sure?" = "¿Estás seguro?";
|
||||
"Copy audio URL" = "Copiar enlace de audio";
|
||||
"Copy quality info" = "Copiar información sobre la calidad";
|
||||
"Copy video URL" = "Copiar enlace de video";
|
||||
"Could not access reel media" = "No se logró acceder a los medios del reel";
|
||||
"Could not access reel photo" = "No se logró acceder a la foto del reel";
|
||||
"Could not extract photo url from post" = "No se logró extraer foto de la publicación";
|
||||
"Could not extract photo url from reel" = "No se logró extraer enlace de la foto del reel";
|
||||
"Could not extract photo url from story" = "No se logró extraer enlace de la foto de la historia";
|
||||
"Could not extract video url from post" = "No se logró extraer enlace del video de la publicación";
|
||||
"Could not extract video url from reel" = "No se logró extraer enlace del video del reel";
|
||||
"Could not extract video url from story" = "No se logró extraer enlace del video de la historia";
|
||||
"Download Quality" = "Calidad de descarga";
|
||||
"FFmpegKit Debug" = "FFmpegKit Debug";
|
||||
"Later" = "Mas tarde";
|
||||
"No!" = "¡No!";
|
||||
"Restart" = "Reiniciar";
|
||||
"Restart required" = "Reinicio requerido";
|
||||
"Yes" = "Si";
|
||||
"You must restart the app to apply this change" = "Debes reiniciar la aplicación para aplicar este cambio";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// ABOUT / CREDITS //
|
||||
// Settings → Credits footer. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"%@ — view source, report issues, see releases" = "%@ — ver código fuente, reportar problemas y ver lanzamientos";
|
||||
"Credits" = "Créditos";
|
||||
"Developer" = "Desarrollador";
|
||||
"Donate to SoCuul" = "Donar a SoCuul";
|
||||
"Original SCInsta developer" = "Desarrollador Original SCInsta";
|
||||
"Ryuk" = "Ryuk";
|
||||
"RyukGram %@\n\nInstagram v%@\n\nBased on SCInsta by SoCuul" = "RyukGram %@\n\nInstagram v%@\n\nBasado en SCInsta por SoCuul";
|
||||
"RyukGram on GitHub" = "RyukGram en GitHub";
|
||||
"SoCuul" = "SoCuul";
|
||||
"Support the original developer" = "Apoyar al desarrollador original";
|
||||
"View Repo" = "Ver Repo";
|
||||
"View the source code on GitHub" = "Ver el código fuente en GitHub";
|
||||
/* [ADDED_BY_TRANSLATOR] */ "Translator" = "Traductor";
|
||||
/* [ADDED_BY_TRANSLATOR] */ "Flamako" = "Flamako";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// HD DOWNLOADS //
|
||||
// Enhanced / HD downloads settings (DASH + FFmpegKit encoding). //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Download video at the highest available quality" = "Descargar video en la mejor calidad disponible";
|
||||
"Downloads HD video via DASH streams and encodes to H.264. Requires FFmpegKit." = "Descarga el video en HD mediante transmisión DASH y lo codifica en H.264. Requiere FFmpegKit.";
|
||||
"Encoding speed" = "Velocidad de codificación";
|
||||
"Enhanced downloads" = "Mejorar descargas";
|
||||
"FFmpegKit is not available. Install the sideloaded IPA or the _ffmpeg .deb variant to enable." = "FFmpegKit no está disponible. Instala el archivo IPA descargado o la variante .deb de _ffmpeg para activarlo.";
|
||||
"Faster = lower quality" = "Más rápido = Menor calidad";
|
||||
"Photo quality" = "Calidad de imagen";
|
||||
"Use highest resolution available" = "Usar la resolución mas alta disponible";
|
||||
"Video quality" = "Calidad de video";
|
||||
"Which quality to download" = "En qué calidad descargar";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// EXPERIMENTAL / DEBUG //
|
||||
// Placeholder rows only shown in the experimental settings sandbox. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
"Navigation Cell" = "Celda de navegación";
|
||||
/* [ADDED_BY_DEV] */ "Localization" = "Traducción";
|
||||
/* [ADDED_BY_DEV] */ "Update localization file" = "Actualizar traducción";
|
||||
/* [ADDED_BY_DEV] */ "Import a .strings file for a language" = "Importa un archivo .strings para añadir un idioma";
|
||||
/* [ADDED_BY_DEV] */ "Import a .strings file to update a translation. Pick a language, select the file, restart." = "Importa un archivo .strings para actualizar una traducción. Escoge un idioma, selecciona el archivo y reinicia";
|
||||
/* [ADDED_BY_DEV] */ "Export English strings" = "Exportar archivo .strings en Inglés";
|
||||
/* [ADDED_BY_DEV] */ "Share the base English .strings file for translating" = "Comparte el archivo .strings en Inglés para traducir";
|
||||
|
||||
@@ -27,6 +27,9 @@ NSString *SCIResolvedLanguageCode(void);
|
||||
// Invalidate cached bundles/strings after a language switch.
|
||||
void SCILocalizationReset(void);
|
||||
|
||||
// Writable path for user-imported lproj overrides (Library/RyukGram.bundle/).
|
||||
NSString *SCILocalizationOverridePath(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,11 @@ static NSBundle *gLanguageBundle = nil;
|
||||
static NSString *gLanguageBundleCode = nil;
|
||||
static dispatch_once_t gResourceOnce;
|
||||
|
||||
NSString *SCILocalizationOverridePath(void) {
|
||||
NSString *lib = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
|
||||
return [lib stringByAppendingPathComponent:@"RyukGram.bundle"];
|
||||
}
|
||||
|
||||
static NSBundle *resolveResourceBundle(void) {
|
||||
// 1) Sideload: cyan copies RyukGram.bundle into the app's resource root.
|
||||
NSString *path = [[NSBundle mainBundle] pathForResource:@"RyukGram" ofType:@"bundle"];
|
||||
@@ -66,9 +71,17 @@ static NSBundle *activeLanguageBundle(void) {
|
||||
NSString *code = preferredLanguageCode(resource);
|
||||
if (gLanguageBundle && [code isEqualToString:gLanguageBundleCode]) return gLanguageBundle;
|
||||
|
||||
NSString *lprojPath = [resource pathForResource:code ofType:@"lproj"];
|
||||
if (!lprojPath) lprojPath = [resource pathForResource:@"en" ofType:@"lproj"];
|
||||
gLanguageBundle = lprojPath ? [NSBundle bundleWithPath:lprojPath] : resource;
|
||||
// User-imported overrides take priority (writable Library dir).
|
||||
NSString *overrideLproj = [[SCILocalizationOverridePath()
|
||||
stringByAppendingPathComponent:[code stringByAppendingString:@".lproj"]]
|
||||
stringByAppendingPathComponent:@"Localizable.strings"];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:overrideLproj]) {
|
||||
gLanguageBundle = [NSBundle bundleWithPath:[overrideLproj stringByDeletingLastPathComponent]];
|
||||
} else {
|
||||
NSString *lprojPath = [resource pathForResource:code ofType:@"lproj"];
|
||||
if (!lprojPath) lprojPath = [resource pathForResource:@"en" ofType:@"lproj"];
|
||||
gLanguageBundle = lprojPath ? [NSBundle bundleWithPath:lprojPath] : resource;
|
||||
}
|
||||
gLanguageBundleCode = [code copy];
|
||||
return gLanguageBundle;
|
||||
}
|
||||
@@ -86,11 +99,39 @@ NSString *SCILocalizedString(NSString *key, NSString *fallback) {
|
||||
}
|
||||
|
||||
NSArray<NSDictionary<NSString *, NSString *> *> *SCIAvailableLanguages(void) {
|
||||
// `code` is what we persist; `native` is shown in the picker (endonyms read best).
|
||||
return @[
|
||||
@{ @"code": @"system", @"native": @"System", @"english": @"System default" },
|
||||
@{ @"code": @"en", @"native": @"English", @"english": @"English" },
|
||||
];
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
[result addObject:@{@"code": @"system", @"native": @"System"}];
|
||||
[result addObject:@{@"code": @"en", @"native": @"English"}];
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSMutableSet *seen = [NSMutableSet setWithObject:@"en"];
|
||||
|
||||
// Scan both shipped bundle + writable override dir for .lproj dirs.
|
||||
NSMutableArray *searchPaths = [NSMutableArray array];
|
||||
NSBundle *res = SCILocalizationBundle();
|
||||
if (res) [searchPaths addObject:res.bundlePath];
|
||||
NSString *overrides = SCILocalizationOverridePath();
|
||||
if ([fm fileExistsAtPath:overrides]) [searchPaths addObject:overrides];
|
||||
|
||||
for (NSString *base in searchPaths) {
|
||||
NSArray *contents = [fm contentsOfDirectoryAtPath:base error:nil];
|
||||
for (NSString *name in [contents sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
if (![name hasSuffix:@".lproj"]) continue;
|
||||
NSString *code = [name stringByDeletingPathExtension];
|
||||
if ([code isEqualToString:@"Base"] || [seen containsObject:code]) continue;
|
||||
NSString *stringsPath = [[base stringByAppendingPathComponent:name]
|
||||
stringByAppendingPathComponent:@"Localizable.strings"];
|
||||
if (![fm fileExistsAtPath:stringsPath]) continue;
|
||||
[seen addObject:code];
|
||||
|
||||
NSLocale *loc = [NSLocale localeWithLocaleIdentifier:code];
|
||||
NSString *native = [loc localizedStringForLanguageCode:code] ?: code;
|
||||
if (native.length) native = [[[native substringToIndex:1] uppercaseString]
|
||||
stringByAppendingString:[native substringFromIndex:1]];
|
||||
[result addObject:@{@"code": code, @"native": native}];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void SCILocalizationReset(void) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#import "SCIExcludedChatsViewController.h"
|
||||
#import "../Features/StoriesAndMessages/SCIExcludedThreads.h"
|
||||
#import "../Networking/SCIInstagramAPI.h"
|
||||
#import "../Utils.h"
|
||||
|
||||
@interface SCIExcludedChatsViewController ()
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
@@ -52,7 +54,9 @@
|
||||
style:UIBarButtonItemStylePlain target:self action:@selector(toggleSort)];
|
||||
self.editBtn = [[UIBarButtonItem alloc]
|
||||
initWithTitle:SCILocalized(@"Select") style:UIBarButtonItemStylePlain target:self action:@selector(toggleEdit)];
|
||||
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn];
|
||||
UIBarButtonItem *addBtn = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addUserTapped)];
|
||||
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn, addBtn];
|
||||
|
||||
[self reload];
|
||||
}
|
||||
@@ -111,6 +115,65 @@
|
||||
[self presentViewController:sheet animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)addUserTapped {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add chat")
|
||||
message:SCILocalized(@"Enter username of the DM thread")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = @"username"; tf.autocapitalizationType = UITextAutocapitalizationTypeNone; }];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Search") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
|
||||
NSString *q = [alert.textFields.firstObject.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (!q.length) return;
|
||||
[self lookupUsername:q];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)lookupUsername:(NSString *)username {
|
||||
// Step 1: resolve user info.
|
||||
[SCIInstagramAPI sendRequestWithMethod:@"GET"
|
||||
path:[NSString stringWithFormat:@"users/web_profile_info/?username=%@", username]
|
||||
body:nil completion:^(NSDictionary *resp, NSError *err) {
|
||||
NSDictionary *user = resp[@"data"][@"user"];
|
||||
if (!user || err) {
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:SCILocalized(@"User '%@' not found"), username]];
|
||||
return;
|
||||
}
|
||||
NSString *pk = [user[@"id"] description] ?: @"";
|
||||
NSString *uname = user[@"username"] ?: username;
|
||||
NSString *fullName = user[@"full_name"] ?: @"";
|
||||
if (!pk.length) { [SCIUtils showErrorHUDWithDescription:SCILocalized(@"Could not resolve user ID")]; return; }
|
||||
|
||||
// Step 2: resolve DM thread with this user.
|
||||
[SCIInstagramAPI sendRequestWithMethod:@"GET"
|
||||
path:[NSString stringWithFormat:@"direct_v2/threads/get_by_participants/?recipient_users=[%@]", pk]
|
||||
body:nil completion:^(NSDictionary *threadResp, NSError *tErr) {
|
||||
NSString *threadId = threadResp[@"thread"][@"thread_id"];
|
||||
NSString *threadName = threadResp[@"thread"][@"thread_title"] ?: uname;
|
||||
if (!threadId.length || tErr) {
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:SCILocalized(@"No DM thread found with @%@"), uname]];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *msg = [NSString stringWithFormat:@"@%@%@", uname, fullName.length ? [NSString stringWithFormat:@" (%@)", fullName] : @""];
|
||||
UIAlertController *confirm = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add to list?")
|
||||
message:msg
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
|
||||
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Add") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
|
||||
[SCIExcludedThreads addOrUpdateEntry:@{
|
||||
@"threadId": threadId,
|
||||
@"threadName": threadName,
|
||||
@"isGroup": @NO,
|
||||
@"users": @[@{@"pk": pk, @"username": uname, @"fullName": fullName}],
|
||||
}];
|
||||
[self reload];
|
||||
}]];
|
||||
[self presentViewController:confirm animated:YES completion:nil];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)toggleSort {
|
||||
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:SCILocalized(@"Sort by")
|
||||
message:nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#import "SCIExcludedStoryUsersViewController.h"
|
||||
#import "../Features/StoriesAndMessages/SCIExcludedStoryUsers.h"
|
||||
#import "../Networking/SCIInstagramAPI.h"
|
||||
#import "../Utils.h"
|
||||
|
||||
@interface SCIExcludedStoryUsersViewController ()
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
@@ -52,7 +54,9 @@
|
||||
style:UIBarButtonItemStylePlain target:self action:@selector(toggleSort)];
|
||||
self.editBtn = [[UIBarButtonItem alloc]
|
||||
initWithTitle:SCILocalized(@"Select") style:UIBarButtonItemStylePlain target:self action:@selector(toggleEdit)];
|
||||
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn];
|
||||
UIBarButtonItem *addBtn = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addUserTapped)];
|
||||
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn, addBtn];
|
||||
|
||||
[self reload];
|
||||
}
|
||||
@@ -82,6 +86,47 @@
|
||||
[self reload];
|
||||
}
|
||||
|
||||
- (void)addUserTapped {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add user")
|
||||
message:SCILocalized(@"Enter username")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = @"username"; tf.autocapitalizationType = UITextAutocapitalizationTypeNone; }];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Search") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
|
||||
NSString *q = [alert.textFields.firstObject.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (!q.length) return;
|
||||
[self lookupUsername:q];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)lookupUsername:(NSString *)username {
|
||||
[SCIInstagramAPI sendRequestWithMethod:@"GET"
|
||||
path:[NSString stringWithFormat:@"users/web_profile_info/?username=%@", username]
|
||||
body:nil completion:^(NSDictionary *resp, NSError *err) {
|
||||
NSDictionary *user = resp[@"data"][@"user"];
|
||||
if (!user || err) {
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:SCILocalized(@"User '%@' not found"), username]];
|
||||
return;
|
||||
}
|
||||
NSString *pk = [user[@"id"] description] ?: @"";
|
||||
NSString *uname = user[@"username"] ?: username;
|
||||
NSString *fullName = user[@"full_name"] ?: @"";
|
||||
if (!pk.length) { [SCIUtils showErrorHUDWithDescription:SCILocalized(@"Could not resolve user ID")]; return; }
|
||||
|
||||
NSString *msg = [NSString stringWithFormat:@"@%@%@", uname, fullName.length ? [NSString stringWithFormat:@" (%@)", fullName] : @""];
|
||||
UIAlertController *confirm = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add to list?")
|
||||
message:msg
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
|
||||
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Add") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
|
||||
[SCIExcludedStoryUsers addOrUpdateEntry:@{@"pk": pk, @"username": uname, @"fullName": fullName}];
|
||||
[self reload];
|
||||
}]];
|
||||
[self presentViewController:confirm animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)toggleSort {
|
||||
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:SCILocalized(@"Sort by")
|
||||
message:nil
|
||||
|
||||
@@ -88,14 +88,12 @@ static char rowStaticRef[] = "row";
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemClose
|
||||
target:self action:@selector(sciDismissSettings)];
|
||||
|
||||
// Compact globe button — English is the only shipped language for now,
|
||||
// so the tap shows an info alert instead of a picker. Re-enable the
|
||||
// menu below once additional translations land.
|
||||
UIImage *globe = [UIImage systemImageNamed:@"globe"];
|
||||
UIBarButtonItem *langItem = [[UIBarButtonItem alloc] initWithImage:globe
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(sciShowLanguageInfo)];
|
||||
target:nil
|
||||
action:nil];
|
||||
langItem.menu = [self sciBuildLanguageMenu];
|
||||
self.navigationItem.rightBarButtonItem = langItem;
|
||||
}
|
||||
}
|
||||
@@ -141,7 +139,16 @@ static char rowStaticRef[] = "row";
|
||||
[actions addObject:action];
|
||||
}
|
||||
|
||||
return [UIMenu menuWithTitle:SCILocalized(@"settings.language.title") children:actions];
|
||||
UIAction *help = [UIAction actionWithTitle:[NSString stringWithFormat:@"❤️ %@", SCILocalized(@"settings.language.help_translate")]
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__unused UIAction *a) {
|
||||
NSURL *url = [NSURL URLWithString:@"https://github.com/faroukbmiled/RyukGram#translating-ryukgram"];
|
||||
if (url) [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
}];
|
||||
|
||||
return [UIMenu menuWithTitle:SCILocalized(@"settings.language.title")
|
||||
children:[actions arrayByAddingObject:help]];
|
||||
}
|
||||
|
||||
- (void)sciApplyLanguageChange {
|
||||
|
||||
@@ -8,6 +8,53 @@
|
||||
#import "SCIEmbedDomainViewController.h"
|
||||
#import "SCIDateFormatPickerVC.h"
|
||||
#import "../SCIFFmpeg.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
// Copies imported .strings into the writable override dir.
|
||||
@interface SCILocImportHelper : NSObject <UIDocumentPickerDelegate>
|
||||
@end
|
||||
@implementation SCILocImportHelper
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
||||
if (!urls.count) return;
|
||||
NSURL *src = urls.firstObject;
|
||||
NSString *code = objc_getAssociatedObject(controller, "sci_lang");
|
||||
if (!code.length) return;
|
||||
|
||||
// Validate it parses
|
||||
NSDictionary *test = [NSDictionary dictionaryWithContentsOfURL:src];
|
||||
if (!test.count) {
|
||||
UIAlertController *a = [UIAlertController alertControllerWithTitle:@"Error"
|
||||
message:@"File is empty or not a valid .strings file." preferredStyle:UIAlertControllerStyleAlert];
|
||||
[a addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
|
||||
UIViewController *top = controller.presentingViewController ?: UIApplication.sharedApplication.keyWindow.rootViewController;
|
||||
[top presentViewController:a animated:YES completion:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// Write to the writable override dir (Library/RyukGram.bundle/<code>.lproj/).
|
||||
NSString *lproj = [NSString stringWithFormat:@"%@.lproj", code];
|
||||
NSString *dir = [SCILocalizationOverridePath() stringByAppendingPathComponent:lproj];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
[fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
NSString *dest = [dir stringByAppendingPathComponent:@"Localizable.strings"];
|
||||
[fm removeItemAtPath:dest error:nil];
|
||||
BOOL ok = [fm copyItemAtPath:src.path toPath:dest error:nil];
|
||||
|
||||
NSString *msg = ok
|
||||
? [NSString stringWithFormat:@"Updated %@ (%ld keys). Restart to apply.", code, (long)test.count]
|
||||
: @"Could not write file.";
|
||||
UIAlertController *a = [UIAlertController alertControllerWithTitle:ok ? @"Done" : @"Error"
|
||||
message:msg preferredStyle:UIAlertControllerStyleAlert];
|
||||
if (ok) {
|
||||
[a addAction:[UIAlertAction actionWithTitle:@"Restart now" style:UIAlertActionStyleDefault
|
||||
handler:^(__unused UIAlertAction *x) { [SCIUtils showRestartConfirmation]; }]];
|
||||
}
|
||||
[a addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
|
||||
UIViewController *top = UIApplication.sharedApplication.keyWindow.rootViewController;
|
||||
while (top.presentedViewController) top = top.presentedViewController;
|
||||
[top presentViewController:a animated:YES completion:nil];
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation SCITweakSettings
|
||||
|
||||
@@ -182,7 +229,7 @@
|
||||
@"header": SCILocalized(@"Seen receipts"),
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Disable story seen receipt") subtitle:SCILocalized(@"Hides the notification for others when you view their story") defaultsKey:@"no_seen_receipt"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Keep stories visually unseen") subtitle:SCILocalized(@"Prevents stories from visually marking as seen in the tray (keeps colorful ring)") defaultsKey:@"no_seen_visual"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Keep stories visually seen locally") subtitle:SCILocalized(@"Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server") defaultsKey:@"keep_seen_visual_local"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Mark seen on story like") subtitle:SCILocalized(@"Marks a story as seen the moment you tap the heart, even with seen blocking on") defaultsKey:@"seen_on_story_like"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Mark seen on story reply") subtitle:SCILocalized(@"Marks a story as seen when you send a reply or emoji reaction, even with seen blocking on") defaultsKey:@"seen_on_story_reply"],
|
||||
[SCISetting menuCellWithTitle:SCILocalized(@"Manual seen button mode") subtitle:SCILocalized(@"Button = single-tap mark seen. Toggle = tap toggles story read receipts on/off (eye fills blue when on)") menu:[self menus][@"story_seen_mode"]],
|
||||
@@ -255,6 +302,7 @@
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting menuCellWithTitle:SCILocalized(@"Tap Controls") subtitle:SCILocalized(@"Change what happens when you tap on a reel") menu:[self menus][@"reels_tap_control"]],
|
||||
[SCISetting menuCellWithTitle:SCILocalized(@"Auto-scroll reels") subtitle:SCILocalized(@"IG default: native behavior. RyukGram: re-advances after swiping back.") menu:[self menus][@"auto_scroll_reels_mode"]],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Always show progress scrubber") subtitle:SCILocalized(@"Forces the progress bar to appear on every reel") defaultsKey:@"reels_show_scrubber"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Disable auto-unmuting reels") subtitle:SCILocalized(@"Prevents reels from unmuting when the volume/silent button is pressed") defaultsKey:@"disable_auto_unmuting_reels" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm reel refresh") subtitle:SCILocalized(@"Shows an alert when you trigger a reels refresh") defaultsKey:@"refresh_reel_confirm"],
|
||||
@@ -468,8 +516,10 @@
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Posts/Stories") subtitle:SCILocalized(@"Shows an alert when you click the like button on posts or stories to confirm the like") defaultsKey:@"like_confirm"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Reels") subtitle:SCILocalized(@"Shows an alert when you click the like button on reels to confirm the like") defaultsKey:@"like_confirm_reels"]
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Posts") subtitle:SCILocalized(@"Shows an alert when you click the like button on posts to confirm the like") defaultsKey:@"like_confirm"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Reels") subtitle:SCILocalized(@"Shows an alert when you click the like button on reels to confirm the like") defaultsKey:@"like_confirm_reels"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm story like") subtitle:SCILocalized(@"Shows an alert when you click the like button on stories to confirm the like") defaultsKey:@"story_like_confirm"],
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm story emoji reaction") subtitle:SCILocalized(@"Shows an alert before sending an emoji reaction on a story") defaultsKey:@"emoji_reaction_confirm"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@@ -569,6 +619,22 @@
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"ladybug"]
|
||||
navSections:@[@{
|
||||
@"header": SCILocalized(@"Localization"),
|
||||
@"footer": SCILocalized(@"Import a .strings file to update a translation. Pick a language, select the file, restart."),
|
||||
@"rows": @[
|
||||
[SCISetting buttonCellWithTitle:SCILocalized(@"Update localization file")
|
||||
subtitle:SCILocalized(@"Import a .strings file for a language")
|
||||
icon:[SCISymbol symbolWithName:@"square.and.arrow.down"]
|
||||
action:^(void) { [self presentLocalizationImport]; }
|
||||
],
|
||||
[SCISetting buttonCellWithTitle:SCILocalized(@"Export English strings")
|
||||
subtitle:SCILocalized(@"Share the base English .strings file for translating")
|
||||
icon:[SCISymbol symbolWithName:@"square.and.arrow.up"]
|
||||
action:^(void) { [self exportEnglishStrings]; }
|
||||
],
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"FLEX",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:SCILocalized(@"Enable FLEX gesture") subtitle:SCILocalized(@"Hold 5 fingers on the screen to open FLEX") defaultsKey:@"flex_instagram"],
|
||||
@@ -682,6 +748,102 @@
|
||||
return SCILocalized(@"settings.title");
|
||||
}
|
||||
|
||||
// MARK: - Localization import
|
||||
|
||||
static UIViewController *sciTopVC(void) {
|
||||
UIViewController *top = nil;
|
||||
for (UIWindow *w in UIApplication.sharedApplication.windows) {
|
||||
if (!w.isKeyWindow) continue;
|
||||
top = w.rootViewController;
|
||||
while (top.presentedViewController) top = top.presentedViewController;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
|
||||
+ (void)exportEnglishStrings {
|
||||
NSBundle *res = SCILocalizationBundle();
|
||||
NSString *path = [res pathForResource:@"en" ofType:@"lproj"];
|
||||
if (path) path = [path stringByAppendingPathComponent:@"Localizable.strings"];
|
||||
if (!path || ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"English .strings file not found"];
|
||||
return;
|
||||
}
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
UIActivityViewController *ac = [[UIActivityViewController alloc] initWithActivityItems:@[url] applicationActivities:nil];
|
||||
UIViewController *top = sciTopVC();
|
||||
if (!top) return;
|
||||
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
||||
ac.popoverPresentationController.sourceView = top.view;
|
||||
ac.popoverPresentationController.sourceRect = CGRectMake(CGRectGetMidX(top.view.bounds), CGRectGetMidY(top.view.bounds), 1, 1);
|
||||
}
|
||||
[top presentViewController:ac animated:YES completion:nil];
|
||||
}
|
||||
|
||||
+ (void)presentLocalizationImport {
|
||||
NSArray *langs = SCIAvailableLanguages();
|
||||
|
||||
UIAlertController *picker = [UIAlertController alertControllerWithTitle:@"Update localization"
|
||||
message:@"Pick a language to update, or add a new one"
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
for (NSDictionary *lang in langs) {
|
||||
NSString *code = lang[@"code"];
|
||||
if ([code isEqualToString:@"system"]) continue;
|
||||
NSString *title = [NSString stringWithFormat:@"%@ (%@)", lang[@"native"], code];
|
||||
[picker addAction:[UIAlertAction actionWithTitle:title
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(__unused UIAlertAction *a) {
|
||||
[self importStringsForLanguage:code];
|
||||
}]];
|
||||
}
|
||||
|
||||
[picker addAction:[UIAlertAction actionWithTitle:@"+ Add new language"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(__unused UIAlertAction *a) {
|
||||
[self promptNewLanguageCode];
|
||||
}]];
|
||||
[picker addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[sciTopVC() presentViewController:picker animated:YES completion:nil];
|
||||
}
|
||||
|
||||
+ (void)promptNewLanguageCode {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Add language"
|
||||
message:@"Enter the language code (e.g. fr, de, ja)"
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = @"fr"; }];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Next" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
|
||||
NSString *code = [alert.textFields.firstObject.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (code.length < 2 || code.length > 5) return;
|
||||
[self importStringsForLanguage:code];
|
||||
}]];
|
||||
[sciTopVC() presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
+ (void)importStringsForLanguage:(NSString *)langCode {
|
||||
UIViewController *top = sciTopVC();
|
||||
if (!top) return;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
UIDocumentPickerViewController *dp = [[UIDocumentPickerViewController alloc]
|
||||
initWithDocumentTypes:@[@"public.plain-text", @"com.apple.xcode.strings-text", @"public.data"] inMode:UIDocumentPickerModeImport];
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
dp.allowsMultipleSelection = NO;
|
||||
dp.delegate = (id<UIDocumentPickerDelegate>)[self sharedImportHelper];
|
||||
objc_setAssociatedObject(dp, "sci_lang", [langCode copy], OBJC_ASSOCIATION_COPY_NONATOMIC);
|
||||
[top presentViewController:dp animated:YES completion:nil];
|
||||
}
|
||||
|
||||
+ (id)sharedImportHelper {
|
||||
static id helper = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
helper = [SCILocImportHelper new];
|
||||
});
|
||||
return helper;
|
||||
}
|
||||
|
||||
// MARK: - Menus
|
||||
|
||||
@@ -876,6 +1038,15 @@
|
||||
]
|
||||
]],
|
||||
|
||||
@"auto_scroll_reels_mode": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:SCILocalized(@"Off") image:nil action:@selector(menuChanged:)
|
||||
propertyList:@{@"defaultsKey": @"auto_scroll_reels_mode", @"value": @"off"}],
|
||||
[UICommand commandWithTitle:SCILocalized(@"IG default") image:nil action:@selector(menuChanged:)
|
||||
propertyList:@{@"defaultsKey": @"auto_scroll_reels_mode", @"value": @"ig"}],
|
||||
[UICommand commandWithTitle:SCILocalized(@"RyukGram") image:nil action:@selector(menuChanged:)
|
||||
propertyList:@{@"defaultsKey": @"auto_scroll_reels_mode", @"value": @"custom"}],
|
||||
]],
|
||||
|
||||
@"launch_tab": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:SCILocalized(@"Default") image:nil action:@selector(menuChanged:)
|
||||
propertyList:@{@"defaultsKey": @"launch_tab", @"value": @"default"}],
|
||||
|
||||
+3
-2
@@ -14,7 +14,7 @@
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// * Tweak version *
|
||||
NSString *SCIVersionString = @"v1.2.0";
|
||||
NSString *SCIVersionString = @"v1.2.1";
|
||||
|
||||
// Variables that work across features
|
||||
BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
@@ -86,9 +86,10 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
@"enable_notes_customization": @(YES),
|
||||
@"custom_note_themes": @(YES),
|
||||
@"disable_auto_unmuting_reels": @(NO),
|
||||
@"auto_scroll_reels_mode": @"off",
|
||||
@"settings_shortcut": @(YES),
|
||||
@"doom_scrolling_reel_count": @(1),
|
||||
@"no_seen_visual": @(YES),
|
||||
@"keep_seen_visual_local": @(NO),
|
||||
@"send_audio_as_file": @(YES),
|
||||
@"download_audio_message": @(NO),
|
||||
@"save_to_ryukgram_album": @(NO),
|
||||
|
||||
Reference in New Issue
Block a user