Merge pull request #9 from tootsuite/feature/sign-up into /develop
Add sign up scene
This commit is contained in:
commit
71a485fc4a
|
@ -18,7 +18,7 @@ public final class CoreDataStack {
|
|||
}
|
||||
|
||||
public convenience init(databaseName: String = "shared") {
|
||||
let storeURL = URL.storeURL(for: "group.com.joinmastodon.mastodon-temp", databaseName: databaseName)
|
||||
let storeURL = URL.storeURL(for: "group.org.joinmastodon.mastodon-temp", databaseName: databaseName)
|
||||
let storeDescription = NSPersistentStoreDescription(url: storeURL)
|
||||
self.init(persistentStoreDescriptions: [storeDescription])
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@
|
|||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||
DB0AC70A25CD2E0300D75117 /* HomeViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */; };
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||
|
@ -71,6 +73,10 @@
|
|||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
|
||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
|
||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
||||
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -101,6 +107,9 @@
|
|||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
||||
DBD4ED1125CC0FEB0041B741 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */; };
|
||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -205,6 +214,8 @@
|
|||
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = "<group>"; };
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -228,6 +239,9 @@
|
|||
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = "<group>"; };
|
||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
||||
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -260,6 +274,9 @@
|
|||
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
||||
DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
|
||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -274,6 +291,7 @@
|
|||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */,
|
||||
|
@ -347,6 +365,7 @@
|
|||
2D42FF8325C82245004A627A /* Button */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */,
|
||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
|
||||
);
|
||||
path = Button;
|
||||
|
@ -372,6 +391,7 @@
|
|||
2D69CFF225CA9E2200C3A1B2 /* Protocol */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
|
||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
||||
);
|
||||
|
@ -460,6 +480,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0140A625C40C0900F9F3CF /* PinBased */,
|
||||
DBE0821A25CD382900FD6BBD /* Register */,
|
||||
DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */,
|
||||
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */,
|
||||
);
|
||||
|
@ -540,13 +561,14 @@
|
|||
children = (
|
||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||
2D69CFF225CA9E2200C3A1B2 /* Protocol */,
|
||||
2D76319C25C151DE00929FB9 /* Diffiable */,
|
||||
DB8AF52A25C13561002E6C99 /* State */,
|
||||
2D61335525C1886800CAE157 /* Service */,
|
||||
DB8AF55525C1379F002E6C99 /* Scene */,
|
||||
DB8AF54125C13647002E6C99 /* Coordinator */,
|
||||
DB8AF56225C138BC002E6C99 /* Extension */,
|
||||
DB5086CB25CC0DB400C2C187 /* Preference */,
|
||||
2D69CFF225CA9E2200C3A1B2 /* Protocol */,
|
||||
DB98338425C945ED00AD9700 /* Generated */,
|
||||
DB3D0FF825BAA6B200EAA174 /* Resources */,
|
||||
DB3D0FF725BAA68500EAA174 /* Supporting Files */,
|
||||
|
@ -584,6 +606,7 @@
|
|||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||
);
|
||||
path = APIService;
|
||||
sourceTree = "<group>";
|
||||
|
@ -598,6 +621,14 @@
|
|||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB5086CB25CC0DB400C2C187 /* Preference */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */,
|
||||
);
|
||||
path = Preference;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -688,10 +719,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7631A425C1532200929FB9 /* Share */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
DB01409B25C40BB600F9F3CF /* Authentication */,
|
||||
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */,
|
||||
DBD4ED0B25CC0FD40041B741 /* HomeTimeline */,
|
||||
);
|
||||
path = Scene;
|
||||
sourceTree = "<group>";
|
||||
|
@ -732,6 +763,25 @@
|
|||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBD4ED0B25CC0FD40041B741 /* HomeTimeline */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */,
|
||||
DB0AC70925CD2E0300D75117 /* HomeViewController+DebugAction.swift */,
|
||||
DBD4ED1025CC0FEB0041B741 /* HomeViewModel.swift */,
|
||||
);
|
||||
path = HomeTimeline;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBE0821A25CD382900FD6BBD /* Register */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */,
|
||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */,
|
||||
);
|
||||
path = Register;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -751,11 +801,11 @@
|
|||
buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */;
|
||||
buildPhases = (
|
||||
7A04933A2AB1D5B758D4F908 /* [CP] Check Pods Manifest.lock */,
|
||||
DB3D100425BAA71500EAA174 /* ShellScript */,
|
||||
DB427DCE25BAA00100D1B89D /* Sources */,
|
||||
DB427DCF25BAA00100D1B89D /* Frameworks */,
|
||||
DB427DD025BAA00100D1B89D /* Resources */,
|
||||
5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */,
|
||||
DB3D100425BAA71500EAA174 /* ShellScript */,
|
||||
DB89BA0825C10FD0008580ED /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
|
@ -770,6 +820,7 @@
|
|||
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
|
||||
2D42FF6025C8177C004A627A /* ActiveLabel */,
|
||||
DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
|
||||
DB5086B725CC0D6400C2C187 /* Kingfisher */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -896,6 +947,7 @@
|
|||
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
|
||||
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
|
||||
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
|
||||
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1088,17 +1140,22 @@
|
|||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||
DB0AC70A25CD2E0300D75117 /* HomeViewController+DebugAction.swift in Sources */,
|
||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
|
@ -1110,10 +1167,12 @@
|
|||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
||||
DBD4ED1125CC0FEB0041B741 /* HomeViewModel.swift in Sources */,
|
||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||
|
@ -1131,6 +1190,7 @@
|
|||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||
|
@ -1377,6 +1437,7 @@
|
|||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -1401,6 +1462,7 @@
|
|||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
@ -1677,6 +1739,14 @@
|
|||
minimumVersion = 4.1.0;
|
||||
};
|
||||
};
|
||||
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 6.1.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -1704,6 +1774,11 @@
|
|||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||
productName = AlamofireImage;
|
||||
};
|
||||
DB5086B725CC0D6400C2C187 /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
<integer>8</integer>
|
||||
</dict>
|
||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -46,6 +46,15 @@
|
|||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "daebf8ddf974164d1b9a050c8231e263f3106b09",
|
||||
"version": "6.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
|
|
|
@ -39,6 +39,7 @@ extension SceneCoordinator {
|
|||
enum Scene {
|
||||
case authentication(viewModel: AuthenticationViewModel)
|
||||
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
|
||||
case mastodonRegister(viewModel: MastodonRegisterViewModel)
|
||||
|
||||
case alertController(alertController: UIAlertController)
|
||||
}
|
||||
|
@ -120,6 +121,10 @@ private extension SceneCoordinator {
|
|||
let _viewController = MastodonPinBasedAuthenticationViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonRegister(let viewModel):
|
||||
let _viewController = MastodonRegisterViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .alertController(let alertController):
|
||||
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||
assert(
|
||||
|
|
|
@ -24,3 +24,9 @@ extension MastodonUser.Property {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
public func avatarImageURL() -> URL? {
|
||||
return URL(string: avatar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,19 @@ internal enum Asset {
|
|||
internal static let arrowTriangle2Circlepath = ImageAsset(name: "Arrows/arrow.triangle.2.circlepath")
|
||||
}
|
||||
internal enum Colors {
|
||||
internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark")
|
||||
internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray")
|
||||
internal static let tootWhite = ColorAsset(name: "Colors/Toot.White")
|
||||
internal static let likeOrange = ColorAsset(name: "Colors/like.orange")
|
||||
internal enum Background {
|
||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
||||
}
|
||||
internal enum Button {
|
||||
internal static let highlight = ColorAsset(name: "Colors/Button/highlight")
|
||||
}
|
||||
internal enum Label {
|
||||
internal static let primary = ColorAsset(name: "Colors/Label/primary")
|
||||
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
|
||||
}
|
||||
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||
}
|
||||
internal enum ToolBar {
|
||||
internal static let bookmark = ImageAsset(name: "ToolBar/bookmark")
|
||||
|
|
|
@ -2,6 +2,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>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
@ -42,7 +44,7 @@
|
|||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<string>Main</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// SplashPreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2020-2-4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UserDefaults {
|
||||
// TODO: splash scene
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// AvatarConfigurableView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-2-4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AlamofireImage
|
||||
import Kingfisher
|
||||
|
||||
protocol AvatarConfigurableView {
|
||||
static var configurableAvatarImageViewSize: CGSize { get }
|
||||
static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { get }
|
||||
var configurableAvatarImageView: UIImageView? { get }
|
||||
var configurableAvatarButton: UIButton? { get }
|
||||
var configurableVerifiedBadgeImageView: UIImageView? { get }
|
||||
func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input)
|
||||
func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration)
|
||||
}
|
||||
|
||||
extension AvatarConfigurableView {
|
||||
|
||||
static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { return .mini }
|
||||
|
||||
public func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input) {
|
||||
// TODO: set badge
|
||||
configurableVerifiedBadgeImageView?.isHidden = true
|
||||
|
||||
let cornerRadius = Self.configurableAvatarImageViewSize.width * 0.5
|
||||
// let scale = (configurableAvatarImageView ?? configurableAvatarButton)?.window?.screen.scale ?? UIScreen.main.scale
|
||||
|
||||
let placeholderImage: UIImage = {
|
||||
let placeholderImage = input.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageViewSize, color: .systemFill)
|
||||
return placeholderImage.af.imageRoundedIntoCircle()
|
||||
}()
|
||||
|
||||
// cancel previous task
|
||||
configurableAvatarImageView?.af.cancelImageRequest()
|
||||
configurableAvatarImageView?.kf.cancelDownloadTask()
|
||||
configurableAvatarButton?.af.cancelImageRequest(for: .normal)
|
||||
configurableAvatarButton?.kf.cancelImageDownloadTask()
|
||||
|
||||
// reset layer attributes
|
||||
configurableAvatarImageView?.layer.masksToBounds = false
|
||||
configurableAvatarImageView?.layer.cornerRadius = 0
|
||||
configurableAvatarImageView?.layer.cornerCurve = .circular
|
||||
|
||||
configurableAvatarButton?.layer.masksToBounds = false
|
||||
configurableAvatarButton?.layer.cornerRadius = 0
|
||||
configurableAvatarButton?.layer.cornerCurve = .circular
|
||||
|
||||
defer {
|
||||
let configuration = AvatarConfigurableViewConfiguration(input: input)
|
||||
avatarConfigurableView(self, didFinishConfiguration: configuration)
|
||||
}
|
||||
|
||||
// set placeholder if no asset
|
||||
guard let avatarImageURL = input.avatarImageURL else {
|
||||
configurableAvatarImageView?.image = placeholderImage
|
||||
configurableAvatarButton?.setImage(placeholderImage, for: .normal)
|
||||
return
|
||||
}
|
||||
|
||||
if let avatarImageView = configurableAvatarImageView {
|
||||
// set avatar (GIF using Kingfisher)
|
||||
switch avatarImageURL.pathExtension {
|
||||
case "gif":
|
||||
avatarImageView.kf.setImage(
|
||||
with: avatarImageURL,
|
||||
placeholder: placeholderImage,
|
||||
options: [
|
||||
.transition(.fade(0.2))
|
||||
]
|
||||
)
|
||||
avatarImageView.layer.masksToBounds = true
|
||||
avatarImageView.layer.cornerRadius = cornerRadius
|
||||
avatarImageView.layer.cornerCurve = .circular
|
||||
default:
|
||||
let filter = ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize)
|
||||
avatarImageView.af.setImage(
|
||||
withURL: avatarImageURL,
|
||||
placeholderImage: placeholderImage,
|
||||
filter: filter,
|
||||
imageTransition: .crossDissolve(0.3),
|
||||
runImageTransitionIfCached: false,
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if let avatarButton = configurableAvatarButton {
|
||||
switch avatarImageURL.pathExtension {
|
||||
case "gif":
|
||||
avatarButton.kf.setImage(
|
||||
with: avatarImageURL,
|
||||
for: .normal,
|
||||
placeholder: placeholderImage,
|
||||
options: [
|
||||
.transition(.fade(0.2))
|
||||
]
|
||||
)
|
||||
avatarButton.layer.masksToBounds = true
|
||||
avatarButton.layer.cornerRadius = cornerRadius
|
||||
avatarButton.layer.cornerCurve = .circular
|
||||
default:
|
||||
let filter = ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize)
|
||||
avatarButton.af.setImage(
|
||||
for: .normal,
|
||||
url: avatarImageURL,
|
||||
placeholderImage: placeholderImage,
|
||||
filter: filter,
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) { }
|
||||
|
||||
}
|
||||
|
||||
struct AvatarConfigurableViewConfiguration {
|
||||
|
||||
enum BadgeAppearanceStyle {
|
||||
case mini
|
||||
case normal
|
||||
}
|
||||
|
||||
struct Input {
|
||||
let avatarImageURL: URL?
|
||||
let placeholderImage: UIImage?
|
||||
let blocked: Bool
|
||||
let verified: Bool
|
||||
|
||||
init(avatarImageURL: URL?, placeholderImage: UIImage? = nil, blocked: Bool = false, verified: Bool = false) {
|
||||
self.avatarImageURL = avatarImageURL
|
||||
self.placeholderImage = placeholderImage
|
||||
self.blocked = blocked
|
||||
self.verified = verified
|
||||
}
|
||||
}
|
||||
|
||||
let input: Input
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x37",
|
||||
"green" : "0x2D",
|
||||
"red" : "0x29"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x24",
|
||||
"red" : "0x20"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x43",
|
||||
"green" : "0x36",
|
||||
"red" : "0x32"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.839",
|
||||
"green" : "0.573",
|
||||
"red" : "0.204"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x84",
|
||||
"green" : "0x69",
|
||||
"red" : "0x60"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "extended-srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "55",
|
||||
"green" : "45",
|
||||
"red" : "41"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "55",
|
||||
"green" : "45",
|
||||
"red" : "41"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "132",
|
||||
"green" : "105",
|
||||
"red" : "96"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "132",
|
||||
"green" : "105",
|
||||
"red" : "96"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "10",
|
||||
"green" : "159",
|
||||
"red" : "255"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "10",
|
||||
"green" : "159",
|
||||
"red" : "255"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x0A",
|
||||
"green" : "0x9F",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import UITextField_Shake
|
||||
|
||||
final class AuthenticationViewController: UIViewController, NeedsDependency {
|
||||
|
||||
|
@ -19,6 +20,14 @@ final class AuthenticationViewController: UIViewController, NeedsDependency {
|
|||
|
||||
var viewModel: AuthenticationViewModel!
|
||||
|
||||
let domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "Domain:"
|
||||
return label
|
||||
}()
|
||||
|
||||
let domainTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.placeholder = "example.com"
|
||||
|
@ -28,8 +37,39 @@ final class AuthenticationViewController: UIViewController, NeedsDependency {
|
|||
return textField
|
||||
}()
|
||||
|
||||
private(set) lazy var signInBarButtonItem = UIBarButtonItem(title: "Sign In", style: .plain, target: self, action: #selector(AuthenticationViewController.signInBarButtonItemPressed(_:)))
|
||||
let activityIndicatorBarButtonItem = UIBarButtonItem.activityIndicatorBarButtonItem
|
||||
let signInButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal)
|
||||
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled)
|
||||
button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
|
||||
button.setTitle("Sign in", for: .normal)
|
||||
button.layer.masksToBounds = true
|
||||
button.layer.cornerRadius = 8
|
||||
button.layer.cornerCurve = .continuous
|
||||
return button
|
||||
}()
|
||||
|
||||
let signUpButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .subheadline)
|
||||
button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal)
|
||||
button.setTitleColor(.systemGray, for: .disabled)
|
||||
button.setTitle("Sign up", for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let signInActivityIndicatorView: UIActivityIndicatorView = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
return activityIndicatorView
|
||||
}()
|
||||
|
||||
let signUpActivityIndicatorView: UIActivityIndicatorView = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
return activityIndicatorView
|
||||
}()
|
||||
}
|
||||
|
||||
extension AuthenticationViewController {
|
||||
|
@ -38,16 +78,54 @@ extension AuthenticationViewController {
|
|||
super.viewDidLoad()
|
||||
|
||||
title = "Authentication"
|
||||
view.backgroundColor = .systemBackground
|
||||
navigationItem.rightBarButtonItem = signInBarButtonItem
|
||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
|
||||
domainLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(domainLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
domainLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16),
|
||||
domainLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
domainLabel.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
])
|
||||
|
||||
domainTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(domainTextField)
|
||||
NSLayoutConstraint.activate([
|
||||
domainTextField.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 8),
|
||||
domainTextField.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 8),
|
||||
domainTextField.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: 8),
|
||||
domainTextField.heightAnchor.constraint(equalToConstant: 44), // FIXME:
|
||||
domainTextField.topAnchor.constraint(equalTo: domainLabel.bottomAnchor, constant: 8),
|
||||
domainTextField.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
domainTextField.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
])
|
||||
|
||||
signInButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(signInButton)
|
||||
NSLayoutConstraint.activate([
|
||||
signInButton.topAnchor.constraint(equalTo: domainTextField.bottomAnchor, constant: 20),
|
||||
signInButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
signInButton.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
signInButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
signInActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(signInActivityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
signInActivityIndicatorView.centerXAnchor.constraint(equalTo: signInButton.centerXAnchor),
|
||||
signInActivityIndicatorView.centerYAnchor.constraint(equalTo: signInButton.centerYAnchor),
|
||||
])
|
||||
|
||||
signUpButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(signUpButton)
|
||||
NSLayoutConstraint.activate([
|
||||
signUpButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 8),
|
||||
signUpButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
signUpButton.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(signUpActivityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor),
|
||||
signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor),
|
||||
])
|
||||
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: domainTextField)
|
||||
|
@ -62,10 +140,30 @@ extension AuthenticationViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isAuthenticating in
|
||||
guard let self = self else { return }
|
||||
self.navigationItem.rightBarButtonItem = isAuthenticating ? self.activityIndicatorBarButtonItem : self.signInBarButtonItem
|
||||
isAuthenticating ? self.signInActivityIndicatorView.startAnimating() : self.signInActivityIndicatorView.stopAnimating()
|
||||
self.signInButton.setTitle(isAuthenticating ? "" : "Sign in", for: .normal)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.isRegistering
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isRegistering in
|
||||
guard let self = self else { return }
|
||||
isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating()
|
||||
self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.isIdle
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isIdle in
|
||||
guard let self = self else { return }
|
||||
self.signInButton.isEnabled = isIdle
|
||||
self.signUpButton.isEnabled = isIdle
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
viewModel.authenticated
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] domain, user in
|
||||
|
@ -91,11 +189,6 @@ extension AuthenticationViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.isSignInButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: signInBarButtonItem)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.error
|
||||
.compactMap { $0 }
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -111,6 +204,10 @@ extension AuthenticationViewController {
|
|||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
signInButton.addTarget(self, action: #selector(AuthenticationViewController.signInButtonPressed(_:)), for: .touchUpInside)
|
||||
signUpButton.addTarget(self, action: #selector(AuthenticationViewController.signUpButtonPressed(_:)), for: .touchUpInside)
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -123,29 +220,21 @@ extension AuthenticationViewController {
|
|||
|
||||
extension AuthenticationViewController {
|
||||
|
||||
@objc private func signInBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
@objc private func signInButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let domain = viewModel.domain.value else {
|
||||
// TODO: alert error
|
||||
guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else {
|
||||
domainTextField.shake()
|
||||
return
|
||||
}
|
||||
guard !viewModel.isAuthenticating.value else { return }
|
||||
guard viewModel.isIdle.value else { return }
|
||||
viewModel.isAuthenticating.value = true
|
||||
context.apiService.createApplication(domain: domain)
|
||||
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||
let application = response.value
|
||||
guard let clientID = application.clientID,
|
||||
let clientSecret = application.clientSecret else {
|
||||
guard let info = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
|
||||
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
|
||||
return AuthenticationViewModel.AuthenticateInfo(
|
||||
domain: domain,
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
url: url
|
||||
)
|
||||
return info
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
|
@ -162,7 +251,7 @@ extension AuthenticationViewController {
|
|||
}
|
||||
} receiveValue: { [weak self] info in
|
||||
guard let self = self else { return }
|
||||
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.url)
|
||||
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL)
|
||||
self.viewModel.authenticate(
|
||||
info: info,
|
||||
pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher
|
||||
|
@ -176,6 +265,54 @@ extension AuthenticationViewController {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
@objc private func signUpButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else {
|
||||
domainTextField.shake()
|
||||
return
|
||||
}
|
||||
guard viewModel.isIdle.value else { return }
|
||||
viewModel.isRegistering.value = true
|
||||
context.apiService.instance(domain: domain)
|
||||
.compactMap { [weak self] response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
guard response.value.registrations != false else {
|
||||
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
|
||||
}
|
||||
return self.context.apiService.createApplication(domain: domain)
|
||||
}
|
||||
.switchToLatest()
|
||||
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||
let application = response.value
|
||||
guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
return authenticateInfo
|
||||
}
|
||||
.compactMap { [weak self] authenticateInfo -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
return self.context.apiService.applicationAccessToken(domain: domain, clientID: authenticateInfo.clientID, clientSecret: authenticateInfo.clientSecret)
|
||||
}
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.isRegistering.value = false
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
self.viewModel.error.send(error)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let mastodonRegisterViewModel = MastodonRegisterViewModel(domain: domain, applicationToken: response.value)
|
||||
self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
|
|
|
@ -25,8 +25,10 @@ final class AuthenticationViewModel {
|
|||
// output
|
||||
let viewHierarchyShouldReset: Bool
|
||||
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||
let isSignInButtonEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||
let isDomainValid = CurrentValueSubject<Bool, Never>(false)
|
||||
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
||||
let isRegistering = CurrentValueSubject<Bool, Never>(false)
|
||||
let isIdle = CurrentValueSubject<Bool, Never>(true)
|
||||
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
|
||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||
|
||||
|
@ -59,21 +61,70 @@ final class AuthenticationViewModel {
|
|||
.assign(to: \.value, on: domain)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isAuthenticating.eraseToAnyPublisher(),
|
||||
isRegistering.eraseToAnyPublisher()
|
||||
)
|
||||
.map { !$0 && !$1 }
|
||||
.assign(to: \.value, on: self.isIdle)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
domain
|
||||
.map { $0 != nil }
|
||||
.assign(to: \.value, on: isSignInButtonEnabled)
|
||||
.assign(to: \.value, on: isDomainValid)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AuthenticationViewModel {
|
||||
enum AuthenticationError: Error, LocalizedError {
|
||||
case badCredentials
|
||||
case registrationClosed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .badCredentials: return "Bad Credentials"
|
||||
case .registrationClosed: return "Registration Closed"
|
||||
}
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
case .badCredentials: return "Credentials invalid."
|
||||
case .registrationClosed: return "Server disallow registration."
|
||||
}
|
||||
}
|
||||
|
||||
var helpAnchor: String? {
|
||||
switch self {
|
||||
case .badCredentials: return "Please try again."
|
||||
case .registrationClosed: return "Please try another domain."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationViewModel {
|
||||
|
||||
struct AuthenticateInfo {
|
||||
let domain: String
|
||||
let clientID: String
|
||||
let clientSecret: String
|
||||
let url: URL
|
||||
let authorizeURL: URL
|
||||
|
||||
init?(domain: String, application: Mastodon.Entity.Application) {
|
||||
self.domain = domain
|
||||
guard let clientID = application.clientID,
|
||||
let clientSecret = application.clientSecret else { return nil }
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
self.authorizeURL = {
|
||||
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
|
||||
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
|
||||
return url
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject<String, Never>) {
|
||||
|
@ -144,7 +195,7 @@ extension AuthenticationViewModel {
|
|||
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
|
||||
mastodonUserRequest.fetchLimit = 1
|
||||
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
|
||||
return Fail(error: APIService.APIError.explicit(.badCredentials)).eraseToAnyPublisher()
|
||||
return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let property = MastodonAuthentication.Property(
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
//
|
||||
// MastodonRegisterViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-5.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import UITextField_Shake
|
||||
|
||||
final class MastodonRegisterViewController: UIViewController, NeedsDependency {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: MastodonRegisterViewModel!
|
||||
|
||||
let usernameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "Username:"
|
||||
return label
|
||||
}()
|
||||
|
||||
let usernameTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.placeholder = "Username"
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
return textField
|
||||
}()
|
||||
|
||||
let emailLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "Email:"
|
||||
return label
|
||||
}()
|
||||
|
||||
let emailTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.placeholder = "example@gmail.com"
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.keyboardType = .emailAddress
|
||||
return textField
|
||||
}()
|
||||
|
||||
let passwordLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "Password:"
|
||||
return label
|
||||
}()
|
||||
|
||||
let passwordTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.placeholder = "Password"
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.keyboardType = .asciiCapable
|
||||
textField.isSecureTextEntry = true
|
||||
return textField
|
||||
}()
|
||||
|
||||
let signUpButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal)
|
||||
button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled)
|
||||
button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
|
||||
button.setTitle("Sign up", for: .normal)
|
||||
button.layer.masksToBounds = true
|
||||
button.layer.cornerRadius = 8
|
||||
button.layer.cornerCurve = .continuous
|
||||
return button
|
||||
}()
|
||||
|
||||
let signUpActivityIndicatorView: UIActivityIndicatorView = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
return activityIndicatorView
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Sign Up"
|
||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
|
||||
let stackView = UIStackView()
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(stackView)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16),
|
||||
stackView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
])
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
|
||||
stackView.addArrangedSubview(usernameLabel)
|
||||
stackView.addArrangedSubview(usernameTextField)
|
||||
stackView.addArrangedSubview(emailLabel)
|
||||
stackView.addArrangedSubview(emailTextField)
|
||||
stackView.addArrangedSubview(passwordLabel)
|
||||
stackView.addArrangedSubview(passwordTextField)
|
||||
|
||||
signUpButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addArrangedSubview(signUpButton)
|
||||
NSLayoutConstraint.activate([
|
||||
signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(signUpActivityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor),
|
||||
signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor),
|
||||
])
|
||||
|
||||
viewModel.isRegistering
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isRegistering in
|
||||
guard let self = self else { return }
|
||||
isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating()
|
||||
self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal)
|
||||
self.signUpButton.isEnabled = !isRegistering
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.error
|
||||
.compactMap { $0 }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
let alertController = UIAlertController(error, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
self.coordinator.present(
|
||||
scene: .alertController(alertController: alertController),
|
||||
from: nil,
|
||||
transition: .alertController(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewController {
|
||||
|
||||
@objc private func signUpButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let username = usernameTextField.text else {
|
||||
usernameTextField.shake()
|
||||
return
|
||||
}
|
||||
|
||||
guard let email = emailTextField.text else {
|
||||
emailTextField.shake()
|
||||
return
|
||||
}
|
||||
|
||||
guard let password = passwordTextField.text else {
|
||||
passwordTextField.shake()
|
||||
return
|
||||
}
|
||||
|
||||
guard !viewModel.isRegistering.value else { return }
|
||||
viewModel.isRegistering.value = true
|
||||
|
||||
let query = Mastodon.API.Account.RegisterQuery(
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
agreement: true, // TODO:
|
||||
locale: "en" // TODO:
|
||||
)
|
||||
|
||||
context.apiService.accountRegister(
|
||||
domain: viewModel.domain,
|
||||
query: query,
|
||||
authorization: viewModel.applicationAuthorization
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.isRegistering.value = false
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
self.viewModel.error.send(error)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let _ = response.value
|
||||
// TODO:
|
||||
let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
alertController.addAction(okAction)
|
||||
self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// MastodonRegisterViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
final class MastodonRegisterViewModel {
|
||||
|
||||
// input
|
||||
let domain: String
|
||||
let applicationToken: Mastodon.Entity.Token
|
||||
let isRegistering = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
// output
|
||||
let applicationAuthorization: Mastodon.API.OAuth.Authorization
|
||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||
|
||||
init(domain: String, applicationToken: Mastodon.Entity.Token) {
|
||||
self.domain = domain
|
||||
self.applicationToken = applicationToken
|
||||
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// HomeViewController+DebugAction.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-5.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
#if DEBUG
|
||||
extension HomeViewController {
|
||||
var debugMenu: UIMenu {
|
||||
let menu = UIMenu(
|
||||
title: "Debug Tools",
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
options: .displayInline,
|
||||
children: [
|
||||
UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.signOutAction(action)
|
||||
}
|
||||
]
|
||||
)
|
||||
return menu
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeViewController {
|
||||
|
||||
@objc private func signOutAction(_ sender: UIAction) {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
let currentAccountCount = context.authenticationService.mastodonAuthentications.value.count
|
||||
let isAuthenticationExistWhenSignOut = currentAccountCount - 1 > 0
|
||||
// prepare advance
|
||||
let authenticationViewModel = AuthenticationViewModel(context: context, coordinator: coordinator, isAuthenticationExist: isAuthenticationExistWhenSignOut)
|
||||
|
||||
context.authenticationService.signOutMastodonUser(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isSignOut):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
|
||||
guard isSignOut else { return }
|
||||
if !isAuthenticationExistWhenSignOut {
|
||||
self.coordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// HomeViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class HomeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: HomeViewModel!
|
||||
|
||||
let avatarBarButtonItem = AvatarBarButtonItem()
|
||||
|
||||
}
|
||||
|
||||
extension HomeViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Home"
|
||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
navigationItem.leftBarButtonItem = avatarBarButtonItem
|
||||
avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(HomeViewController.avatarBarButtonItemDidPressed(_:)), for: .touchUpInside)
|
||||
#if DEBUG
|
||||
avatarBarButtonItem.avatarButton.menu = debugMenu
|
||||
avatarBarButtonItem.avatarButton.showsMenuAsPrimaryAction = true
|
||||
#endif
|
||||
|
||||
Publishers.CombineLatest(
|
||||
context.authenticationService.activeMastodonAuthentication.eraseToAnyPublisher(),
|
||||
viewModel.viewDidAppear.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] activeMastodonAuthentication, _ in
|
||||
guard let self = self else { return }
|
||||
guard let user = activeMastodonAuthentication?.user,
|
||||
let avatarImageURL = user.avatarImageURL() else {
|
||||
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: nil)
|
||||
self.avatarBarButtonItem.configure(withConfigurationInput: input)
|
||||
return
|
||||
}
|
||||
let input = AvatarConfigurableViewConfiguration.Input(avatarImageURL: avatarImageURL)
|
||||
self.avatarBarButtonItem.configure(withConfigurationInput: input)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.viewDidAppear.send()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HomeViewController {
|
||||
@objc private func avatarBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// HomeViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class HomeViewModel {
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// HomeViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class HomeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
}
|
||||
|
||||
extension HomeViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Home"
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ class MainTabBarController: UITabBarController {
|
|||
switch self {
|
||||
case .home:
|
||||
let _viewController = HomeViewController()
|
||||
_viewController.viewModel = HomeViewModel(context: context)
|
||||
_viewController.context = context
|
||||
_viewController.coordinator = coordinator
|
||||
viewController = _viewController
|
||||
|
|
|
@ -29,6 +29,7 @@ final class PublicTimelineViewController: UIViewController, NeedsDependency, Tim
|
|||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
@ -41,6 +42,8 @@ extension PublicTimelineViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
|
||||
tableView.refreshControl = refreshControl
|
||||
refreshControl.addTarget(self, action: #selector(PublicTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||
// bind refresh control
|
||||
|
@ -58,10 +61,7 @@ extension PublicTimelineViewController {
|
|||
.store(in: &disposeBag)
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.backgroundColor = Asset.Colors.tootDark.color
|
||||
view.addSubview(tableView)
|
||||
view.backgroundColor = Asset.Colors.tootDark.color
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// AvatarBarButtonItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-2-4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class AvatarBarButtonItem: UIBarButtonItem {
|
||||
|
||||
static let avatarButtonSize = CGSize(width: 32, height: 32)
|
||||
|
||||
let avatarButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
button.widthAnchor.constraint(equalToConstant: avatarButtonSize.width).priority(.defaultHigh),
|
||||
button.heightAnchor.constraint(equalToConstant: avatarButtonSize.height).priority(.defaultHigh),
|
||||
])
|
||||
return button
|
||||
}()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AvatarBarButtonItem {
|
||||
|
||||
private func _init() {
|
||||
customView = avatarButton
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AvatarBarButtonItem: AvatarConfigurableView {
|
||||
static var configurableAvatarImageViewSize: CGSize { return avatarButtonSize }
|
||||
var configurableAvatarImageView: UIImageView? { return nil }
|
||||
var configurableAvatarButton: UIButton? { return avatarButton }
|
||||
var configurableVerifiedBadgeImageView: UIImageView? { return nil }
|
||||
}
|
|
@ -24,13 +24,13 @@ final class TimelinePostView: UIView {
|
|||
|
||||
let visibilityImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.TootTimeline.global.image.withRenderingMode(.alwaysTemplate))
|
||||
imageView.tintColor = Asset.Colors.tootGray.color
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let lockImageView: UIImageView = {
|
||||
let imageview = UIImageView(image: Asset.TootTimeline.textlock.image.withRenderingMode(.alwaysTemplate))
|
||||
imageview.tintColor = Asset.Colors.tootGray.color
|
||||
imageview.tintColor = Asset.Colors.Label.secondary.color
|
||||
imageview.isHidden = true
|
||||
return imageview
|
||||
}()
|
||||
|
@ -38,7 +38,7 @@ final class TimelinePostView: UIView {
|
|||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont(name: "Roboto-Medium", size: 14)
|
||||
label.textColor = Asset.Colors.tootWhite.color
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
|
||||
label.text = "Alice"
|
||||
return label
|
||||
|
@ -46,7 +46,7 @@ final class TimelinePostView: UIView {
|
|||
|
||||
let usernameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.tootGray.color
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = UIFont(name: "Roboto-Regular", size: 14)
|
||||
label.text = "@alice"
|
||||
return label
|
||||
|
@ -56,7 +56,7 @@ final class TimelinePostView: UIView {
|
|||
let label = UILabel()
|
||||
label.font = UIFont(name: "Roboto-Regular", size: 14)
|
||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .left : .right
|
||||
label.textColor = Asset.Colors.tootGray.color
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "1d"
|
||||
return label
|
||||
}()
|
||||
|
|
|
@ -11,7 +11,8 @@ import Combine
|
|||
final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell {
|
||||
override func _init() {
|
||||
super._init()
|
||||
|
||||
backgroundColor = .clear
|
||||
|
||||
activityIndicatorView.isHidden = false
|
||||
activityIndicatorView.startAnimating()
|
||||
}
|
||||
|
|
|
@ -45,7 +45,8 @@ class TimelineLoaderTableViewCell: UITableViewCell {
|
|||
|
||||
func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Colors.tootDark.color
|
||||
backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
|
||||
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(loadMoreButton)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -21,7 +21,7 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell {
|
|||
override func _init() {
|
||||
super._init()
|
||||
|
||||
backgroundColor = Asset.Colors.tootDark.color
|
||||
backgroundColor = .clear
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -50,7 +50,7 @@ final class TimelinePostTableViewCell: UITableViewCell {
|
|||
extension TimelinePostTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
self.backgroundColor = Asset.Colors.tootDark.color
|
||||
self.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
self.selectionStyle = .none
|
||||
timelinePostView.translatesAutoresizingMaskIntoConstraints = false
|
||||
timelinePostViewTopLayoutConstraint = timelinePostView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TimelinePostTableViewCell.verticalMargin)
|
||||
|
|
|
@ -95,7 +95,7 @@ extension ActionToolbarContainer {
|
|||
|
||||
let buttons = [replyButton, retootButton, starButton,bookmartButton, moreButton]
|
||||
buttons.forEach { button in
|
||||
button.tintColor = Asset.Colors.tootGray.color
|
||||
button.tintColor = Asset.Colors.Label.secondary.color
|
||||
button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
||||
button.setTitle("", for: .normal)
|
||||
button.setTitleColor(.secondaryLabel, for: .normal)
|
||||
|
@ -165,7 +165,7 @@ extension ActionToolbarContainer {
|
|||
}
|
||||
|
||||
private func isstarButtonHighlightStateDidChange(to isHighlight: Bool) {
|
||||
let tintColor = isHighlight ? Asset.Colors.likeOrange.color : Asset.Colors.tootGray.color
|
||||
let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Label.secondary.color
|
||||
starButton.tintColor = tintColor
|
||||
starButton.setTitleColor(tintColor, for: .normal)
|
||||
starButton.setTitleColor(tintColor, for: .highlighted)
|
||||
|
|
|
@ -17,7 +17,6 @@ extension APIService {
|
|||
enum ErrorReason {
|
||||
// application internal error
|
||||
case authenticationMissing
|
||||
case badCredentials
|
||||
case badRequest
|
||||
case badResponse
|
||||
case requestThrottle
|
||||
|
@ -42,7 +41,6 @@ extension APIService.APIError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch errorReason {
|
||||
case .authenticationMissing: return "Fail to Authenticatie"
|
||||
case .badCredentials: return "Bad Credentials"
|
||||
case .badRequest: return "Bad Request"
|
||||
case .badResponse: return "Bad Response"
|
||||
case .requestThrottle: return "Request Throttled"
|
||||
|
@ -61,7 +59,6 @@ extension APIService.APIError: LocalizedError {
|
|||
var failureReason: String? {
|
||||
switch errorReason {
|
||||
case .authenticationMissing: return "Account credential not found."
|
||||
case .badCredentials: return "Credentials invalid."
|
||||
case .badRequest: return "Request invalid."
|
||||
case .badResponse: return "Response invalid."
|
||||
case .requestThrottle: return "Request too frequency."
|
||||
|
@ -76,7 +73,6 @@ extension APIService.APIError: LocalizedError {
|
|||
var helpAnchor: String? {
|
||||
switch errorReason {
|
||||
case .authenticationMissing: return "Please request after authenticated."
|
||||
case .badCredentials: return "Please try again.."
|
||||
case .badRequest: return "Please try again."
|
||||
case .badResponse: return "Please try again."
|
||||
case .requestThrottle: return "Please try again later."
|
||||
|
|
|
@ -43,4 +43,17 @@ extension APIService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func accountRegister(
|
||||
domain: String,
|
||||
query: Mastodon.API.Account.RegisterQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
|
||||
return Mastodon.API.Account.register(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,5 +31,23 @@ extension APIService {
|
|||
query: query
|
||||
)
|
||||
}
|
||||
|
||||
func applicationAccessToken(
|
||||
domain: String,
|
||||
clientID: String,
|
||||
clientSecret: String
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
|
||||
let query = Mastodon.API.OAuth.AccessTokenQuery(
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
code: nil,
|
||||
grantType: "client_credentials"
|
||||
)
|
||||
return Mastodon.API.OAuth.accessToken(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ extension APIService {
|
|||
limit: Int = 100,
|
||||
local: Bool? = nil,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
let query = Mastodon.API.Timeline.HomeTimelineQuery(
|
||||
|
@ -39,7 +39,7 @@ extension APIService {
|
|||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||
return APIService.Persist.persistTimeline(
|
||||
managedObjectContext: self.backgroundManagedObjectContext,
|
||||
domain: domain,
|
||||
|
@ -50,7 +50,7 @@ extension APIService {
|
|||
log: OSLog.api
|
||||
)
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// APIService+Instance.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func instance(
|
||||
domain: String
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Instance>, Error> {
|
||||
return Mastodon.API.Instance.instance(session: session, domain: domain)
|
||||
}
|
||||
|
||||
}
|
|
@ -22,7 +22,7 @@ extension APIService {
|
|||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||
limit: Int = 100
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let query = Mastodon.API.Timeline.PublicTimelineQuery(
|
||||
local: nil,
|
||||
remote: nil,
|
||||
|
@ -38,7 +38,7 @@ extension APIService {
|
|||
domain: domain,
|
||||
query: query
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||
return APIService.Persist.persistTimeline(
|
||||
managedObjectContext: self.backgroundManagedObjectContext,
|
||||
domain: domain,
|
||||
|
@ -49,7 +49,7 @@ extension APIService {
|
|||
log: OSLog.api
|
||||
)
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
|
|
|
@ -16,7 +16,7 @@ extension APIService.CoreData {
|
|||
static func createOrMergeToot(
|
||||
into managedObjectContext: NSManagedObjectContext,
|
||||
for requestMastodonUser: MastodonUser?,
|
||||
entity: Mastodon.Entity.Toot,
|
||||
entity: Mastodon.Entity.Status,
|
||||
domain: String,
|
||||
networkDate: Date,
|
||||
log: OSLog
|
||||
|
@ -83,7 +83,7 @@ extension APIService.CoreData {
|
|||
}
|
||||
}
|
||||
|
||||
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Toot, networkDate: Date) {
|
||||
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Status, networkDate: Date) {
|
||||
guard networkDate > toot.updatedAt else { return }
|
||||
|
||||
// merge
|
||||
|
|
|
@ -24,7 +24,7 @@ extension APIService.Persist {
|
|||
managedObjectContext: NSManagedObjectContext,
|
||||
domain: String,
|
||||
query: Mastodon.API.Timeline.TimelineQuery,
|
||||
response: Mastodon.Response.Content<[Mastodon.Entity.Toot]>,
|
||||
response: Mastodon.Response.Content<[Mastodon.Entity.Status]>,
|
||||
persistType: PersistTimelineType,
|
||||
requestMastodonUserID: MastodonUser.ID?, // could be nil when response from public endpoint
|
||||
log: OSLog
|
||||
|
|
|
@ -12,6 +12,18 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id" : "9DA3EAE9-A502-49E9-BC74-5ED71250F4A8",
|
||||
"name" : "mastodon.social",
|
||||
"options" : {
|
||||
"environmentVariableEntries" : [
|
||||
{
|
||||
"key" : "domain",
|
||||
"value" : "mastodon.social"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id" : "C5184AF3-B83B-4A7E-949C-6B1AA3ABE7D1",
|
||||
"name" : "pawoo.net",
|
||||
|
@ -23,6 +35,18 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id" : "71C5BF3D-A8CE-468B-B22E-E6DA4AD5AF4E",
|
||||
"name" : "freespeechextremist.com",
|
||||
"options" : {
|
||||
"environmentVariableEntries" : [
|
||||
{
|
||||
"key" : "domain",
|
||||
"value" : "freespeechextremist.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
|
@ -32,6 +56,7 @@
|
|||
{
|
||||
"skippedTests" : [
|
||||
"MastodonSDKTests\/testCreateAnAnpplication()",
|
||||
"MastodonSDKTests\/testHomeTimeline()",
|
||||
"MastodonSDKTests\/testVerifyAppCredentials()"
|
||||
],
|
||||
"target" : {
|
||||
|
|
|
@ -13,6 +13,9 @@ extension Mastodon.API.Account {
|
|||
static func verifyCredentialsEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/verify_credentials")
|
||||
}
|
||||
static func registerEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts")
|
||||
}
|
||||
|
||||
public static func verifyCredentials(
|
||||
session: URLSession,
|
||||
|
@ -31,4 +34,50 @@ extension Mastodon.API.Account {
|
|||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func register(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: RegisterQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: registerEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
public struct RegisterQuery: Codable, PostQuery {
|
||||
public let reason: String?
|
||||
public let username: String
|
||||
public let email: String
|
||||
public let password: String
|
||||
public let agreement: Bool
|
||||
public let locale: String
|
||||
|
||||
public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) {
|
||||
self.reason = reason
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.agreement = agreement
|
||||
self.locale = locale
|
||||
}
|
||||
|
||||
var body: Data? {
|
||||
return try? Mastodon.API.encoder.encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// Mastodon+API+Instance.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Instance {
|
||||
|
||||
static func instanceEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("instance")
|
||||
}
|
||||
|
||||
/// Information about the server
|
||||
///
|
||||
/// - Since: 1.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/2/5
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/instance/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - Returns: `AnyPublisher` contains `Instance` nested in the response
|
||||
public static func instance(
|
||||
session: URLSession,
|
||||
domain: String
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Instance>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: instanceEndpointURL(domain: domain),
|
||||
query: nil,
|
||||
authorization: nil
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Instance.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ extension Mastodon.API.Timeline {
|
|||
session: URLSession,
|
||||
domain: String,
|
||||
query: PublicTimelineQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: publicTimelineEndpointURL(domain: domain),
|
||||
query: query,
|
||||
|
@ -29,7 +29,7 @@ extension Mastodon.API.Timeline {
|
|||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Status].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -40,7 +40,7 @@ extension Mastodon.API.Timeline {
|
|||
domain: String,
|
||||
query: HomeTimelineQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: homeTimelineEndpointURL(domain: domain),
|
||||
query: query,
|
||||
|
@ -48,7 +48,7 @@ extension Mastodon.API.Timeline {
|
|||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Status].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -57,8 +57,8 @@ extension Mastodon.API.Timeline {
|
|||
}
|
||||
|
||||
public protocol TimelineQueryType {
|
||||
var maxID: Mastodon.Entity.Toot.ID? { get }
|
||||
var sinceID: Mastodon.Entity.Toot.ID? { get }
|
||||
var maxID: Mastodon.Entity.Status.ID? { get }
|
||||
var sinceID: Mastodon.Entity.Status.ID? { get }
|
||||
}
|
||||
|
||||
extension Mastodon.API.Timeline {
|
||||
|
@ -70,18 +70,18 @@ extension Mastodon.API.Timeline {
|
|||
public let local: Bool?
|
||||
public let remote: Bool?
|
||||
public let onlyMedia: Bool?
|
||||
public let maxID: Mastodon.Entity.Toot.ID?
|
||||
public let sinceID: Mastodon.Entity.Toot.ID?
|
||||
public let minID: Mastodon.Entity.Toot.ID?
|
||||
public let maxID: Mastodon.Entity.Status.ID?
|
||||
public let sinceID: Mastodon.Entity.Status.ID?
|
||||
public let minID: Mastodon.Entity.Status.ID?
|
||||
public let limit: Int?
|
||||
|
||||
public init(
|
||||
local: Bool? = nil,
|
||||
remote: Bool? = nil,
|
||||
onlyMedia: Bool? = nil,
|
||||
maxID: Mastodon.Entity.Toot.ID? = nil,
|
||||
sinceID: Mastodon.Entity.Toot.ID? = nil,
|
||||
minID: Mastodon.Entity.Toot.ID? = nil,
|
||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||
minID: Mastodon.Entity.Status.ID? = nil,
|
||||
limit: Int? = nil
|
||||
) {
|
||||
self.local = local
|
||||
|
@ -108,16 +108,16 @@ extension Mastodon.API.Timeline {
|
|||
}
|
||||
|
||||
public struct HomeTimelineQuery: Codable, TimelineQuery, GetQuery {
|
||||
public let maxID: Mastodon.Entity.Toot.ID?
|
||||
public let sinceID: Mastodon.Entity.Toot.ID?
|
||||
public let minID: Mastodon.Entity.Toot.ID?
|
||||
public let maxID: Mastodon.Entity.Status.ID?
|
||||
public let sinceID: Mastodon.Entity.Status.ID?
|
||||
public let minID: Mastodon.Entity.Status.ID?
|
||||
public let limit: Int?
|
||||
public let local: Bool?
|
||||
|
||||
public init(
|
||||
maxID: Mastodon.Entity.Toot.ID? = nil,
|
||||
sinceID: Mastodon.Entity.Toot.ID? = nil,
|
||||
minID: Mastodon.Entity.Toot.ID? = nil,
|
||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||
minID: Mastodon.Entity.Status.ID? = nil,
|
||||
limit: Int? = nil,
|
||||
local: Bool? = nil
|
||||
) {
|
||||
|
|
|
@ -84,6 +84,7 @@ extension Mastodon.API {
|
|||
extension Mastodon.API {
|
||||
public enum Account { }
|
||||
public enum App { }
|
||||
public enum Instance { }
|
||||
public enum OAuth { }
|
||||
public enum Timeline { }
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ extension Mastodon.Entity {
|
|||
/// - Since: 1.1.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// 2021/2/5
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/instance/)
|
||||
public struct Instance: Codable {
|
||||
|
@ -28,7 +28,7 @@ extension Mastodon.Entity {
|
|||
public let registrations: Bool?
|
||||
public let approvalRequired: Bool?
|
||||
public let invitesEnabled: Bool?
|
||||
public let urls: [InstanceURL]?
|
||||
public let urls: InstanceURL?
|
||||
public let statistics: Statistics?
|
||||
|
||||
public let thumbnail: String?
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
public typealias Toot = Status
|
||||
|
||||
|
||||
/// Status
|
||||
///
|
||||
/// - Since: 0.1.0
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// MastodonSDK+API+Instance.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-5.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import XCTest
|
||||
import Combine
|
||||
@testable import MastodonSDK
|
||||
|
||||
extension MastodonSDKTests {
|
||||
|
||||
func testInstance() throws {
|
||||
try _testInstance(domain: domain)
|
||||
}
|
||||
|
||||
func _testInstance(domain: String) throws {
|
||||
let theExpectation = expectation(description: "Fetch Instance Infomation")
|
||||
|
||||
Mastodon.API.Instance.instance(session: session, domain: domain)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
XCTFail(error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
XCTAssertNotEqual(response.value.uri, "")
|
||||
print(response.value)
|
||||
theExpectation.fulfill()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
wait(for: [theExpectation], timeout: 10.0)
|
||||
}
|
||||
|
||||
}
|
6
Podfile
6
Podfile
|
@ -6,10 +6,14 @@ target 'Mastodon' do
|
|||
|
||||
# Pods for Mastodon
|
||||
|
||||
# misc
|
||||
# UI
|
||||
pod 'UITextField+Shake', '~> 1.2'
|
||||
|
||||
# misc
|
||||
pod 'SwiftGen', '~> 6.4.0'
|
||||
pod 'DateToolsSwift', '~> 5.0.0'
|
||||
pod 'Kanna', '~> 5.2.2'
|
||||
|
||||
target 'MastodonTests' do
|
||||
inherit! :search_paths
|
||||
# Pods for testing
|
||||
|
|
|
@ -2,23 +2,27 @@ PODS:
|
|||
- DateToolsSwift (5.0.0)
|
||||
- Kanna (5.2.4)
|
||||
- SwiftGen (6.4.0)
|
||||
- "UITextField+Shake (1.2.1)"
|
||||
|
||||
DEPENDENCIES:
|
||||
- DateToolsSwift (~> 5.0.0)
|
||||
- Kanna (~> 5.2.2)
|
||||
- SwiftGen (~> 6.4.0)
|
||||
- "UITextField+Shake (~> 1.2)"
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DateToolsSwift
|
||||
- Kanna
|
||||
- SwiftGen
|
||||
- "UITextField+Shake"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6
|
||||
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
|
||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||
|
||||
PODFILE CHECKSUM: 8b24099ae9ac02698d464cc508af9550352c85cb
|
||||
PODFILE CHECKSUM: 30e8e3a555251a512e7b5e91183747152f126e7a
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
|
|
@ -43,13 +43,14 @@ arch -x86_64 pod install
|
|||
|
||||
## Acknowledgements
|
||||
|
||||
- [ActiveLabel](https://github.com/optonaut/ActiveLabel.swift)
|
||||
- [ActiveLabel](https://github.com/TwidereProject/ActiveLabel.swift)
|
||||
- [AlamofireImage](https://github.com/Alamofire/AlamofireImage)
|
||||
- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator)
|
||||
- [Alamofire](https://github.com/Alamofire/Alamofire)
|
||||
- [CommonOSLog](https://github.com/mainasuk/CommonOSLog)
|
||||
- [DateToolSwift](https://github.com/MatthewYork/DateTools)
|
||||
- [Kanna](https://github.com/tid-kijyun/Kanna)
|
||||
- [Kingfisher](https://github.com/onevcat/Kingfisher)
|
||||
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||
|
||||
|
|
Loading…
Reference in New Issue