feat(ios): share extension ile Ratebubble iOS istemcisini ekle ve paylaşım akışını düzelt
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
---
|
||||
date: 2026-03-01
|
||||
topic: ios-share-extension-v1
|
||||
---
|
||||
|
||||
# iOS Share Extension v1
|
||||
|
||||
## What We're Building
|
||||
Projeye ikinci bir frontend olarak native iOS uygulaması eklenecek. Uygulamanın v1 ana işlevi, Netflix uygulamasından paylaşılan içerik linkini almak ve backend API'ye göndererek metadata sonucunu kullanıcıya göstermek.
|
||||
|
||||
Akış: Netflix içerik sayfası -> Paylaş -> bizim iOS app (Share Extension) -> URL alma -> backend `/api/getinfo` isteği -> sonuçları metin olarak gösterme (`title`, `year`, vb.).
|
||||
|
||||
## Why This Approach
|
||||
Share Extension seçimi, iOS paylaşım menüsüne doğal şekilde entegre olur ve kullanıcı davranışıyla birebir örtüşür. Deep link tabanlı alternatiflere göre daha güvenilir URL yakalama sağlar ve v1 için en düşük sürtünmeyle çalışır.
|
||||
|
||||
V1 kapsamını sadece “link al, API çağır, sonucu göster” ile sınırlamak, YAGNI prensibine uygundur ve ürünü hızlıca canlı doğrulamaya taşır.
|
||||
|
||||
## Key Decisions
|
||||
- Entegrasyon tipi: Share Extension (zorunlu).
|
||||
- Kapsam: Sadece Netflix paylaşım linki ile `/api/getinfo` çağrısı.
|
||||
- Yetkilendirme: `X-API-Key` olarak `API_KEY_MOBILE` kullanılacak.
|
||||
- Görsellik: UI/UX tasarımı v1 sonrası iterasyona bırakılacak.
|
||||
|
||||
## Open Questions
|
||||
- Share Extension URL’yi doğrudan API’ye mi gönderecek, yoksa ana app’e handoff edip ana app mi çağrı yapacak?
|
||||
- Başarısız API yanıtlarında kullanıcıya minimum hangi hata metni gösterilecek?
|
||||
|
||||
## Next Steps
|
||||
-> `/workflows:plan`
|
||||
39
ios/README.md
Normal file
39
ios/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Ratebubble iOS (v1)
|
||||
|
||||
Bu klasör, Ratebubble için iOS ana uygulama + Share Extension iskeletini içerir.
|
||||
|
||||
## Hedef
|
||||
- Netflix uygulamasından paylaşılan URL'yi almak
|
||||
- Ana app'e handoff etmek
|
||||
- Backend `/api/getinfo` endpointine gönderip sonucu göstermek
|
||||
|
||||
## Gereksinimler
|
||||
- Xcode 15+
|
||||
- iOS 16+
|
||||
- (Opsiyonel) XcodeGen
|
||||
|
||||
## Proje Oluşturma (XcodeGen)
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
xcodegen generate
|
||||
open Ratebubble.xcodeproj
|
||||
```
|
||||
|
||||
Eğer `xcodegen` kurulu değilse:
|
||||
|
||||
```bash
|
||||
brew install xcodegen
|
||||
```
|
||||
|
||||
## Yapılandırma
|
||||
`Ratebubble/Resources/Config.xcconfig` dosyasında:
|
||||
- `API_BASE_URL`
|
||||
- `MOBILE_API_KEY`
|
||||
- `APP_GROUP_ID`
|
||||
- `APP_URL_SCHEME`
|
||||
|
||||
değerlerini ortamına göre güncelle.
|
||||
|
||||
## Not
|
||||
Share Extension, URL'yi App Group `UserDefaults` içine yazar ve custom URL scheme ile ana app'i açar.
|
||||
454
ios/Ratebubble.xcodeproj/project.pbxproj
Normal file
454
ios/Ratebubble.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,454 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 63;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0315885AA91662FE48BBC594 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E2C9211EC8E30B83ABBCFB /* ContentView.swift */; };
|
||||
0C602ACFD0DC100ECCFA84A5 /* SharedPayloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */; };
|
||||
17157E8CF084270023A56C06 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA40D68C79904DEE4125D874 /* Models.swift */; };
|
||||
186EA66540EFA4CEA949617D /* RatebubbleShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7539736C403588176FB7D80C /* RatebubbleShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
3993F1B14739F8177B844EE0 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */; };
|
||||
3E1D36E3AE2CDBD9C402E921 /* RatebubbleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC8FF0D6783D4BFC590D370 /* RatebubbleApp.swift */; };
|
||||
77D601458525635F705EA471 /* SharedPayloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */; };
|
||||
886AF155C91E366210524E45 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37749607968C1A751317B7BD /* MainViewModel.swift */; };
|
||||
A547A283AD74B06B25FDB424 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C1C571628107632B4A2F90 /* ShareViewController.swift */; };
|
||||
B31B1B7EC5BAE4FB9011C94C /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */; };
|
||||
F670AE07545EBB4ABE2889F7 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA40D68C79904DEE4125D874 /* Models.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
2FFA444A9840D8FEE9B59CB3 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 7944D79A93FF32A326D78AB8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D66EABC12ABB12431BD3554C;
|
||||
remoteInfo = RatebubbleShare;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
87D7EB893873C1953A4B8483 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
186EA66540EFA4CEA949617D /* RatebubbleShare.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
19F8E500E3A70D57454E49A6 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
37749607968C1A751317B7BD /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
|
||||
72C1C571628107632B4A2F90 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
7539736C403588176FB7D80C /* RatebubbleShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RatebubbleShare.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7FEF3C0785A60EA1B560627C /* Ratebubble.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ratebubble.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
96E2C9211EC8E30B83ABBCFB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
AA40D68C79904DEE4125D874 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
|
||||
CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedPayloadStore.swift; sourceTree = "<group>"; };
|
||||
D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
|
||||
FEC8FF0D6783D4BFC590D370 /* RatebubbleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatebubbleApp.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0EF0C634DF5D5383BE2D7771 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */,
|
||||
AA40D68C79904DEE4125D874 /* Models.swift */,
|
||||
CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */,
|
||||
);
|
||||
name = Shared;
|
||||
path = Ratebubble/Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
108289137F406D4F63BFC3AE /* ShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
72C1C571628107632B4A2F90 /* ShareViewController.swift */,
|
||||
);
|
||||
name = ShareExtension;
|
||||
path = Ratebubble/ShareExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1C0E0078471B0855017A64DD = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EF496B275DF9294D162CFA6E /* App */,
|
||||
BFD8EEAEAAE9601A16238D2D /* Resources */,
|
||||
0EF0C634DF5D5383BE2D7771 /* Shared */,
|
||||
108289137F406D4F63BFC3AE /* ShareExtension */,
|
||||
E032C44ACA0299B26854540A /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFD8EEAEAAE9601A16238D2D /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
19F8E500E3A70D57454E49A6 /* Config.xcconfig */,
|
||||
);
|
||||
name = Resources;
|
||||
path = Ratebubble/Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E032C44ACA0299B26854540A /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7FEF3C0785A60EA1B560627C /* Ratebubble.app */,
|
||||
7539736C403588176FB7D80C /* RatebubbleShare.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EF496B275DF9294D162CFA6E /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
96E2C9211EC8E30B83ABBCFB /* ContentView.swift */,
|
||||
37749607968C1A751317B7BD /* MainViewModel.swift */,
|
||||
FEC8FF0D6783D4BFC590D370 /* RatebubbleApp.swift */,
|
||||
);
|
||||
name = App;
|
||||
path = Ratebubble/App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
1E5B7142527B8E64D5BB475A /* Ratebubble */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = DE6F9D82ED742462C54ED81A /* Build configuration list for PBXNativeTarget "Ratebubble" */;
|
||||
buildPhases = (
|
||||
21317B3B7EEF00F8D3448F1A /* Sources */,
|
||||
87D7EB893873C1953A4B8483 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
B360B60BC3BB55F1DEFD5F02 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Ratebubble;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = Ratebubble;
|
||||
productReference = 7FEF3C0785A60EA1B560627C /* Ratebubble.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
D66EABC12ABB12431BD3554C /* RatebubbleShare */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3BCC0C2B27341BF3ADDCB3B9 /* Build configuration list for PBXNativeTarget "RatebubbleShare" */;
|
||||
buildPhases = (
|
||||
B64AC19652E4EA372111002C /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = RatebubbleShare;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = RatebubbleShare;
|
||||
productReference = 7539736C403588176FB7D80C /* RatebubbleShare.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
7944D79A93FF32A326D78AB8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
};
|
||||
buildConfigurationList = BA6BB0478ED529686B10D5F3 /* Build configuration list for PBXProject "Ratebubble" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 1C0E0078471B0855017A64DD;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
1E5B7142527B8E64D5BB475A /* Ratebubble */,
|
||||
D66EABC12ABB12431BD3554C /* RatebubbleShare */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
21317B3B7EEF00F8D3448F1A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B31B1B7EC5BAE4FB9011C94C /* APIClient.swift in Sources */,
|
||||
0315885AA91662FE48BBC594 /* ContentView.swift in Sources */,
|
||||
886AF155C91E366210524E45 /* MainViewModel.swift in Sources */,
|
||||
17157E8CF084270023A56C06 /* Models.swift in Sources */,
|
||||
3E1D36E3AE2CDBD9C402E921 /* RatebubbleApp.swift in Sources */,
|
||||
77D601458525635F705EA471 /* SharedPayloadStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B64AC19652E4EA372111002C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3993F1B14739F8177B844EE0 /* APIClient.swift in Sources */,
|
||||
F670AE07545EBB4ABE2889F7 /* Models.swift in Sources */,
|
||||
A547A283AD74B06B25FDB424 /* ShareViewController.swift in Sources */,
|
||||
0C602ACFD0DC100ECCFA84A5 /* SharedPayloadStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
B360B60BC3BB55F1DEFD5F02 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D66EABC12ABB12431BD3554C /* RatebubbleShare */;
|
||||
targetProxy = 2FFA444A9840D8FEE9B59CB3 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
03B172804432CFDEF0B15A28 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/RatebubbleShare.entitlements;
|
||||
DEVELOPMENT_TEAM = S34SFUY9SC;
|
||||
INFOPLIST_FILE = "Ratebubble/Resources/RatebubbleShare-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble.share;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2C4CEF1731FBB13FC87F825C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.9;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3AE70298876BFAD681CF451B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/Ratebubble.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
DEVELOPMENT_TEAM = S34SFUY9SC;
|
||||
INFOPLIST_FILE = "Ratebubble/Resources/Ratebubble-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3B575860868E0B1AF5B7D410 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.9;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
5C1878476CD3B8F15D53C417 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/Ratebubble.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
DEVELOPMENT_TEAM = S34SFUY9SC;
|
||||
INFOPLIST_FILE = "Ratebubble/Resources/Ratebubble-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A005F6D39148A83B59423F17 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/RatebubbleShare.entitlements;
|
||||
DEVELOPMENT_TEAM = S34SFUY9SC;
|
||||
INFOPLIST_FILE = "Ratebubble/Resources/RatebubbleShare-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble.share;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
3BCC0C2B27341BF3ADDCB3B9 /* Build configuration list for PBXNativeTarget "RatebubbleShare" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A005F6D39148A83B59423F17 /* Debug */,
|
||||
03B172804432CFDEF0B15A28 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
BA6BB0478ED529686B10D5F3 /* Build configuration list for PBXProject "Ratebubble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3B575860868E0B1AF5B7D410 /* Debug */,
|
||||
2C4CEF1731FBB13FC87F825C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
DE6F9D82ED742462C54ED81A /* Build configuration list for PBXNativeTarget "Ratebubble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3AE70298876BFAD681CF451B /* Debug */,
|
||||
5C1878476CD3B8F15D53C417 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 7944D79A93FF32A326D78AB8 /* Project object */;
|
||||
}
|
||||
7
ios/Ratebubble.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
ios/Ratebubble.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
|
||||
BuildableName = "Ratebubble.app"
|
||||
BlueprintName = "Ratebubble"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
|
||||
BuildableName = "Ratebubble.app"
|
||||
BlueprintName = "Ratebubble"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
|
||||
BuildableName = "Ratebubble.app"
|
||||
BlueprintName = "Ratebubble"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2620"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D66EABC12ABB12431BD3554C"
|
||||
BuildableName = "RatebubbleShare.appex"
|
||||
BlueprintName = "RatebubbleShare"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
|
||||
BuildableName = "Ratebubble.app"
|
||||
BlueprintName = "Ratebubble"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
|
||||
BuildableName = "Ratebubble.app"
|
||||
BlueprintName = "Ratebubble"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
|
||||
BuildableName = "Ratebubble.app"
|
||||
BlueprintName = "Ratebubble"
|
||||
ReferencedContainer = "container:Ratebubble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
82
ios/Ratebubble/App/ContentView.swift
Normal file
82
ios/Ratebubble/App/ContentView.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var viewModel = MainViewModel()
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Paylaşılan Link") {
|
||||
TextField("https://www.netflix.com/tr/title/...", text: $viewModel.sharedURL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.keyboardType(.URL)
|
||||
|
||||
Button("Backend'den Getir") {
|
||||
Task { await viewModel.fetch() }
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
Section {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Veri alınıyor...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let error = viewModel.errorMessage {
|
||||
Section("Hata") {
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
|
||||
if let result = viewModel.result {
|
||||
Section("Sonuç") {
|
||||
KeyValueRow(key: "Provider", value: result.provider)
|
||||
KeyValueRow(key: "Title", value: result.title)
|
||||
KeyValueRow(key: "Year", value: result.year.map(String.init) ?? "-")
|
||||
KeyValueRow(key: "Type", value: result.type)
|
||||
KeyValueRow(key: "Age Rating", value: result.ageRating ?? "-")
|
||||
KeyValueRow(key: "Current Season", value: result.currentSeason.map(String.init) ?? "-")
|
||||
KeyValueRow(key: "Genres", value: result.genres.joined(separator: ", "))
|
||||
KeyValueRow(key: "Cast", value: result.cast.joined(separator: ", "))
|
||||
KeyValueRow(key: "Plot", value: result.plot ?? "-")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Ratebubble")
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.consumeSharedURLIfAny()
|
||||
}
|
||||
.onOpenURL { _ in
|
||||
viewModel.consumeSharedURLIfAny()
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .active {
|
||||
viewModel.consumeSharedURLIfAny()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct KeyValueRow: View {
|
||||
let key: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(key)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value.isEmpty ? "-" : value)
|
||||
.font(.body)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
36
ios/Ratebubble/App/MainViewModel.swift
Normal file
36
ios/Ratebubble/App/MainViewModel.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
final class MainViewModel: ObservableObject {
|
||||
@Published var sharedURL: String = ""
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var result: GetInfoResponse?
|
||||
@Published var errorMessage: String?
|
||||
|
||||
func consumeSharedURLIfAny() {
|
||||
guard let incoming = SharedPayloadStore.consumeIncomingURL(), !incoming.isEmpty else {
|
||||
return
|
||||
}
|
||||
sharedURL = incoming
|
||||
Task { await fetch() }
|
||||
}
|
||||
|
||||
func fetch() async {
|
||||
guard !sharedURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
errorMessage = "Paylaşılan URL boş olamaz."
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
result = nil
|
||||
|
||||
do {
|
||||
result = try await APIClient.shared.getInfo(url: sharedURL)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
10
ios/Ratebubble/App/RatebubbleApp.swift
Normal file
10
ios/Ratebubble/App/RatebubbleApp.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct RatebubbleApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
5
ios/Ratebubble/Resources/Config.xcconfig
Normal file
5
ios/Ratebubble/Resources/Config.xcconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
SLASH = /
|
||||
API_BASE_URL = http:$(SLASH)$(SLASH)localhost:3000
|
||||
MOBILE_API_KEY = mobile-dev-key-change-me
|
||||
APP_GROUP_ID = group.net.wisecolt.ratebubble
|
||||
APP_URL_SCHEME = ratebubble
|
||||
39
ios/Ratebubble/Resources/Ratebubble-Info.plist
Normal file
39
ios/Ratebubble/Resources/Ratebubble-Info.plist
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>$(APP_URL_SCHEME)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>API_BASE_URL</key>
|
||||
<string>$(API_BASE_URL)</string>
|
||||
<key>MOBILE_API_KEY</key>
|
||||
<string>$(MOBILE_API_KEY)</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
10
ios/Ratebubble/Resources/Ratebubble.entitlements
Normal file
10
ios/Ratebubble/Resources/Ratebubble.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(APP_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
36
ios/Ratebubble/Resources/RatebubbleShare-Info.plist
Normal file
36
ios/Ratebubble/Resources/RatebubbleShare-Info.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Ratebubble Share</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<string>TRUEPREDICATE</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
10
ios/Ratebubble/Resources/RatebubbleShare.entitlements
Normal file
10
ios/Ratebubble/Resources/RatebubbleShare.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(APP_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
107
ios/Ratebubble/ShareExtension/ShareViewController.swift
Normal file
107
ios/Ratebubble/ShareExtension/ShareViewController.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class ShareViewController: UIViewController {
|
||||
private let statusLabel = UILabel()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
Task { await handleIncomingShare() }
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .systemBackground
|
||||
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusLabel.textAlignment = .center
|
||||
statusLabel.numberOfLines = 0
|
||||
statusLabel.text = "Paylaşılan bağlantı alınıyor..."
|
||||
|
||||
view.addSubview(statusLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
|
||||
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
|
||||
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func updateStatus(_ text: String) {
|
||||
statusLabel.text = text
|
||||
}
|
||||
|
||||
private func handleIncomingShare() async {
|
||||
guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||
let providers = item.attachments else {
|
||||
updateStatus("Paylaşılan içerik okunamadı.")
|
||||
return
|
||||
}
|
||||
|
||||
for provider in providers {
|
||||
if let extracted = await extractURL(from: provider), isSupportedStreamingURL(extracted) {
|
||||
SharedPayloadStore.saveIncomingURL(extracted.absoluteString)
|
||||
updateStatus("Bağlantı alındı, uygulama açılıyor...")
|
||||
openHostApp()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus("Geçerli bir Netflix/Prime Video linki bulunamadı.")
|
||||
}
|
||||
|
||||
private func extractURL(from provider: NSItemProvider) async -> URL? {
|
||||
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
||||
return await withCheckedContinuation { continuation in
|
||||
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, _ in
|
||||
continuation.resume(returning: item as? URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
|
||||
return await withCheckedContinuation { continuation in
|
||||
provider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, _ in
|
||||
if let raw = item as? String, let url = URL(string: raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
|
||||
continuation.resume(returning: url)
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func isSupportedStreamingURL(_ url: URL) -> Bool {
|
||||
let host = url.host?.lowercased() ?? ""
|
||||
let netflixHosts = ["www.netflix.com", "netflix.com", "www.netflix.com.tr", "netflix.com.tr"]
|
||||
let primeHosts = ["www.primevideo.com", "primevideo.com", "www.amazon.com", "amazon.com"]
|
||||
|
||||
let isNetflix = netflixHosts.contains(host)
|
||||
let isPrime = primeHosts.contains(host)
|
||||
guard isNetflix || isPrime else { return false }
|
||||
|
||||
let path = url.path.lowercased()
|
||||
if path.contains("/title/") || path.contains("/watch/") || path.contains("/detail/") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Some share links can be shortened/redirect style without a canonical path.
|
||||
return !path.isEmpty && path != "/"
|
||||
}
|
||||
|
||||
private func openHostApp() {
|
||||
guard let url = URL(string: "\(SharedConfig.appURLScheme)://ingest") else {
|
||||
extensionContext?.completeRequest(returningItems: nil)
|
||||
return
|
||||
}
|
||||
|
||||
extensionContext?.open(url) { success in
|
||||
// If opening succeeded, the system should transition to the host app.
|
||||
// Completing the extension request immediately can bounce back to the source app.
|
||||
guard !success else { return }
|
||||
self.extensionContext?.completeRequest(returningItems: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
ios/Ratebubble/Shared/APIClient.swift
Normal file
65
ios/Ratebubble/Shared/APIClient.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
|
||||
enum APIClientError: LocalizedError {
|
||||
case invalidBaseURL
|
||||
case invalidResponse
|
||||
case server(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidBaseURL:
|
||||
return "API_BASE_URL geçersiz."
|
||||
case .invalidResponse:
|
||||
return "Sunucudan geçerli yanıt alınamadı."
|
||||
case .server(let message):
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class APIClient {
|
||||
static let shared = APIClient()
|
||||
|
||||
private init() {}
|
||||
|
||||
private var baseURL: URL? {
|
||||
guard let raw = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String else {
|
||||
return nil
|
||||
}
|
||||
return URL(string: raw)
|
||||
}
|
||||
|
||||
private var mobileAPIKey: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "MOBILE_API_KEY") as? String
|
||||
?? "mobile-dev-key-change-me"
|
||||
}
|
||||
|
||||
func getInfo(url: String) async throws -> GetInfoResponse {
|
||||
guard let baseURL else { throw APIClientError.invalidBaseURL }
|
||||
|
||||
var request = URLRequest(url: baseURL.appending(path: "/api/getinfo"))
|
||||
request.httpMethod = "POST"
|
||||
request.timeoutInterval = 20
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(mobileAPIKey, forHTTPHeaderField: "X-API-Key")
|
||||
request.httpBody = try JSONEncoder().encode(GetInfoRequest(url: url))
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw APIClientError.invalidResponse
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let envelope = try decoder.decode(APIEnvelope<GetInfoResponse>.self, from: data)
|
||||
|
||||
if (200..<300).contains(http.statusCode), envelope.success, let payload = envelope.data {
|
||||
return payload
|
||||
}
|
||||
|
||||
if let errorMessage = envelope.error?.message {
|
||||
throw APIClientError.server(errorMessage)
|
||||
}
|
||||
|
||||
throw APIClientError.server("İstek başarısız oldu (\(http.statusCode)).")
|
||||
}
|
||||
}
|
||||
29
ios/Ratebubble/Shared/Models.swift
Normal file
29
ios/Ratebubble/Shared/Models.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
struct GetInfoRequest: Encodable {
|
||||
let url: String
|
||||
}
|
||||
|
||||
struct APIErrorPayload: Decodable, Error {
|
||||
let code: String
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct APIEnvelope<T: Decodable>: Decodable {
|
||||
let success: Bool
|
||||
let data: T?
|
||||
let error: APIErrorPayload?
|
||||
}
|
||||
|
||||
struct GetInfoResponse: Decodable {
|
||||
let provider: String
|
||||
let title: String
|
||||
let year: Int?
|
||||
let plot: String?
|
||||
let ageRating: String?
|
||||
let type: String
|
||||
let genres: [String]
|
||||
let cast: [String]
|
||||
let backdrop: String?
|
||||
let currentSeason: Int?
|
||||
}
|
||||
31
ios/Ratebubble/Shared/SharedPayloadStore.swift
Normal file
31
ios/Ratebubble/Shared/SharedPayloadStore.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
enum SharedConfig {
|
||||
static var appGroupID: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_ID") as? String
|
||||
?? "group.net.wisecolt.ratebubble"
|
||||
}
|
||||
|
||||
static var appURLScheme: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "APP_URL_SCHEME") as? String
|
||||
?? "ratebubble"
|
||||
}
|
||||
}
|
||||
|
||||
enum SharedKeys {
|
||||
static let incomingURL = "incoming_shared_url"
|
||||
}
|
||||
|
||||
enum SharedPayloadStore {
|
||||
static func saveIncomingURL(_ url: String) {
|
||||
guard let defaults = UserDefaults(suiteName: SharedConfig.appGroupID) else { return }
|
||||
defaults.set(url, forKey: SharedKeys.incomingURL)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
static func consumeIncomingURL() -> String? {
|
||||
guard let defaults = UserDefaults(suiteName: SharedConfig.appGroupID) else { return nil }
|
||||
defer { defaults.removeObject(forKey: SharedKeys.incomingURL) }
|
||||
return defaults.string(forKey: SharedKeys.incomingURL)
|
||||
}
|
||||
}
|
||||
58
ios/project.yml
Normal file
58
ios/project.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Ratebubble
|
||||
options:
|
||||
deploymentTarget:
|
||||
iOS: 16.0
|
||||
configs:
|
||||
Debug: debug
|
||||
Release: release
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: net.wisecolt.ratebubble
|
||||
SWIFT_VERSION: 5.9
|
||||
targets:
|
||||
Ratebubble:
|
||||
type: application
|
||||
platform: iOS
|
||||
deploymentTarget: "16.0"
|
||||
sources:
|
||||
- path: Ratebubble/App
|
||||
- path: Ratebubble/Shared
|
||||
configFiles:
|
||||
Debug: Ratebubble/Resources/Config.xcconfig
|
||||
Release: Ratebubble/Resources/Config.xcconfig
|
||||
info:
|
||||
path: Ratebubble/Resources/Ratebubble-Info.plist
|
||||
properties:
|
||||
UILaunchScreen: {}
|
||||
CFBundleURLTypes:
|
||||
- CFBundleTypeRole: Editor
|
||||
CFBundleURLSchemes:
|
||||
- $(APP_URL_SCHEME)
|
||||
entitlements:
|
||||
path: Ratebubble/Resources/Ratebubble.entitlements
|
||||
properties:
|
||||
com.apple.security.application-groups:
|
||||
- $(APP_GROUP_ID)
|
||||
dependencies:
|
||||
- target: RatebubbleShare
|
||||
|
||||
RatebubbleShare:
|
||||
type: app-extension
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: net.wisecolt.ratebubble.share
|
||||
platform: iOS
|
||||
deploymentTarget: "16.0"
|
||||
sources:
|
||||
- path: Ratebubble/ShareExtension
|
||||
- path: Ratebubble/Shared
|
||||
configFiles:
|
||||
Debug: Ratebubble/Resources/Config.xcconfig
|
||||
Release: Ratebubble/Resources/Config.xcconfig
|
||||
info:
|
||||
path: Ratebubble/Resources/RatebubbleShare-Info.plist
|
||||
entitlements:
|
||||
path: Ratebubble/Resources/RatebubbleShare.entitlements
|
||||
properties:
|
||||
com.apple.security.application-groups:
|
||||
- $(APP_GROUP_ID)
|
||||
13
ios/scripts/bootstrap.sh
Executable file
13
ios/scripts/bootstrap.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v xcodegen >/dev/null 2>&1; then
|
||||
echo "xcodegen bulunamadı. Kurulum: brew install xcodegen"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcodegen generate
|
||||
|
||||
echo "Tamamlandı: ios/Ratebubble.xcodeproj oluşturuldu."
|
||||
Reference in New Issue
Block a user