Merge pull request #697 from mastodon/690-better-onboarding
Better onboarding
|
@ -241,7 +241,21 @@
|
|||
"welcome": {
|
||||
"slogan": "Social networking\nback in your hands.",
|
||||
"get_started": "Get Started",
|
||||
"log_in": "Log In"
|
||||
"log_in": "Log In",
|
||||
"education": {
|
||||
"what_is_mastodon": {
|
||||
"title": "What is",
|
||||
"description": "Imagine you have an email address that ends with @example.com.\n\nYou can still send and receive emails from anyone, even if their email ends in @gmail.com or @icloud.com or @example.com.",
|
||||
},
|
||||
"mastodon_is_like_that": {
|
||||
"title": "Mastodon is like that",
|
||||
"description": "Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.",
|
||||
},
|
||||
"how_do_i_pick_a_server": {
|
||||
"title": "How do I pick a server?",
|
||||
"description": "Different people choose different servers for any number of reasons. art.example is a great place for artists, while glasgow.example might be a good pick for Scots.\n\nYou can’t go wrong with any of our recommend servers, so regardless of which one you pick (or if you enter your own in the server search bar), you’ll never miss a beat anywhere.",
|
||||
},
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Welcome back",
|
||||
|
@ -251,8 +265,7 @@
|
|||
}
|
||||
},
|
||||
"server_picker": {
|
||||
"title": "Mastodon is made of users in different servers.",
|
||||
"subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.",
|
||||
"title": "Pick Server",
|
||||
"button": {
|
||||
"category": {
|
||||
"all": "All",
|
||||
|
@ -287,9 +300,19 @@
|
|||
"no_results": "No results"
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy",
|
||||
"policy": {
|
||||
"ios": "Privacy Policy - Mastodon for iOS";
|
||||
"server" = "Privacy Policy - %s";
|
||||
|
||||
},
|
||||
"button": {
|
||||
"confirm": "I agree"
|
||||
}
|
||||
}
|
||||
"register": {
|
||||
"title": "Let’s get you set up on %s",
|
||||
"lets_get_you_set_up_on_domain": "Let’s get you set up on %s",
|
||||
"title": "Create account",
|
||||
"input": {
|
||||
"avatar": {
|
||||
"delete": "Delete"
|
||||
|
@ -306,6 +329,7 @@
|
|||
},
|
||||
"password": {
|
||||
"placeholder": "password",
|
||||
"confirmation_placeholder": "Confirm Password",
|
||||
"require": "Your password needs at least:",
|
||||
"character_limit": "8 characters",
|
||||
"accessibility": {
|
||||
|
@ -359,8 +383,7 @@
|
|||
},
|
||||
"confirm_email": {
|
||||
"title": "One last thing.",
|
||||
"subtitle": "Tap the link we emailed to you to verify your account.",
|
||||
"tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account",
|
||||
"tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we sent you to verify %@. We’ll wait right here.",
|
||||
"button": {
|
||||
"open_email_app": "Open Email App",
|
||||
"resend": "Resend"
|
||||
|
|
|
@ -241,7 +241,21 @@
|
|||
"welcome": {
|
||||
"slogan": "Social networking\nback in your hands.",
|
||||
"get_started": "Get Started",
|
||||
"log_in": "Log In"
|
||||
"log_in": "Log In",
|
||||
"education": {
|
||||
"what_is_mastodon": {
|
||||
"title": "What is",
|
||||
"description": "Imagine you have an email address that ends with @example.com.\n\nYou can still send and receive emails from anyone, even if their email ends in @gmail.com or @icloud.com or @example.com.",
|
||||
},
|
||||
"mastodon_is_like_that": {
|
||||
"title": "Mastodon is like that",
|
||||
"description": "Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.",
|
||||
},
|
||||
"how_do_i_pick_a_server": {
|
||||
"title": "How do I pick a server?",
|
||||
"description": "Different people choose different servers for any number of reasons. art.example is a great place for artists, while glasgow.example might be a good pick for Scots.\n\nYou can’t go wrong with any of our recommend servers, so regardless of which one you pick (or if you enter your own in the server search bar), you’ll never miss a beat anywhere.",
|
||||
},
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Welcome back",
|
||||
|
@ -251,9 +265,10 @@
|
|||
}
|
||||
},
|
||||
"server_picker": {
|
||||
"title": "Mastodon is made of users in different servers.",
|
||||
"subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.",
|
||||
"title": "Pick Server",
|
||||
"button": {
|
||||
"language": "Language",
|
||||
"signup_speed": "Sign-up Speed",
|
||||
"category": {
|
||||
"all": "All",
|
||||
"all_accessiblity_description": "Category: All",
|
||||
|
@ -285,18 +300,39 @@
|
|||
"finding_servers": "Finding available servers...",
|
||||
"bad_network": "Something went wrong while loading the data. Check your internet connection.",
|
||||
"no_results": "No results"
|
||||
},
|
||||
"signup_speed": {
|
||||
"all": "All",
|
||||
"instant": "Instant Sign-up",
|
||||
"manually_reviewed": "Manual Review"
|
||||
},
|
||||
"language": {
|
||||
"all": "All"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search name or URL"
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy",
|
||||
"policy": {
|
||||
"ios": "Privacy Policy - Mastodon for iOS",
|
||||
"server": "Privacy Policy - %s"
|
||||
},
|
||||
"button": {
|
||||
"confirm": "I agree"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "Let’s get you set up on %s",
|
||||
"lets_get_you_set_up_on_domain": "Let’s get you set up on %s",
|
||||
"title": "Create account",
|
||||
"input": {
|
||||
"avatar": {
|
||||
"delete": "Delete"
|
||||
},
|
||||
"username": {
|
||||
"placeholder": "username",
|
||||
"duplicate_prompt": "This username is taken."
|
||||
"duplicate_prompt": "This username is taken.",
|
||||
"suggestion": "amazing_%@"
|
||||
},
|
||||
"display_name": {
|
||||
"placeholder": "display name"
|
||||
|
@ -306,6 +342,7 @@
|
|||
},
|
||||
"password": {
|
||||
"placeholder": "password",
|
||||
"confirmation_placeholder": "Confirm Password",
|
||||
"require": "Your password needs at least:",
|
||||
"character_limit": "8 characters",
|
||||
"accessibility": {
|
||||
|
@ -354,15 +391,13 @@
|
|||
"terms_of_service": "terms of service",
|
||||
"privacy_policy": "privacy policy",
|
||||
"button": {
|
||||
"confirm": "I Agree"
|
||||
"confirm": "I agree"
|
||||
}
|
||||
},
|
||||
"confirm_email": {
|
||||
"title": "One last thing.",
|
||||
"subtitle": "Tap the link we emailed to you to verify your account.",
|
||||
"tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account",
|
||||
"title": "Check Your Inbox",
|
||||
"tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we sent you to verify %@. We’ll wait right here.",
|
||||
"button": {
|
||||
"open_email_app": "Open Email App",
|
||||
"resend": "Resend"
|
||||
},
|
||||
"dont_receive_email": {
|
||||
|
@ -375,6 +410,11 @@
|
|||
"description": "We just sent you an email. Check your junk folder if you haven’t.",
|
||||
"mail": "Mail",
|
||||
"open_email_client": "Open Email Client"
|
||||
},
|
||||
"didnt_get_link": {
|
||||
"prefix": "Didn't get a Link?",
|
||||
"resend_in": "Resend (%@)",
|
||||
"resend_now": "Resend now."
|
||||
}
|
||||
},
|
||||
"home_timeline": {
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.swift */; };
|
||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */; };
|
||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */; };
|
||||
0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2FD25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift */; };
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; };
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; };
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; };
|
||||
|
@ -69,7 +67,6 @@
|
|||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; };
|
||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; };
|
||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; };
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; };
|
||||
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; };
|
||||
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */; };
|
||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */; };
|
||||
|
@ -110,11 +107,17 @@
|
|||
9E44C7202967AD17004B2A72 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; };
|
||||
9E44C7222967AD17004B2A72 /* MastodonSDKDynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; };
|
||||
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; };
|
||||
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
|
||||
D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; };
|
||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8A6FE5B293244B500666A47 /* WelcomeContentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5A293244B500666A47 /* WelcomeContentPage.swift */; };
|
||||
D8A6FE5F29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5E29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift */; };
|
||||
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; };
|
||||
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
|
@ -539,8 +542,6 @@
|
|||
0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerViewController.swift; sourceTree = "<group>"; };
|
||||
0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerViewModel.swift; sourceTree = "<group>"; };
|
||||
0FB3D2FD25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeadlineTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = "<group>"; };
|
||||
0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||
164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; };
|
||||
|
@ -591,7 +592,6 @@
|
|||
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = "<group>"; };
|
||||
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
|
||||
2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewController+Avatar.swift"; sourceTree = "<group>"; };
|
||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewController.swift; sourceTree = "<group>"; };
|
||||
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -655,11 +655,23 @@
|
|||
C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = "<group>"; };
|
||||
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
|
||||
D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = "<group>"; tabWidth = 4; };
|
||||
D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = "<group>"; };
|
||||
D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = "<group>"; };
|
||||
D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8916DBF29211BE500124085 /* ContentSizedTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizedTableView.swift; sourceTree = "<group>"; };
|
||||
D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = "<group>"; };
|
||||
D8A6FE5A293244B500666A47 /* WelcomeContentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentPage.swift; sourceTree = "<group>"; };
|
||||
D8A6FE5E29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D8A6FE6129325F5900666A47 /* Intents.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Intents.stringsdict; sourceTree = "<group>"; };
|
||||
D8A6FE6229325F5900666A47 /* app.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = app.json; sourceTree = "<group>"; };
|
||||
D8A6FE6329325F5900666A47 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = "<group>"; };
|
||||
DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
|
@ -1187,6 +1199,10 @@
|
|||
0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */,
|
||||
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */,
|
||||
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */,
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
);
|
||||
path = PickServer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1203,7 +1219,7 @@
|
|||
0FB3D30D25E525C000AAD544 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */,
|
||||
D8363B1529469CE200A74079 /* OnboardingNextView.swift */,
|
||||
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */,
|
||||
DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */,
|
||||
);
|
||||
|
@ -1563,6 +1579,16 @@
|
|||
path = Bookmark;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8099076294BC2BA0050219F /* Privacy */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */,
|
||||
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */,
|
||||
D809907B294D25510050219F /* PrivacyViewModel.swift */,
|
||||
);
|
||||
path = Privacy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1575,9 +1601,32 @@
|
|||
path = Login;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6FE5929323DC200666A47 /* Pages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8A6FE5A293244B500666A47 /* WelcomeContentPage.swift */,
|
||||
D8A6FE5E29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift */,
|
||||
);
|
||||
path = Pages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6FE6029325F5900666A47 /* Localization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8A6FE6129325F5900666A47 /* Intents.stringsdict */,
|
||||
D8A6FE6229325F5900666A47 /* app.json */,
|
||||
D8A6FE6329325F5900666A47 /* README.md */,
|
||||
D8A6FE6429325F5900666A47 /* StringsConvertor */,
|
||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */,
|
||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */,
|
||||
);
|
||||
path = Localization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8099076294BC2BA0050219F /* Privacy */,
|
||||
D8A6AB68291C50F3003AB663 /* Login */,
|
||||
DB68A03825E900CC00CFDF14 /* Share */,
|
||||
0FAA0FDD25E0B5700017CCDE /* Welcome */,
|
||||
|
@ -1781,6 +1830,7 @@
|
|||
1EBA4F56E920856A3FC84ACB /* Pods */,
|
||||
3FE14AD363ED19AE7FF210A6 /* Frameworks */,
|
||||
DB98335F25C93B0400AD9700 /* Recovered References */,
|
||||
D8A6FE6029325F5900666A47 /* Localization */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -1866,12 +1916,6 @@
|
|||
DB4F097826A039B400D62E92 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
DB0617F427855AB90030EE79 /* ServerRuleSection.swift */,
|
||||
DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */,
|
||||
DB0618022785A7100030EE79 /* RegisterSection.swift */,
|
||||
DB0618042785A73D0030EE79 /* RegisterItem.swift */,
|
||||
);
|
||||
|
@ -2103,7 +2147,6 @@
|
|||
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */,
|
||||
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */,
|
||||
DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */,
|
||||
0FB3D2FD25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift */,
|
||||
DB0617EE277F12720030EE79 /* NavigationActionView.swift */,
|
||||
);
|
||||
path = Share;
|
||||
|
@ -2167,6 +2210,8 @@
|
|||
DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */,
|
||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */,
|
||||
DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */,
|
||||
DB0617F427855AB90030EE79 /* ServerRuleSection.swift */,
|
||||
DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */,
|
||||
);
|
||||
path = ServerRules;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2498,6 +2543,7 @@
|
|||
DBABE3F125ECAC4E00879EE5 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8A6FE5929323DC200666A47 /* Pages */,
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */,
|
||||
DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */,
|
||||
DB0617EA277EF3820030EE79 /* GradientBorderView.swift */,
|
||||
|
@ -2609,7 +2655,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */,
|
||||
2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */,
|
||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */,
|
||||
DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */,
|
||||
DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */,
|
||||
|
@ -3214,6 +3259,7 @@
|
|||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
||||
DB6180F626391D580018D199 /* MediaPreviewableViewController.swift in Sources */,
|
||||
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */,
|
||||
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */,
|
||||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
|
||||
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */,
|
||||
DBF1D24E269DAF5D00C1C08A /* SearchDetailViewController.swift in Sources */,
|
||||
|
@ -3287,6 +3333,7 @@
|
|||
DB603113279EBEBA00A935FE /* DataSourceFacade+Block.swift in Sources */,
|
||||
DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */,
|
||||
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */,
|
||||
D8A6FE5F29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift in Sources */,
|
||||
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */,
|
||||
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */,
|
||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||
|
@ -3344,6 +3391,7 @@
|
|||
2A506CF6292D040100059C37 /* HashtagTimelineHeaderView.swift in Sources */,
|
||||
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
|
||||
DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */,
|
||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */,
|
||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||
DB0FCB842796B2A2006C02E2 /* FavoriteViewController+DataSourceProvider.swift in Sources */,
|
||||
DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */,
|
||||
|
@ -3352,7 +3400,6 @@
|
|||
6213AF5A28939C8400BCADB6 /* BookmarkViewModel.swift in Sources */,
|
||||
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */,
|
||||
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */,
|
||||
0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */,
|
||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */,
|
||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||
|
@ -3393,7 +3440,6 @@
|
|||
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||
DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */,
|
||||
|
@ -3434,7 +3480,6 @@
|
|||
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
||||
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
|
@ -3479,6 +3524,7 @@
|
|||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
||||
DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */,
|
||||
DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */,
|
||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */,
|
||||
DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */,
|
||||
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */,
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||
|
@ -3564,6 +3610,7 @@
|
|||
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */,
|
||||
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */,
|
||||
DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */,
|
||||
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */,
|
||||
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */,
|
||||
DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */,
|
||||
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
||||
|
@ -3574,6 +3621,7 @@
|
|||
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
||||
DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */,
|
||||
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
||||
D8A6FE5B293244B500666A47 /* WelcomeContentPage.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -1,257 +1,250 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
|
||||
"version" : "5.6.2"
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Alamofire",
|
||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
|
||||
"version": "5.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "AlamofireImage",
|
||||
"repositoryURL": "https://github.com/Alamofire/AlamofireImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
|
||||
"version": "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CommonOSLog",
|
||||
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c121624a30698e9886efe38aebb36ff51c01b6c2",
|
||||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FaviconFinder",
|
||||
"repositoryURL": "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
|
||||
"version": "3.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FLAnimatedImage",
|
||||
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f",
|
||||
"version": "1.0.17"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FPSIndicator",
|
||||
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e4a5067ccd5293b024c767f09e51056afd4a4796",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Fuzi",
|
||||
"repositoryURL": "https://github.com/cezheng/Fuzi.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f08c8323da21e985f3772610753bcfc652c2103f",
|
||||
"version": "3.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainAccess",
|
||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version": "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "44e891bdb61426a95e31492a67c7c0dfad1f87c5",
|
||||
"version": "7.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "MetaTextKit",
|
||||
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "dcd5255d6930c2fab408dc8562c577547e477624",
|
||||
"version": "2.2.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "NextLevelSessionExporter",
|
||||
"repositoryURL": "https://github.com/NextLevel/NextLevelSessionExporter.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b6c0cce1aa37fe1547d694f958fac3c3524b74da",
|
||||
"version": "0.4.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nuke",
|
||||
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97",
|
||||
"version": "10.11.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "NukeFLAnimatedImagePlugin",
|
||||
"repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Pageboy",
|
||||
"repositoryURL": "https://github.com/uias/Pageboy",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5",
|
||||
"version": "3.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "PanModal",
|
||||
"repositoryURL": "https://github.com/slackhq/PanModal.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b012aecb6b67a8e46369227f893c12544846613f",
|
||||
"version": "1.2.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SDWebImage",
|
||||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb",
|
||||
"version": "5.14.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Stripes",
|
||||
"repositoryURL": "https://github.com/eneko/Stripes.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d533fd44b8043a3abbf523e733599173d6f98c11",
|
||||
"version": "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-collections",
|
||||
"repositoryURL": "https://github.com/apple/swift-collections.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f504716c27d2e5d4144fa4794b12129301d17729",
|
||||
"version": "1.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "546610d52b19be3e19935e0880bb06b9c03f5cef",
|
||||
"version": "1.14.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-zlib-support",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftSoup",
|
||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6778575285177365cbad3e5b8a72f2a20583cfec",
|
||||
"version": "2.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Introspect",
|
||||
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f2616860a41f9d9932da412a8978fec79c06fe24",
|
||||
"version": "0.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TabBarPager",
|
||||
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Tabman",
|
||||
"repositoryURL": "https://github.com/uias/Tabman",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
|
||||
"version": "2.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TOCropViewController",
|
||||
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d0470491f56e734731bbf77991944c0dfdee3e0e",
|
||||
"version": "2.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UIHostingConfigurationBackport",
|
||||
"repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UITextView+Placeholder",
|
||||
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version": "1.4.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "alamofireimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/AlamofireImage.git",
|
||||
"state" : {
|
||||
"revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
|
||||
"version" : "4.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "commonoslog",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/MainasuK/CommonOSLog",
|
||||
"state" : {
|
||||
"revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2",
|
||||
"version" : "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "faviconfinder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state" : {
|
||||
"revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
|
||||
"version" : "3.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "flanimatedimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Flipboard/FLAnimatedImage.git",
|
||||
"state" : {
|
||||
"revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f",
|
||||
"version" : "1.0.17"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fpsindicator",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/MainasuK/FPSIndicator.git",
|
||||
"state" : {
|
||||
"revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fuzi",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cezheng/Fuzi.git",
|
||||
"state" : {
|
||||
"revision" : "f08c8323da21e985f3772610753bcfc652c2103f",
|
||||
"version" : "3.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||
"state" : {
|
||||
"revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5",
|
||||
"version" : "7.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "metatextkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
"state" : {
|
||||
"revision" : "dcd5255d6930c2fab408dc8562c577547e477624",
|
||||
"version" : "2.2.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nextlevelsessionexporter",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/NextLevel/NextLevelSessionExporter.git",
|
||||
"state" : {
|
||||
"revision" : "b6c0cce1aa37fe1547d694f958fac3c3524b74da",
|
||||
"version" : "0.4.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke.git",
|
||||
"state" : {
|
||||
"revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97",
|
||||
"version" : "10.11.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke-flanimatedimage-plugin",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
|
||||
"state" : {
|
||||
"revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16",
|
||||
"version" : "8.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "pageboy",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/uias/Pageboy",
|
||||
"state" : {
|
||||
"revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5",
|
||||
"version" : "3.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "panmodal",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/slackhq/PanModal.git",
|
||||
"state" : {
|
||||
"revision" : "b012aecb6b67a8e46369227f893c12544846613f",
|
||||
"version" : "1.2.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state" : {
|
||||
"revision" : "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb",
|
||||
"version" : "5.14.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "stripes",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/eneko/Stripes.git",
|
||||
"state" : {
|
||||
"revision" : "d533fd44b8043a3abbf523e733599173d6f98c11",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
|
||||
"version" : "1.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio.git",
|
||||
"state" : {
|
||||
"revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef",
|
||||
"version" : "1.14.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-zlib-support",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-zlib-support.git",
|
||||
"state" : {
|
||||
"revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd",
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "6778575285177365cbad3e5b8a72f2a20583cfec",
|
||||
"version" : "2.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-introspect",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
|
||||
"version" : "0.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tabbarpager",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TwidereProject/TabBarPager.git",
|
||||
"state" : {
|
||||
"revision" : "488aa66d157a648901b61721212c0dec23d27ee5",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tabman",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/uias/Tabman",
|
||||
"state" : {
|
||||
"revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
|
||||
"version" : "2.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "thirdpartymailer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vtourraine/ThirdPartyMailer.git",
|
||||
"state" : {
|
||||
"revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController.git",
|
||||
"state" : {
|
||||
"revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e",
|
||||
"version" : "2.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "uihostingconfigurationbackport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git",
|
||||
"state" : {
|
||||
"revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "uitextview-placeholder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/MainasuK/UITextView-Placeholder.git",
|
||||
"state" : {
|
||||
"revision" : "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version" : "1.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ extension SceneCoordinator {
|
|||
case welcome
|
||||
case mastodonPickServer(viewMode: MastodonPickServerViewModel)
|
||||
case mastodonRegister(viewModel: MastodonRegisterViewModel)
|
||||
case mastodonPrivacyPolicies(viewModel: PrivacyViewModel)
|
||||
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
|
||||
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
|
||||
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
||||
|
@ -413,6 +414,9 @@ private extension SceneCoordinator {
|
|||
loginViewController.delegate = self
|
||||
|
||||
viewController = loginViewController
|
||||
case .mastodonPrivacyPolicies(let viewModel):
|
||||
let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel)
|
||||
viewController = privacyViewController
|
||||
case .mastodonResendEmail(let viewModel):
|
||||
let _viewController = MastodonResendEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
//
|
||||
// CategoryPickerSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021/3/5.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum CategoryPickerSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension CategoryPickerSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency
|
||||
) -> UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
guard let _ = dependency else { return nil }
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell
|
||||
cell.categoryView.titleLabel.text = item.title
|
||||
cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in
|
||||
cell.categoryView.highlightedIndicatorView.alpha = cell.isSelected ? 1 : 0
|
||||
cell.categoryView.titleLabel.textColor = cell.isSelected ? Asset.Colors.Label.primary.color : Asset.Colors.Label.secondary.color
|
||||
}
|
||||
.store(in: &cell.observations)
|
||||
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityLabel = item.accessibilityDescription
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
import Combine
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
import ThirdPartyMailer
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
|
@ -26,19 +25,10 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc
|
|||
|
||||
let stackView = UIStackView()
|
||||
|
||||
let largeTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.systemFont(ofSize: 34, weight: .bold))
|
||||
label.textColor = .label
|
||||
label.text = L10n.Scene.ConfirmEmail.title
|
||||
return label
|
||||
}()
|
||||
|
||||
private(set) lazy var subtitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: UIFont.systemFont(ofSize: 20))
|
||||
label.textColor = .secondaryLabel
|
||||
label.text = L10n.Scene.ConfirmEmail.tapTheLinkWeEmailedToYouToVerifyYourAccount
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.systemFont(ofSize: 17))
|
||||
label.textColor = .label
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
@ -50,41 +40,53 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc
|
|||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
return navigationActionView
|
||||
|
||||
let resendEmailButton: UIButton = {
|
||||
|
||||
let boldFont = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))
|
||||
let regularFont = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
|
||||
var buttonConfiguration = UIButton.Configuration.plain()
|
||||
var boldResendString = AttributedString(L10n.Scene.ConfirmEmail.DidntGetLink.resendIn(60), attributes: .init([.font: boldFont]))
|
||||
var attributedTitle = AttributedString(L10n.Scene.ConfirmEmail.DidntGetLink.prefix, attributes: .init([.font: regularFont]))
|
||||
|
||||
attributedTitle.append(AttributedString(" "))
|
||||
attributedTitle.append(boldResendString)
|
||||
|
||||
buttonConfiguration.attributedTitle = attributedTitle
|
||||
|
||||
let button = UIButton(configuration: buttonConfiguration)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.isEnabled = false
|
||||
|
||||
return button
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
|
||||
var resendButtonTimer: Timer?
|
||||
}
|
||||
|
||||
extension MastodonConfirmEmailViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem()
|
||||
|
||||
setupOnboardingAppearance()
|
||||
configureTitleLabel()
|
||||
configureMargin()
|
||||
|
||||
subtitleLabel.text = L10n.Scene.ConfirmEmail.tapTheLinkWeEmailedToYouToVerifyYourAccount(viewModel.email)
|
||||
|
||||
resendEmailButton.addTarget(self, action: #selector(MastodonConfirmEmailViewController.resendButtonPressed(_:)), for: .touchUpInside)
|
||||
|
||||
// stackView
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .fill
|
||||
stackView.spacing = 10
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 10, left: 0, bottom: 23, right: 0)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.addArrangedSubview(largeTitleLabel)
|
||||
stackView.addArrangedSubview(subtitleLabel)
|
||||
stackView.addArrangedSubview(emailImageView)
|
||||
stackView.addArrangedSubview(resendEmailButton)
|
||||
emailImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
emailImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
stackView.addArrangedSubview(navigationActionView)
|
||||
|
||||
view.addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -136,37 +138,60 @@ extension MastodonConfirmEmailViewController {
|
|||
}
|
||||
.store(in: &self.disposeBag)
|
||||
|
||||
|
||||
navigationActionView.backButton.setTitle(L10n.Scene.ConfirmEmail.Button.resend, for: .normal)
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(MastodonConfirmEmailViewController.resendButtonPressed(_:)), for: .touchUpInside)
|
||||
|
||||
navigationActionView.nextButton.setTitle(L10n.Scene.ConfirmEmail.Button.openEmailApp, for: .normal)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(MastodonConfirmEmailViewController.openEmailButtonPressed(_:)), for: .touchUpInside)
|
||||
title = L10n.Scene.ConfirmEmail.title
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configureTitleLabel()
|
||||
configureMargin()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
let nowIn60Seconds = Date().addingTimeInterval(60)
|
||||
let boldFont = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))
|
||||
let regularFont = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
|
||||
let timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] in
|
||||
guard Date() < nowIn60Seconds else {
|
||||
self?.resendEmailButton.isEnabled = true
|
||||
|
||||
var configuration = self?.resendEmailButton.configuration
|
||||
|
||||
let boldResendString = AttributedString(L10n.Scene.ConfirmEmail.DidntGetLink.resendNow, attributes: .init([.font: boldFont]))
|
||||
var attributedTitle = AttributedString(L10n.Scene.ConfirmEmail.DidntGetLink.prefix, attributes: .init([.font: regularFont]))
|
||||
|
||||
attributedTitle.append(AttributedString(" "))
|
||||
attributedTitle.append(boldResendString)
|
||||
|
||||
configuration?.attributedTitle = attributedTitle
|
||||
self?.resendEmailButton.configuration = configuration
|
||||
self?.resendEmailButton.setNeedsUpdateConfiguration()
|
||||
|
||||
$0.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
var configuration = self?.resendEmailButton.configuration
|
||||
|
||||
let boldResendString = AttributedString(L10n.Scene.ConfirmEmail.DidntGetLink.resendIn(Int(nowIn60Seconds.timeIntervalSinceNow) + 1), attributes: .init([.font: boldFont]))
|
||||
var attributedTitle = AttributedString(L10n.Scene.ConfirmEmail.DidntGetLink.prefix, attributes: .init([.font: regularFont]))
|
||||
|
||||
attributedTitle.append(AttributedString(" "))
|
||||
attributedTitle.append(boldResendString)
|
||||
|
||||
configuration?.attributedTitle = attributedTitle
|
||||
self?.resendEmailButton.configuration = configuration
|
||||
self?.resendEmailButton.setNeedsUpdateConfiguration()
|
||||
}
|
||||
|
||||
RunLoop.main.add(timer, forMode: .default)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonConfirmEmailViewController {
|
||||
private func configureTitleLabel() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
navigationItem.title = L10n.Scene.ConfirmEmail.title.replacingOccurrences(of: "\n", with: " ")
|
||||
largeTitleLabel.isHidden = true
|
||||
default:
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
navigationItem.title = nil
|
||||
largeTitleLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private func configureMargin() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
|
@ -179,19 +204,6 @@ extension MastodonConfirmEmailViewController {
|
|||
}
|
||||
|
||||
extension MastodonConfirmEmailViewController {
|
||||
@objc private func openEmailButtonPressed(_ sender: UIButton) {
|
||||
let alertController = UIAlertController(title: L10n.Scene.ConfirmEmail.OpenEmailApp.title, message: L10n.Scene.ConfirmEmail.OpenEmailApp.description, preferredStyle: .alert)
|
||||
let openEmailAction = UIAlertAction(title: L10n.Scene.ConfirmEmail.Button.openEmailApp, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.showEmailAppAlert()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(openEmailAction)
|
||||
alertController.addAction(cancelAction)
|
||||
alertController.preferredAction = openEmailAction
|
||||
_ = self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
@objc private func resendButtonPressed(_ sender: UIButton) {
|
||||
let alertController = UIAlertController(title: L10n.Scene.ConfirmEmail.DontReceiveEmail.title, message: L10n.Scene.ConfirmEmail.DontReceiveEmail.description, preferredStyle: .alert)
|
||||
let resendAction = UIAlertAction(title: L10n.Scene.ConfirmEmail.DontReceiveEmail.resendEmail, style: .default) { _ in
|
||||
|
@ -205,29 +217,6 @@ extension MastodonConfirmEmailViewController {
|
|||
alertController.addAction(okAction)
|
||||
_ = self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
func showEmailAppAlert() {
|
||||
let clients = ThirdPartyMailClient.clients
|
||||
let availableClients = clients.filter { client -> Bool in
|
||||
ThirdPartyMailer.isMailClientAvailable(client)
|
||||
}
|
||||
let alertController = UIAlertController(title: L10n.Scene.ConfirmEmail.OpenEmailApp.openEmailClient, message: nil, preferredStyle: .alert)
|
||||
|
||||
let alertAction = UIAlertAction(title: L10n.Scene.ConfirmEmail.OpenEmailApp.mail, style: .default) { _ in
|
||||
UIApplication.shared.open(URL(string: "message://")!, options: [:], completionHandler: nil)
|
||||
}
|
||||
alertController.addAction(alertAction)
|
||||
_ = availableClients.compactMap { client -> UIAlertAction in
|
||||
let alertAction = UIAlertAction(title: client.name, style: .default) { _ in
|
||||
ThirdPartyMailer.open(client, completionHandler: nil)
|
||||
}
|
||||
alertController.addAction(alertAction)
|
||||
return alertAction
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
_ = self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PanPopableViewController
|
||||
|
|
|
@ -13,9 +13,7 @@ class MastodonLoginView: UIView {
|
|||
|
||||
// List with (filtered) domains
|
||||
|
||||
let titleLabel: UILabel
|
||||
let subtitleLabel: UILabel
|
||||
private let headerStackView: UIStackView
|
||||
let explanationTextLabel: UILabel
|
||||
|
||||
let searchTextField: UITextField
|
||||
private let searchTextFieldLeftView: UIView
|
||||
|
@ -28,22 +26,12 @@ class MastodonLoginView: UIView {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.font = MastodonLoginViewController.largeTitleFont
|
||||
titleLabel.textColor = MastodonLoginViewController.largeTitleTextColor
|
||||
titleLabel.text = L10n.Scene.Login.title
|
||||
titleLabel.numberOfLines = 0
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.font = MastodonLoginViewController.subTitleFont
|
||||
subtitleLabel.textColor = MastodonLoginViewController.subTitleTextColor
|
||||
subtitleLabel.text = L10n.Scene.Login.subtitle
|
||||
subtitleLabel.numberOfLines = 0
|
||||
|
||||
headerStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
headerStackView.axis = .vertical
|
||||
headerStackView.spacing = 16
|
||||
headerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
explanationTextLabel = UILabel()
|
||||
explanationTextLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
explanationTextLabel.font = MastodonLoginViewController.subTitleFont
|
||||
explanationTextLabel.textColor = MastodonLoginViewController.subTitleTextColor
|
||||
explanationTextLabel.text = L10n.Scene.Login.subtitle
|
||||
explanationTextLabel.numberOfLines = 0
|
||||
|
||||
searchTextFieldMagnifyingGlass = UIImageView(image: UIImage(
|
||||
systemName: "magnifyingglass",
|
||||
|
@ -81,7 +69,7 @@ class MastodonLoginView: UIView {
|
|||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(headerStackView)
|
||||
addSubview(explanationTextLabel)
|
||||
addSubview(searchTextField)
|
||||
addSubview(tableView)
|
||||
addSubview(navigationActionView)
|
||||
|
@ -99,12 +87,11 @@ class MastodonLoginView: UIView {
|
|||
let bottomConstraint = safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor)
|
||||
|
||||
let constraints = [
|
||||
explanationTextLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||
explanationTextLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
explanationTextLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
|
||||
headerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||
headerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
headerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
|
||||
searchTextField.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 32),
|
||||
searchTextField.topAnchor.constraint(equalTo: explanationTextLabel.bottomAnchor, constant: 32),
|
||||
searchTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
searchTextField.heightAnchor.constraint(equalToConstant: 55),
|
||||
trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 16),
|
||||
|
|
|
@ -11,6 +11,7 @@ import MastodonCore
|
|||
import MastodonAsset
|
||||
import Combine
|
||||
import AuthenticationServices
|
||||
import MastodonLocalization
|
||||
|
||||
protocol MastodonLoginViewControllerDelegate: AnyObject {
|
||||
func backButtonPressed(_ viewController: MastodonLoginViewController)
|
||||
|
@ -97,6 +98,9 @@ class MastodonLoginViewController: UIViewController, NeedsDependency {
|
|||
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
setupOnboardingAppearance()
|
||||
|
||||
title = L10n.Scene.Login.title
|
||||
navigationItem.hidesBackButton = true
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
|
|
@ -29,7 +29,7 @@ class MastodonLoginViewModel {
|
|||
}
|
||||
|
||||
func updateServers() {
|
||||
appContext?.apiService.servers().sink(receiveCompletion: { [weak self] completion in
|
||||
appContext?.apiService.servers(registrations: "all").sink(receiveCompletion: { [weak self] completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
guard let self = self else { return }
|
||||
|
|
|
@ -12,51 +12,31 @@ import MastodonLocalization
|
|||
|
||||
/// Note: update Equatable when change case
|
||||
enum CategoryPickerItem {
|
||||
case all
|
||||
case language(language: String?)
|
||||
case signupSpeed(manuallyReviewed: Bool?)
|
||||
case category(category: Mastodon.Entity.Category)
|
||||
}
|
||||
|
||||
extension CategoryPickerItem {
|
||||
|
||||
var emoji: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "💬"
|
||||
case .category(let category):
|
||||
switch category.category {
|
||||
case .academia:
|
||||
return "📚"
|
||||
case .activism:
|
||||
return "✊"
|
||||
case .food:
|
||||
return "🍕"
|
||||
case .furry:
|
||||
return "🦁"
|
||||
case .games:
|
||||
return "🕹"
|
||||
case .general:
|
||||
return "🐘"
|
||||
case .journalism:
|
||||
return "📰"
|
||||
case .lgbt:
|
||||
return "🏳️🌈"
|
||||
case .regional:
|
||||
return "📍"
|
||||
case .art:
|
||||
return "🎨"
|
||||
case .music:
|
||||
return "🎼"
|
||||
case .tech:
|
||||
return "📱"
|
||||
case ._other:
|
||||
return "❓"
|
||||
}
|
||||
}
|
||||
}
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return L10n.Scene.ServerPicker.Button.Category.all
|
||||
case .language(let language):
|
||||
if let language {
|
||||
return language
|
||||
} else {
|
||||
return L10n.Scene.ServerPicker.Button.language
|
||||
}
|
||||
case .signupSpeed(let manuallyReviewed):
|
||||
if let manuallyReviewed {
|
||||
if manuallyReviewed {
|
||||
return L10n.Scene.ServerPicker.SignupSpeed.manuallyReviewed
|
||||
} else {
|
||||
return L10n.Scene.ServerPicker.SignupSpeed.instant
|
||||
}
|
||||
} else {
|
||||
return L10n.Scene.ServerPicker.Button.signupSpeed
|
||||
}
|
||||
case .category(let category):
|
||||
switch category.category {
|
||||
case .academia:
|
||||
|
@ -91,8 +71,22 @@ extension CategoryPickerItem {
|
|||
|
||||
var accessibilityDescription: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return L10n.Scene.ServerPicker.Button.Category.allAccessiblityDescription
|
||||
case .language(let language):
|
||||
if let language {
|
||||
return language
|
||||
} else {
|
||||
return L10n.Scene.ServerPicker.Button.language
|
||||
}
|
||||
case .signupSpeed(let manuallyReviewed):
|
||||
if let manuallyReviewed {
|
||||
if manuallyReviewed {
|
||||
return L10n.Scene.ServerPicker.SignupSpeed.manuallyReviewed
|
||||
} else {
|
||||
return L10n.Scene.ServerPicker.SignupSpeed.instant
|
||||
}
|
||||
} else {
|
||||
return L10n.Scene.ServerPicker.Button.signupSpeed
|
||||
}
|
||||
case .category(let category):
|
||||
switch category.category {
|
||||
case .academia:
|
||||
|
@ -129,10 +123,12 @@ extension CategoryPickerItem {
|
|||
extension CategoryPickerItem: Equatable {
|
||||
static func == (lhs: CategoryPickerItem, rhs: CategoryPickerItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.all, .all):
|
||||
return true
|
||||
case (.category(let categoryLeft), .category(let categoryRight)):
|
||||
return categoryLeft.category.rawValue == categoryRight.category.rawValue
|
||||
case (.language(let languageLeft), .language(let languageRight)):
|
||||
return languageLeft == languageRight
|
||||
case (.signupSpeed(let leftManualReview), .signupSpeed(let rightManualReview)):
|
||||
return leftManualReview == rightManualReview
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -142,10 +138,20 @@ extension CategoryPickerItem: Equatable {
|
|||
extension CategoryPickerItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .all:
|
||||
hasher.combine(String(describing: CategoryPickerItem.all.self))
|
||||
case .language(let language):
|
||||
if let language {
|
||||
return hasher.combine(language)
|
||||
} else {
|
||||
return hasher.combine("no_language_selected")
|
||||
}
|
||||
case .category(let category):
|
||||
hasher.combine(category.category.rawValue)
|
||||
case .signupSpeed(let manuallyReviewed):
|
||||
if let manuallyReviewed {
|
||||
return hasher.combine(manuallyReviewed)
|
||||
} else {
|
||||
return hasher.combine("no_signup_speed_selected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// CategoryPickerSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021/3/5.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum CategoryPickerSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension CategoryPickerSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency,
|
||||
viewModel: MastodonPickServerViewModel
|
||||
) -> UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
guard let _ = dependency else { return nil }
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PickServerCategoryCollectionViewCell.reuseIdentifier, for: indexPath) as! PickServerCategoryCollectionViewCell
|
||||
|
||||
cell.titleLabel.text = item.title
|
||||
|
||||
switch item {
|
||||
case .category(_):
|
||||
cell.chevron.isHidden = true
|
||||
cell.menuButton.isUserInteractionEnabled = false
|
||||
cell.menuButton.isHidden = true
|
||||
cell.menuButton.menu = nil
|
||||
case .language(_):
|
||||
guard viewModel.allLanguages.value.isNotEmpty else { break }
|
||||
|
||||
let allLanguagesAction = UIAction(title: L10n.Scene.ServerPicker.Language.all) { _ in
|
||||
viewModel.selectedLanguage.value = nil
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
cell.titleLabel.text = L10n.Scene.ServerPicker.Button.language
|
||||
}
|
||||
|
||||
let languageActions = viewModel.allLanguages.value.compactMap { language in
|
||||
UIAction(title: language.language ?? language.locale) { action in
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
viewModel.selectedLanguage.value = language.locale
|
||||
cell.titleLabel.text = language.language
|
||||
}
|
||||
}
|
||||
|
||||
var allActions = [allLanguagesAction]
|
||||
allActions.append(contentsOf: languageActions)
|
||||
|
||||
let languageMenu = UIMenu(title: L10n.Scene.ServerPicker.Button.language,
|
||||
children: allActions)
|
||||
|
||||
cell.chevron.isHidden = false
|
||||
cell.menuButton.isUserInteractionEnabled = true
|
||||
cell.menuButton.isHidden = false
|
||||
cell.menuButton.menu = languageMenu
|
||||
cell.menuButton.showsMenuAsPrimaryAction = true
|
||||
|
||||
case .signupSpeed(_):
|
||||
let doesntMatterAction = UIAction(title: L10n.Scene.ServerPicker.SignupSpeed.all) { _ in
|
||||
viewModel.manualApprovalRequired.value = nil
|
||||
cell.titleLabel.text = L10n.Scene.ServerPicker.Button.signupSpeed
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
}
|
||||
|
||||
let manualApprovalAction = UIAction(title: L10n.Scene.ServerPicker.SignupSpeed.manuallyReviewed) { action in
|
||||
viewModel.manualApprovalRequired.value = true
|
||||
cell.titleLabel.text = action.title
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
}
|
||||
|
||||
let instantSignupAction = UIAction(title: L10n.Scene.ServerPicker.SignupSpeed.instant) { action in
|
||||
viewModel.manualApprovalRequired.value = false
|
||||
cell.titleLabel.text = action.title
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
}
|
||||
|
||||
let signupSpeedMenu = UIMenu(title: L10n.Scene.ServerPicker.Button.signupSpeed,
|
||||
children: [doesntMatterAction, manualApprovalAction, instantSignupAction])
|
||||
|
||||
cell.chevron.isHidden = false
|
||||
cell.menuButton.isUserInteractionEnabled = true
|
||||
cell.menuButton.isHidden = false
|
||||
cell.menuButton.menu = signupSpeedMenu
|
||||
cell.menuButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in
|
||||
|
||||
let textColor: UIColor
|
||||
let backgroundColor: UIColor
|
||||
let borderColor: UIColor
|
||||
|
||||
if cell.isSelected {
|
||||
textColor = .white
|
||||
backgroundColor = Asset.Colors.Brand.blurple.color
|
||||
borderColor = Asset.Colors.Brand.blurple.color
|
||||
} else {
|
||||
textColor = .label
|
||||
backgroundColor = .clear
|
||||
borderColor = .separator
|
||||
}
|
||||
|
||||
cell.backgroundColor = backgroundColor
|
||||
cell.titleLabel.textColor = textColor
|
||||
cell.layer.borderColor = borderColor.cgColor
|
||||
cell.chevron.tintColor = textColor
|
||||
}
|
||||
.store(in: &cell.observations)
|
||||
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityLabel = item.accessibilityDescription
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,13 +6,46 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
class PickServerCategoryCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
static let reuseIdentifier = "PickServerCategoryCollectionViewCell"
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
let chevron: UIImageView = {
|
||||
let chevron = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
chevron.translatesAutoresizingMaskIntoConstraints = false
|
||||
return chevron
|
||||
}()
|
||||
|
||||
let menuButton: UIButton = {
|
||||
let menuButton = UIButton()
|
||||
menuButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
return menuButton
|
||||
}()
|
||||
|
||||
private let container: UIStackView = {
|
||||
let container = UIStackView()
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.axis = .horizontal
|
||||
container.spacing = 4
|
||||
container.distribution = .fillProportionally
|
||||
container.alignment = .center
|
||||
return container
|
||||
}()
|
||||
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
var categoryView = PickServerCategoryView()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
observations.removeAll()
|
||||
|
@ -20,26 +53,45 @@ class PickServerCategoryCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoryCollectionViewCell {
|
||||
private func configure() {
|
||||
backgroundColor = .clear
|
||||
|
||||
categoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(categoryView)
|
||||
NSLayoutConstraint.activate([
|
||||
categoryView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
categoryView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
categoryView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: categoryView.bottomAnchor),
|
||||
])
|
||||
container.addArrangedSubview(titleLabel)
|
||||
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
container.addArrangedSubview(chevron)
|
||||
|
||||
menuButton.addTarget(self, action: #selector(PickServerCategoryCollectionViewCell.didPressButton(_:)), for: .touchUpInside)
|
||||
|
||||
layer.borderColor = UIColor.black.cgColor
|
||||
layer.borderWidth = 1.0
|
||||
applyCornerRadius(radius: 15)
|
||||
|
||||
contentView.addSubview(container)
|
||||
contentView.addSubview(menuButton)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
|
||||
var constraints = [
|
||||
container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6),
|
||||
container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
|
||||
contentView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 12),
|
||||
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 6),
|
||||
|
||||
chevron.heightAnchor.constraint(equalToConstant: 16),
|
||||
chevron.widthAnchor.constraint(equalToConstant: 14),
|
||||
]
|
||||
|
||||
constraints.append(contentsOf: menuButton.pinToParent())
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
//MARK: - Actions
|
||||
|
||||
@objc func didPressButton(_ sender: Any) {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import MastodonAsset
|
|||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
|
@ -40,26 +41,26 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
return navigationActionView
|
||||
|
||||
let onboardingNextView: OnboardingNextView = {
|
||||
let onboardingNextView = OnboardingNextView()
|
||||
onboardingNextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
onboardingNextView.backgroundColor = UIColor.secondarySystemBackground
|
||||
return onboardingNextView
|
||||
}()
|
||||
|
||||
var mastodonAuthenticationController: MastodonAuthenticationController?
|
||||
|
||||
deinit {
|
||||
tableViewObservation = nil
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
|
||||
let searchController: UISearchController = {
|
||||
let searchController = UISearchController(searchResultsController: nil)
|
||||
searchController.searchBar.placeholder = L10n.Scene.ServerPicker.Search.placeholder
|
||||
return searchController
|
||||
}()
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
|
@ -67,22 +68,9 @@ extension MastodonPickServerViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem()
|
||||
|
||||
setupOnboardingAppearance()
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
|
||||
#if DEBUG
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil)
|
||||
let children: [UIMenuElement] = [
|
||||
UIAction(title: "Dismiss", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
})
|
||||
]
|
||||
navigationItem.rightBarButtonItem?.menu = UIMenu(title: "Debug Tool", image: nil, identifier: nil, options: [], children: children)
|
||||
#endif
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -92,37 +80,22 @@ extension MastodonPickServerViewController {
|
|||
tableView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
view.addSubview(onboardingNextView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
onboardingNextView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
onboardingNextView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: onboardingNextView.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView
|
||||
onboardingNextView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = self.navigationActionView.frame.height
|
||||
let inset = self.onboardingNextView.frame.height
|
||||
self.viewModel.additionalTableViewInsets.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
// fix AutoLayout warning when observe before view appear
|
||||
viewModel.viewWillAppear
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.tableViewObservation = self.tableView.observe(\.contentSize, options: [.initial, .new]) { [weak self] tableView, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateEmptyStateViewLayout()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
emptyStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(emptyStateView)
|
||||
emptyStateViewLeadingLayoutConstraint = emptyStateView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor)
|
||||
|
@ -131,7 +104,7 @@ extension MastodonPickServerViewController {
|
|||
emptyStateView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
emptyStateViewLeadingLayoutConstraint,
|
||||
emptyStateViewTrailingLayoutConstraint,
|
||||
navigationActionView.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21),
|
||||
onboardingNextView.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21),
|
||||
])
|
||||
view.sendSubviewToBack(emptyStateView)
|
||||
|
||||
|
@ -150,12 +123,6 @@ extension MastodonPickServerViewController {
|
|||
)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel
|
||||
.selectedServer
|
||||
.map { $0 != nil }
|
||||
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.Merge(
|
||||
viewModel.error,
|
||||
authenticationViewModel.error
|
||||
|
@ -203,7 +170,11 @@ extension MastodonPickServerViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isAuthenticating in
|
||||
guard let self = self else { return }
|
||||
isAuthenticating ? self.navigationActionView.nextButton.showLoading() : self.navigationActionView.nextButton.stopLoading()
|
||||
if isAuthenticating {
|
||||
self.onboardingNextView.showLoading()
|
||||
} else {
|
||||
self.onboardingNextView.stopLoading()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -234,8 +205,29 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(MastodonPickServerViewController.backButtonDidPressed(_:)), for: .touchUpInside)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(MastodonPickServerViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
|
||||
onboardingNextView.nextButton.addTarget(self, action: #selector(MastodonPickServerViewController.next(_:)), for: .touchUpInside)
|
||||
|
||||
viewModel.allLanguages
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let snapshot = self?.viewModel.serverSectionHeaderView.diffableDataSource?.snapshot() else { return }
|
||||
|
||||
self?.viewModel.serverSectionHeaderView.diffableDataSource?.applySnapshotUsingReloadData(snapshot) {
|
||||
guard let self = self, let viewModel = self.viewModel else { return }
|
||||
guard let indexPath = viewModel.serverSectionHeaderView.diffableDataSource?.indexPath(for: .category(category: .init(category: Mastodon.Entity.Category.Kind.general.rawValue, serversCount: 0))) else { return }
|
||||
|
||||
viewModel.serverSectionHeaderView.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .right)
|
||||
|
||||
let firstIndex = IndexPath(item: 0, section: 0)
|
||||
viewModel.serverSectionHeaderView.collectionView.scrollToItem(at: firstIndex, at: .left, animated: false)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
title = L10n.Scene.ServerPicker.title
|
||||
|
||||
navigationItem.searchController = searchController
|
||||
searchController.searchResultsUpdater = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -254,20 +246,24 @@ extension MastodonPickServerViewController {
|
|||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
setupNavigationBarAppearance()
|
||||
updateEmptyStateViewLayout()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
|
||||
@objc private func backButtonDidPressed(_ sender: UIButton) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func nextButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let server = viewModel.selectedServer.value else { return }
|
||||
|
||||
@objc private func next(_ sender: UIButton) {
|
||||
|
||||
let server: Mastodon.Entity.Server
|
||||
|
||||
if let selectedServer = viewModel.selectedServer.value {
|
||||
server = selectedServer
|
||||
} else if let randomServer = viewModel.chooseRandomServer() {
|
||||
server = randomServer
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
authenticationViewModel.isAuthenticating.send(true)
|
||||
|
||||
context.apiService.instance(domain: server.domain)
|
||||
|
@ -412,38 +408,30 @@ extension MastodonPickServerViewController: UITableViewDelegate {
|
|||
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
private func updateEmptyStateViewLayout() {
|
||||
// guard let diffableDataSource = self.viewModel.diffableDataSource else { return }
|
||||
// guard let indexPath = diffableDataSource.indexPath(for: .search) else { return }
|
||||
// let rectInTableView = tableView.rectForRow(at: indexPath)
|
||||
//
|
||||
// emptyStateView.topPaddingViewTopLayoutConstraint.constant = rectInTableView.maxY
|
||||
//
|
||||
// switch traitCollection.horizontalSizeClass {
|
||||
// case .regular:
|
||||
// emptyStateViewLeadingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin
|
||||
// emptyStateViewTrailingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin
|
||||
// default:
|
||||
// let margin = tableView.layoutMarginsGuide.layoutFrame.origin.x
|
||||
// emptyStateViewLeadingLayoutConstraint.constant = margin
|
||||
// emptyStateViewTrailingLayoutConstraint.constant = margin
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PickServerServerSectionTableHeaderViewDelegate
|
||||
extension MastodonPickServerViewController: PickServerServerSectionTableHeaderViewDelegate {
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let diffableDataSource = headerView.diffableDataSource else { return }
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||
viewModel.selectCategoryItem.value = item ?? .all
|
||||
}
|
||||
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?) {
|
||||
viewModel.searchText.send(searchText ?? "")
|
||||
guard let diffableDataSource = headerView.diffableDataSource,
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
switch item {
|
||||
case .category(_):
|
||||
viewModel.selectCategoryItem.value = item
|
||||
case .language(_), .signupSpeed(_):
|
||||
break
|
||||
// gets handled by button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
extension MastodonPickServerViewController: OnboardingViewControllerAppearance { }
|
||||
|
||||
// MARK: - UISearchResultsUpdating
|
||||
|
||||
extension MastodonPickServerViewController: UISearchResultsUpdating {
|
||||
func updateSearchResults(for searchController: UISearchController) {
|
||||
guard let searchText = searchController.searchBar.text else { return }
|
||||
viewModel.searchText.send(searchText)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonPickServerViewModel {
|
||||
|
||||
|
@ -18,7 +19,8 @@ extension MastodonPickServerViewModel {
|
|||
// set section header
|
||||
serverSectionHeaderView.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource(
|
||||
for: serverSectionHeaderView.collectionView,
|
||||
dependency: dependency
|
||||
dependency: dependency,
|
||||
viewModel: self
|
||||
)
|
||||
var sectionHeaderSnapshot = NSDiffableDataSourceSnapshot<CategoryPickerSection, CategoryPickerItem>()
|
||||
sectionHeaderSnapshot.appendSections([.main])
|
||||
|
@ -26,8 +28,12 @@ extension MastodonPickServerViewModel {
|
|||
serverSectionHeaderView.delegate = pickServerServerSectionTableHeaderViewDelegate
|
||||
serverSectionHeaderView.diffableDataSource?.apply(sectionHeaderSnapshot, animatingDifferences: false) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let indexPath = self.serverSectionHeaderView.diffableDataSource?.indexPath(for: .all) else { return }
|
||||
self.serverSectionHeaderView.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredHorizontally)
|
||||
guard let indexPath = self.serverSectionHeaderView.diffableDataSource?.indexPath(for: .category(category: .init(category: Mastodon.Entity.Category.Kind.general.rawValue, serversCount: 0))) else { return }
|
||||
|
||||
self.serverSectionHeaderView.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .right)
|
||||
|
||||
let firstIndex = IndexPath(item: 0, section: 0)
|
||||
self.serverSectionHeaderView.collectionView.scrollToItem(at: firstIndex, at: .left, animated: false)
|
||||
}
|
||||
|
||||
// set tableView
|
||||
|
@ -38,7 +44,6 @@ extension MastodonPickServerViewModel {
|
|||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
|
||||
snapshot.appendSections([.header, .servers])
|
||||
snapshot.appendItems([.header], toSection: .header)
|
||||
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
|
||||
loadIndexedServerStateMachine.enter(LoadIndexedServerState.Loading.self)
|
||||
|
@ -61,7 +66,6 @@ extension MastodonPickServerViewModel {
|
|||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
|
||||
snapshot.appendSections([.header, .servers])
|
||||
snapshot.appendItems([.header], toSection: .header)
|
||||
|
||||
// TODO: handle filter
|
||||
var serverItems: [PickServerItem] = []
|
||||
|
|
|
@ -15,6 +15,7 @@ import OrderedCollections
|
|||
import Tabman
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
class MastodonPickServerViewModel: NSObject {
|
||||
|
||||
|
@ -32,12 +33,16 @@ class MastodonPickServerViewModel: NSObject {
|
|||
let context: AppContext
|
||||
var categoryPickerItems: [CategoryPickerItem] = {
|
||||
var items: [CategoryPickerItem] = []
|
||||
items.append(.all)
|
||||
items.append(.language(language: nil))
|
||||
items.append(.signupSpeed(manuallyReviewed: nil))
|
||||
items.append(contentsOf: APIService.stubCategories().map { CategoryPickerItem.category(category: $0) })
|
||||
return items
|
||||
}()
|
||||
let selectCategoryItem = CurrentValueSubject<CategoryPickerItem, Never>(.all)
|
||||
let selectCategoryItem = CurrentValueSubject<CategoryPickerItem, Never>(.category(category: Mastodon.Entity.Category(category: Mastodon.Entity.Category.Kind.general.rawValue, serversCount: 0)))
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
let selectedLanguage = CurrentValueSubject<String?, Never>(nil)
|
||||
let manualApprovalRequired = CurrentValueSubject<Bool?, Never>(nil)
|
||||
let allLanguages = CurrentValueSubject<[Mastodon.Entity.Language], Never>([])
|
||||
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Server]?, Never>([]) // set nil when loading
|
||||
let viewWillAppear = PassthroughSubject<Void, Never>()
|
||||
|
@ -69,7 +74,7 @@ class MastodonPickServerViewModel: NSObject {
|
|||
init(context: AppContext) {
|
||||
self.context = context
|
||||
super.init()
|
||||
|
||||
|
||||
configure()
|
||||
}
|
||||
|
||||
|
@ -82,6 +87,14 @@ class MastodonPickServerViewModel: NSObject {
|
|||
extension MastodonPickServerViewModel {
|
||||
|
||||
private func configure() {
|
||||
|
||||
context.apiService.languages().sink { completion in
|
||||
|
||||
} receiveValue: { response in
|
||||
self.allLanguages.value = response.value
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isLoadingIndexedServers,
|
||||
loadingIndexedServersError
|
||||
|
@ -99,16 +112,22 @@ extension MastodonPickServerViewModel {
|
|||
}
|
||||
.assign(to: \.value, on: emptyStateViewState)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
|
||||
|
||||
Publishers.CombineLatest4(
|
||||
indexedServers.eraseToAnyPublisher(),
|
||||
selectCategoryItem.eraseToAnyPublisher(),
|
||||
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates()
|
||||
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
|
||||
Publishers.CombineLatest(
|
||||
selectedLanguage.eraseToAnyPublisher(),
|
||||
manualApprovalRequired.eraseToAnyPublisher()
|
||||
).map { selectedLanguage, manualApprovalRequired -> (selectedLanguage: String?, manualApprovalRequired: Bool?) in
|
||||
(selectedLanguage, manualApprovalRequired)
|
||||
}
|
||||
)
|
||||
.map { indexedServers, selectCategoryItem, searchText -> [Mastodon.Entity.Server] in
|
||||
.map { indexedServers, selectCategoryItem, searchText, filters -> [Mastodon.Entity.Server] in
|
||||
// ignore approval required servers when sign-up
|
||||
var indexedServers = indexedServers
|
||||
indexedServers = indexedServers.filter { !$0.approvalRequired }
|
||||
// Note:
|
||||
// sort by calculate last week users count
|
||||
// and make medium size (~800) server to top
|
||||
|
@ -154,10 +173,10 @@ extension MastodonPickServerViewModel {
|
|||
|
||||
// Filter the indexed servers by category or search text
|
||||
switch selectCategoryItem {
|
||||
case .all:
|
||||
return MastodonPickServerViewModel.filterServers(servers: indexedServers, category: nil, searchText: searchText)
|
||||
case .language(_), .signupSpeed(_):
|
||||
return MastodonPickServerViewModel.filterServers(servers: indexedServers, language: filters.selectedLanguage, manualApprovalRequired: filters.manualApprovalRequired, category: nil, searchText: searchText)
|
||||
case .category(let category):
|
||||
return MastodonPickServerViewModel.filterServers(servers: indexedServers, category: category.category.rawValue, searchText: searchText)
|
||||
return MastodonPickServerViewModel.filterServers(servers: indexedServers, language: filters.selectedLanguage, manualApprovalRequired: filters.manualApprovalRequired, category: category.category.rawValue, searchText: searchText)
|
||||
}
|
||||
}
|
||||
.assign(to: \.value, on: filteredIndexedServers)
|
||||
|
@ -209,17 +228,30 @@ extension MastodonPickServerViewModel {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func chooseRandomServer() -> Mastodon.Entity.Server? {
|
||||
|
||||
let language = Locale.autoupdatingCurrent.languageCode?.lowercased() ?? "en"
|
||||
|
||||
let servers = indexedServers.value
|
||||
guard servers.isNotEmpty else { return nil }
|
||||
|
||||
let randomServer = servers.filter {
|
||||
$0.language.lowercased() == language
|
||||
}.randomElement()
|
||||
|
||||
return randomServer ?? servers.randomElement() ?? servers.first
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MastodonPickServerViewModel {
|
||||
private static func filterServers(servers: [Mastodon.Entity.Server], category: String?, searchText: String) -> [Mastodon.Entity.Server] {
|
||||
return servers
|
||||
// 1. Filter the category
|
||||
private static func filterServers(servers: [Mastodon.Entity.Server], language: String? = nil, manualApprovalRequired: Bool? = nil, category: String?, searchText: String) -> [Mastodon.Entity.Server] {
|
||||
let filteredServers = servers
|
||||
// 1. Filter the category
|
||||
.filter {
|
||||
guard let category = category else { return true }
|
||||
return $0.category.caseInsensitiveCompare(category) == .orderedSame
|
||||
}
|
||||
// 2. Filter the searchText
|
||||
// 2. Filter the searchText
|
||||
.filter {
|
||||
let searchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !searchText.isEmpty else {
|
||||
|
@ -227,6 +259,18 @@ extension MastodonPickServerViewModel {
|
|||
}
|
||||
return $0.domain.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
.filter {
|
||||
guard let language else { return true }
|
||||
|
||||
return $0.language.lowercased() == language.lowercased()
|
||||
}
|
||||
.filter {
|
||||
guard let manualApprovalRequired else { return true }
|
||||
|
||||
print("\($0.domain) \($0.approvalRequired) < \(manualApprovalRequired)")
|
||||
return $0.approvalRequired == manualApprovalRequired
|
||||
}
|
||||
return filteredServers
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import MastodonSDK
|
|||
|
||||
/// Note: update Equatable when change case
|
||||
enum PickServerItem {
|
||||
case header
|
||||
case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute)
|
||||
case loader(attribute: LoaderItemAttribute)
|
||||
}
|
||||
|
@ -59,8 +58,6 @@ extension PickServerItem {
|
|||
extension PickServerItem: Equatable {
|
||||
static func == (lhs: PickServerItem, rhs: PickServerItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.header, .header):
|
||||
return true
|
||||
case (.server(let serverLeft, _), .server(let serverRight, _)):
|
||||
return serverLeft.domain == serverRight.domain
|
||||
case (.loader(let attributeLeft), loader(let attributeRight)):
|
||||
|
@ -74,8 +71,6 @@ extension PickServerItem: Equatable {
|
|||
extension PickServerItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .header:
|
||||
hasher.combine(String(describing: PickServerItem.header.self))
|
||||
case .server(let server, _):
|
||||
hasher.combine(server.domain)
|
||||
case .loader(let attribute):
|
|
@ -20,7 +20,6 @@ extension PickServerSection {
|
|||
for tableView: UITableView,
|
||||
dependency: NeedsDependency
|
||||
) -> UITableViewDiffableDataSource<PickServerSection, PickServerItem> {
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
|
||||
|
@ -29,9 +28,6 @@ extension PickServerSection {
|
|||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let _ = dependency else { return nil }
|
||||
switch item {
|
||||
case .header:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell
|
||||
return cell
|
||||
case .server(let server, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
|
||||
PickServerSection.configure(cell: cell, server: server, attribute: attribute)
|
||||
|
@ -67,45 +63,13 @@ extension PickServerSection {
|
|||
]
|
||||
)
|
||||
}()
|
||||
cell.usersValueLabel.attributedText = {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let attachment = NSTextAttachment(image: UIImage(systemName: "person.2.fill")!)
|
||||
let attachmentAttributedString = NSAttributedString(attachment: attachment)
|
||||
attributedString.append(attachmentAttributedString)
|
||||
attributedString.append(NSAttributedString(string: " "))
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineHeightMultiple = 1.12
|
||||
let valueAttributedString = NSAttributedString(
|
||||
string: server.totalUsers.asAbbreviatedCountString(),
|
||||
attributes: [
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
attributedString.append(valueAttributedString)
|
||||
|
||||
return attributedString
|
||||
}()
|
||||
cell.langValueLabel.attributedText = {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let attachment = NSTextAttachment(image: UIImage(systemName: "text.bubble.fill")!)
|
||||
let attachmentAttributedString = NSAttributedString(attachment: attachment)
|
||||
attributedString.append(attachmentAttributedString)
|
||||
attributedString.append(NSAttributedString(string: " "))
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineHeightMultiple = 1.12
|
||||
let valueAttributedString = NSAttributedString(
|
||||
string: server.language.uppercased(),
|
||||
attributes: [
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
attributedString.append(valueAttributedString)
|
||||
|
||||
return attributedString
|
||||
}()
|
||||
|
||||
if let proxiedThumbnail = server.proxiedThumbnail, let thumbnailUrl = URL(string: proxiedThumbnail) {
|
||||
cell.thumbnailImageView.af.setImage(withURL: thumbnailUrl, completion: { _ in
|
||||
OperationQueue.main.addOperation {
|
||||
cell.thumbnailImageView.isHidden = false
|
||||
}
|
||||
})
|
||||
}
|
||||
attribute.isLast
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] isLast in
|
|
@ -20,14 +20,25 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let containerView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.axis = .vertical
|
||||
view.spacing = 4
|
||||
return view
|
||||
}()
|
||||
|
||||
let thumbnailImageView: UIImageView = {
|
||||
let thumbnail = UIImageView()
|
||||
thumbnail.translatesAutoresizingMaskIntoConstraints = false
|
||||
thumbnail.backgroundColor = Asset.Colors.brand.color
|
||||
thumbnail.layer.cornerRadius = 8
|
||||
thumbnail.contentMode = .scaleAspectFill
|
||||
thumbnail.layer.masksToBounds = true
|
||||
return thumbnail
|
||||
}()
|
||||
|
||||
let domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -37,7 +48,6 @@ class PickServerCell: UITableViewCell {
|
|||
let checkbox: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
|
@ -45,50 +55,21 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let descriptionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.numberOfLines = 0
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let infoStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 16
|
||||
return stackView
|
||||
}()
|
||||
|
||||
let separator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.System.separator.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let langValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}()
|
||||
|
||||
let usersValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}()
|
||||
|
||||
private var collapseConstraints: [NSLayoutConstraint] = []
|
||||
private var expandConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
thumbnailImageView.isHidden = true
|
||||
thumbnailImageView.image = nil
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
|
@ -109,52 +90,45 @@ extension PickServerCell {
|
|||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
|
||||
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(checkbox)
|
||||
NSLayoutConstraint.activate([
|
||||
checkbox.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: 1),
|
||||
checkbox.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||
checkbox.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||
])
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
contentView.addSubview(containerView)
|
||||
contentView.addSubview(thumbnailImageView)
|
||||
contentView.addSubview(checkbox)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
thumbnailImageView.heightAnchor.constraint(equalToConstant: 32),
|
||||
thumbnailImageView.widthAnchor.constraint(equalTo: thumbnailImageView.heightAnchor),
|
||||
|
||||
thumbnailImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
thumbnailImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
|
||||
containerView.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 22),
|
||||
containerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: thumbnailImageView.trailingAnchor, constant: 16),
|
||||
checkbox.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 16),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 11),
|
||||
checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||
])
|
||||
|
||||
|
||||
containerView.addArrangedSubview(domainLabel)
|
||||
containerView.addArrangedSubview(descriptionLabel)
|
||||
containerView.setCustomSpacing(6, after: descriptionLabel)
|
||||
containerView.addArrangedSubview(infoStackView)
|
||||
|
||||
infoStackView.addArrangedSubview(usersValueLabel)
|
||||
infoStackView.addArrangedSubview(langValueLabel)
|
||||
infoStackView.addArrangedSubview(UIView())
|
||||
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separator)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
separator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: separator.trailingAnchor),
|
||||
separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1),
|
||||
contentView.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 16),
|
||||
checkbox.heightAnchor.constraint(equalToConstant: 20),
|
||||
checkbox.widthAnchor.constraint(equalTo: checkbox.heightAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if selected {
|
||||
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
checkbox.tintColor = Asset.Colors.Label.primary.color
|
||||
checkbox.image = UIImage(systemName: "checkmark")
|
||||
checkbox.tintColor = Asset.Colors.brand.color
|
||||
} else {
|
||||
checkbox.image = UIImage(systemName: "circle")
|
||||
checkbox.tintColor = Asset.Colors.Label.secondary.color
|
||||
checkbox.image = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// OnboardingNextView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 2022-12-12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class OnboardingNextView: UIView {
|
||||
|
||||
static let buttonHeight: CGFloat = 50
|
||||
|
||||
private let container: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
stackView.alignment = .center
|
||||
return stackView
|
||||
}()
|
||||
|
||||
let nextButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.layer.cornerRadius = 14
|
||||
button.backgroundColor = Asset.Colors.Brand.blurple.color
|
||||
button.setTitle(L10n.Common.Controls.Actions.next, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let explanationLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 0
|
||||
label.textColor = .secondaryLabel
|
||||
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
label.text = L10n.Scene.ServerPicker.noServerSelectedHint
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var activityIndicator: UIActivityIndicatorView = {
|
||||
let indicator = UIActivityIndicatorView(style: .medium)
|
||||
indicator.color = .white
|
||||
indicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
return indicator
|
||||
}()
|
||||
private var isLoading: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
private func _init() {
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addArrangedSubview(nextButton)
|
||||
container.addArrangedSubview(explanationLabel)
|
||||
|
||||
addSubview(container)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
container.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
||||
container.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 16),
|
||||
safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 16),
|
||||
|
||||
nextButton.widthAnchor.constraint(equalTo: container.widthAnchor),
|
||||
explanationLabel.widthAnchor.constraint(equalTo: container.widthAnchor),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
nextButton.heightAnchor.constraint(greaterThanOrEqualToConstant: NavigationActionView.buttonHeight)
|
||||
])
|
||||
}
|
||||
|
||||
func showLoading() {
|
||||
guard isLoading == false else { return }
|
||||
nextButton.isEnabled = false
|
||||
isLoading = true
|
||||
nextButton.setTitle("", for: .disabled)
|
||||
|
||||
nextButton.addSubview(activityIndicator)
|
||||
NSLayoutConstraint.activate([
|
||||
activityIndicator.centerXAnchor.constraint(equalTo: nextButton.centerXAnchor),
|
||||
activityIndicator.centerYAnchor.constraint(equalTo: nextButton.centerYAnchor),
|
||||
])
|
||||
activityIndicator.startAnimating()
|
||||
}
|
||||
|
||||
func stopLoading() {
|
||||
guard isLoading else { return }
|
||||
isLoading = false
|
||||
if activityIndicator.superview == nextButton {
|
||||
activityIndicator.removeFromSuperview()
|
||||
}
|
||||
nextButton.isEnabled = true
|
||||
nextButton.setTitle(L10n.Common.Controls.Actions.next, for: .disabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
//
|
||||
// PickServerCategoryView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
class PickServerCategoryView: UIView {
|
||||
|
||||
let highlightedIndicatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Label.primary.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoryView {
|
||||
|
||||
private func configure() {
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.spacing = 2
|
||||
container.distribution = .fillProportionally
|
||||
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(container)
|
||||
container.pinToParent()
|
||||
|
||||
container.addArrangedSubview(titleLabel)
|
||||
highlightedIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addArrangedSubview(highlightedIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
highlightedIndicatorView.heightAnchor.constraint(equalToConstant: 3)//.priority(.required - 1),
|
||||
])
|
||||
titleLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG && canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
struct PickServerCategoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UIViewPreview {
|
||||
PickServerCategoryView()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -14,16 +14,14 @@ import MastodonLocalization
|
|||
|
||||
protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject {
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?)
|
||||
}
|
||||
|
||||
final class PickServerServerSectionTableHeaderView: UIView {
|
||||
|
||||
static let collectionViewHeight: CGFloat = 30
|
||||
static let searchTextFieldHeight: CGFloat = 38
|
||||
static let spacing: CGFloat = 11
|
||||
static let spacing: CGFloat = 16
|
||||
|
||||
static let height: CGFloat = collectionViewHeight + spacing + searchTextFieldHeight + spacing
|
||||
static let height: CGFloat = collectionViewHeight + spacing
|
||||
|
||||
weak var delegate: PickServerServerSectionTableHeaderViewDelegate?
|
||||
|
||||
|
@ -58,66 +56,7 @@ final class PickServerServerSectionTableHeaderView: UIView {
|
|||
view.layer.masksToBounds = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let searchTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.backgroundColor = Asset.Scene.Onboarding.searchBarBackground.color
|
||||
textField.leftView = {
|
||||
let imageView = UIImageView(
|
||||
image: UIImage(
|
||||
systemName: "magnifyingglass",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||
)
|
||||
)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
|
||||
let containerView = UIView()
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
|
||||
imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
|
||||
let paddingView = UIView()
|
||||
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(paddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||
paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
])
|
||||
return containerView
|
||||
}()
|
||||
textField.leftViewMode = .always
|
||||
textField.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
textField.tintColor = Asset.Colors.Label.primary.color
|
||||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.adjustsFontForContentSizeCategory = true
|
||||
textField.attributedPlaceholder = NSAttributedString(
|
||||
string: L10n.Scene.ServerPicker.Input.searchServersOrEnterUrl,
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 15, weight: .regular),
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
]
|
||||
)
|
||||
textField.clearButtonMode = .whileEditing
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .done
|
||||
textField.keyboardType = .URL
|
||||
textField.borderStyle = .none
|
||||
|
||||
textField.layer.masksToBounds = true
|
||||
textField.layer.cornerRadius = 10
|
||||
textField.layer.cornerCurve = .continuous
|
||||
|
||||
return textField
|
||||
}()
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -133,7 +72,6 @@ final class PickServerServerSectionTableHeaderView: UIView {
|
|||
|
||||
collectionView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerServerSectionTableHeaderView {
|
||||
|
@ -148,28 +86,11 @@ extension PickServerServerSectionTableHeaderView {
|
|||
collectionView.topAnchor.constraint(equalTo: topAnchor),
|
||||
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
collectionView.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.collectionViewHeight).priority(.required - 1),
|
||||
collectionView.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.collectionViewHeight),
|
||||
bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing),
|
||||
])
|
||||
|
||||
searchTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(searchTextField)
|
||||
NSLayoutConstraint.activate([
|
||||
searchTextField.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing),
|
||||
searchTextField.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
searchTextField.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing),
|
||||
searchTextField.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.searchTextFieldHeight).priority(.required - 1),
|
||||
])
|
||||
|
||||
collectionView.delegate = self
|
||||
searchTextField.delegate = self
|
||||
searchTextField.addTarget(self, action: #selector(PickServerServerSectionTableHeaderView.textFieldDidChange(_:)), for: .editingChanged)
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerServerSectionTableHeaderView {
|
||||
@objc private func textFieldDidChange(_ textField: UITextField) {
|
||||
delegate?.pickServerServerSectionTableHeaderView(self, searchTextDidChange: textField.text)
|
||||
collectionView.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,10 +98,11 @@ extension PickServerServerSectionTableHeaderView {
|
|||
extension PickServerServerSectionTableHeaderView: UICollectionViewDelegate {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
|
||||
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
delegate?.pickServerServerSectionTableHeaderView(self, collectionView: collectionView, didSelectItemAt: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerServerSectionTableHeaderView {
|
||||
|
@ -191,8 +113,11 @@ extension PickServerServerSectionTableHeaderView {
|
|||
}
|
||||
|
||||
override func accessibilityElement(at index: Int) -> Any? {
|
||||
if let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) { return item }
|
||||
return searchTextField
|
||||
if let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) {
|
||||
return item
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// PrivacyTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 15.12.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PrivacyTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "PrivacyTableViewCell"
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
//
|
||||
// PrivacyTableViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 15.12.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
|
||||
enum PrivacyRow {
|
||||
case iOSApp
|
||||
case server(domain: String)
|
||||
|
||||
var url: URL? {
|
||||
switch self {
|
||||
case .iOSApp:
|
||||
return URL(string: "https://joinmastodon.org/ios/privacy")
|
||||
case .server(let domain):
|
||||
return URL(string: "https://\(domain)/privacy-policy")
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .iOSApp:
|
||||
return L10n.Scene.Privacy.Policy.ios
|
||||
case .server(let domain):
|
||||
return L10n.Scene.Privacy.Policy.server(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrivacyTableViewController: UIViewController, NeedsDependency {
|
||||
|
||||
var context: AppContext!
|
||||
var coordinator: SceneCoordinator!
|
||||
|
||||
private let tableView: UITableView
|
||||
let viewModel: PrivacyViewModel
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator, viewModel: PrivacyViewModel) {
|
||||
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
|
||||
self.viewModel = viewModel
|
||||
|
||||
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
tableView.register(PrivacyTableViewCell.self, forCellReuseIdentifier: PrivacyTableViewCell.reuseIdentifier)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
view.addSubview(tableView)
|
||||
setupConstraints()
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: L10n.Scene.Privacy.Button.confirm, style: .done, target: self, action: #selector(PrivacyTableViewController.nextButtonPressed(_:)))
|
||||
|
||||
title = L10n.Scene.Privacy.title
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) won't been implemented, please don't use Storyboards.") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: tableView.bottomAnchor)
|
||||
]
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupOnboardingAppearance()
|
||||
}
|
||||
|
||||
//MARK: - Actions
|
||||
@objc private func backButtonPressed(_ sender: UIButton) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func nextButtonPressed(_ sender: UIButton) {
|
||||
let viewModel = MastodonRegisterViewModel(
|
||||
context: context,
|
||||
domain: viewModel.domain,
|
||||
authenticateInfo: viewModel.authenticateInfo,
|
||||
instance: viewModel.instance,
|
||||
applicationToken: viewModel.applicationToken
|
||||
)
|
||||
_ = coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show)
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivacyTableViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.rows.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: PrivacyTableViewCell.reuseIdentifier, for: indexPath) as? PrivacyTableViewCell else { fatalError("Wrong cell?") }
|
||||
|
||||
let row = viewModel.rows[indexPath.row]
|
||||
|
||||
var contentConfiguration = cell.defaultContentConfiguration()
|
||||
contentConfiguration.textProperties.color = Asset.Colors.Brand.blurple.color
|
||||
contentConfiguration.text = row.title
|
||||
|
||||
cell.contentConfiguration = contentConfiguration
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivacyTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
let row = viewModel.rows[indexPath.row]
|
||||
guard let url = row.url else { return }
|
||||
|
||||
_ = coordinator.present(scene: .safari(url: url), from: self, transition: .safariPresent(animated: true))
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let wrapper = UIView()
|
||||
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
label.text = L10n.Scene.Privacy.description
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
wrapper.addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 16),
|
||||
label.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
||||
wrapper.trailingAnchor.constraint(equalTo: label.trailingAnchor),
|
||||
wrapper.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 16),
|
||||
])
|
||||
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
|
||||
extension PrivacyTableViewController: OnboardingViewControllerAppearance { }
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// PrivacyViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 16.12.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
final class PrivacyViewModel {
|
||||
|
||||
// input
|
||||
let domain: String
|
||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||
let rows: [PrivacyRow]
|
||||
let instance: Mastodon.Entity.Instance
|
||||
let applicationToken: Mastodon.Entity.Token
|
||||
|
||||
init(
|
||||
domain: String,
|
||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
||||
rows: [PrivacyRow],
|
||||
instance: Mastodon.Entity.Instance,
|
||||
applicationToken: Mastodon.Entity.Token
|
||||
) {
|
||||
self.domain = domain
|
||||
self.authenticateInfo = authenticateInfo
|
||||
self.rows = rows
|
||||
self.instance = instance
|
||||
self.applicationToken = applicationToken
|
||||
}
|
||||
}
|
|
@ -20,70 +20,13 @@ struct MastodonRegisterView: View {
|
|||
var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
let margin: CGFloat = 16
|
||||
|
||||
// header
|
||||
HStack {
|
||||
Text(L10n.Scene.Register.title(viewModel.domain))
|
||||
.font(Font(MastodonPickServerViewController.largeTitleFont as CTFont))
|
||||
.foregroundColor(Color(Asset.Colors.Label.primary.color))
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, margin)
|
||||
|
||||
// Avatar selector
|
||||
Menu {
|
||||
// Photo Library
|
||||
Button {
|
||||
viewModel.avatarMediaMenuActionPublisher.send(.photoLibrary)
|
||||
} label: {
|
||||
Label(L10n.Scene.Compose.MediaSelection.photoLibrary, systemImage: "photo")
|
||||
}
|
||||
// Camera
|
||||
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||
Button {
|
||||
viewModel.avatarMediaMenuActionPublisher.send(.camera)
|
||||
} label: {
|
||||
Label(L10n.Scene.Compose.MediaSelection.camera, systemImage: "camera")
|
||||
}
|
||||
}
|
||||
// Browse
|
||||
Button {
|
||||
viewModel.avatarMediaMenuActionPublisher.send(.browse)
|
||||
} label: {
|
||||
Label(L10n.Scene.Compose.MediaSelection.browse, systemImage: "folder")
|
||||
}
|
||||
// Delete
|
||||
if viewModel.avatarImage != nil {
|
||||
Divider()
|
||||
Button(role: .destructive) {
|
||||
viewModel.avatarMediaMenuActionPublisher.send(.delete)
|
||||
} label: {
|
||||
Label(L10n.Scene.Register.Input.Avatar.delete, systemImage: "delete.left")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
let avatarImage = viewModel.avatarImage ?? Asset.Scene.Onboarding.avatarPlaceholder.image
|
||||
Image(uiImage: avatarImage)
|
||||
.resizable()
|
||||
.frame(width: 88, height: 88, alignment: .center)
|
||||
.overlay(ZStack {
|
||||
Color.black.opacity(0.5)
|
||||
.frame(height: 22, alignment: .bottom)
|
||||
Text(L10n.Common.Controls.Actions.edit)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}, alignment: .bottom)
|
||||
.cornerRadius(22)
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0))
|
||||
|
||||
// Display Name & Uesrname
|
||||
VStack(alignment: .leading, spacing: 11) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
TextField(L10n.Scene.Register.Input.DisplayName.placeholder.localizedCapitalized, text: $viewModel.name)
|
||||
.textContentType(.name)
|
||||
.disableAutocorrection(true)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.displayNameValidateState))
|
||||
HStack {
|
||||
Text("@")
|
||||
TextField(L10n.Scene.Register.Input.Username.placeholder.localizedCapitalized, text: $viewModel.username)
|
||||
.textContentType(.username)
|
||||
.autocapitalization(.none)
|
||||
|
@ -98,15 +41,24 @@ struct MastodonRegisterView: View {
|
|||
.modifier(FormTextFieldModifier(validateState: viewModel.usernameValidateState))
|
||||
.environment(\.layoutDirection, .leftToRight) // force LTR
|
||||
if let errorPrompt = viewModel.usernameErrorPrompt {
|
||||
Text(errorPrompt)
|
||||
.modifier(FormFootnoteModifier())
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(errorPrompt)
|
||||
.font(Font(UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))))
|
||||
//FIXME: Better way than comparing strings
|
||||
if errorPrompt == L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) {
|
||||
Button {
|
||||
viewModel.usernameErrorPrompt = nil
|
||||
viewModel.usernameValidateState = .empty
|
||||
viewModel.username = L10n.Scene.Register.Input.Username.suggestion(viewModel.username)
|
||||
} label: {
|
||||
Text(L10n.Scene.Register.Input.Username.suggestion(viewModel.username))
|
||||
.foregroundColor(Asset.Colors.Brand.blurple.swiftUIColor)
|
||||
.font(Font(UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .bold))))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, margin)
|
||||
.padding(.bottom, 22)
|
||||
|
||||
// Email & Password & Password hint
|
||||
VStack(alignment: .leading, spacing: 11) {
|
||||
TextField(L10n.Scene.Register.Input.Email.placeholder.localizedCapitalized, text: $viewModel.email)
|
||||
.textContentType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
|
@ -117,9 +69,19 @@ struct MastodonRegisterView: View {
|
|||
Text(errorPrompt)
|
||||
.modifier(FormFootnoteModifier())
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.horizontal, margin)
|
||||
.padding(.bottom, 32)
|
||||
|
||||
// Email & Password & Password hint
|
||||
VStack(alignment: .leading, spacing: margin) {
|
||||
SecureField(L10n.Scene.Register.Input.Password.placeholder.localizedCapitalized, text: $viewModel.password)
|
||||
.textContentType(.newPassword)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.passwordValidateState))
|
||||
SecureField(L10n.Scene.Register.Input.Password.confirmationPlaceholder.localizedCapitalized, text: $viewModel.passwordConfirmation)
|
||||
.textContentType(.newPassword)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.passwordValidateState))
|
||||
Text(L10n.Scene.Register.Input.Password.hint)
|
||||
.modifier(FormFootnoteModifier(foregroundColor: .secondary))
|
||||
if let errorPrompt = viewModel.passwordErrorPrompt {
|
||||
|
@ -162,24 +124,18 @@ struct MastodonRegisterView: View {
|
|||
let borderColor: Color = {
|
||||
switch validateState {
|
||||
case .empty: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
case .invalid: return Color(Asset.Colors.TextField.invalid.color)
|
||||
case .invalid: return Color(Asset.Colors.TextField.invalid.color.withAlphaComponent(0.25))
|
||||
case .valid: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
}
|
||||
}()
|
||||
|
||||
Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
borderColor
|
||||
.cornerRadius(10)
|
||||
.shadow(color: .black.opacity(0.125), radius: 1, x: 0, y: 2)
|
||||
|
||||
content
|
||||
.padding()
|
||||
.background(Color(Asset.Scene.Onboarding.textFieldBackground.color))
|
||||
.background(borderColor)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(borderColor, lineWidth: 1)
|
||||
.animation(.easeInOut, value: validateState)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +146,6 @@ struct MastodonRegisterView: View {
|
|||
content
|
||||
.font(.footnote)
|
||||
.foregroundColor(foregroundColor)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,7 +169,7 @@ extension View {
|
|||
|
||||
#if DEBUG
|
||||
struct MastodonRegisterView_Previews: PreviewProvider {
|
||||
static var viewMdoel: MastodonRegisterViewModel {
|
||||
static var viewModel: MastodonRegisterViewModel {
|
||||
let domain = "mstdn.jp"
|
||||
return MastodonRegisterViewModel(
|
||||
context: .shared,
|
||||
|
@ -240,55 +195,28 @@ struct MastodonRegisterView_Previews: PreviewProvider {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
static var viewMdoel2: MastodonRegisterViewModel {
|
||||
let domain = "mstdn.jp"
|
||||
return MastodonRegisterViewModel(
|
||||
context: .shared,
|
||||
domain: domain,
|
||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo(
|
||||
domain: domain,
|
||||
application: Mastodon.Entity.Application(
|
||||
name: "Preview",
|
||||
website: nil,
|
||||
vapidKey: nil,
|
||||
redirectURI: nil,
|
||||
clientID: "",
|
||||
clientSecret: ""
|
||||
),
|
||||
redirectURI: ""
|
||||
)!,
|
||||
instance: Mastodon.Entity.Instance(domain: "mstdn.jp", approvalRequired: true),
|
||||
applicationToken: Mastodon.Entity.Token(
|
||||
accessToken: "",
|
||||
tokenType: "",
|
||||
scope: "",
|
||||
createdAt: Date()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
NavigationView {
|
||||
MastodonRegisterView(viewModel: viewMdoel)
|
||||
MastodonRegisterView(viewModel: viewModel)
|
||||
.navigationBarTitle(Text(""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
NavigationView {
|
||||
MastodonRegisterView(viewModel: viewMdoel)
|
||||
MastodonRegisterView(viewModel: viewModel)
|
||||
.navigationBarTitle(Text(""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
NavigationView {
|
||||
MastodonRegisterView(viewModel: viewMdoel)
|
||||
MastodonRegisterView(viewModel: viewModel)
|
||||
.navigationBarTitle(Text(""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.environment(\.sizeCategory, .accessibilityExtraLarge)
|
||||
NavigationView {
|
||||
MastodonRegisterView(viewModel: viewMdoel2)
|
||||
MastodonRegisterView(viewModel: viewModel)
|
||||
.navigationBarTitle(Text(""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
//
|
||||
// MastodonRegisterViewController+Avatar.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/3/2.
|
||||
//
|
||||
|
||||
import CropViewController
|
||||
import Foundation
|
||||
import OSLog
|
||||
import PhotosUI
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension MastodonRegisterViewController {
|
||||
private func cropImage(image: UIImage, pickerViewController: UIViewController) {
|
||||
DispatchQueue.main.async {
|
||||
let cropController = CropViewController(croppingStyle: .default, image: image)
|
||||
cropController.delegate = self
|
||||
cropController.setAspectRatioPreset(.presetSquare, animated: true)
|
||||
cropController.aspectRatioPickerButtonHidden = true
|
||||
cropController.aspectRatioLockEnabled = true
|
||||
|
||||
// fix iPad compatibility issue
|
||||
// ref: https://github.com/TimOliver/TOCropViewController/issues/365#issuecomment-550239604
|
||||
cropController.modalTransitionStyle = .crossDissolve
|
||||
cropController.transitioningDelegate = nil
|
||||
|
||||
pickerViewController.dismiss(animated: true, completion: {
|
||||
self.present(cropController, animated: true, completion: nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PHPickerViewControllerDelegate
|
||||
extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) else {
|
||||
picker.dismiss(animated: true, completion: {})
|
||||
return
|
||||
}
|
||||
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
|
||||
guard let self = self else { return }
|
||||
guard let image = image as? UIImage else {
|
||||
DispatchQueue.main.async {
|
||||
guard let error = error else { return }
|
||||
let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
_ = self.coordinator.present(
|
||||
scene: .alertController(alertController: alertController),
|
||||
from: nil,
|
||||
transition: .alertController(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.cropImage(image: image, pickerViewController: picker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIImagePickerControllerDelegate
|
||||
extension MastodonRegisterViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
|
||||
guard let image = info[.originalImage] as? UIImage else { return }
|
||||
|
||||
cropImage(image: image, pickerViewController: picker)
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIDocumentPickerDelegate
|
||||
extension MastodonRegisterViewController: UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let url = urls.first else { return }
|
||||
|
||||
do {
|
||||
guard url.startAccessingSecurityScopedResource() else { return }
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
let imageData = try Data(contentsOf: url)
|
||||
guard let image = UIImage(data: imageData) else { return }
|
||||
cropImage(image: image, pickerViewController: controller)
|
||||
} catch {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CropViewControllerDelegate
|
||||
extension MastodonRegisterViewController: CropViewControllerDelegate {
|
||||
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
||||
self.viewModel.avatarImage = image
|
||||
cropViewController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,48 +32,22 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
var viewModel: MastodonRegisterViewModel!
|
||||
private(set) lazy var mastodonRegisterView = MastodonRegisterView(viewModel: viewModel)
|
||||
|
||||
// picker
|
||||
private(set) lazy var imagePicker: PHPickerViewController = {
|
||||
var configuration = PHPickerConfiguration()
|
||||
configuration.filter = .images
|
||||
configuration.selectionLimit = 1
|
||||
var activityIndicator: UIActivityIndicatorView = {
|
||||
let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
activityIndicator.color = Asset.Colors.Brand.blurple.color
|
||||
return activityIndicator
|
||||
}()
|
||||
|
||||
let imagePicker = PHPickerViewController(configuration: configuration)
|
||||
imagePicker.delegate = self
|
||||
return imagePicker
|
||||
}()
|
||||
private(set) lazy var imagePickerController: UIImagePickerController = {
|
||||
let imagePickerController = UIImagePickerController()
|
||||
imagePickerController.sourceType = .camera
|
||||
imagePickerController.delegate = self
|
||||
return imagePickerController
|
||||
}()
|
||||
|
||||
private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
|
||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image])
|
||||
documentPickerController.delegate = self
|
||||
return documentPickerController
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
return navigationActionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||
func nextBarButtonItem() -> UIBarButtonItem {
|
||||
return UIBarButtonItem(title: L10n.Common.Controls.Actions.next, style: .done, target: self, action: #selector(MastodonRegisterViewController.nextButtonPressed(_:)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem()
|
||||
|
||||
|
||||
setupOnboardingAppearance()
|
||||
viewModel.backgroundColor = view.backgroundColor ?? .clear
|
||||
defer {
|
||||
|
@ -86,34 +60,14 @@ extension MastodonRegisterViewController {
|
|||
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(hostingViewController.view)
|
||||
hostingViewController.view.pinToParent()
|
||||
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = navigationActionView.frame.height
|
||||
self.viewModel.bottomPaddingHeight = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(MastodonRegisterViewController.backButtonPressed(_:)), for: .touchUpInside)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(MastodonRegisterViewController.nextButtonPressed(_:)), for: .touchUpInside)
|
||||
|
||||
|
||||
navigationItem.rightBarButtonItem = nextBarButtonItem()
|
||||
|
||||
viewModel.$isAllValid
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isAllValid in
|
||||
guard let self = self else { return }
|
||||
self.navigationActionView.nextButton.isEnabled = isAllValid
|
||||
self.navigationItem.rightBarButtonItem?.isEnabled = isAllValid
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -137,7 +91,7 @@ extension MastodonRegisterViewController {
|
|||
.sink { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
guard let error = error as? Mastodon.API.Error else { return }
|
||||
let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert)
|
||||
let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.SignUpFailure.title, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
_ = self.coordinator.present(
|
||||
|
@ -148,30 +102,27 @@ extension MastodonRegisterViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.avatarMediaMenuActionPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .photoLibrary:
|
||||
self.present(self.imagePicker, animated: true, completion: nil)
|
||||
case .camera:
|
||||
self.present(self.imagePickerController, animated: true, completion: nil)
|
||||
case .browse:
|
||||
self.present(self.documentPickerController, animated: true, completion: nil)
|
||||
case .delete:
|
||||
self.viewModel.avatarImage = nil
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.$isRegistering
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isRegistering in
|
||||
guard let self = self else { return }
|
||||
isRegistering ? self.navigationActionView.nextButton.showLoading() : self.navigationActionView.nextButton.stopLoading()
|
||||
|
||||
let rightBarButtonItem: UIBarButtonItem
|
||||
if isRegistering {
|
||||
self.activityIndicator.startAnimating()
|
||||
|
||||
rightBarButtonItem = UIBarButtonItem(customView: self.activityIndicator)
|
||||
rightBarButtonItem.isEnabled = false
|
||||
} else {
|
||||
self.activityIndicator.stopAnimating()
|
||||
|
||||
rightBarButtonItem = self.nextBarButtonItem()
|
||||
}
|
||||
self.navigationItem.rightBarButtonItem = rightBarButtonItem
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
title = L10n.Scene.Register.title
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -180,18 +131,8 @@ extension MastodonRegisterViewController {
|
|||
viewModel.viewDidAppear.send()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewController {
|
||||
|
||||
@objc private func backButtonPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
//MARK: - Actions
|
||||
@objc private func nextButtonPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
guard viewModel.isAllValid else { return }
|
||||
|
||||
guard !viewModel.isRegistering else { return }
|
||||
|
@ -304,16 +245,9 @@ extension MastodonRegisterViewController {
|
|||
let userToken = response.value
|
||||
let updateCredentialQuery: Mastodon.API.Account.UpdateCredentialQuery = {
|
||||
let displayName: String? = self.viewModel.name.isEmpty ? nil : self.viewModel.name
|
||||
let avatar: Mastodon.Query.MediaAttachment? = {
|
||||
guard let avatarImage = self.viewModel.avatarImage else { return nil }
|
||||
guard avatarImage.size.width <= MastodonRegisterViewController.avatarImageMaxSizeInPixel.width else {
|
||||
return .png(avatarImage.af.imageScaled(to: MastodonRegisterViewController.avatarImageMaxSizeInPixel).pngData())
|
||||
}
|
||||
return .png(avatarImage.pngData())
|
||||
}()
|
||||
return Mastodon.API.Account.UpdateCredentialQuery(
|
||||
displayName: displayName,
|
||||
avatar: avatar
|
||||
avatar: nil
|
||||
)
|
||||
}()
|
||||
let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken, updateCredentialQuery: updateCredentialQuery)
|
||||
|
@ -321,5 +255,4 @@ extension MastodonRegisterViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,11 +25,11 @@ final class MastodonRegisterViewModel: ObservableObject {
|
|||
let viewDidAppear = CurrentValueSubject<Void, Never>(Void())
|
||||
|
||||
@Published var backgroundColor: UIColor = Asset.Scene.Onboarding.background.color
|
||||
@Published var avatarImage: UIImage? = nil
|
||||
@Published var name = ""
|
||||
@Published var username = ""
|
||||
@Published var email = ""
|
||||
@Published var password = ""
|
||||
@Published var passwordConfirmation = ""
|
||||
@Published var reason = ""
|
||||
|
||||
@Published var usernameErrorPrompt: String? = nil
|
||||
|
@ -54,7 +54,6 @@ final class MastodonRegisterViewModel: ObservableObject {
|
|||
@Published var isAllValid = false
|
||||
@Published var error: Error? = nil
|
||||
|
||||
let avatarMediaMenuActionPublisher = PassthroughSubject<AvatarMediaMenuAction, Never>()
|
||||
let endEditing = PassthroughSubject<Void, Never>()
|
||||
|
||||
init(
|
||||
|
@ -151,10 +150,15 @@ final class MastodonRegisterViewModel: ObservableObject {
|
|||
.assign(to: \.emailValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$password
|
||||
.map { password in
|
||||
Publishers.CombineLatest($password, $passwordConfirmation)
|
||||
.map { password, confirmation in
|
||||
guard !password.isEmpty else { return .empty }
|
||||
return password.count >= 8 ? .valid : .invalid
|
||||
|
||||
if password.count >= 8 && password == confirmation {
|
||||
return .valid
|
||||
} else {
|
||||
return .invalid
|
||||
}
|
||||
}
|
||||
.assign(to: \.passwordValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
@ -281,52 +285,3 @@ extension MastodonRegisterViewModel {
|
|||
return attributeString
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
|
||||
enum AvatarMediaMenuAction {
|
||||
case photoLibrary
|
||||
case camera
|
||||
case browse
|
||||
case delete
|
||||
}
|
||||
|
||||
private func createAvatarMediaContextMenu() -> UIMenu {
|
||||
var children: [UIMenuElement] = []
|
||||
|
||||
// Photo Library
|
||||
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.avatarMediaMenuActionPublisher.send(.photoLibrary)
|
||||
}
|
||||
children.append(photoLibraryAction)
|
||||
|
||||
// Camera
|
||||
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.avatarMediaMenuActionPublisher.send(.camera)
|
||||
})
|
||||
children.append(cameraAction)
|
||||
}
|
||||
|
||||
// Browse
|
||||
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.avatarMediaMenuActionPublisher.send(.browse)
|
||||
}
|
||||
children.append(browseAction)
|
||||
|
||||
// Delete
|
||||
if avatarImage != nil {
|
||||
let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.avatarMediaMenuActionPublisher.send(.delete)
|
||||
}
|
||||
children.append(deleteAction)
|
||||
}
|
||||
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,18 +21,12 @@ final class ServerRulesTableViewCell: UITableViewCell {
|
|||
|
||||
let ruleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let separalerLine: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.System.separator.color
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
|
@ -49,12 +43,11 @@ extension ServerRulesTableViewCell {
|
|||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
indexImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(indexImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
indexImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: ServerRulesTableViewCell.margin),
|
||||
indexImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 11),
|
||||
indexImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: indexImageView.bottomAnchor, constant: ServerRulesTableViewCell.margin),
|
||||
indexImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
|
@ -65,21 +58,12 @@ extension ServerRulesTableViewCell {
|
|||
ruleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(ruleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
ruleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: ServerRulesTableViewCell.margin),
|
||||
ruleLabel.leadingAnchor.constraint(equalTo: indexImageView.trailingAnchor, constant: 16),
|
||||
ruleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: ruleLabel.bottomAnchor, constant: ServerRulesTableViewCell.margin),
|
||||
ruleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 11),
|
||||
ruleLabel.leadingAnchor.constraint(equalTo: indexImageView.trailingAnchor, constant: 14),
|
||||
ruleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor, constant: 16),
|
||||
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: ruleLabel.bottomAnchor, constant: 11),
|
||||
ruleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
])
|
||||
|
||||
separalerLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separalerLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separalerLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
separalerLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
separalerLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separalerLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,31 +26,15 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
|||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: MastodonServerRulesViewModel!
|
||||
|
||||
let stackView = UIStackView()
|
||||
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.register(ServerRulesTableViewCell.self, forCellReuseIdentifier: String(describing: ServerRulesTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.sectionHeaderTopPadding = 0
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
return navigationActionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonServerRulesViewController {
|
||||
|
@ -58,39 +42,18 @@ extension MastodonServerRulesViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem()
|
||||
|
||||
setupOnboardingAppearance()
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
tableView.pinToParent()
|
||||
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = navigationActionView.frame.height
|
||||
self.tableView.contentInset.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(tableView: tableView)
|
||||
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(MastodonServerRulesViewController.backButtonPressed(_:)), for: .touchUpInside)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(MastodonServerRulesViewController.nextButtonPressed(_:)), for: .touchUpInside)
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: L10n.Scene.ServerRules.Button.confirm, style: .done, target: self, action: #selector(MastodonServerRulesViewController.nextButtonPressed(_:)))
|
||||
title = L10n.Scene.ServerRules.title
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -102,25 +65,16 @@ extension MastodonServerRulesViewController {
|
|||
}
|
||||
|
||||
extension MastodonServerRulesViewController {
|
||||
|
||||
@objc private func backButtonPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func nextButtonPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
let viewModel = MastodonRegisterViewModel(
|
||||
context: context,
|
||||
domain: viewModel.domain,
|
||||
authenticateInfo: viewModel.authenticateInfo,
|
||||
instance: viewModel.instance,
|
||||
applicationToken: viewModel.applicationToken
|
||||
)
|
||||
_ = coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show)
|
||||
@objc private func nextButtonPressed(_ sender: UIButton) {
|
||||
let domain = viewModel.domain
|
||||
let viewModel = PrivacyViewModel(domain: domain, authenticateInfo: viewModel.authenticateInfo, rows: [.iOSApp, .server(domain: domain)], instance: viewModel.instance, applicationToken: viewModel.applicationToken)
|
||||
|
||||
_ = coordinator.present(scene: .mastodonPrivacyPolicies(viewModel: viewModel), from: self, transition: .show)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
|
@ -129,20 +83,24 @@ extension MastodonServerRulesViewController: OnboardingViewControllerAppearance
|
|||
// MARK: - UITableViewDelegate
|
||||
extension MastodonServerRulesViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource,
|
||||
section < diffableDataSource.snapshot().numberOfSections
|
||||
else { return .leastNonzeroMagnitude }
|
||||
|
||||
let sectionItem = diffableDataSource.snapshot().sectionIdentifiers[section]
|
||||
switch sectionItem {
|
||||
case .header:
|
||||
return .leastNonzeroMagnitude
|
||||
case .rules:
|
||||
return 16
|
||||
}
|
||||
let wrapper = UIView()
|
||||
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = MastodonPickServerViewController.subTitleFont
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
label.text = L10n.Scene.ServerRules.subtitle(viewModel.domain)
|
||||
wrapper.addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 16),
|
||||
label.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
||||
wrapper.trailingAnchor.constraint(equalTo: label.trailingAnchor),
|
||||
wrapper.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 16),
|
||||
])
|
||||
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ extension MastodonServerRulesViewModel {
|
|||
diffableDataSource = ServerRuleSection.tableViewDiffableDataSource(tableView: tableView)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ServerRuleSection, ServerRuleItem>()
|
||||
snapshot.appendSections([.header, .rules])
|
||||
snapshot.appendItems([.header(domain: domain)], toSection: .header)
|
||||
snapshot.appendSections([.rules])
|
||||
let ruleItems: [ServerRuleItem] = rules.enumerated().map { i, rule in
|
||||
let ruleContext = ServerRuleItem.RuleContext(index: i, rule: rule)
|
||||
return ServerRuleItem.rule(ruleContext)
|
||||
|
|
|
@ -36,27 +36,4 @@ final class MastodonServerRulesViewModel {
|
|||
self.instance = instance
|
||||
self.applicationToken = applicationToken
|
||||
}
|
||||
|
||||
var rulesAttributedString: NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "\n")
|
||||
let configuration = UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .title3))
|
||||
let separatorString = Array(repeating: " ", count: 4).joined()
|
||||
for (i, rule) in rules.enumerated() {
|
||||
guard i < 50 else {
|
||||
return NSAttributedString(string: "\(i)" + separatorString + rule.text.trimmingCharacters(in: .whitespacesAndNewlines) + "\n\n")
|
||||
}
|
||||
let imageName = String(i + 1) + ".circle.fill"
|
||||
let image = UIImage(systemName: imageName, withConfiguration: configuration)!
|
||||
let attachment = NSTextAttachment()
|
||||
attachment.image = image.withTintColor(Asset.Colors.Label.primary.color)
|
||||
let imageAttribute = NSMutableAttributedString(attachment: attachment)
|
||||
imageAttribute.addAttributes([NSAttributedString.Key.baselineOffset : -1.5], range: NSRange(location: 0, length: imageAttribute.length))
|
||||
|
||||
let ruleString = NSAttributedString(string: separatorString + rule.text.trimmingCharacters(in: .whitespacesAndNewlines) + "\n\n")
|
||||
attributedString.append(imageAttribute)
|
||||
attributedString.append(ruleString)
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import Foundation
|
|||
import MastodonSDK
|
||||
|
||||
enum ServerRuleItem: Hashable {
|
||||
case header(domain: String)
|
||||
case rule(RuleContext)
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import MastodonAsset
|
|||
import MastodonLocalization
|
||||
|
||||
enum ServerRuleSection: Hashable {
|
||||
case header
|
||||
case rules
|
||||
}
|
||||
|
||||
|
@ -20,14 +19,10 @@ extension ServerRuleSection {
|
|||
) -> UITableViewDiffableDataSource<ServerRuleSection, ServerRuleItem> {
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
||||
switch item {
|
||||
case .header(let domain):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell
|
||||
cell.titleLabel.text = L10n.Scene.ServerRules.title
|
||||
cell.subTitleLabel.text = L10n.Scene.ServerRules.subtitle(domain)
|
||||
return cell
|
||||
case .rule(let ruleContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ServerRulesTableViewCell.self), for: indexPath) as! ServerRulesTableViewCell
|
||||
cell.indexImageView.image = UIImage(systemName: "\(ruleContext.index + 1).circle.fill") ?? UIImage(systemName: "questionmark.circle.fill")
|
||||
cell.indexImageView.image = UIImage(systemName: "\(ruleContext.index + 1).circle") ?? UIImage(systemName: "questionmark.circle")
|
||||
cell.indexImageView.tintColor = Asset.Colors.Brand.lightBlurple.color
|
||||
cell.ruleLabel.text = ruleContext.rule.text
|
||||
return cell
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// OnboardingHeadlineTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class OnboardingHeadlineTableViewCell: UITableViewCell {
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = MastodonPickServerViewController.largeTitleFont
|
||||
label.textColor = MastodonPickServerViewController.largeTitleTextColor
|
||||
label.text = L10n.Scene.ServerPicker.title
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let subTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = MastodonPickServerViewController.subTitleFont
|
||||
label.textColor = MastodonPickServerViewController.subTitleTextColor
|
||||
label.text = L10n.Scene.ServerPicker.subtitle
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingHeadlineTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.spacing = 16
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(container)
|
||||
NSLayoutConstraint.activate([
|
||||
container.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11),
|
||||
])
|
||||
|
||||
container.addArrangedSubview(titleLabel)
|
||||
container.addArrangedSubview(subTitleLabel)
|
||||
}
|
||||
|
||||
}
|
|
@ -60,6 +60,7 @@ extension OnboardingViewControllerAppearance {
|
|||
func setupNavigationBarAppearance() {
|
||||
// use TransparentBackground so view push / dismiss will be more visual nature
|
||||
// please add opaque background for status bar manually if needs
|
||||
navigationController?.navigationBar.tintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// WelcomeContentPageView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 26.11.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class WelcomeContentCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
static let identifier = "WelcomeContentCollectionViewCell"
|
||||
|
||||
//TODO: Put in ScrollView?
|
||||
private let contentStackView: UIStackView
|
||||
private let titleView: UILabel
|
||||
private let label: UILabel
|
||||
private let blurryBackgroundView: UIVisualEffectView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
titleView = UILabel()
|
||||
titleView.font = WelcomeViewController.largeTitleFont
|
||||
titleView.textColor = WelcomeViewController.largeTitleTextColor.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light))
|
||||
titleView.adjustsFontForContentSizeCategory = true
|
||||
titleView.numberOfLines = 0
|
||||
|
||||
label = UILabel()
|
||||
label.font = WelcomeViewController.subTitleFont
|
||||
label.textColor = WelcomeViewController.largeTitleTextColor.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light))
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
|
||||
contentStackView = UIStackView(arrangedSubviews: [titleView, label, UIView()])
|
||||
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentStackView.axis = .vertical
|
||||
contentStackView.alignment = .leading
|
||||
contentStackView.spacing = 8
|
||||
|
||||
blurryBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight))
|
||||
blurryBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
blurryBackgroundView.applyCornerRadius(radius: 8)
|
||||
|
||||
blurryBackgroundView.contentView.addSubview(contentStackView)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(blurryBackgroundView)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
blurryBackgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurryBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
trailingAnchor.constraint(equalTo: blurryBackgroundView.trailingAnchor, constant: 16),
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: blurryBackgroundView.bottomAnchor),
|
||||
|
||||
contentStackView.topAnchor.constraint(equalTo: blurryBackgroundView.contentView.topAnchor, constant: 8),
|
||||
contentStackView.leadingAnchor.constraint(equalTo: blurryBackgroundView.contentView.leadingAnchor, constant: 8),
|
||||
blurryBackgroundView.contentView.trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor),
|
||||
blurryBackgroundView.contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor),
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
func update(with page: WelcomeContentPage) {
|
||||
titleView.attributedText = page.title
|
||||
label.text = page.content
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// WelcomeContentPage.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 26.11.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
|
||||
enum WelcomeContentPage: CaseIterable {
|
||||
case whatIsMastodon
|
||||
case mastodonIsLikeThat
|
||||
case howDoIPickAServer
|
||||
|
||||
var backgroundColor: UIColor {
|
||||
switch self {
|
||||
case .whatIsMastodon:
|
||||
return .green
|
||||
case .mastodonIsLikeThat:
|
||||
return .red
|
||||
case .howDoIPickAServer:
|
||||
return .blue
|
||||
}
|
||||
}
|
||||
|
||||
var title: NSAttributedString {
|
||||
switch self {
|
||||
case .whatIsMastodon:
|
||||
let image = Asset.Scene.Welcome.mastodonLogo.image
|
||||
let attachment = NSTextAttachment(image: image)
|
||||
let attributedString = NSMutableAttributedString(string: "\(L10n.Scene.Welcome.Education.WhatIsMastodon.title) ")
|
||||
|
||||
attachment.bounds = CGRect(
|
||||
x: 0,
|
||||
y: WelcomeViewController.largeTitleFont.descender - 5,
|
||||
width: image.size.width,
|
||||
height: image.size.height
|
||||
)
|
||||
|
||||
attributedString.append(NSAttributedString(attachment: attachment))
|
||||
attributedString.append(NSAttributedString(string: " ?"))
|
||||
return attributedString
|
||||
case .mastodonIsLikeThat:
|
||||
return NSAttributedString(string: L10n.Scene.Welcome.Education.MastodonIsLikeThat.title)
|
||||
case .howDoIPickAServer:
|
||||
return NSAttributedString(string: L10n.Scene.Welcome.Education.HowDoIPickAServer.title)
|
||||
}
|
||||
}
|
||||
|
||||
var content: String {
|
||||
switch self {
|
||||
case .whatIsMastodon:
|
||||
return L10n.Scene.Welcome.Education.WhatIsMastodon.description
|
||||
case .mastodonIsLikeThat:
|
||||
return L10n.Scene.Welcome.Education.MastodonIsLikeThat.description
|
||||
case .howDoIPickAServer:
|
||||
return L10n.Scene.Welcome.Education.HowDoIPickAServer.description
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -11,12 +11,18 @@ import MastodonCore
|
|||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
fileprivate extension CGFloat {
|
||||
static let cloudsStartPosition = -20.0
|
||||
static let centerHillStartPosition = 20.0
|
||||
static let airplaneStartPosition = -178.0
|
||||
static let leftHillStartPosition = 30.0
|
||||
static let rightHillStartPosition = -200.0
|
||||
static let leftHillSpeed = 6.0
|
||||
static let centerHillSpeed = 40.0
|
||||
static let rightHillSpeed = 6.0
|
||||
}
|
||||
|
||||
final class WelcomeIllustrationView: UIView {
|
||||
|
||||
let cloudBaseImageView = UIImageView()
|
||||
let rightHillImageView = UIImageView()
|
||||
let leftHillImageView = UIImageView()
|
||||
let centerHillImageView = UIImageView()
|
||||
|
||||
private let cloudBaseImage = Asset.Scene.Welcome.Illustration.cloudBase.image
|
||||
private let cloudBaseExtendImage = Asset.Scene.Welcome.Illustration.cloudBaseExtend.image
|
||||
|
@ -25,20 +31,50 @@ final class WelcomeIllustrationView: UIView {
|
|||
private let elephantThreeOnGrassImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrass.image
|
||||
private let elephantThreeOnGrassExtendImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassExtend.image
|
||||
|
||||
// layout outside
|
||||
var cloudsLeftAnchor: NSLayoutConstraint?
|
||||
var elephantOnAirplaneLeftConstraint: NSLayoutConstraint?
|
||||
var leftHillLeftConstraint: NSLayoutConstraint?
|
||||
var centerHillLeftConstraint: NSLayoutConstraint?
|
||||
var rightHillRightConstraint: NSLayoutConstraint?
|
||||
|
||||
let elephantOnAirplaneWithContrailImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
||||
var layout: Layout = .compact {
|
||||
didSet {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
let rightHillImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let leftHillImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let centerHillImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.elephantThreeOnGrass.image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let cloudBaseImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.Scene.Welcome.Illustration.cloudBase.image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.alpha = 0.3
|
||||
return imageView
|
||||
}()
|
||||
|
||||
var aspectLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -49,20 +85,9 @@ final class WelcomeIllustrationView: UIView {
|
|||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WelcomeIllustrationView {
|
||||
enum Layout {
|
||||
case compact
|
||||
case regular
|
||||
|
||||
var artworkImageSize: CGSize {
|
||||
switch self {
|
||||
case .compact: return CGSize(width: 375, height: 1500)
|
||||
case .regular: return CGSize(width: 547, height: 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
// modifiers for animations
|
||||
private lazy var cloudsDrag: CGFloat = -(bounds.width / 64)
|
||||
private lazy var airplaneDrag: CGFloat = bounds.width / 64
|
||||
}
|
||||
|
||||
extension WelcomeIllustrationView {
|
||||
|
@ -70,167 +95,92 @@ extension WelcomeIllustrationView {
|
|||
private func _init() {
|
||||
backgroundColor = Asset.Scene.Welcome.Illustration.backgroundCyan.color
|
||||
|
||||
let topPaddingView = UIView()
|
||||
|
||||
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(topPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
topPaddingView.topAnchor.constraint(equalTo: topAnchor),
|
||||
topPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
|
||||
cloudBaseImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(cloudBaseImageView)
|
||||
|
||||
let cloudsLeftAnchor = cloudBaseImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: .cloudsStartPosition)
|
||||
NSLayoutConstraint.activate([
|
||||
cloudBaseImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||
cloudBaseImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
cloudBaseImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
cloudsLeftAnchor,
|
||||
cloudBaseImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.4),
|
||||
cloudBaseImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
cloudBaseImageView.topAnchor.constraint(equalTo: topAnchor)
|
||||
])
|
||||
self.cloudsLeftAnchor = cloudsLeftAnchor
|
||||
|
||||
let rightHillRightConstraint = rightAnchor.constraint(equalTo: rightHillImageView.rightAnchor, constant: .rightHillStartPosition)
|
||||
addSubview(rightHillImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
rightHillImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.64),
|
||||
rightHillRightConstraint,
|
||||
bottomAnchor.constraint(equalTo: rightHillImageView.bottomAnchor, constant: 149),
|
||||
])
|
||||
self.rightHillRightConstraint = rightHillRightConstraint
|
||||
|
||||
let leftHillLeftConstraint = leftAnchor.constraint(equalTo: leftHillImageView.leftAnchor, constant: .leftHillStartPosition)
|
||||
addSubview(leftHillImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
leftHillImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.72),
|
||||
leftHillLeftConstraint,
|
||||
bottomAnchor.constraint(equalTo: leftHillImageView.bottomAnchor, constant: 187),
|
||||
])
|
||||
|
||||
[
|
||||
rightHillImageView,
|
||||
leftHillImageView,
|
||||
centerHillImageView,
|
||||
].forEach { imageView in
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(imageView)
|
||||
imageView.pinTo(to: cloudBaseImageView)
|
||||
}
|
||||
self.leftHillLeftConstraint = leftHillLeftConstraint
|
||||
|
||||
aspectLayoutConstraint = cloudBaseImageView.widthAnchor.constraint(equalTo: cloudBaseImageView.heightAnchor, multiplier: layout.artworkImageSize.width / layout.artworkImageSize.height)
|
||||
aspectLayoutConstraint.isActive = true
|
||||
let centerHillLeftConstraint = leftAnchor.constraint(equalTo: centerHillImageView.leftAnchor, constant: .centerHillStartPosition)
|
||||
|
||||
addSubview(centerHillImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
centerHillLeftConstraint,
|
||||
bottomAnchor.constraint(equalTo: centerHillImageView.bottomAnchor, constant: -18),
|
||||
centerHillImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.2),
|
||||
])
|
||||
|
||||
self.centerHillLeftConstraint = centerHillLeftConstraint
|
||||
|
||||
addSubview(elephantOnAirplaneWithContrailImageView)
|
||||
|
||||
let elephantOnAirplaneLeftConstraint = elephantOnAirplaneWithContrailImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: .airplaneStartPosition) // add 12pt bleeding
|
||||
NSLayoutConstraint.activate([
|
||||
elephantOnAirplaneLeftConstraint,
|
||||
elephantOnAirplaneWithContrailImageView.bottomAnchor.constraint(equalTo: leftHillImageView.topAnchor),
|
||||
// make a little bit large
|
||||
elephantOnAirplaneWithContrailImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.84),
|
||||
])
|
||||
|
||||
self.elephantOnAirplaneLeftConstraint = elephantOnAirplaneLeftConstraint
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
func setup() {
|
||||
|
||||
switch layout {
|
||||
case .compact:
|
||||
layoutCompact()
|
||||
case .regular:
|
||||
layoutRegular()
|
||||
// set illustration
|
||||
guard superview == nil else {
|
||||
return
|
||||
}
|
||||
contentMode = .scaleAspectFit
|
||||
|
||||
aspectLayoutConstraint.isActive = false
|
||||
aspectLayoutConstraint = cloudBaseImageView.widthAnchor.constraint(equalTo: cloudBaseImageView.heightAnchor, multiplier: layout.artworkImageSize.width / layout.artworkImageSize.height)
|
||||
aspectLayoutConstraint.isActive = true
|
||||
cloudBaseImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -5, maxX: 5, minY: -5, maxY: 5)
|
||||
)
|
||||
rightHillImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -15, maxX: 25, minY: -10, maxY: 10)
|
||||
)
|
||||
leftHillImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -25, maxX: 15, minY: -15, maxY: 15)
|
||||
)
|
||||
centerHillImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -14, maxX: 14, minY: -5, maxY: 25)
|
||||
)
|
||||
|
||||
elephantOnAirplaneWithContrailImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -20, maxX: 12, minY: -20, maxY: 12) // maxX should not larger then the bleeding (12pt)
|
||||
)
|
||||
}
|
||||
|
||||
private func layoutCompact() {
|
||||
let size = layout.artworkImageSize
|
||||
let width = size.width
|
||||
let height = size.height
|
||||
|
||||
cloudBaseImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw cloud
|
||||
cloudBaseImage.draw(at: CGPoint(x: 0, y: height - cloudBaseImage.size.height))
|
||||
}
|
||||
|
||||
rightHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw elephantThreeOnGrassWithTreeTwoImage
|
||||
// elephantThreeOnGrassWithTreeTwo.bottomY - 25 align to elephantThreeOnGrassImage.centerY
|
||||
elephantThreeOnGrassWithTreeTwoImage.draw(at: CGPoint(x: width - elephantThreeOnGrassWithTreeTwoImage.size.width, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeTwoImage.size.height + 25))
|
||||
}
|
||||
|
||||
leftHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw elephantThreeOnGrassWithTreeThree
|
||||
// elephantThreeOnGrassWithTreeThree.bottomY + 30 align to elephantThreeOnGrassImage.centerY
|
||||
elephantThreeOnGrassWithTreeThreeImage.draw(at: CGPoint(x: 0, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeThreeImage.size.height - 30))
|
||||
}
|
||||
|
||||
centerHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw elephantThreeOnGrass
|
||||
elephantThreeOnGrassImage.draw(at: CGPoint(x: 0, y: height - elephantThreeOnGrassImage.size.height))
|
||||
}
|
||||
func update(contentOffset: CGFloat) {
|
||||
cloudsLeftAnchor?.constant = contentOffset / cloudsDrag + .cloudsStartPosition
|
||||
elephantOnAirplaneLeftConstraint?.constant = contentOffset / airplaneDrag + .airplaneStartPosition
|
||||
leftHillLeftConstraint?.constant = contentOffset / .leftHillSpeed + .leftHillStartPosition
|
||||
centerHillLeftConstraint?.constant = contentOffset / .centerHillSpeed + .centerHillStartPosition
|
||||
rightHillRightConstraint?.constant = contentOffset / .rightHillSpeed + .rightHillStartPosition
|
||||
}
|
||||
|
||||
private func layoutRegular() {
|
||||
let size = layout.artworkImageSize
|
||||
let width = size.width
|
||||
let height = size.height
|
||||
|
||||
cloudBaseImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw cloud
|
||||
cloudBaseExtendImage.draw(at: CGPoint(x: 0, y: height - cloudBaseExtendImage.size.height))
|
||||
|
||||
rightHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw elephantThreeOnGrassWithTreeTwoImage
|
||||
// elephantThreeOnGrassWithTreeTwo.bottomY - 25 align to elephantThreeOnGrassImage.centerY
|
||||
elephantThreeOnGrassWithTreeTwoImage.draw(at: CGPoint(x: width - elephantThreeOnGrassWithTreeTwoImage.size.width, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeTwoImage.size.height - 20))
|
||||
}
|
||||
|
||||
leftHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw elephantThreeOnGrassWithTreeThree
|
||||
// elephantThreeOnGrassWithTreeThree.bottomY + 30 align to elephantThreeOnGrassImage.centerY
|
||||
elephantThreeOnGrassWithTreeThreeImage.draw(at: CGPoint(x: -160, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeThreeImage.size.height - 80))
|
||||
}
|
||||
|
||||
centerHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
|
||||
// clear background
|
||||
UIColor.clear.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
|
||||
// draw elephantThreeOnGrass
|
||||
elephantThreeOnGrassExtendImage.draw(at: CGPoint(x: 0, y: height - elephantThreeOnGrassExtendImage.size.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct WelcomeIllustrationView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
let view = WelcomeIllustrationView()
|
||||
view.layout = .compact
|
||||
return view
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 1500))
|
||||
UIViewPreview(width: 547) {
|
||||
let view = WelcomeIllustrationView()
|
||||
view.layout = .regular
|
||||
return view
|
||||
}
|
||||
.previewLayout(.fixed(width: 547, height: 1500))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by BradGao on 2021/2/20.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
|
@ -14,7 +13,9 @@ import MastodonLocalization
|
|||
|
||||
final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
let logger = Logger(subsystem: "WelcomeViewController", category: "ViewController")
|
||||
private enum Constants {
|
||||
static let topAnchorInset: CGFloat = 20
|
||||
}
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -24,29 +25,11 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
|||
private(set) lazy var viewModel = WelcomeViewModel(context: context)
|
||||
|
||||
let welcomeIllustrationView = WelcomeIllustrationView()
|
||||
var welcomeIllustrationViewBottomAnchorLayoutConstraint: NSLayoutConstraint?
|
||||
|
||||
private(set) lazy var dismissBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(WelcomeViewController.dismissBarButtonItemDidPressed(_:)))
|
||||
|
||||
private(set) lazy var logoImageView: UIImageView = {
|
||||
let image = Asset.Scene.Welcome.mastodonLogo.image
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private(set) lazy var sloganLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Scene.Welcome.slogan
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let buttonContainer = UIStackView()
|
||||
let educationPages: [WelcomeContentPage] = [.whatIsMastodon, .mastodonIsLikeThat, .howDoIPickAServer]
|
||||
|
||||
private(set) lazy var signUpButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton()
|
||||
|
@ -64,27 +47,40 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
|||
}()
|
||||
let signUpButtonShadowView = UIView()
|
||||
|
||||
private(set) lazy var signInButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton()
|
||||
button.adjustsBackgroundImageWhenUserInterfaceStyleChanges = false
|
||||
private(set) lazy var signInButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.contentEdgeInsets = WelcomeViewController.actionButtonPadding
|
||||
button.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||
button.setTitle(L10n.Scene.Welcome.logIn, for: .normal)
|
||||
let backgroundImageColor = Asset.Scene.Welcome.signInButtonBackground.color
|
||||
let backgroundImageHighlightedColor = Asset.Scene.Welcome.signInButtonBackground.color.withAlphaComponent(0.8)
|
||||
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
|
||||
button.setBackgroundImage(.placeholder(color: backgroundImageHighlightedColor), for: .highlighted)
|
||||
let titleColor: UIColor = UIColor.white.withAlphaComponent(0.9)
|
||||
button.setTitleColor(titleColor, for: .normal)
|
||||
button.setTitleColor(titleColor.withAlphaComponent(0.3), for: .highlighted)
|
||||
return button
|
||||
}()
|
||||
let signInButtonShadowView = UIView()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
private(set) lazy var pageCollectionView: UICollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.scrollDirection = .horizontal
|
||||
flowLayout.minimumLineSpacing = 0
|
||||
flowLayout.itemSize = CGSize(width: self.view.frame.width, height: 400)
|
||||
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.isPagingEnabled = true
|
||||
collectionView.backgroundColor = nil
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.bounces = false
|
||||
collectionView.register(WelcomeContentCollectionViewCell.self, forCellWithReuseIdentifier: WelcomeContentCollectionViewCell.identifier)
|
||||
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private(set) var pageControl: UIPageControl = {
|
||||
let pageControl = UIPageControl(frame: .zero)
|
||||
pageControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
return pageControl
|
||||
}()
|
||||
}
|
||||
|
||||
extension WelcomeViewController {
|
||||
|
@ -96,11 +92,19 @@ extension WelcomeViewController {
|
|||
preferredContentSize = CGSize(width: 547, height: 678)
|
||||
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
view.overrideUserInterfaceStyle = .light
|
||||
|
||||
setupOnboardingAppearance()
|
||||
setupIllustrationLayout()
|
||||
|
||||
view.addSubview(welcomeIllustrationView)
|
||||
welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
welcomeIllustrationView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
welcomeIllustrationView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: welcomeIllustrationView.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: welcomeIllustrationView.bottomAnchor)
|
||||
])
|
||||
|
||||
buttonContainer.axis = .vertical
|
||||
buttonContainer.spacing = 12
|
||||
|
@ -130,14 +134,32 @@ extension WelcomeViewController {
|
|||
buttonContainer.sendSubviewToBack(signUpButtonShadowView)
|
||||
signUpButtonShadowView.pinTo(to: signUpButton)
|
||||
|
||||
signInButtonShadowView.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonContainer.addSubview(signInButtonShadowView)
|
||||
buttonContainer.sendSubviewToBack(signInButtonShadowView)
|
||||
signInButtonShadowView.pinTo(to: signInButton)
|
||||
|
||||
signUpButton.addTarget(self, action: #selector(signUpButtonDidClicked(_:)), for: .touchUpInside)
|
||||
signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside)
|
||||
|
||||
pageCollectionView.delegate = self
|
||||
pageCollectionView.dataSource = self
|
||||
view.addSubview(pageCollectionView)
|
||||
|
||||
pageControl.numberOfPages = self.educationPages.count
|
||||
pageControl.addTarget(self, action: #selector(WelcomeViewController.pageControlDidChange(_:)), for: .valueChanged)
|
||||
view.addSubview(pageControl)
|
||||
|
||||
let scrollView = pageCollectionView as UIScrollView
|
||||
scrollView.delegate = self
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
pageCollectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: computedTopAnchorInset),
|
||||
pageCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: pageCollectionView.trailingAnchor),
|
||||
pageControl.topAnchor.constraint(equalTo: pageCollectionView.bottomAnchor, constant: 16),
|
||||
|
||||
pageControl.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: pageControl.trailingAnchor),
|
||||
buttonContainer.topAnchor.constraint(equalTo: pageControl.bottomAnchor, constant: 16),
|
||||
])
|
||||
|
||||
|
||||
viewModel.$needsShowDismissEntry
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] needsShowDismissEntry in
|
||||
|
@ -149,7 +171,7 @@ extension WelcomeViewController {
|
|||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
|
||||
setupButtonShadowView()
|
||||
}
|
||||
|
||||
|
@ -161,18 +183,27 @@ extension WelcomeViewController {
|
|||
if view.safeAreaInsets.bottom == 0 {
|
||||
overlap += 56
|
||||
}
|
||||
welcomeIllustrationViewBottomAnchorLayoutConstraint?.constant = overlap
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
|
||||
view.layoutIfNeeded()
|
||||
|
||||
|
||||
setupIllustrationLayout()
|
||||
setupButtonShadowView()
|
||||
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.scrollDirection = .horizontal
|
||||
flowLayout.minimumLineSpacing = 0
|
||||
flowLayout.itemSize = CGSize(width: self.view.frame.width, height: 400)
|
||||
|
||||
pageCollectionView.setCollectionViewLayout(flowLayout, animated: true)
|
||||
}
|
||||
|
||||
private var computedTopAnchorInset: CGFloat {
|
||||
(navigationController?.navigationBar.bounds.height ?? UINavigationBar().bounds.height) + Constants.topAnchorInset
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WelcomeViewController {
|
||||
|
@ -189,17 +220,6 @@ extension WelcomeViewController {
|
|||
byRoundingCorners: .allCorners,
|
||||
cornerRadii: CGSize(width: 10, height: 10)
|
||||
)
|
||||
signInButtonShadowView.layer.setupShadow(
|
||||
color: .black,
|
||||
alpha: 0.25,
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
roundedRect: signInButtonShadowView.bounds,
|
||||
byRoundingCorners: .allCorners,
|
||||
cornerRadii: CGSize(width: 10, height: 10)
|
||||
)
|
||||
}
|
||||
|
||||
private func updateButtonContainerLayoutMargins(traitCollection: UITraitCollection) {
|
||||
|
@ -223,94 +243,13 @@ extension WelcomeViewController {
|
|||
}
|
||||
|
||||
private func setupIllustrationLayout() {
|
||||
welcomeIllustrationView.layout = {
|
||||
switch traitCollection.userInterfaceIdiom {
|
||||
case .phone:
|
||||
return .compact
|
||||
default:
|
||||
return .regular
|
||||
}
|
||||
}()
|
||||
|
||||
// set logo
|
||||
if logoImageView.superview == nil {
|
||||
view.addSubview(logoImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
logoImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
logoImageView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 35),
|
||||
view.readableContentGuide.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor, constant: 35),
|
||||
logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 75.0/269.0),
|
||||
])
|
||||
logoImageView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
}
|
||||
|
||||
// set illustration
|
||||
guard welcomeIllustrationView.superview == nil else {
|
||||
return
|
||||
}
|
||||
welcomeIllustrationView.contentMode = .scaleAspectFit
|
||||
|
||||
welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false
|
||||
welcomeIllustrationViewBottomAnchorLayoutConstraint = welcomeIllustrationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 5)
|
||||
|
||||
view.addSubview(welcomeIllustrationView)
|
||||
NSLayoutConstraint.activate([
|
||||
view.leftAnchor.constraint(equalTo: welcomeIllustrationView.leftAnchor, constant: 15),
|
||||
welcomeIllustrationView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 15),
|
||||
welcomeIllustrationViewBottomAnchorLayoutConstraint!.priority(.required - 1),
|
||||
])
|
||||
|
||||
welcomeIllustrationView.cloudBaseImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -5, maxX: 5, minY: -5, maxY: 5)
|
||||
)
|
||||
welcomeIllustrationView.rightHillImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -15, maxX: 25, minY: -10, maxY: 10)
|
||||
)
|
||||
welcomeIllustrationView.leftHillImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -25, maxX: 15, minY: -15, maxY: 15)
|
||||
)
|
||||
welcomeIllustrationView.centerHillImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -14, maxX: 14, minY: -5, maxY: 25)
|
||||
)
|
||||
|
||||
let topPaddingView = UIView()
|
||||
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(topPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
topPaddingView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor),
|
||||
topPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
|
||||
topPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
|
||||
])
|
||||
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(welcomeIllustrationView.elephantOnAirplaneWithContrailImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
view.leftAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.leftAnchor, constant: 12), // add 12pt bleeding
|
||||
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||
// make a little bit large
|
||||
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.84),
|
||||
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.heightAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor, multiplier: 105.0/318.0),
|
||||
])
|
||||
let bottomPaddingView = UIView()
|
||||
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(bottomPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
bottomPaddingView.topAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.bottomAnchor),
|
||||
bottomPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
|
||||
bottomPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
|
||||
bottomPaddingView.bottomAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 4),
|
||||
])
|
||||
|
||||
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.addMotionEffect(
|
||||
UIInterpolatingMotionEffect.motionEffect(minX: -20, maxX: 12, minY: -20, maxY: 12) // maxX should not larger then the bleeding (12pt)
|
||||
)
|
||||
|
||||
view.bringSubviewToFront(logoImageView)
|
||||
view.bringSubviewToFront(sloganLabel)
|
||||
welcomeIllustrationView.setup()
|
||||
}
|
||||
}
|
||||
|
||||
extension WelcomeViewController {
|
||||
|
||||
//MARK: - Actions
|
||||
@objc
|
||||
private func signUpButtonDidClicked(_ sender: UIButton) {
|
||||
_ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context)), from: self, transition: .show)
|
||||
|
@ -325,27 +264,24 @@ extension WelcomeViewController {
|
|||
private func dismissBarButtonItemDidPressed(_ sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
extension WelcomeViewController: OnboardingViewControllerAppearance {
|
||||
func setupNavigationBarAppearance() {
|
||||
// always transparent
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
navigationItem.compactScrollEdgeAppearance = barAppearance
|
||||
@objc
|
||||
private func pageControlDidChange(_ sender: UIPageControl) {
|
||||
let item = sender.currentPage
|
||||
let indexPath = IndexPath(item: item, section: 0)
|
||||
|
||||
pageCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
extension WelcomeViewController: OnboardingViewControllerAppearance {}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
|
||||
// update button layout
|
||||
updateButtonContainerLayoutMargins(traitCollection: traitCollection)
|
||||
|
||||
|
@ -376,3 +312,38 @@ extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UIScrollViewDelegate
|
||||
extension WelcomeViewController: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let contentOffset = scrollView.contentOffset.x
|
||||
welcomeIllustrationView.update(contentOffset: contentOffset)
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
pageControl.currentPage = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width)
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
pageControl.currentPage = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UICollectionViewDelegate
|
||||
extension WelcomeViewController: UICollectionViewDelegate { }
|
||||
|
||||
//MARK: - UICollectionViewDataSource
|
||||
extension WelcomeViewController: UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
educationPages.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: WelcomeContentCollectionViewCell.identifier, for: indexPath) as? WelcomeContentCollectionViewCell else { fatalError("WTF? Wrong cell?") }
|
||||
|
||||
let page = educationPages[indexPath.item]
|
||||
cell.update(with: page)
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,6 @@ let package = Package(
|
|||
.package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "2.2.5"),
|
||||
.package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"),
|
||||
.package(url: "https://github.com/uias/Tabman", from: "2.13.0"),
|
||||
.package(url: "https://github.com/vtourraine/ThirdPartyMailer.git", from: "2.1.0"),
|
||||
.package(url: "https://github.com/woxtu/UIHostingConfigurationBackport.git", from: "0.1.0"),
|
||||
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"),
|
||||
.package(url: "https://github.com/eneko/Stripes.git", from: "0.2.0"),
|
||||
|
@ -126,7 +125,6 @@ let package = Package(
|
|||
.product(name: "UITextView+Placeholder", package: "UITextView-Placeholder"),
|
||||
.product(name: "UIHostingConfigurationBackport", package: "UIHostingConfigurationBackport"),
|
||||
.product(name: "TabBarPager", package: "TabBarPager"),
|
||||
.product(name: "ThirdPartyMailer", package: "ThirdPartyMailer"),
|
||||
.product(name: "OrderedCollections", package: "swift-collections"),
|
||||
.product(name: "Tabman", package: "Tabman"),
|
||||
.product(name: "MetaTextKit", package: "MetaTextKit"),
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFB",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x55"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFA",
|
||||
"green" : "0x8A",
|
||||
"red" : "0x85"
|
||||
}
|
||||
},
|
||||
"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" : "0xFA",
|
||||
"green" : "0x8A",
|
||||
"red" : "0x85"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.353",
|
||||
"green" : "0.251",
|
||||
"red" : "0.875"
|
||||
"blue" : "0x30",
|
||||
"green" : "0x3B",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 132 KiB |
|
@ -1,8 +1,19 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo.small.pdf",
|
||||
"idiom" : "universal"
|
||||
"filename" : "wordmark-white-text.01e9f493 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wordmark-white-text.01e9f493 1@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wordmark-white-text.01e9f493 1@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
|
|
@ -1,648 +0,0 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 2 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << >>
|
||||
/BBox [ 0.000000 0.000000 269.000000 75.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 4.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
0.000000 67.000000 m
|
||||
261.000000 67.000000 l
|
||||
261.000000 0.000000 l
|
||||
0.000000 0.000000 l
|
||||
0.000000 67.000000 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
234
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
<< /Length 4 0 R
|
||||
/Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ]
|
||||
/Domain [ 0.000000 1.000000 ]
|
||||
/FunctionType 4
|
||||
>>
|
||||
stream
|
||||
{ 0.388235 exch 0.392157 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.050980 mul 0.388235 add exch dup 0.000000 sub -0.164706 mul 0.392157 add exch dup 0.000000 sub -0.200000 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.337255 exch 0.227451 exch 0.800000 exch } if pop }
|
||||
endstream
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
339
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 6 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << /Pattern << /P1 << /Matrix [ 0.000000 -65.993195 65.993195 0.000000 -61.993195 70.224548 ]
|
||||
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
|
||||
/ColorSpace /DeviceRGB
|
||||
/Function 3 0 R
|
||||
/Domain [ 0.000000 1.000000 ]
|
||||
/ShadingType 2
|
||||
/Extend [ true true ]
|
||||
>>
|
||||
/PatternType 2
|
||||
/Type /Pattern
|
||||
>> >> >>
|
||||
/BBox [ 0.000000 0.000000 269.000000 75.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 3.632446 cm
|
||||
/Pattern cs
|
||||
/P1 scn
|
||||
60.826767 51.982300 m
|
||||
59.885666 59.068882 53.787518 64.663071 46.568073 65.739265 c
|
||||
45.346546 65.922020 40.730431 66.592102 30.036348 66.592102 c
|
||||
29.956215 66.592102 l
|
||||
19.252211 66.592102 16.959164 65.922020 15.737550 65.739265 c
|
||||
8.708311 64.683380 2.299911 59.667900 0.737860 52.489872 c
|
||||
-0.003115 48.956699 -0.083220 45.037697 0.056964 41.443653 c
|
||||
0.257227 36.286026 0.297280 31.148746 0.757885 26.011383 c
|
||||
1.078305 22.600037 1.629032 19.219128 2.420071 15.889084 c
|
||||
3.902017 9.736488 9.889900 4.619389 15.757648 2.538147 c
|
||||
22.035824 0.365425 28.794806 0.000015 35.263187 1.492470 c
|
||||
35.974140 1.665001 36.675091 1.857872 37.376038 2.081261 c
|
||||
38.948124 2.588921 40.790466 3.157455 42.152252 4.152424 c
|
||||
42.172348 4.162594 42.182354 4.182858 42.192364 4.203201 c
|
||||
42.202370 4.223465 42.212376 4.243816 42.212376 4.274334 c
|
||||
42.212376 9.249176 l
|
||||
42.212376 9.249176 42.212376 9.289791 42.192364 9.310051 c
|
||||
42.192364 9.330315 42.172348 9.350658 42.152252 9.360832 c
|
||||
42.132240 9.370922 42.112228 9.381180 42.092300 9.391270 c
|
||||
42.072121 9.391270 42.052189 9.391270 42.032177 9.391270 c
|
||||
37.886612 8.386127 33.631145 7.878468 29.375511 7.888641 c
|
||||
22.035824 7.888641 20.063231 11.421898 19.502539 12.883831 c
|
||||
19.051918 14.152893 18.761566 15.482990 18.641405 16.823097 c
|
||||
18.641405 16.843441 18.641405 16.863705 18.651411 16.884052 c
|
||||
18.651411 16.904316 18.671425 16.924664 18.691521 16.934837 c
|
||||
18.711451 16.944927 18.731462 16.955097 18.751476 16.965271 c
|
||||
18.821604 16.965271 l
|
||||
22.896957 15.970303 27.082464 15.462643 31.277977 15.462643 c
|
||||
32.289371 15.462643 33.290596 15.462646 34.301991 15.493084 c
|
||||
38.517517 15.614994 42.963440 15.828209 47.118843 16.650486 c
|
||||
47.218906 16.670830 47.329144 16.691097 47.419201 16.711441 c
|
||||
53.967884 17.990677 60.196030 21.990898 60.826767 32.123367 c
|
||||
60.846947 32.519287 60.906982 36.306290 60.906982 36.712379 c
|
||||
60.906982 38.123699 61.357521 46.692589 60.836857 51.961868 c
|
||||
60.826767 51.982300 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 16.353134 47.918640 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
0.000000 3.736245 m
|
||||
0.000000 5.807401 1.632209 7.472382 3.654834 7.472382 c
|
||||
5.677542 7.472382 7.309585 5.797228 7.309585 3.736245 c
|
||||
7.309585 1.675262 5.677542 0.000028 3.654834 0.000028 c
|
||||
1.632209 0.000028 0.000000 1.675262 0.000000 3.736245 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 32.524460 30.973694 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
38.200230 17.462723 m
|
||||
38.200230 0.284256 l
|
||||
31.541471 0.284256 l
|
||||
31.541471 16.955149 l
|
||||
31.541471 20.467976 30.099640 22.244776 27.205791 22.244776 c
|
||||
24.011585 22.244776 22.399469 20.122755 22.399469 15.950090 c
|
||||
22.399469 6.822678 l
|
||||
15.790834 6.822678 l
|
||||
15.790834 15.950090 l
|
||||
15.790834 20.143185 14.198734 22.244776 10.984432 22.244776 c
|
||||
8.100758 22.244776 6.648746 20.467976 6.648746 16.955149 c
|
||||
6.648746 0.294346 l
|
||||
0.000000 0.294346 l
|
||||
0.000000 17.462723 l
|
||||
0.000000 20.965542 0.871139 23.757576 2.623425 25.828648 c
|
||||
4.435832 27.899887 6.808930 28.945639 9.742804 28.945639 c
|
||||
13.147311 28.945639 15.730712 27.605450 17.432966 24.925154 c
|
||||
19.095194 22.082256 l
|
||||
20.757338 24.925154 l
|
||||
22.459507 27.595276 25.032986 28.945639 28.447416 28.945639 c
|
||||
31.381290 28.945639 33.754387 27.889629 35.566792 25.828648 c
|
||||
37.319077 23.757576 38.190220 20.985806 38.190220 17.462723 c
|
||||
38.200230 17.462723 l
|
||||
h
|
||||
61.110268 8.924355 m
|
||||
62.491982 10.416807 63.143234 12.264570 63.143234 14.518509 c
|
||||
63.143234 16.772449 62.481976 18.640476 61.110268 20.061882 c
|
||||
59.788589 21.554337 58.106689 22.265121 56.073723 22.265121 c
|
||||
54.041008 22.265121 52.368858 21.554337 51.037174 20.061882 c
|
||||
49.715416 18.640476 49.054577 16.772449 49.054577 14.518509 c
|
||||
49.054577 12.264570 49.715416 10.396461 51.037174 8.924355 c
|
||||
52.358852 7.502947 54.041008 6.781986 56.073723 6.781986 c
|
||||
58.106689 6.781986 59.778584 7.492773 61.110268 8.924355 c
|
||||
h
|
||||
63.143234 28.255198 m
|
||||
69.701591 28.255198 l
|
||||
69.701591 0.781738 l
|
||||
63.143234 0.781738 l
|
||||
63.143234 4.020473 l
|
||||
61.160301 1.330006 58.416882 -0.000011 54.852188 -0.000011 c
|
||||
51.287495 -0.000011 48.543831 1.370613 46.110611 4.172821 c
|
||||
43.717499 6.974941 42.505974 10.437069 42.505974 14.498163 c
|
||||
42.505974 18.559340 43.727505 21.970684 46.110611 24.772808 c
|
||||
48.553837 27.574930 51.457687 28.996338 54.852188 28.996338 c
|
||||
58.246773 28.996338 61.160301 27.676495 63.143234 24.996117 c
|
||||
63.143234 28.234852 l
|
||||
63.143234 28.255198 l
|
||||
h
|
||||
91.770676 15.036257 m
|
||||
93.703575 13.543886 94.665031 11.462475 94.615005 8.832880 c
|
||||
94.615005 6.030758 93.653549 3.827599 91.670616 2.304710 c
|
||||
89.688515 0.812176 87.294495 0.050774 84.390968 0.050774 c
|
||||
79.154297 0.050774 75.599525 2.253845 73.716660 6.579025 c
|
||||
79.405289 10.041067 l
|
||||
80.164940 7.685646 81.837677 6.467285 84.390968 6.467285 c
|
||||
86.734138 6.467285 87.895714 7.228771 87.895714 8.822790 c
|
||||
87.895714 9.980196 86.373070 11.025948 83.269424 11.838133 c
|
||||
82.097839 12.163090 81.127220 12.498138 80.375908 12.772230 c
|
||||
79.303558 13.208757 78.392975 13.706240 77.631653 14.315380 c
|
||||
75.749619 15.807833 74.789009 17.777590 74.789009 20.305622 c
|
||||
74.789009 22.996006 75.699593 25.138290 77.531593 26.681526 c
|
||||
79.415298 28.275459 81.706749 29.037029 84.451004 29.037029 c
|
||||
88.826294 29.037029 92.020851 27.118137 94.113853 23.219398 c
|
||||
88.526115 19.929964 l
|
||||
87.715591 21.798073 86.333038 22.732088 84.451004 22.732088 c
|
||||
82.468071 22.732088 81.506630 21.970686 81.506630 20.478231 c
|
||||
81.506630 19.320744 83.029274 18.274992 86.132919 17.462723 c
|
||||
88.526115 16.914539 90.408142 16.092180 91.770676 15.036257 c
|
||||
91.780693 15.036257 l
|
||||
91.770676 15.036257 l
|
||||
h
|
||||
112.618164 21.452854 m
|
||||
106.870323 21.452854 l
|
||||
106.870323 10.020803 l
|
||||
106.870323 8.650179 107.381485 7.817646 108.352104 7.441906 c
|
||||
109.063393 7.167816 110.485130 7.117115 112.628166 7.218597 c
|
||||
112.628166 0.791912 l
|
||||
108.212013 0.243645 105.008308 0.690430 103.126274 2.162537 c
|
||||
101.243401 3.583942 100.331985 6.233803 100.331985 10.010632 c
|
||||
100.331985 21.452854 l
|
||||
95.915833 21.452854 l
|
||||
95.915833 28.265369 l
|
||||
100.331985 28.265369 l
|
||||
100.331985 33.808784 l
|
||||
106.890335 35.951027 l
|
||||
106.890335 28.255198 l
|
||||
112.638176 28.255198 l
|
||||
112.638176 21.442680 l
|
||||
112.628166 21.442680 l
|
||||
112.618164 21.452854 l
|
||||
h
|
||||
133.525681 9.086706 m
|
||||
134.847351 10.508114 135.507782 12.325527 135.507782 14.528599 c
|
||||
135.507782 16.731840 134.847351 18.528820 133.525681 19.970573 c
|
||||
132.193161 21.391897 130.571289 22.112776 128.589188 22.112776 c
|
||||
126.606262 22.112776 124.984390 21.402071 123.651871 19.970573 c
|
||||
122.381058 18.478121 121.719803 16.680973 121.719803 14.528599 c
|
||||
121.719803 12.376308 122.381058 10.579159 123.651871 9.086706 c
|
||||
124.974380 7.665382 126.606262 6.944508 128.589188 6.944508 c
|
||||
130.571289 6.944508 132.193161 7.655127 133.525681 9.086706 c
|
||||
h
|
||||
119.037262 4.203255 m
|
||||
116.443100 7.005379 115.171455 10.406551 115.171455 14.528599 c
|
||||
115.171455 18.650732 116.443100 22.001038 119.037262 24.803242 c
|
||||
121.629745 27.605450 124.834297 29.026855 128.589188 29.026855 c
|
||||
132.343262 29.026855 135.558640 27.605450 138.141953 24.803242 c
|
||||
140.725266 22.001038 142.056961 18.538994 142.056961 14.528599 c
|
||||
142.056961 10.518290 140.725266 7.005379 138.141953 4.203255 c
|
||||
135.547806 1.401051 132.394119 0.030510 128.589188 0.030510 c
|
||||
124.784264 0.030510 121.619743 1.401051 119.037262 4.203255 c
|
||||
h
|
||||
163.985123 8.934444 m
|
||||
165.306808 10.426897 165.968063 12.274660 165.968063 14.528599 c
|
||||
165.968063 16.782539 165.306808 18.650732 163.985123 20.072056 c
|
||||
162.664291 21.564592 160.982376 22.275211 158.948578 22.275211 c
|
||||
156.916458 22.275211 155.233719 21.564592 153.862839 20.072056 c
|
||||
152.540329 18.650732 151.879898 16.782539 151.879898 14.528599 c
|
||||
151.879898 12.274660 152.540329 10.406551 153.862839 8.934444 c
|
||||
155.243713 7.513037 156.966492 6.792244 158.948578 6.792244 c
|
||||
160.932358 6.792244 162.654282 7.502947 163.985123 8.934444 c
|
||||
h
|
||||
165.968063 39.260834 m
|
||||
172.526413 39.260834 l
|
||||
172.526413 0.791912 l
|
||||
165.968063 0.791912 l
|
||||
165.968063 4.030647 l
|
||||
164.035995 1.340179 161.291748 0.010162 157.727798 0.010162 c
|
||||
154.163025 0.010162 151.379578 1.380703 148.925522 4.182991 c
|
||||
146.532318 6.985115 145.321548 10.447161 145.321548 14.508337 c
|
||||
145.321548 18.569429 146.542328 21.980774 148.925522 24.782898 c
|
||||
151.358734 27.585186 154.312286 29.006512 157.727798 29.006512 c
|
||||
161.141647 29.006512 164.035995 27.686666 165.968063 25.006289 c
|
||||
165.968063 39.250683 l
|
||||
165.968063 39.260834 l
|
||||
h
|
||||
195.566971 9.117228 m
|
||||
196.888641 10.538635 197.549896 12.355879 197.549896 14.559118 c
|
||||
197.549896 16.762192 196.888641 18.559340 195.566971 20.001011 c
|
||||
194.245285 21.422335 192.623413 22.143211 190.630478 22.143211 c
|
||||
188.638367 22.143211 187.026520 21.432508 185.694000 20.001011 c
|
||||
184.422363 18.508474 183.761093 16.711493 183.761093 14.559118 c
|
||||
183.761093 12.406662 184.422363 10.609680 185.694000 9.117228 c
|
||||
187.016510 7.695736 188.648376 6.974945 190.630478 6.974945 c
|
||||
192.613403 6.974945 194.235291 7.685648 195.566971 9.117228 c
|
||||
h
|
||||
181.077713 4.233692 m
|
||||
178.495224 7.035900 177.212753 10.437071 177.212753 14.559118 c
|
||||
177.212753 18.681084 178.485229 22.031557 181.077713 24.833679 c
|
||||
183.671036 27.635885 186.875580 29.057293 190.630478 29.057293 c
|
||||
194.385376 29.057293 197.599915 27.635885 200.183228 24.833679 c
|
||||
202.775726 22.031557 204.098236 18.569429 204.098236 14.559118 c
|
||||
204.098236 10.548725 202.775726 7.035900 200.183228 4.233692 c
|
||||
197.589905 1.431572 194.435410 0.060863 190.630478 0.060863 c
|
||||
186.825546 0.060863 183.661026 1.431572 181.077713 4.233692 c
|
||||
h
|
||||
232.475540 17.696289 m
|
||||
232.475540 0.822433 l
|
||||
225.917206 0.822433 l
|
||||
225.917206 16.812975 l
|
||||
225.917206 18.630386 225.466064 20.001011 224.535477 21.036505 c
|
||||
223.674088 21.970686 222.452469 22.457994 220.879807 22.457994 c
|
||||
217.175766 22.457994 215.292923 20.204056 215.292923 15.645479 c
|
||||
215.292923 0.812172 l
|
||||
208.734528 0.812172 l
|
||||
208.734528 28.265369 l
|
||||
215.292923 28.265369 l
|
||||
215.292923 25.178900 l
|
||||
216.864761 27.757797 219.367996 29.026855 222.862732 29.026855 c
|
||||
225.656174 29.026855 227.949310 28.041977 229.732117 26.011431 c
|
||||
231.564957 23.980885 232.475540 21.229462 232.475540 17.665854 c
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
9965
|
||||
endobj
|
||||
|
||||
7 0 obj
|
||||
<< /Type /XObject
|
||||
/Subtype /Image
|
||||
/BitsPerComponent 8
|
||||
/Length 8 0 R
|
||||
/Height 148
|
||||
/Width 538
|
||||
/ColorSpace /DeviceGray
|
||||
/Filter [ /FlateDecode ]
|
||||
>>
|
||||
stream
|
||||
xí½C#[Ö†;Ý<>;$<24>$<24>@pwwwwww·¶™~ŸwW<02>¦<EFBFBD>̽÷LŸó±°PU[×»—îªú׿þ}ðÓ¿>ðÙüÇ_Èü‚¬¿üÖa®§?CL]0qäoAtUôñÃÇ<C383>Ÿ>ñ£¯—ôQÄoN|üÈ¥\Q’Òïô;¤åÄôiêü¤¹û•§ŽÞ¢Ï¢Ð<C2A2><C390>PCa†Âù6¤O¢P¾Â8Íu!!\Ï@?Q¢¦wú
Ò3¹Ì¡È̞ǯIôL˜ |