Merge pull request #697 from mastodon/690-better-onboarding

Better onboarding
This commit is contained in:
Marcus Kida 2023-01-09 10:55:37 +01:00 committed by GitHub
commit 7041a81fcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2057 additions and 2478 deletions

View File

@ -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 cant 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), youll 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": "Lets get you set up on %s",
"lets_get_you_set_up_on_domain": "Lets 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 %@. Well wait right here.",
"button": {
"open_email_app": "Open Email App",
"resend": "Resend"

View File

@ -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 cant 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), youll 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": "Lets get you set up on %s",
"lets_get_you_set_up_on_domain": "Lets 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 %@. Well 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 havent.",
"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": {

View File

@ -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;
};

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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),

View File

@ -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) {

View File

@ -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 }

View File

@ -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")
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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] = []

View File

@ -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
}
}

View File

@ -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):

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -0,0 +1,12 @@
//
// PrivacyTableViewCell.swift
// Mastodon
//
// Created by Nathan Mattes on 15.12.22.
//
import UIKit
class PrivacyTableViewCell: UITableViewCell {
static let reuseIdentifier = "PrivacyTableViewCell"
}

View File

@ -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 { }

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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),
])
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -9,7 +9,6 @@ import Foundation
import MastodonSDK
enum ServerRuleItem: Hashable {
case header(domain: String)
case rule(RuleContext)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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"),

View File

@ -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
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -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
}
}

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -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" : {

View File

@ -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˜ô4DFFDEECQ11üŽ±(66&6†¬oÎGEGEr©ý5ð?%fùSHóÅlÆÆDEE„‡…œûÉB…ÁDDd”úŸ<>˜˜$²Ùí6<36>ßv»ƒód³q.11!!!>..Nˆ‰ŠŒÐ0 <~Õ‘þ¯éÃÄqDdt\B¢fÑòkΗ`Ž‘ .ÛìŽd§ÓåNMMKó@^¯××Eæ/ÿzÒ T·Ëét¦$; 2)1!.&:2tpPõ;½¢?…†GÅ&:œ®4&1Í•œú+J ÌÇO!¡áÑ1q IvGŠËM—3|™Ù9¹¹¹yyùù…~*òÿr¬ ?//77';;+3Ó—îõ€ ’”6~¹ÁþÏ }gsz|Ùyù…ùyÙ><3E>Ëòk-$Ìäŧ¤EL|-Ùéö¦gfçæ•WVVUÕT×ÖÖÖ‰êêEæ7ÿÖÕÕÖÔTWUUVV”—•äåfefxÓÜ)Ž¤øØè(Ì°A+¿(iðüч¿<E280A1>q6WFnQyUM]mUyqn†ÛŠùÎé_†æPâòddåæWU×646µ´¶µwtvw÷ôôöôöööYÔo~ø¿·§§«««³³½£½µ¥¥©±¾®¶ºª¢¬¤0/ÇçMu:ãbPŸ¿°ÜÀLfô|ý¥ýûð1$<:Ñå˯¨kîìîéîl©«È÷¹b"~)¡<>¼.ÐzÀ";¿¸¬ª¶¡¹­³«§¯pxdtl|brrjjzzzffv2¿ ŽLOMMMNLŒ<4C><C592><EFBFBD><EFBFBD> ô÷ötu´·46TW”æfz]Ɇ¥ShêW"ú#P@Æ×þKÁ2"cžÜò†ÎþщɉÑ<E280B0>ÎÆò<¯#."äXCqI·73¿¸¼¦±¥£§pdlbjfn~~qiyeuu ZßXßØÜØØØ„øÍ×Æ:´¶¶º²²¼¼´´°0?7;=59>:4Ð×ÕÖTWUšŸ<C5A1>ž
6¢#˜|ø@s¿I‰€ 9bxbDdþZð~ <0C>JpúŠë:‡¦VV—¦†:êŠ|΄Èп°¿Mfv°ì.ovAiuckWßðøäìüÒò*@ØÞÙÙÝÛ;888<<:<:::æçèøXßüåçè<C3A7>“ûû{{»;;ÛÛ[ëàdianzbd ·£©¶¼('ÃíHD¥0ó¿2XèúÐ0¼sâ4xŒkYÿ>|
<EFBFBD>Jtg—5÷O.mìîín.M ´”g»“¢Ñ¼œÿ`„Œ8[Š'« ¼¶¹³odrf~emc{wïàðøøôôìüüüâââòòòJtÍ·!>ñùŠã—\svvzrrr RÉæúêâÜÔè@ú³(Ûã´!6ó/ƒ †M0XÄÄÆ'B DˆÀüuÝûcKÍ«ê<18>ß<`’÷·F;«sÓlÑa¿Ê1Ca1‰öÔŒüÒÚæ®<C3A6>±™ùå<C3B9>í½ƒ£“Ós°p}ss'º¿¸Ð7ôøðøøÈßBœ¹Ó×íííÍ X&g§Ç‡û»kK³C=­õeù™©Žx¦žEùŒša‡„EFÇ&$É=w»íñˆµÐ¿n¹~
ܦ{
jº'wO¯n®N÷V&»k ¼¶˜ð_Cfh†B#cS<9EUM<55>ã³KëÛûG'gW7·ƒ@ðåËW¾¿òí§oÐ×o|0Ĺ/¢ÏŸ??~6÷€äæúòâôø`gcynb¨«¹º8Û“œ†áý+@ƒa˲ŠM´§¤z}YDbÒS 1Hµ¿ªw>†ÅÚ½…õ½Ó뇗,«ËÃõéÞúÂt{ ˇóÿsb†Â¢bmÎŒüòºö¾±™å<E284A2>ÝÓóËÛûÇÇÏ°þûþý»~,ú÷÷ÿ›?úË'ó³ßtpùnÁåËgdÊýÍõÅéÑÞÖòÜø@{}Yž7%)NRãÀÆE£Ѡi¾œB"6eÅùYH5!—Ó }2Šêûf6Ž®XNWÇ3ý EéŽØð_Á7Á“‰awgV6u O-nî<1D>]ÞÜÝ b4€ù À¢çO?!.Ð7(ùúõóãÃÝíåùÑÞÆÒÌpwceA†+)6¨×¯0nT ÀpgäWÖ5†©«(ô¹ &üeŽÁ‡Oá1!cvóøêáóýõñæl}Q†=6,ä/êÂo<C382>djXT¼=5«¨¦¥wlnmç\FPÀi?}ü÷¿ÿÃ<C3BF>Ÿ¬ãÿ±þ!_¾<Þß^<5E>í¬Í<C2AC>õ¶TZÐÀÖ íÿ)1n Ý™õm=ý=<1D>yÞ丈пªoÒ& £¡™qýð2$3°3þ×2ˆ³§e—Ô¶L.nîŸ\ ˜ûDAHx“^žþlŽÇ‡Ë“½<E2809C>…‰þÖêBb|±¿€íÍÀ#bœ¾¢êæî¡É™Ùé±AR¢Bÿª¾!3Ð&…Ò&ÇB†ŽØ°¿ÌÖù)!SµrR³Kê:§Wvϯï%.`ë%?Eüÿú Áñ¥rws~°µ4e á”ÄþŸK<C5B8><4B>x²7¿²¥o|vy}}ea¢¯™`Bb°åü_@B™a!ãHÈðË ÎÿO‰`KD¬-5«¤®cxfuçøâæáµ¼øžyÿ;Üøúåáöüh{YÐ(ÈHIˆþéDâí¿!JQ˜â¯éÃ\ÆDWficÏøÂúîÁþÎÚìp[ea¦ßE†êãâŸ?G¦ ù ÑdFQ2mòè—Å¿ƒ UÀY~ÿWd
ëÏo±>c]Yh‡g×vO.<18>ðÓ¢ÿˆ÷|¿¢ghü@¯/æ®þŽJ¹»<ÚYžh­D™àw:iñï3ñ?G~Jœ &Ž¼ „%a¦´Üª¶¡Ùõý“sp»4ÖY<C396>ŸöÛÈ0] º¨CCæØ%®·È”Ƴ}BÆÛ¾ ‡(ɵó‡ÿ q½Õ¶ˆÏü¼MèÚ°¨Ä”ô¢šŽ¡ÙµýÓ«{#0àækwùó#}Ëø|Aæ´ q{ÉìOô5•ç¤Úbÿˆ(JÉ·ö†ÉÙå'tê‰øï5q”8P(?„¼5œñõ°&âÞºîÉå<C389>Ó뫳ýµ©žÚ<02>=:\×]üD*EÅôÄÔlºÄÿ\ûÖÕ¯ˆ*MѾ uï£_øeFQ†dÆ+Ï™&Tž
h×UP G9ûš¬cœ <0B>Žð‡*hš<19>øaN‰ëеO~eëàôê^þ¿EÁ§ÍgðÂï`ÂÚ<C382>ÔØZïi(Ér%úM<C3BA>ŸôŒ¾™y€±ááÚ9Èx8ÆhÌ9ÎòcˆÁõè,S)2S™ ®·är®À#sf4 Ê/o€­ 3y¸NT˹ŒŸ‰B¦GÔk*V½Vî‡K_giñÓ§PºDY(<ÌdïB#âé…õý–×
26,d 3AÝÏmkPQjœ*¬1qú%Ѥœç[ù<12>
(NyæD¥(O C1ÏS³Ëšú¦VvO®î¿~ƒ‰E"2<ö&sz:e}ø_/Èú÷?@ãóÝÅáæüHg-ùÄÄmØ`ži  DWLl\<«<>¥L³Áɉ€Ž0ð§z(‰?J2$*š’ *§½·\j.äj‡KÞi¹•m# Û§W˜°¥±$S ÃÑvZÄg¦6”.™Qoê<E2809A>6}R½TÌeoŠÒ%FCQ•UI<55>&"*>9£˜x†ì º`ÉŒt;ȪLM
"´)—ÙH€Ì„«í «Eæz3Gð?,”Iâ_*Ц,UÑ%CœW¥!Tmd|Š¯¨®{|iûøòîóWbÁÄ__G_Ò×XôâJ£PÎÖg‡Új
Ò<EFBFBD>&M¯~ì52M„µå09%ÅéL¶Ûb5
ì£À %Ö<ÕÃ$2‡°/6!Ñf×þUgJJ²Ã ÃÍ4R9—DÅ%¹|Eµ]ãË{çw<C3A7>Ÿ¯<>·æ‡ZÑH2snÕjU«i¦#£ãâ“è‘R½I qðØÏ®{ƒhPíÑ¥xu‰ÑÐ#»<>ü]Lt\¢ËWÜ€Ìð#p6y 2µÑU®Ô6жËåLvØm‰q±tÔ Š ŸÈ\*¹ÆA-)Ä[D=H°Ñù”ÀœÄ0y,¬§¶$UkKËÃÛ8¼¸Åø„<C3B8>Oôýg ÿ‰Tø]4®O÷V§°BóÓSc#<23>,ã¦+/I#Ó¢[²+Íîóefffx=n§Ù9<18>0Þl·NàˆY9ª†Ê€TxDLœ!ié™™YY™>³ëОdÒe\É%ä¨hîŸY?¸¸ÿüåñætgq¬³®8Ûër$iÎU«¿sªEÅÄ“{s<73>±Yª8Ý“êL†Aä á­sí+â l
<EFBFBD>4]²Šò¥Ñ¤Àaã€Ñ&òM 2e’ˆþÂç¨åýR=éL†i;#Ý Š¶Ão4ÎÕ†hUÅ%1AvmÕŽe WšOz`öêª8]ê:ŸˆÑ†E&¤ j{§V÷ÏoP%/Öø úÏO¸8üCÉÀ‰`úþõËýÕñVh3Ðp%ÅEY€Ñ-õK3ÁL:Ó2²r ŠJJ¡â¢\R^ÌEb|Ë6Å•êñz<$H“âÉíˆN¢ã“Ý^_N~QqiTZ\˜¯½·ÉšF — 0ݾü²úÎù­£Ëû/_?ßží¯NõµTçfz©ÕF­Ìµª^S<>`KIMÏÌÉÓÆXò,¥%ô)+#ÍåHŒ³äÆ«@S,wíˆòeçk4%ÅEù9™ÞT—+5=¯¢ypnÓŠgo*obì MXWÛÎTofnA!åi»¬¤¸€AyÝ)> Ÿ¶ŸŠ0 1ñ¶dgªÇãMKu;Sº$Å¥<C385>Èô Œ.i'¦Çi'Å ž
¢‚ð»'¿ªctaÌ~ù?/é‡/É:ý\ñÐÏ$±B{›«
|Êl2¡/¡A5•1 v§73¯¸¼ª¶¡©¹¹¥¹±¾¦ª´<C2AA>Á0N¡Â—Í,äåd¦»íX Ì,/Mb¢#5=» ¤²¦¾©¹¥µ…²uÕ%ÙÚ<„øEÛFÇÛ\Þ¬ÂòúŽA ¬Óë<07>q<EFBFBD>y<O¿®²¤®¹Ì8†Z=²9Ó|¹…¥ÔÛØÜѧÚʲ¢¼,¯[9d&øÅH ™.¡4ÝêREMsSc] =ÊÉÊÌÊ-®nžß:Af<41>˜°<>ÁhDp6GÄijJ|y…e´ÝÔÒ
Ѹª0»å¼·%(Ê<C383>ˆIp¸q~~A~^v&²<>u”•_\^Mçé=Å«+Šó3=N[¼5{VaCØŸXç¥<C3A7>Ô»ÏϪÄ2>õã'˜ËwÐ<77>ß!.~ð‡15.6Æ{[ª‹²=)‰&óÔ+¦âSHxtlÓ“•_ZÝÐÚIJcìëîh†kùÙHÂÌœ²ªÚºÚêÊÒü¬´d+rÅ+=»°¼¶©½§o`hìïélkª­(ÎI×< ŒâíÒê¦Î¡©¥­£‹[\²/÷×g{ks£ý]­ 5åÅùà úF<C3BA>Tm"9Ù<roÍí]ªw„ŠèSKCuiAvºËž<00>t9ƒx"Ê"l¢ìîôœÂŠÚÆöîÞþAÆ20ÐÛE<C39B>*KK*ºF·Oü2Ãh“§è8å™ ¸´¬ü’*ÚîVÛÃ##ÃCý½]í<>5f·\2+Ìì–£…XñÑ ”kkk«+Jòssó˜²ê†¶®^UÀœôv¶6V—åg¦%'šõùÜóH5&¥æV¶c<C2B6>Ÿ\?~}fü†Ä<E280A0>ô|q€^áô‹ÿ„©ñps~¸¹8Ù¯¤|ºËgb^<5E>^YSïpûòJjš:z‡Ç´ U{Kg&GX(ÅE¥5 -ÚmÝÖXUœ<55>f<EFBFBD><66>2ž¦ŠÚS3 ÊëZºF'¦gçæg§ÇGº[êÊ ˜*q v—¯ ¢¡£otfyëàüZκ¤ÙÉîÚâôøp_WkceqVª=F;uå“-©†·ƒÔK<C394>TïÜôäØp_gs-§¥$ù'803 ›¤OVaE}«¿KŒfnVû »Û:u<>mýSË»{t@†ñZ©Ê+Þî¢íªÆöÚf6—h|vjb×Wf{<7B>(f,«i"«J„Wi]õ2GM 5U•Uš²¾aõ`AŧÇGú:šªs¼X|HG?®DÌc\rFQ}Ïô"Ão}>óÓ pø5é°E/NZ‡­CVì ÁoÑó§'¿Äɇ»HÊûÜ6I kH JËž©p¤eV5vö<76>NkËáÖ6´µ±º879ÜÛÑÒØPßÐÒÙÇ,MMŽ õ´Ô<C2B4>‰!Ù<>Áò)"*ÁÁ”Ô<E2809D>#˜]ZY×Ö<>í­Íõ•Å™É¡îæšâl<C3A2>ÓaOv¥çW4v M.¬í`À²'{ÛkË 3ãƒÝÍÕ¾”ø(¤#"&%XXÙÐÞ;25¿¸º¾¹µ½³KÅkËódâºZjJ…r„Œt#<31>)ŒMrysKjZzèÒ¢º©GK3`°»££³odn}ÿìæñd†ì ƒ ¦Ä?mwô<77>LÎ-­nlîìîjTëË3Ú-WWçKe…!6?Ñ&SHôL!MŽMN2GÈÌöÖŽ®ÞÁ±éõ€Ò_Y˜ëïh(ÏËp
C¿‰r%¸²ÊZ†æ·°¾<1ï‰ 6éÇK,â8ß?’¥•^)”Ï÷×çÇVR¾º0h<÷
­
0“=9¥µ­½£3kÛ»Úrxzrr|¸¿³¹:?56ØÛÓÓ30:9»°¼ºº8;ÖÛTjÁTìÊáÉ)©kŸ[ÙÜÙ?<>=ÓÆT•ÝXžïï¨+ÍÍHu¹ÓsK뻆§—6÷<36>/”Aüfõëæòôè`wkmif´§‰äš-Û5<>nóæÒ£žé…ÕÍ]ê=9;=;==>:ØÛ^_šìl¨,ô¹íñÑAÐ@Ú(Ï<>o\PÙØ981·²¡.<2E>œ˜Ñìn¬ÌMé&Œ±éåí#´¹… cgÄÀe±Å$:é-³12µ°ºµ»t| ˜£Ãýmª˜êjª*Ô–#:«¶)&<26>3·¢©wtziumei~zbtxhxlrvimÛê<C39B>™“íõÅ©áî¦
üÄ$
û˜*PÞ ¯ºslyïì6ÈÊø#ô3`ÀÏô|Éó'H, àx²¿IR¾­¦Èç61/«WÌexLBŠ7§aP+úðäÌlC½¹½¹:?;ÞßZ[œ›š×öå<C3B6>í½ý½íÕ¹ÎÚ—ÈCØ`OË)­ïžZÚÜ;<½¸¼¢¤]‡‡{KÓC<C393> åùY9EU­SË[ÖVLo¤Ú·/Ÿî®/ÏO<C38F>÷·Wg‡Ûkò=vSkDŒño;%c¶÷<C2B6>Î.®®oon¡ë«ó“ÃÝ<C383>å™ÑÞ暢ÌT»,w£T6wfQUsÏèÌŠéÒÕµFsCɳ£½ÍÕ¥ùÙÙ¹¥ “—@›€Œ€Ì I+@™žWÞ`fc{ÿ˜¶™ ˆ*ÔÕ½­•Ù±¾¶Úl<E28099>Œ--1¬"«ÞšΑٕí}æhsmyq~nY·spr~Ino®)~rÈúí‘Ÿ¨'eé5„2!h¯MˆkrÛ^ñõ‰à9¿ƒÉ0Úüú ½.`ÑÓQÊãÝõÙáÖòÔ@[uaFJb ¢½yå<79>]#3+ۧ׷÷Ú…ª<E280A6>ƒ€ãì„Å¿¶¼¼²¶¹ hÎÏŽvV¦zŠ3Râ" OÆØÜÙ%ƒÙÕ<C399>£³«ë»ûÑãÃÃýÝÍåÙ! éj(/ÌË-,«ï[ØÒƒ‡Ï_Ð%tÈ
´Brmy²·¾(#9>ÙŒJ˜«‰>9¿º¥ÞGmw5<77>º¾8;Ü]_˜å2MÐ÷ÖX´ôÄXwVQMkÿÄÂÆîÑùÕÍÝ=CÑ£«óã½<C3A3>Íu´åÞñ9f†A†egÄHÁ2eeS7ÀÚÚ?¹ m5-¢í»[&dosqj°£¾4ÇãH<C3A3>B<01>!„/‰E¬ìž<>ŸKno¢“÷N˜¢8"rg¡ëq0<71>Öò4Ä¥øJq™®Ç5g†øË÷Ÿ¡—?WËáo7GÛ+Óý-•y¦WšM ú Ù“WÑÔ3:·¾w|¡™„sß¾@ÌœfóäpoG*Fƒ¼½8Úœl.Ët²X1ÈS2Šj•4Þ;FhÅ?ýPsuf¸£¾²¤¨¤ª¹³ÃÂdmøÓ°ƒÖgšJ|Ô<1A><>áÌ(¨jî˜îÑ—¯†ðuUñÕéÁÖòôPGm Ð ëW ®Ÿ@…¾o˜ÄÐ=½„'Œƒ²Œ)xŽLßß?<¡T
2¬H—|ËžfDù•Í}c¦m þ¨òßÔ4ô™®^<5E>î¬Ì w5” &<26>rÈŒø”Ì2EI)Ô/N<><0F>ŽŽOYÔÁ×gÖ§œ<C2A7>ñžÆR4ç“äþ>k¢+»²mtiçôæó7¿QaÍÒdNë×óú/@ÁŸß&•Ó<E280A2> QŽw'zʲÜ Ýä<C39D>ô4ú L[&6N¯Ìf
4<EFBFBD>YûW§§þ15<ÙYn«Èrâ3"¹Éƒ´Î<>4æ¬ÊP†„éo««(­¨m$öyy«èD—Ð`Òh—ÇóƒÍ¥J¡DÃœôüªÖþÉ¥­ƒ3ª}ülõˆ&˜ÏšàcºÚâLw©Áhp7alAuÛàôòÎ1̇­*ÀhèšFsH??;Ó`…2dÆLÈP ”Ü<E2809D>‰ÙY4±¸‰¾Ñ<C2BE>(My¾ ªL#ÎöpgCivš•Áþ‚ý˜]NBh÷ès «âºD<C2BA>èV
…éÊç{îq©E>&+<2B><><EFBFBD>ñŸ5-·ºkbeÿÜx&?„9_ÿïŸÀg2^ÐóçO†ô¯¿¨þ ÕaÀѶzU(©<>ÐÀÈ@¥»³Jqðç7ÎoÈf0xFQ­·üჴ4C|`.¿‘‡Ù[m¯ÈváÑëF@⹓
]Q”UÉÌ3!27˜q~¸Á6VWT<57>Œéµýs%¾+À§>Ñ)<29><>Ô`²%‰JÉÇÅbÓ¥ì› îáïü0ˆ»¿G5ð? Ðí®Í u7Iæ£tAhD¼ÝWÙ:0½²{|qû𙥮^¡Ìí-UÜi,Ò1N #3ìÆžÖ2ñä<37>/¢÷d)ʛƴÍ×ãÝ:an¤«Ž ¶µYNžEvEûØÒ.ö#F3(ËBw<42>]a¶]«ÁÏêëóù8ÔV•ëV !Ôî)¨í™Z;¼ÀÌx2 Í$Y„]Æïß  kƒéu©çËžÏ`ðÑ«<C391>ÙÁò Ö"CŒÌo $=sñx‡¹ÆÂbi ô žYÁxÀëî|o d0'±Q1¶ÔÜ*3[ÇWpi­<0E>0t+Õ ™Þ…Ñ®æÚʪÚV;'Z‡<5A><E280A1>J UÄÄ3Õ{kÓ}<7D>Å™ÄÅRe—5ö
·†9T|s@V<>zé/3ØV]<5D>ÎâSpem3iìI$Æ¥’¦Xq-Pÿç0êæŽáh4° ¢M(ÈÀl¢<òsqûð`©íû;xKÓ瘲7ÔÉAÆy¶¿>?ÜQS”¢…<C2A2>äET´<54>*¼ýŒDÔ°høêò\~šœóK<C3B3>Ü@ãæloe²·¡$3Å,O€ñá#hºö-+›óæzæâ+²dîo“¹Äü
ü±~¿Eß¾ÄÚ]žè®+LwÈseD±õmƒ³òï ¸{QÆxð»{(ä3ÀÁraFàç÷ïB†%3bcHhÕ÷N!
5%¯N<C2AF>v‰8¬®âÅá¨\®XÇm¬./¯nìVÁì†Æ<18>&¦ã5ïo,ŽuÕf8íIv§¯XÖêæöAŠ77Øœ8<C593>ûûøšbsøîêdWfSE®_¦c2%`Öu<C396>-’Æc¿Pöêü;ÉŒS 5´4ºðýë#‘®2B Cª¼iûðÙ@4QÞÌÆþÞ>¥ñ«¤Ü袷3­UÉÆ<ZÈX_ŒŠ¤˜à'‡{;[[;ÌÇå5#G©Ü_ŸlÏ<6C>´Wå¥&E[êäã§p Ð P…U¢Ÿââ7X ¡£ùm]ôTÅÓ‡·ˆß¿<\É€l)ÇLÀÛC¯&¹²ÖO­îÊ`}>܃C +KKË«ÄŽ™)1KÚ²<C39A>1&d$Æ’ÐÎ*ošÃž¾‡ ckeafjbjVAˆƒ£ã£ýÍʼn¾¶ºòòÚ¶¾ñ…<C3B1>=\1 ¾4ÎNŽµ)1?ÝépÈÈhž[Gœƒðv}~BÈCwtð’«þ`7 ™MfWfš1Öæɯ¦,ŒU(·«³#|«ÕåEÿhŽižÒ udˆÐ°ˆ8»—e2LüüÖãûûofuçl]pîîâMÉ<4D>¤mÉc ãN2d`Ž0—<;¿Ä|0r7_ï.V§zêÒ“ã"pm@†eÁ¶ /X
&­ÖoÑ«ó¯þ}E¿}6@ß¿‘ßÜ]ï¬ÉO“el梠¶“Ev"÷^S<>å½¾Dc”TÁØäÌâ*~£<16>¦ 2$3ܶø8)“X
gS#ý=Ý=
·Ü\3‰¤ü¢Šºöþ‰ù@u(&>Dàöñþ LIq¦¡ú§W÷ÎnXžÈ䓃íuŸS§gWuÛt«ŒÜÂHg Áº¡Ä!µý¸ -*B¬ûÛT>9>:22J܉8~$ˆc4 dhïx½ö<C2BD>öïÉB±LÎÄå‰ÂÍMOŒOw":b/çXø c]–ɆRv<07>!Ðkh8¨„ÅF‡úIC<49>N46-B<03>hk~ˆåéB ¢N@Fd3»¢mlqm„Jßøùž<E28098>ú?ùÿ<Ó3~,Ò/Ž½øç‰@ƽîä”5îˆ%r•äÎ.oÀi¸¸e¸²ëö7—fˆzÜmmíèî!Þ³{t<>Å,3Ú*…Œ‡¬¦ÉÕí@" r¼½49ØÕÜPWOnmhlznanf¬¿½®¬ '“Œ\y}[1B¼ÁË&Hëꊨ=!x¬=mõ<04>ÝN§ÛWXÛÅ!‡`.þì± âá}Ý]]ݽ‚Ü
. ÊâŸï¯Ïè)2ÃьނšÎÑÅÊ2Svm~j„ˆx[kKkg7±ÿÅulSp#Ïùû7xÐ&*Ï2©a™` ™ò×çG;”èéêìêê鞘]ÖÝAj[ “­<>¶#<>6¹Gf˜¡<CB9C>ÐoBézÚMc ó1¹°.h 55‡ã<E280A1>ÕyiIVd$ºr*;ÆV ²˜f˜ùÿü!2Vë3ÛŸt4ð;ˆ8¸Â}~¼AÉ 5—¡N¢#¢âžÂº®qâ ¬#F{uÌTŽt6×WW”—•W«ì™^–“À:£:?2Ú+³S“Ú…=½qxe”ÉÅÁú,~HéÌ2RK]Êqöw·ÔæùHI{3Iäêæîùµ½AIi“ó£­eƒÃúêR­dû=¹-ƒ³‡ø·@÷Ÿ+Fz;šj««kêšÚH†,¬S§ÑE§;Kãݵ…^ QÑ Î¬2k_Ð<5F>|T$[+”mo¬­ª(++¯"Ü?>GL©ÁÊ̸o\wÄEÇôm˜]GÑöý<C3B6>BƒÓ”oª«­ª®©mlé¤÷ UExèlweÂÒ Qˆ<1B> ´ûxv´·µ¾ª¼¤¨¸´ª®µw 3x®ËsmJ;`qC<E28093>XH‰©¹Õ<C2B9>8­¤Ó˜æÿ†~§Ãå÷ Ò<>'|°´ñ-ଠ®¹$>30 0ˆLÍŽô4×æeggeçW °`&“š¾S~dH$$&§7öÏÉž˜Ü«Sý-5ÅyÙY*X×ÔÚÚÖ\«Ä¤+ÙnOv{•©·&M!©*Cajˆ$|uyQ~vº;Ù‘ìö=AU«ö`kqr<71>eYqA^^^aIe=Ù<>ÙUÝ<55>{î.-_KVO,g~u§×%P·Ðe •%¹Ù™™MMSçðôÊö1ÐAŸ|û*™aì ¸eDN×øÚF X$¨;€za>mWÔ¶tϬÐ6Rãñþ
O{HB#1&*Æ # 3¾~¾=?ؘÃ)«*ÎËò¥gdæU5uã
t&À†µ×\ê“¡2>†F%øÅi…eLu0™<E284A2>TðSàcàïïÑo_'x|ûr±¿2ÑU“—æˆ<C3A6>‰#<53>~JÞ>œ™èQCEAvzšÛår¹=¾¼ÒZÅ}¶ðµÌŒÌ°ì <0C>QÒ8€
Ÿ¿|f ­Nö6WfgxÓ}Ùù%å•e…¹îäDíîµ%»ÓsŠ«{Ç—$®ÁÒÝùÁ:ÑX—Ÿíó¸Sì6[JZ®<5A>U¢¶[ }­5eùYéžÔ4<C394>×—[TIÊvfuÿWì;•\®Í÷8k—`eK¹õ·ÖäezS<7A>N§+ ±UVß>Hžö±t2d01,“’&DÆiûNm<4E>÷)§Él¸Ó<9$ö;§<>¥Ú&²ƒÐèR
)>&yƒÌXÞÅÅ€cݤŒ|i®äädgš/¿¼¡{L#gM|¾=Ý^ ·L$æ“]ù5„3È0$†ý7Dq¾ÍÏê
:ô‘ó,“«£õ©Þºæ2.!'º×Ì%sq9;¬ähºöe&$$$ÚS<YEU-˜ Mæ¿AÆþ2ðñûgMÞøëãí¹U…9éž4ØÈËÏÓμ³u‡%™ß³ŠªÍX$¾…c?ÑÓTQ<54>å5[4“RÒ ª;Ðô@Õ8ÛKÚŒV˜E&ßfK²ÙSR3rKu×úr‡K.Ögú´ÿw75»¢uXŽŸ7rúÉ-SÖž4m§W÷ƒ+#´¤[Y¼BñÈØ8{+…¶)o—&Ôvfm'%iG§R°íŠøb¡¢Ë.1_ÏR<E28098>³Ifm2$2V§úšËó¼N{B\l\‚Í¥=†ýÓk\`7·*Ç-d`²èR8ãË‚(ÀÝéçgDoÕ÷Ã1‰qá‰Ñ$Ñ_¼Îa³%€EruŒ¥ßUOÌW»Ù"##£¢Íö™²¤²÷oA2;#%½Xwˆ1œ/Ÿ±36æ†:êÌúöxÓÓ}¾t«=AÛý í¸u¤e—6ô"4爄“íÅ‘öêŸÛ‘§µIÄ2<08>è<†¬iO µDC1ñz ùüQí%}®£¯¾ÔvM¬ª£Ò%@f`ø€pl”Ml¼=5«Xa=b%â “ñ„Œ¸xGzaÌ¡mÒó7gûkK* ±$ÄEƒì˜8m)®í¦8ú<04>u¶»4ÚQ™“j<6A>Ç”—6Af|QµÈ„Žj2ª‰1á1 k:¸àôšà€%¸«óR£AÆ¿@†ÍSP‡¼2`ôu~Îû<C38E>„Šõzúà§×ÿ¿ ]=_‰wrs²57HòÊmOr«7[˜ã­`<ô²ì42Ûð2„p‡¶x:} ÉÄ9ï1Û¾Ü]îØ)¶„“8èˆ@F{EV†hmYAn¦/#=ÝëI}ÚöKN"D‰SÄ=ë®´íÉk®,Tz´î|2Úº¬eýd Êšœ9zÌCTxXh¨öùǃ.”zšL»ýU‰ôŒTwZfqC8½Ôz¾>Þ^!HéÃ<51><C399>ÐЈHüì¬úncÇ<<12>|FF"˜Ë,QNìyðxO¸~~¸½Fyé˜íH vŠ7t£1eY =u^Ô%9çrb 
<73>ni¦SÁ"-$!¡'©þJF<4A>LÐîZÿ<5A>>ÔõÒù«§Lk=óïÏRD|?“Žë'<27>¢·§;8'¥YØ{ðIœÕ*c·æ‡Û¬D¬ÙÇö<C387>LQ‰OWf‰Ö9k„È ŠÌ”Îñeΰ ½×èh¨*+ÊÇæËð 1㢴½…á IrçT"tI/€E¢n¤³HèÇ@‰Ò$HXXgtAuy¼»¾˜ÄˆA×Gí« ×~¤FgY¸uÒPœåõ¤çv3Û(Õ—ƒµ)i mÅG ™Íiø>ò}îñ›¯ü»ýRЩ9í#(ˆ¢«GK‰·ÄW)b¶ôåÈËÇÔbº:Ú˜h¢ƒI‰v7¥4òà{0x<30>=ðwa<77>™lÈ°ž2HèJ‰_ØôÛdjÅU¬Ãæ·ùõŠžAb}:ðT¬=ÛYÖJMIví7w^h.÷Wñ¤¬ÇY<C387>…P¢l„Ñé´¼Êv£¿É†™Q ®¤¸¸$”kÛ°˜Á:`<60>žl.Yn¾ZAnVº#®ÂT,p懀NVy«¦_¬¿:Ü<>Å£M@4H'†f<>T ×ü&cF<63>VÑñdzêº'u"PQ£<_z&±ÓÄõ-x¹Aœ<41>æÂá0™/í¶¬EhìSø+¾‰b R­¶D0W²Smã<°LZ ¼ÛýÙ:Z²â“ÓòX»š3òÎ#ˆ_= C23âöLÇY=šYb´ýhsj^µYFRV`jZC·)û/d8<æfÛ£kT6lzâWЧ¤‹^“uiЙ¼"Y4æýÐBFkyvª3%Ç*3lºa4£m•$!˜ <0B>FÄl ª„-°ÙQ'W<57><E28093>”Ž…Gp™Uð_ãšêêÂÔè@—W+¿$-“EŽ¹ej#â 9´>“ž¾Äî©cáÄ<C3A1>Ýûð­Õzâ`m²§N%«Þß#­/$KvEË<45> *îHÙ·Wfgæ"W[¨Bõ7!΃Ka,îÖ¡ã$ Ð,mž[”Ž<ŸBÓ£ŒÐ˜éFœ=—gú¥p¤Ë4cU9iÉödI#3˜Њ,)ñ¥ÄË)…4<E280A6>”ÔF\¢ÅÖgˆ£( ÁÐLªÕzÜ ÚvV‰ 1Õ"Öþ[ÄyI†YŸ¬ß/)pìÛ¦@—çx\.ON…tã£1sdFcæҌڡ˜<C2A1>Ò`ÎÉ0c<1DºœI±Ñ±Š ˜{gŒ5H‰ÜØÖêÒÌÄpw{sMy!A
;éz !èSDé<>Öá€Ì8ZŸî©-0"•¦>…ÇZn0ªþ<01>nX^©§þ h"æ8u gÖÑàÎxQ”œ¼Òú8{a,€ƒUó4G;‰î§Á¨p8Y@} d`fÚ<66>$;Íí®ä¤ÏÉ!t¤´Å„}¤o!Öh;]¶Ñ´{Ýûk“Dð@S³Ëá;È@ù3¸CøNAŠR2ŧ̈֡fYe=<3D><>eˆÌ ²3nÞ²3ôî0ý¡DPúþ û‘áö¢&Æ<>Ÿ·L°l¢Þ:ÂrqA|€ühoCœÝ€Œ'mB|‡HYV‰ö¸™èØ ïr~L
juqvb¤¯«¥¶¼ ˺»‚:AYx|J¦' 36@F¾GÈо&/³´ÉbZxV°6xÙ2ÅZ¸ªX>Rö÷—A}iA^~y£8nó™¶
Tæºå><çâY¦0«ÛpmB&OªUÉN0+[Lr5Ë¥¶)hŠã^bµ<62>ƾEé\I´Ô¤³H È ¦áÈ÷Öç{QEþÒ0?.ÙW" Mô
D “@Èý<14>´42 T¡æ·é‰ãÏãwè©Ä œ‹óÝ¥‘¶Š\<5C>;5<> žXV7°qùQ„òŸùÀ<C3B9>@;yÀÒæ!³E18¶dòÈá+ð„‡£IR];q÷·7VIv t5×”äêÞ s_ˆ´IJV)ÚÞ? l™×'dhA] Ý)¯…GàšÉ£'~bÝGJ# ìp%y¨¥·¡¼0¿°8•ä º³8¬”U|$KR"ÚHáªÜ#+oB4ÎáÎ*f1@)<29>‰ƒÍ@‘½(ÿ)4J[tÚF1g(°0 3\É) ƒ.<2E> sùl_~¢„ªŒ2€Þ1È àæœÙ³n!C3zTÚ.ý@OñÏ` ñßú¡šï¬'Ì£¶Š<ojZF~M—œN&Øb8Ψ<ì ¹ÚcÅFÈhs¡egD“KHO;¤ Â7wǽ¶Êœìh4o‰ˆ4ü¼oAHÙ¢MŒÌ0"Û ƒ„cyF¤\“{ÃñzÊäE‡dDzfùÁ&õ5UWa_¬aäÍ?úü¢, ³)«¢U<C2A2>Ö¬2\ÔAY ¨ä°éÙL_À  X€Peg [zqmHOúÜ~ 1v¸6Ù]Ã*ä)¦Nk%”1¦Ó|ÿB É„Œ7eÆþúŒÜ±œ?ÈŒôÔ4_AM·<4D>!þ|c†ã™ø% ŸÀˆ:£še´ @H†‡â¦»³Jê:‡ÈºYÛÎï´7îöúêâäpwcif¤‡¨¨ <C2A8>ùIúBR<42>• ïµÚ§±@=š@ §Èxm¦4Fš¦W(¸±ëì3qµø$ÄÌúfùÁŦ
òVU
…¨f”k¾ÑL¼eÁ kq<6B>èAÆñ®<14>ÁáEÚ
³ m³ôÕ¶HqŒ¹às§<#ƒâRpFþFºÎúŠ±ÖÈ0<C388>¦`#ØË7!Òe´ .#3žXf-ðWôtö· ùbŠÿür<C3BC>1—ðQv¨…øˆDd1äîdﵕg¿0æ!‰:<3A><>¨Ã9FfXȈ$ô„‡Ÿš]Z×18Izûðøâòæîþth¤v®¬)íQU@¼ˆ<C2BC>Ÿx­öÿjÇk<C387>V¸ÇfžÆÄr¶$µAÆ—<07>1Õ]ãÑ“Á¬È¦¡9tµ€ŒÙþæÊâÂ’*ã°`ÁXÈ@üˆ `e-zN.„C¨,ÔÈÀ˜2òŠža(<28>A<EFBFBD>:*O-sÁ,}'V'd¤¦8±æ±3PpÔDߪsANå(ù)Œ€†…Œk«ÚÀ¾2<>˜NÈŒÞi¾‰álåÏÏé·ÏR?†¾“,•ob!#™a<E284A2><61>Í2Æ@¢àÅc[AF<46> !ƒ\­„Kdè§<C3A8>z°i[ïèôòº6€é  <>M{w«Í+K}-•d¼°åBBŒŠ<>!™Mjx¾‰çI 3@†ô(k?2àÎ dè:Kf_šdì& ƒ mù`!#™HIpY<70>aº`!‹€Â°™!mB¦Àß6=9.·(¸<ÀŠM&TJÛ0X¬€å—Xó&ZñÏ2CÈh²3TpFÈ0Ï.fŠ… ò&Lñý³6yúð=Ÿy<00>ƒÏ—Ao úÈ?°VÒ¡"×ØÕXé ƒ¼çÞÒ8v†+ñIBx3&ä ‰ˆ;ÃÊÂáB<C3A1>ÔzrUcçÀèôÂêÖÎÁÑ*¸Ž`C0€!wbá¡šðZåfþA†Å{­Çd7Z2CaB­ CØ#bnÀ&`}÷;£2`gà×lÉÎÈLI¡'úªFð´<>êÎäd;-CÔ‰,<2C>ù!%cn6.<2E>\Xå`Ã<> Y ²3rj¿69\ÅšÏ5<C38F>³¤ó… ÉŒcÅh<C385>²Ô<C2B2>,P<>2¤ÙŸa(ÀPCÁÿüp߆^Gfmb<6D>Œˆ#ÚD¾ 8ǃ×hfù&²3p-™<>†DjÄÛ]>½Š£³drniukW-µU/ÖäÑG»j‰2Æ~Êáá}žb<C5BE>öZÈ`z@†LÎÊîgò°aZ:ë>˜;ø&r/*<2A>¡$CyQAQ…ºtð—|ãu]¥P€py£Sµ{
ÿ’†µ,z¼Öd·™ƒ6šl2,>-RÛm[ÀÒí{ë ¥ŒÒÌa<C38C>A—Þ<E28094>f}•4öû¡«vŸ|£M@F÷¤B4OÈü5ôíFáø)ðñÅþƒ<C692>ñšžú/=Y åòZq9;Ï0£!ž¡ì±!䃱ˆÉ0êãŒIûl<C3BB>aù& ru<>GT¼ÍéÍ)¬¨mêèš_ZÛÞþ=¶Dg‰q“V¬¸ ™¡(ÈÐÒ"úŒ”Ý1ëu?7Ô¢Íg ]1DL#At²Å‰î©Å3
ˆg` 1ñèFÂ_'i4ÀCÆ ¬‡;éB 3RÀ[Q6qÆëmYèÖm‡Ç$¥š¶<C5A1>ÀF ÿ‰x†Óbb OÈX“ÌPëð]DI¿ÌØÌz ÃÈ -ŠO¡1IÄæ• úÄ® ÒÑgþRÉ·èé¸E%
2°5‰t¹¼HÀ|H@ú:ƒj~º Â<>X<EFBFBD>kâ!â?ath¨Æib¤äê]žÌ¼âòê†À1:=¿ª[BY|LÞ Lšèц¼¸ˆ<08><>6§I?2^Æ@5y\b &ÇÝ^ÅÊ“H¡3"$JâIJˆ¥‰É*škÅ@±š`™àâ<C3A0><C3A2>*íAAä© Mð@¹V=)¶(Ýi#*ï@ao"ó¨LxkB-OªŒ¶U<];µ¸…Êý5„Cž'EfŠ…Œ[$%Øñ.õüI3kpŽôgè1Û<55>26ÀN.<39>ñIJŸÒW¼:ðêß?BgÄ@AFšË•†ÑÅ5Vºò&º " ƒaÀh PÒ¬äML0ú+µ‹' ”˘,%ד’Ý_VnaqyU}sGßðäÂú®uˇ<@) 2uÔ…ÌF³âÐ&X ™ÁäÙÓÅr%B)HŠ/pôÓÒ£IØ¢ê‰iŽ1<C5BD>éweAvVN±É <1B>˜7<CB9C>êæ %ã ™Âh"ÿþ2jp<>6I±ÛH<£é5J+o2Ý×@® žðm`6ðLb(Îœ´°JÆ:*•7q¤æ”“DFf00ì ó#2°g½€ dÈPõè8åÛº,dø<64>~,zþôD&¡«üôñÅg?éе¼ ¿<>a<EFBFBD>áLqgbY¸Ê •‹™ „ø
†4äÚ«Úð4ûW€ÅlŒvXÈPj:÷$Ñ<C/ÝçËÊÉ/*«jh7[5•LA`ßœjíWäN<>Œò[Ff ¦6f¥¥YØŸ`:lCÒ™jôÛõ²¨è!
öñtkíB†IÖIéÌ*ךá+¨7F>\Ÿî,<2C>vT)¬0œT˜$~$ÉÒBµ+²r-dS“,<¹V¬.T” Ý30ÒÆ:A¹Rž™ iòqñ)é…¤jWÔ
TÑQxn» dX2ƒžß­yƒŒŒ"<22>Á4ª²@É ã#4¬ŸÜ*É Ò1&¥&·õÑ Ž¿øÇT`~ÿyêÅh кހd$é=?ÄdxÌ#Ê™ íhe•¸2Í>,¦ìA†|+žñ)$$42:ÞìJózÓ½H-+©jê™]ßǘû"õ³¿:!÷3)6Š™õÇ %3°óæ0ÐýXDú˜ù1Üå´¤ÁòDw}q¦Ë<u‰+`-8´ögÌI`9+ùÒW_íߟ!a€¡q‡ªŸîk*Ív+ÊšÁX,rÍÞt
ÍW¬]ƒŒÄDytËšÂȹ;Û×6¦Ò,7áÛPÓ¶Ö€¹åßXÅñ½®¤<C2AE>±Rl& O(ÆBº, 3Ñq!# 3ÌÐåüg$ãx:<3A>á2<>xwOÚäg,üôCQª4ÇøLVЄ3|ÃZ‰_<E280B0>JêX„²C<12>XmšŒ²ìTG\TDXhhˆöPÅ&¥øŠžöt!rŒÌ<C592>6aA„2YqIš$';Ë—žævº\©éÙ…•MÝc[G,³…¥¡àȈKÎ,…}Ff°Ü·°ó$ð[hM9îòr pušÓ¾*‰—¨ˆð°0³mŽÕYÖØ3©mYèøT˜©®´L x~2X”;£<E280B9>uE>—*…(-»¤^ÂÎn€B†´ +!>ÑIJ<49>€)ÓͽáEÀ2>:2<Üj¼mO,ëŽ>à‡lÀj'X“˜ dX2ƒ©¯dð'd¨ÛVˆŒ(âRÉÄÈÄÔ<ƒ <0C>eÉŒ7 úéç×¼¤ç¯H'+kA(°16zB<7A>e2¶ƺX~žäDí|ŒÔ>P+£€|Äôšd÷W™)2|DGÈÄÐëfóõÑ¢¼¬ôTâÉ(®iœ]?@0#±qNeåa FG£RÑÄ£
2k žÛôXàȨÒ0E„•ã†»¯«Ó-„PIåkŸhLln<>å2¶°ytÉò|¸9ÙAìkh²3½§ƒe‡ 1øDCmÀ*Õ‘—HТ¸¶cdn“\Ñgÿ>PÖ.Ȉ<C388>Mpx êz<C3AA><7A>Re<52>·çk3CmÕE™îä$óØWÚNñZ{Pi§üáö”5†1®} Ï;„Q“²3:ý¨e£<É |&[2ÃøDÈ ’„¹•Òewºé%‰‰°ŽQàï<C3A0>|&øóO( ›ü„ 2<>Ù©öDkó·lK&ãAw|™½ã9éîd["«HOŽÍ,¬Ò3Qtßðûí‡À%õ†ˆA;gV×766ÔèqˆiÎäoAeK¿n„L_±˜ýt;píÀ´1:áÔ‹T-•¾Ô䤄øX&?ÙS <53>©»g Ç,Üeíÿ.Êöè–ÝáLóåÖwŽÌQ¹f˜8ê:Q<>ŸË¨úJí0CÈ':ÕöâÖê¢,¯KûÒ´ùÛSfµù‘ñ]ȵê›#>6Ö¦òØSB-mŸì¬Lö·Ug§»<C2A7>»ÝžŒÿ•_ÖÐ9<ä¥í<C2A5>i;—k¶·Y2ƒ²È ¿6 øÌ8EÖÐ ßQÃŽ—Ìpg[Q7<E28093>aøha„ÏúçmzóL Ü×y#3FZõ´´‡G±.ÁHòÉ뤓Œ¶î7qº…eå•Ô˜ûMŽ.î&„j<E2809E><EFBFBD>jÖø²«Ú»ûúz:«Kuk‡ËíÍÒ´w?ÁBJ
#<23>±ÿFfP"»»±öRS‰ññ‰vwNYó*ÁZ”‡„Pͦ㬠ÌozfnQ¥ŒÝ¡m‰•]ÄN<C384>èRø£Âù#°VK°¶VÏÅs¹Ó2ô¤Úvÿý&ºµ_wÈ- †˜{Ó€U^jJåuïe¶<65>¶=™¹ÅPÃ3kjÞ!Ò´Û×8.:VÈxŽ<78>bYu¾BF˜%3æ¬Pœ|æDÈ@f<>LTÌdìbÌ<62> 8õ2Hü<>>ãœèÏ3`~‡Õ d<>J @ã’ܤž¥[Ðã<C390>ÖÉÌHw3Ë?Ã!;· ¤ª¾}`bqóðüÖºám²¿<ŠøF„Æ%8ÒrJÉ™ŒLLMOŒôµ×Wçeâ¡ee²´AB6)í¨È'¿Ö|Adï!¥h¯¬PâÆa³9\º­­=Ÿïõü<C3B5>¥©Á®¦êòâÂüüÂÒʺÖÞѹÀÍ@èŒ<C3A8>ÙÁf¼ƒ$ë5íÑÔÆn@£‡Û,O v6V—æåädËmª…³ºG<C2BA>Â_ ÈÐ>PƒZä™UÞl <37>åÔpWSmyIQA~AQie}[ßØìšÙ¦DÛ1<Ý <15>¨;–… ¨g™<67>;, !Ãn<C383>a´ ˆØ®rËŒÌH ¼dÎ2Ãâ'Ήõá%YœüñÌ }Ôeúù@†_¸âbÌ=èfâÑš ÖÙÎÚÜø@gk}MeEyEUmlW†]·Þ 2ü2ƒàzê;ÁF
^o \XY[[™Ÿêi©«,-**®ÐMŠ [,|<7C><>Ö% EtÕ‰_ª¨<C2AA>òŸ1®O÷VgGû;ÀbnFj²ÃBÎÒ`Õ,Ü{ÝB¸2;ÖßÙÒPWSSgn¥&XsàÀãý5±îkÕš<C395>ŒI$<24>ØØ<C398>DÛ+³£}MMEeu]Kç€îkÕ<6B>©Æ¿Ñ&Š¦‹ŒˆÖ9<>Р<ñ*½lluvl «µ±®¶¶®¡¹½‡@ÍmßÁ[DâJ<C3A2>"I6÷µb<C2B5>;@;à #44ÚDȘ•6QìdHfÄÏ<>*wd
²î…ÿM¢ë~zþôŠ~zBô„”à«L<C1Pœ˜¨„FEë<45>ňÝ ¿<3>ÔÛÙÑÖÚÞÑÝ?<5¿ºs¤§˜{§$3¤M*sÒIIÉÞj=Ëqc÷`oken|°»­©NsØǺÐ5[ì‘­
GX÷ àaàÐêÞûÝõÅéQ½Ç¾<Ÿ”%é ŸîÁÖm~D¬<04>£Ý ×ÓÝÝÓ78:9¿²µfnx†w2T$2u/<þ¬6v/ɵ1ùËã=jÐhÚÚ;{Æfca²$Ÿ<><C5B8>TWy»åªO¡´(¯ÇËô@}CDþu¾9ùx43иŒW¥5Ožô/> #Oh°oBçh k,P<>!™2ÊÚ𙊌'nÓ<>¿ß$.æû‰ž>ÿAføó& #:"`/Bc|iŸ”€%«øüpgcy~zrltÔzâ„Ý×÷<5F>!mBÜÏN¤¬¾glqãàôüüìhocynb¤¿··o`dR÷Œ_Þ=`f‚—É¥M`NÜ!%Ndˆy§û[k3c<03><>•ùØ™X.eÍ2_1Åý“C=?cvzjrzfÎ<?ã\«–Î^âM<C3A2>t éqÚ,e„Fÿô:¸£ej¿:=ܱž12:61=·¼¾spª` 2˜C¬cË7ñ:bÂq´õLxB7z~†<>ÖÕÙáîÆòmOMÏ<4D>Ú2Ïûƒ³j{·Øˆ«ˆ°0óüŒ·â~dXÏ[2°3´uÀ/3Ú„@<40>¶‡<##À8&=@<40>CÖ§ ýp@ôæÁ·Hw"<22> d†²Eá¬aE{,k]Ð@µŸîm­¯./.-­¬n˜gÜh2ˆréa2O¾IZ2ÎÒ·Oûƒ/onÌF<C38C>Í•ÅÙ™©™Ù…•MæÐÔh,xªSâ¢ÂÂÈy`æ‘%=<3D>Gwxuv|°³¶05ÔÕPšEŒ·´ º}xnŸWìÐÓ¡èÑæÚÚÚúƶ€\êWtõê„PX¯õÌ¢Y$TdQ·ëæD‰@FCY󼞕奥å{pªF<>gî(ŠW ˆÕ­5zÿÎmò§G{;ëkë´½KÛ*Í é±e¢?zæ\4á¥`­½ãdpϘx+o"dÙ ŠŽ ä³ãÌ–&âl ÃpSÌÿÞ>*úù™Ÿ“iHDaÜÎÛsÖ°dÈ Ôì@‚ÂlLzìŸÍSóÐ<>ݽý£S³çá<C3A7>s
2ìn!cè<Ü©£½íMæpc{÷ðôò°æQÇ]fŸEDHHé9EQÅ£Å>½×“´6—§Ú*s=xËvW¦yVîÐÀN½¿»¾<;9>„ŽNN/®n„SV¦ŒÂyªšvˆdzfqC<71>_QcƒÑ3šÝݽ½ƒcpq­<71>EœÒÓ¥Ð&X }ø&vEÄ`x'Þ1¯Ç9!C¿€Ë=8øè@O¥3m q Õj»2ׄωc£øe¨´Ig•uת_fÈÍðïÜÁ¯2Ì<03>ÁYV !@‡oŸ¶î$>êç‡?#sqþ»<Ú ™‘í"YD “ÉTähqÉÐŒ ·z(džíw{k=<0E>™&ðÙÈ c<>&ÙôÞ)óÌGT/<2F>j?<07>˜Ä³kd6ëöòpcnPN2†:“<>©SÑ6<O²FBã+ê†rØ™c²$SãˆSfgûméiYêÐÃýíÍõÕåÅåݱXn˜ƒ<CB9C>­gû™è:ñopGL¢¼¹Ït \bʈ·W †Ñ¨ø<C2A8>È:Q88D8ÄÈŒ<C388>rDIî¬Ò†në¡<C3AB>T@C÷··WW—ÒWúswuºOÛíjA{<7B> ú¤MždƦb ŠñÏ<>6‰@f||ºç@ZÖ îñý½<àöä?ñòêŸ<C3AA>¹E¯x†H`<ÙeM½ãbÆl",ïï¬Gh2|º†n£Ì&9YÙøjŽDY Îü(Á€R×Ì¢®xËäÃRŒ@=Hž'c
<EFBFBD>‡‰D<EFBFBD>N·Ñå4öM+óÚ¼©¡ÈGÐ(6.ÙKr¢OÏsÁò<0E><10>Ú[Šè²þ¥b1gv¸½Ni•h2nš_“03·îË­ÅH¥Ã*zÏ ÆÂ`Ìhna )Ö¢{ÅÒ1dn(çHË-oÄv
Zp<EFBFBD>©@<40>?2DýË ü(&Xì#¥Cš_yW¶6áä±DίÊuÇ3ž!;C*-–ሉ £ÆÙ8mmµ68[Úä-zuSIô‡˜ÿL¯.ÇÜÒ÷÷oßô݉”2BI2Þ¼ÊfE³¬É`)±”¥e™ -¹ëóÓÓS¬ -Wd¸"] x­Öê¶JjæÄ?`Å?”dz»Ðýñ½MÖn?ÖuH©ÛÜ
=vxO¦ ¼cò1ô‰8Yɵ¨hÿ3„ £X梺d®ƒÑÀÀ:Þ]é¬+ÉRÎŒ”¹TF¤¯°š`
°Q\OI<4F>bý#@ÎŒWÊðˆ<CB86>xÓcbÂI!*™
,+•ña$6L ƒÚî¨×£­ã"BIx„6Å@½»×ã^tS#ÈØø&ÚgÝÐ'Ÿúö^NÕ”ÚEVYÈlM!öM¡ Cþ??%â]ü~M¿]êÕYABÄаÛH0“:vš4:ÊUŒ¨lš -~fÁ<66>}«Û0:drHÍâEnÎ ¶”±Rã”jÈ#WÖÞOx _ m„·[«ò½ÈM¥ž$<24>.ŸÛØ?>§<>¾©žV¡hq|TÁÀU-}zG¸®Aú¨Ot^,¿×X»[+„kIò¤á—°Þ™|ô<> ‡‰8ñ,¿ CuÀÔË3ŒìØë›cÃv×6)©âL×SØÇæLÛÀ‡hnðw¶—§‡eNaíðñN_i39\ÖÈõùÑÖÂ0ab¼± ;d’—‘½~}yŒýÚ]#ã =ÈYK¢XÁaÅ<61>~NÿyÍÛWôÃÙ·ÀóD†àŽø}…øˆPÖ™™ #ŽÎà¾YñºZöÛÉÁÎÆÚÊÒòê¦öwâœî¬Lõ½KIŒ1e"}¦ä%J\|¦$«óüxsir€li† ‰OLÇèòRz3ÂÉêûúš+w&zˆ9b#q`ˆ5Ö¦§wÐ%ª5ô€Âº¤ †9ñz_€™Xv´§<02>¶ÁÉ%Ü£óë•TYT
Æä±îž[^ZÕ³=OÎÎNö7æG:ªk<>Cf6ˆ—¥çW4u<34>Ì,o<>]¥GZSvªg N^Êö€c<åð-ÈÂnžœîb<C3AE>ê¡ýÏ{A†¶zVwŒ.lîc©¯Ï·c¿šv9+dÖ6ÖßAF0½búS9>¼:eÑI}e ëÛW§ËÓÚýªu¢·‰è¥L†Þr®Õaóé9©ëÄ&ÇÍûM¶öö÷·üï7±“h4÷ŽÎ.›—\ÂØ8gø\;Þ×ZC*‰/‘ñ/\vCçÐÔâ:näáñ‰yT,6ƒ6ÚÄ„céGèUy\3¹°¶}p¤.™z"'".³£½-5ÅH ˜Ã0˜|Èè=þ¤¸¦­Ÿh'ኚ.1¹N«3S“sz[ þèÒd_³Â&BCA.m—Õw ¦ÃU­cA]h>h{¬·Õj;ÚÆG| ùxC³+[;»[ks£]ÚŒ†œ4ÂŒžÑ1ÙÞÍS»»;ë‹}FǪ]ÎJ×Öë–ç+´ œ P3:æM
:ôñw<08><>6
ìm-ëAî°s4X£ÎŒüò†Î~ó¡=˜…aÁŇ (ñÇ=‰Ó¼iIïœ!ògó¯nJÂÂÁq¢HzÒ1eOŽqóXœÌIŠTê-L\Ì2C¬Jt¹'·¬®­wtj~ymc "F6¦­˜kV>ÒÈ›ç/éÒÑɉ¿bÂK³ãäC¨Ø¼—`P³¡ã<>…Ê}MÎKЙ.<2E>âºìl®ÎM<C38E>öötÏœž#\³<Ïê¯Å𵞤F÷ümçH$>Oh 𪊓㣣}Ú^žèh°ÚŽ
§­Š³Ø\å<>ÝtwyyAS¤GÔ`Q©!£F3tÏÖÜÒòâ좴(Ãj—fÑ5í¢"•hÉŒ Dü—¤Z~‡¾ã»¤89ØÛÙÜÐkȆ»`m*dh[“QZÝÜ=4>»¸º®—mosñÊÂôyƒæ††Æξ¡Ñ‰ÉÉq²#5…>xpB#Yaz®YK÷àø4ÀQQ½´lc•˜×Äpo[}y~†Û†:6À€yȨhíÙä-;z‡Esóós3­Uø€Ìv*ìáó.³ö¾á‰ÙÅåµMÿÛÐ6V—æ§Ç»škJóT±¶ŸfŸÚ-hØ\½¾­w.¨K;Û[[ëV<C3AB>ôäÖú¦¶îþ¡±‰ B¶<1D>å%â°¹¨…òþ¶3 *êÛzh;hPë¦í<C2A6>®æÚÜtƒ
§³¦Ô§P©dàØ90:1>:ØMö??ÔÒ+ˆ~¡ç<¹åõ}ÃãcC½íõe¹ØH° ÓÙŸP"×ôžý”@Ìsõ-æ?<3F>ÔÇ žH…ôÃo$Æa^Ä y!IŠ¦ê ” Ü¢Ïf2bìiYz<>bÿˆÞ8¿¸¿¦H tX¯²,­¬ili'uÑÙÖPUœ<55>Q®Mpè†ØÄd½ç°®µ«>ÏÎÍ/,ÐÌìÔøHO[CeQŽ× ÿ4…<34>i-:Á‘êË/­ªoéìéìïéhª.ÂÍ V&û£JÕž[RÕØÖÕ?2>e*žWÿa͵e…YžS1×[UT>%˜+&5Ú38*èQrž£z÷buEiIIYe]S[gWwW{smy~ºÞƒa!—º>ê> »+=§¸¢žA<C5BE>ŒÓ<>Ÿ7mOŽ š¶Í£b#á»iÚ”¤*ªhhéèìêh©gŠP6¬<07>6öœ+#¯¼¶‘†;ÛkÊòˆÞXØAfDa…ÔôLê±<C3AA>Ff<ÑÓ?/¸ütôçôö%þ£¦2E§pþ7I <0A> õõ2u…Yn±NÑ-zo³^ºÚØÖÝ;08<<<4ÐßMŠ³ª´07ËçËÊ)(.¯4ï"-)ÈLK×êFÞ$ØÝ*YÓØÚaø<<48Ђô¾Ö\_ª5‡<35>)b<04>x3-3¯¨¬ª¶¡±I¯Ù%UžÅ\R+Ò"Ô>Bnnqeà —6$è ¼¯ÕëÒ#[w b Ð0ÐË-Ñ«^{úéºd<C2BA>¼8?[ûò
K+ªkjkªÊŠ¬åà!<21>Ó¶ðžš[Tñ4(šÖëZi»,¸m!ñ]Mæ3GÕ5•ezaoJB´1j|-,åŠêÊR=@7IJ+@©Fkóø…Aߌ _ÑO .öÿýNRëì`ca| »½¹¡¾¦ª¼8ß°ÜèÄd~B³HápA Ùê†æ–֖榆:®ÍËLOu9SœæÏùzÇsVºÛ­Å­€X˜¨ÌJUm}cSKsKScƒAP¸HzzP—Ÿ¬2± vgš7“JJJ ó²3R 1²S­kàoL¼-%Í$+jôÚéææÆzæ½(O/„NŒE̓#]D 
¸FÇ%¥¤eäèµ´´0˜êÊ2óÆj—Ó<5Ñäçfgxœ6KôPXÄtèN *HÍ`:Ì š¡Æ†:Û.<2E><>˜8<CB9C> ®‡hSBÎî2s”ÇJ¢ÂŽEˆ&J<>ÊÊÉËÏËaVS<62>êfstçøêþÅýOnEzI~澤?RPdþNüâhs'A'
óM¿íæÔtJÄ\|$Ÿ[NžÞ _¦7©0qÚP“˜Ÿ`½ÞãI3<49>rÔ¨%Æ,¤$cÎÍ/ò¿„½<E2809E>¢>OjŠ-^ü †šS°aÓkÔ½é>_zºÇ<C2BA>b³æÒºRáÇ–¢;YÔ%Åù9™Ö{áœé^УOzy…Åô—¤=56!Éžìt§y¼ž4ÿˆQØ"È,—W¬.,¦qM5°LhÛÿNz®µˆÚœäP­©©n§=ÉLÑó%´Æcgu Yeà…,Õ%ÀFžn™&¼nv¼EÆnàûMòG½J}|ƒˆdèæ }ÍÕŹYž43qV¿é•!Ù¡¡p8N%Ó¼ð*“¯tó@OxE`2&.>Ñf³ÛmI‰pØ(.l0<6C>ѱ”t¦ªhf¦/#Ã< ÔžDÑpÓW>“Ê°,i.!ÑæH9ìIÌw¯¹Æ`#&>‰ÉLóšŠé;“m qÑ$}¬üH*Ê
¥¨#Åmu‰>y5pÂs1 ':6>1ÑÌ[U™
"´¥Xƒ²À z½i.µmq4¨½¥ÅȪM²™Jc¬ÁpÎOº/šÄ§5C^‰—6Âëgf»ßÏÉðÛ€à·É\þ
ü£òß¿=Þiô¦fš“,wb ßOÂbhŸu$Ó™`³'§ˆ’Ø¥ÿ¡ááLhLŒ&6!©´)ÎiV"¢bÅg{²ÊRÒD+L!ÓÃ…æÊ'bYÓ|<7C>ÐöôØ8QL42¸=_Ê5ªX“É lIñê¾&•Š¹î ¢¨ª7ðw)œ&ÄÓ£ˆ´ÍX U…¨“”{"U`¦ƒA%ÙLã<4C>¶)@?i:¸ÿÑ"rWÕB Üt<C39C>s~²®<08>ˆ0WD1«´ËºD¡2mÝQþ,<2C>l÷^ч~Ÿ~Rèû·Ïd<C38F>´¿¥°=Ñ,8«Á÷2]c6¢caV||œ®ÕD2oP¨°p¾€ ù\Z³Ü`¶³ŸÏVÑ,Wi ·—E\JGWsÖO ˆfÍt?Õ«î«OÏxƒ¨F°2E5(6&&:BL¥<15>&,„Ðü<C390>ÍŠ8 2(K¬UcŠ²jø(oÿér® Ñ<>-|1gtðE­ügöSÈ'.ág<>Q¤ÔR|%MºŠ”ŒûSôæÿŒÌåßLrtE/tËrûïç±úM§L¯ž‰ò!""RO{Ö´i"9¡Sœñ÷õ )Ê1ŠÂfÊ©¤Z<C2A4>ºîÅ•â ç(ÆdQ)?¦R.A2r™nX<08>¨;œšé¾¹”Ó¿EL7¬2%E,Q?ø¦]µ ñˆ½UWQ…ÚVTBÛ<42>ùP÷ ®7õŠøøãEqZÄOôáSDpJ ¡Ï·Èü1¿~F†Í†üÆoÑ󘟷$|ýï¡“
Ñ?ºI—^G!3eÌßš]鿔φÞ(jN2ÛbåðÇjåÍ+ý¤S\ }æȸÔJ¥ÐGõÉ”ý=â*z¤.™ôˆš8.Ò9ÎR!óë5q×PR­:T€cœûqâϧ!ýå?êÒÓýôòÅ#ëùÒ¯ÐðÄRóáÕÉÿ†Ð%˜Ÿc<E280B9>5EéÖ-å=ù)i@|1gúâãŸ"UÎ<55>Hÿxñß¿<C39F>êè<C3AA>Eüû‡‰Ë ©,ÿ“þ×Ïï%-úCWÿw¤ð8©ØÞ©u!ãÏsÞ_ÂÿÇÿÁüz“Œ_rº·Jn´ ]bÒz<7A>˜†ÿþû¿MªW?žþ»R/èÿÅ|ü" A*6OO°%J@ãm¦þÑœÚ3ó8B†•×ù<C397>nï$IˆËŸ^¼Ó¯G4" hTéÎVÝ ÅÂ?N¦„Ÿž
¾ °ïß±1® ~Γ4/ô)Mâ<4D>y¾Ó/G¸­f÷¹ç·þû
~BOŒ½Åú  LlÌ"ÿ‡ïÿþöíËç;Œ1m×w'Åh7<06>x§_<C2A7>pÀ"âS²JÍîqÿ}Ïôê_Cþcþ?oçø~AD>¿~¾¿½Ô¥±n“fŽ•ùIÞé—$B]qÉ>ã¶[<01>Oýk
 :$zùßojyñ@âýhwun`äyHbÕ¢ïôkq[Ó ­Á<E280BA>Ûú3ÂLà7ôDøϹ<E28098>bˆ/<2F>ÚÀ¹¿¹<=ÜÙPžë%!l%õÞé%9'IžÚn³CãK€ûWý¤cß¿}ÿÁf^ÐÓÕþG,˜ ¸N…¾
º÷æä`km~b ½N/[0ÞEƯL8'Qº¼C¯Óz™S³˜,ÅðW[ó?ùúåëW b0¾ô[ß"!H|ýB!m÷ÖVØýíµ…©áîêâ,O²¶<C2B2>¾£ïô‹Ò‡¡zSC…yì5††ŸÃ¢§<C2A2>0[ëÞÜVeÝ4@¾~Óìæ BæVŠÇ‡‡»»ë«Ë󓣃<C2A3><C692>•ù©mÂ,ð¥bch#ë;0~eÂ9ÑÝ{¥ÍƒsÊ©½ihÀïÏ<C3AF>w×——ç<E28094>véßÞ€<C39E>‡‡GHèÿA÷BÚnyq~
*ö¶7Wµ¶¯½¡ª8Çë²FÃ-yGƯMÊ©92”9ÙxúDäR;½÷¡Ã£“ÓÓó‹ Ýóû¯o,Ò'Àpuuy©»yOOOŽö÷v¶6Öf&G»Ûê«Jò|©æfï±Ï_Ÿ>èV%<25>Ù$ü³ ß¿j§÷ÑîæúêÊÊêÚÆÖöîžîÑ?:>9%|C|<ÖÍbwg{K·,/ÌÍL<C38D> t͹y>½’=ZùÕÿ?sAïôÿ a[6Ûº øHìæüp{uavjbbbjvnaiyuu}}sÓÜõíìl‰8°±±¶¶º²¼´¸0?;=5162Ø×ÝÑÒXWYR '¢ÙÌÔwãï@˜ ÖëáÌÃR¿Êãy&ß>ë{«³cƒ}=]]=ýC#cã“SÓ3³ssóó úY˜Ÿ<E280BA><E280BA><EFBFBD>™žšœœèëîlomj¨­*/.Ði³áVã]bü-H/ñHÎ(¶ <0C>û¯ò[…
?@ø@ìöâ`s~¬¯½©¾¶¦¶¾©¥µ½³»§·o``phxhhèïïïëííîêìhoÕµU•å%Eù¹YÖVØèÈðP¢ïãoA04bt7R—y
òI5dÆ­0ØÑPYZXPXT\Z^QUSSWßÐØÔÜÜÜñ§©©¡¡¡¾¾®¶¦ºªª¢¼Læss2µÝÛ™l×îîÈ0,O Œwú{<7B>ñD4ô´ïSüÖ'a ƒŒ½•É¾æÊ¢\_º7=#3;'7¯  °ØÜÊ!Ò½ ÅÅEE……ùyy¹9YY™Únïv¥8l B[«ÃB1<ÁÅ;2þ.2ˆhëµ:º¹•è&p&?2zËrÓÝÉG²ÓåNóx<ÞŒ ŸÏ—™Éw¦Ï—y½ž´4áL1[ÞâÌÖj´ââ]<5D>ü½C#Æ®GlŒ-í"4Þ@Æ—»ƒµ©Þ†ì´dxŸ<>˜d³Ùì€$%Å™b~`Æaw˜{iâµå=:Phkµ”È;.þnDêuU¦Ç®^êN5Ð`™¡}ÿ¢'hÎ 4ëmñzýB¤¹<C2A4>#&V7=ă~ø¤º“&ZR""ÜlšGTX¨x‡Åß<C385>>„õÔv<C394>#4®ž®áœ<\Ÿn/è•fÞä¸(nny°nzàÛ|éÿ8ŠŒ€@D âKÂ;‰Ntg#4fÖ.¬gæòÿÁÐГºÍ½C.ó:uíüX$Áò8 <0A>‰wúÓ=ýHÒÔ£Ï<C2A3>¯ï?¿65¾ë)•[óÃzü‰yƒÑ*Ø.Wƒo>ò¯AJHâòcãsú(¡áÊÖ£ÏWöôþžWÐ qBDcÕÜ£œá$ón8o}L˜û®Ì<C2AE>y!X"ÖÍï2ãïMþõ¡a÷äUµ˜W<CB9C>ÝX¯Úx&sÿ<73>õ\ƒª²¥Iqº{ƒBôdrD˜XìQâZ`hPý;ý}IqÐHó¸ÔN½«Mïƒ{<05>¯Ÿï.·ÇûZªsÒSSˆiâŠà…ˆ¢ô¨|ù²ø±Îg²#‰I¸žeKõïô÷¥‰ièq© æÕ•—Fp˜¡ñxw~¸¹81Ð^_^˜ëóºá¾=))1Id³+´átºR‰~)î•áÕ£‰°GÞÕÉß<C389>>~
<EFBFBD>ˆsxrËú&—¶<E28094> Hl =ZKoñên©« w
÷í„ÒRÓ<^â¡™YÙyzLPiiIQ^fZ²qõniüÝéFhT|²7¿²¹ory…òð©6ü¤}]×ç‡;«óƒÝ­ 5•eÅEù¤H åg+#ÏVß@­±¦²8+Íõ'nh~§_”¤O¢<13>éU-ý@ã3ôs°±ñýÛ7½wðxosynb¸¯»½¥±¾®¦ºººŠŸšÚºzÒ®ÊÍ÷õ ôv6Uúœ‰º×è5Ï·öT ½âêþ Ò)DB??Þ^<5E>ím¬,LOŽêa”ÝÝ]¢îíÕÓówçæfÆ­GÛbÃßeÆßžah¸|…U­ û'—hÜW <1C>î4»¿¹<;:ØÙ\]Z˜™žœœœ˜àפ¶wÍ/,.¯¬mlln®-NuTçëMÏOŸ|§¿-ÉÔˆˆzGÀØœÞÓrs/EÐÐÅ`ãêâôè`w{sc}}U[†W×V××76·wvööŽŽŽö6Æ{êô`üˆwmòO I<>ˆX=:½¢±kxfyëàÔ¼îÛ÷€û݆î9;;Ñ6ñƒ}ÑÁÁááÑñ‰î5¸¼¼º8;$&ÖÛP”ž‡ÌxGÆߟH<C5B8>„„êý é¹z—Ƥyw̵y¿“Ñ)|[ØÐIno¯¯¯./uß ¤÷”]ë-gwww7—'»+2¤MÞñO ?4R¼Ù%5-½£zëŒÞHt¯×†}8 °ñíë×ÏŸ??€<>»ûû{ý<@<40>¢‡{˜£ÍÅÑ®Ú|½®ä]ü3©<>B‰Žw¤fT4t ŒÏ­<1D>êµ¹xð#_¿~ùúE߆,°Ü^_ï®Î ¶Væè <0A>ŸÞ#]ÿ¡áæ=-¹¥5ÍÝC“ó+zçÔ9à@r€ða"úÆgÁòB/*;9Ü^™î¬+ö¥$¼?TçŸC C·ŽILFl”×µö MÎ.­oïé…dÖ}ðŸHÂB·9ßÞ\ëEeûÛëSÃ] „3ìqïÏáúq Œ 4ŠÍéÍ*ª¬kíì<0E>OÍëê°2±+øacȽºº<;=>ÜßÞX]œíoo(Ïó&'GÆ?ˆ€1/ëÝ19EåuÍ<75>}ÃãÓóËkÛ{ûzÓ<7A>ü¼S¾¯ø}q~~ª·”ïmoê¦w"çMÕ%zÜÝ÷ŽŒØ <0A>ŽC¥däè<C3A4>Sm]}CãSsz³Û¶âY0‰i)¢q°¿§¨×ÚÊâìÔØpWkcui~Gïè C™¼#ãE!a1ñIÉnoV^qyMCkGÏÀðèÄô,øPØs}}sc3pãûÂìôäøð`oGKCMyq~×eÞ]õŒ }øˆJèWZ†õB²ÆÖö®>süÄäÔôìÌÌììôÔÔäÄØÈ<C398>…ÐÚT_,²}§#1Ö<$ãÿ@ØÀ܈ˆŠ<CB86>·%»<Y¹…ÅeæMwäÚ»zzúDý½=ºñ½­E·½——è<E28094>ˆõâ«wëó<4B><C388>Póî.ëÅsY9z7\Ye¥Ù<C2A5>!ª««©®¬(/ÓmïY>oªÞqúé=ÆõO¦<1F>-
8b¬wz3|YÙ¹æFxÿÆwŸÏëq;SzQ™öŒŒ2ÁߟG˜Þ*§7jk¸¹Þ+J÷zÒôÂGg²Ýÿú:ÝôŽ<1A>(ýNÿlÂâÀÕ›Ý"ts\|BRÍf·Ù!›Í–””¯d€
ÁâòRïô<C3B4><7F>²GqVÂÂ""õ*L ¢ûÞ!>‚‰, ƒ
®|ÇÅÿ-‚áŸBtçêÓ}ià
ù„
*ÌuïôŽ>Àw„@Èd„H‡ÞñðND0úÅ÷;ý£èÿ•BY<42>
endstream
endobj
8 0 obj
25939
endobj
9 0 obj
<< /Type /XObject
/Subtype /Image
/BitsPerComponent 8
/Length 10 0 R
/Height 148
/SMask 7 0 R
/Width 538
/ColorSpace /DeviceRGB
/Filter [ /FlateDecode ]
>>
stream
xìÝK®-IrfiCÅG«Hª<><>Ó¸=69DÎÇfÎü  [ 4ÛçøuF†ó¬†àWQQQ1}˜í{ÝÃã8~Güû¿ÿ{v¸5wt!ÁbÄ<62>§?Oö ÿ7ß|óûÁ­ÜÙ=»†æ7ÿ[aÓ1bçº.V×u]ÿcA q,ÿqš, b4<62>k ¹ÇKì½8¦†&<c1âo¾ùÓÑEË^cáÚ¦¯e‡k5gA|ó_ŒÙV"Òìµ^é YüÛ¿ý}žg1üë¿þëØ?~$@gƒŸå!@Üà!?œçy,T2(òX1h~óÍ7ÿ¹¸Y˜ÛçnâX77»£+D¢Q†ƒøæ¿íföZØkhâ\_ ‡Á›ÿ}ññññ/ÿò/zÿÛ šçÿ_h=Èþ šÕÆâ)<29>9aêP€bÀ©¶¬:U ÅÃÄñz:Ä7ß|óÇà<1E>¸ÖëpÝGwšè‚nq×V¤&®×_/@ó›¿\ì lht$Îõí lº­w0œôò‡®^üÓ?ýÓ?þã?jþ¿ÿÿçÂØø‡øn" Ù i"˜šfgCaÙ>7øøø8Výì¹8Öˆc=øØo¾ùæ·á¹VðÆpé¼+8ÿð‡?¸¤n«û«É†ìÚêåézånJüßü%bï¼Wã\øµ`uÁ·ÃWþÛ}Í°ø¿Äßÿýß³H°o©<05>]€ØÉ3Ö\ø¿šPILmN¬jfÅkú¾8· = ÇtòéëõMñÍ7ßüZæîø(xuôÒÐtÝPWÕµ~½.)<29>xÖØ®¤l ¾ùK¡ýºÖŸ.‰óõÇ<C3B5><C387>õcÞÏ{Ö«Øv{?ÛwM81üÝßýÝßþíßì ‰¿ù¿Éˆ4{cw¦³A—<E28094>Ñ
8•J<05>âAÀ7±sî³âØû¬Ð<C2AC>áëµß|óͯâZwJ³ßop »•î©[4øõº¡^/n¥Š±ÞBÇ7ixm:ÇzÚAûè#bCç#b½<E280B9>ý~°Ý6ÝÖóÃ1áTdÿÏ Oø#}³ý×ͦ#½[ñÔ*‰´"C<1E>|"X<uß”s<E2809D>äk-õa¿ù¯ÁÛÝÌ™ýæ<C3BD>äZ_Â<>4ÖÛÃ{£—†ËØ Ä[ü -Ï÷¾ü¥ÐNµká¥êç„<C3A7>ýü8á§÷?»³{Ò,ˆ1š@b,n" #Ti•¾,ðPM>—þ*Œð íÌa¾ÖŸS@ó—Nû˜E"ûÍ
×úKòã…ˆ‹æÞ¹}àq7‡šü EŠ÷<03>þøø8Ïóº®ãß7®<37>m¹öÝr{ç;Òßüxµ²ó¦u@ØqØý~õWE<7F><45>ш­9ðƒ<00>` …e#í)ˆêöYñMñ1<C3B1>oÊùúG*°hì7)ìûEãÚ°­¡÷Z?²šÙo~-Ýyž®<C5BE>—Iov=1ÝJ4êc}MÚ o~ŸØn¸8ÇÚôã…W¨ŸöÑo¯V/XÎÁ^ƒáÜàg‡gÄ°7?Óñµç3<C3A7>[sà÷ :À>+˜ÏŠEðMá÷yu¤}jiX4Kz¿ùÝÒ±H°vÐ<76>‡?uÇ_ê²q®—-FñßüZ:Ëë/<2F>ýý†¿ÜÐD­ÛÇ´ûXŒ;ÈËF9¿ù}r­<66>ÛdÓyì<79>¿Þ$à¥êÕŠ66„MjfƒFâÿX$²±kÔÌ"<>]ãLdÍ»ÃÉÞÈÉÝseçy;Ï}Sö?¤ßü…àýƒká¨{¹qúUÀ™÷;aàq¦÷úþšüÜ"kúiê}âfuÅàÒÅóÎ#Þ(¿lm<6C><<×ußüΰѰ5ç jÇá'„ßäl=Ø_» Ä  G"F}2þ]€#nŒÒ,Ô<L3ÁzR̃;Ò³ ñ±þõ6\ +Ió{þÀy§iÆÇúŸ<øaàï^þùŸÿÙ<C3BF>„A/¼¸8×ÿÒ<C3BF>øæ7`é,~Ÿf?P»D]+-h¸˜Á]@WO¯±6¥<røæ÷ƒ±Ñn™=š»c×ì¸_ãþ0¢¶ÕæfAÀÖ³ žì~FììÎщì Î<>ñ$²Ø$XÑ£± <8ëH³-ˆÅñ‡¯ ºE³€ñXXRöß mÇõÚØ2/%~îÚJ?œüôx\ûë[£×÷¯…?šÝõa](7Ë ·ü ÀIaÝ»FÁ/Þýº}ó{ÀÕ€M±5š®뇙eãü°ƒí8ìlˆaa»¡4FDÍì[êÊ‚<00>D6ÒÙØõ°;ßj"Òcw< øÙ°š„µ8,,ëOs¾ÅçúWM,)Oö?;mĵþèÝß<C39D>Ø,±q¾u𷧬QàôMñ—½>(þc¬kR¶o~×zù[yÖÚ²]%7<75>`1½é]dSl™íp×ìE ¿ù=à^€8×ÿ°½+f³xìka+aÇ]6zб“'û„?èH³ @ <>t,ˆ#vrfMt…õÚq)¼Žü¸ýÃþà[l;ä°¶öÍŸ[Ûq¼ðÛØf6ÎööÑ›
<4Ë/ÀÅŸPÜ{®_ Ç7¿»À¶E†k<17> ºØÁv°ýÙDNh~ógçZœçy¬_k®Ø|Jl´kå#»9ØâÁ <20>``wÆ“ÈÞÈÉÀˆü npÀˆ<C380>œ, >£^Ö
me,‘…²\š>(,ü‚=¿ß9¿¼mì…7<E280A6>M9^Ì9÷½°ƒ´­´¡ YMèãï¾l®ßÃýñD69…}ó«hÑ|¬¤/¯‰¦EfÑ<66>ê± B—{çG¯f”ö?/vÁ½8ןJ4Ãw¿ö]4íò[tEšE" Fü$O€â-º<>`oä DÐqÓÖ!œpðÀ!ï<>Ó9÷Îa¯ï×Îà\Çå<0F>~2iÂuΡšp\
o¿J"!èo~Í<>ð¶qG¬§UµþüÚšÃýˆ u‰<75>x»f¬ï¿_rþæÏÅq½>%ó§ûëîØ2÷ËÞÙDˆdÛV6FAïä ,F<™®] q³OÆO½“‡»Æ4,,HX%MX4óA±°VØRCó?#×ÞB6È_pqÚ²Î9lå~"½Áâ¦ô5¡¿ùµ´nþlâô¶±<ÖÙjîW캮o/|Mhøš°ßüiO¯ë:Ö­~­µ¹<èÙÄAv<05>ÍaoÒ1z~Rƒtìú3Ä€Àì·&xZÖ] v¼¯¼µËëÀ·ÔßüYpÚgý;ê½¼—X{<H°ºÐu€dÏâZ?ˆo~Íuèkbæ…ÜÍzÂ!¦]dc¿ù3bOÑF|¬ÿ¡„O ëO%¬»cãlßÀiOAO<>E";Üš;·®½¹ë<C2B9>§Ÿ'èž ñVìð Ó<1C>4k}`Åh8í~;ÿôúŸëžçyüù°ÑH°±ëßjC";hþè´û£"á‹à/$í<C3AD>ÒlãìàXÛJ ¸Fqý‰¿&%gc×Áó»bJ"<22>`oäl#zçØ…VVµþ;<ù!R<áKäך¯m-çÉ$!0b‡ó/)•úO„äîÎõ?Q´)Ý/÷ÅNõ)ö± ½Y MxŠÐ1ÜšÏ[<5B>ÄØxê,F < ~ K:~t9ð¿“ ÈŽ³H° †[ó-?óGb
 @€Øá‰]ÿ 6Âi÷³;N{GÝ®…­¿ÂÕ`ý“•çO½ ‰æ"@`DÜšÿ+iêÝ •³à‰Ñ6µÖ
CÓj³ûúC“´0ñ~ ¸\þtÃÓ×ôof†'²×Zö!çï<C3A7>)Œ€²Ù<C2B2>ÿÆ[çoC*çë—ø”ø adæ~ `í#Áî<=?»Æ­§fˆáÖÄxF ãyŠ˜fb·A#ÁÆ®QÓZ<C393>¶z°Œ^VšpæýéÏßwÑ×k£é?%¿^¸kàaq¾ Á­+cì Nì$q-ŽU§òëÆ/ô²0j¨É¾E$?Wžý¯¹l“Ͳqa,'!F¤x¯>_"ž?~H(-4³¿<C2B3>ƲkŽõ˜ pn¯.ˆ4Šçz‰ìÿÌôµ³s­¬&8^š¥ÑFÀò²ƒ•ßá<C39F>½° ½¬Œõ=²²Éyü4j4¯5özUu®EƹÄù²õ^¯04ü? Ù0âì:ò°j;Ö
ƒ@wG“Áym§%è?†k­‰¹è.—kâ²ØÓ6ËÆaöÀˆ<C380>Ý™£‰áÖ|R;h>ÿˆoýœ žðƒØyzbü¬XXCðÀŸø|¬‰<C2AC>ÿøA!²ÿ‰\ë8Áæžc½÷Ø<C3B7>«™_I…]/ªj,ÏαEKËBpðd†k½äq¬ü¬Â”§ThüAŸ <0B> Í0)ˆ A\óõÛ‰g~;Ñö+ìcp²AÃnº„kâ²øy 6K'íñÊß\ÙŸG¼±áY ™uõ¤ƒaeø£°s­q-$¤ÿȼc®c<C2AE>´Ê€ Â8<<>b†xó°m„åµÈv=ô‚1,Œj#ä×üÔÌƵ8V1ðXå©S6hZjÖi!àqøpŠ4Ä@úZId4}­l×: ¡9ˆÑ…cÓ{ŒÂªP©
V64?VñÐ žsÕoø±R<C2B1>ømÌØ<C38C>׎»#ý<>¶Y ì)ûˆØuð€xÂ<78>ÃÍskÞÐ âÆÏ;o<>ôXOÆ¿ . ï.6ÔŽÛG³¿™[™Ïó<ÖD’»@ûýÆÂß¹ 7ñ¿­ÿó/Nc¦6þs<C3BE>4ЬæñB ˆH%5¯õB Í·\ãEyBI
óžQ§êÄŸº<ŽšáùýxålRv4ˆþp<C3BE>Ù [Â&bü Ü /=/1ÚÏeX45LæsýM¦IÎÏЯ
!¡¦E<C2A6>ßÃzd<7A>?ë@è….Âfï8ϵBfpfÿxä ÉÍb®cm\Ûõ
³P-½ZðE°¤VZ6øa Þ(û8™ÅCýÇâ\_mÅ(Iý˜GPh "Lg͉0µrBrÍßÀõ* JÌ°¶,ò,̨x/?ñ±žâù xˆ° ,ŒeqýÖâ<C396>2öXK!­‰h[cƒ;ÅÂƱ61žšñ«0ˆAsÐñºØA 6èAs¨96v½³ûÓÙ A€À«ÙûÊòú 8öÚ¦·‰×ÚÁ<C39A>ç7s­ NàcýB0Ñœ—Î<E28094>%Å°ü Ð5qh8lu~¬„ƒÌlâhÈùz£ÒÙP­HùÝ“Ê£ë#¦’\þdÇÒà„¦—¹skˆ<6B>šH%§Ì0×DÇŠaáŸz”Ó<E2809D>·;öˆ¶Yí] VhâûÑUP€°DòCçka78ÅV¯u˜]S§YàÁwtq²ƒ!mŠ l\ <41>¤$תù|m »`/´wPç É»f§ZÇ^¸öÂòr¶Ôý‰.`"ìr¾Eˆá<Ïc‰Y1<59> EÒ¨ì¬
ÙabÐÖ ×O¯¹ˆ7P=´T•ä$´J—5[¯Ê£!º„yË«¼9êS$!›á}®ß!„ØŸ§øs­wÄM±Sö+øí`#¢fö3ô Ä~$Ø9wéì i#†§gGï~ <20>Ø-ˆh aaYW¦mµïŽ„Mt~öÌþJ<4A>íÿË ÎOÁþš=4w«KL…y}±3<Jèâ;¨Î¡³ÊÉÍïpŠéx{®ó<<3C>žÄu]ÇB°<C2B0>×[…€zPmY8œ,ò`³»wTBKÊcAÌã…^V©lëuĆ<C384>½ÛáÏêávØJ«Çº±Á
¸žjhÔ ÷‰E€ÚÜ"Àƒ°vM…{LôÈÙ楃Àb/*C3ZŠk½ÜˆìC ëiÍ “²
f ]Š˜jYš§ç²¼­3¬ü½Ð+¬9ûžšµðš­ŒÍb ;X*d«vP8NâÕO[s'ß‚(ÃúàÚÞ¨
ĹŽ§HBUÑåLjübØÁ±™ÅT<18>Q'èHgÏÆÇú¦(Ãú(^Uàÿ®ía•gYšÅ<1E>€½1h5³78#=6è 1"jfM_SÌn1b‡ÄˆÓȘ<CB9C>ÞB­³ÓëÀt˜;Ø×Ú;â'¹Ö(Hâ<H(mçŠÓ‰bá.°¡†±A P q W¡.§Ôñ 4T]´$X8œzÅЮ•JÔMôD,ˆëõ/„k
6$Tƒbh(ŒµnH°œ!L0ÏÌîÙÍîRsšÝ,0¦•aň$¶Æ6<C386>¶k @€ÄF•(€U³ ³uÛç<Ïc=8ûä\¿E µy9XgÃ5%dU(?<©&L:pb ^Œ2X€•¶¥8Ó+bhȵÞÃRYOi­³ÕžõTs¨'DvP*<2A>xT-Áæ·¶tŸÝ©7š^ãÊ«Ô'»Öœ §Z <0A>MŠ ]¬Õ&XhÒ,ñ,d°mýÇ뵌cav7r^¯+ _gI:PR Ö\°ìf,’屘”ä8UÊ®™QLîÀ8<C380>´el} <_#Æ#œ¯+˜Gf1ïì&ìãŽ0v #~âY$v£‰ A|F½ÙáÖÄxF€<06>1ÍD#Â2†……¶Îv\˜óàbýÝMÇ<4D>×Ï]ù=à<ÏcýPd%t6-Ό阚UC¤Yþ˜ÈÊÃäé <12>“-8 C@˜ÝÉôPîÔ±~ÊÎ<13>纮9®°` Ÿ„êa«ðiëeÅCI†+SBi‰<69><75>p«o¿¤â<C2A4>2%´AaË>cÂËB* Tƒ¹ÌHxR»czv6xø•g׬U»&`YN¶)²Qµ 0]„!Ê€$Êpºf),kÒ뵚?<3F>x<03>õ8Ry4ßMS˜ˆ0)«e¬Úžð‡HaFÁƱà<C2B1>0+  ueC¬ÂX¥²;ã¹Ö#XsçÁšÓ³æÕPUP! Å° @ QÌ AÂÞâX"³kÍq}²ì<ºŽÅÇŠwXHȪŠmlžzÍ8ðÔ%¬ÚX¥Þà‡.ˆ<CB86><nºÕpn­ŒJlô ÒZ¶œ3<C593>Ͳ};œìpkãOdwx@ì<=qókˆ§Î¾å³®ñ¸ 6ž:;L3Ám1 X^6ul"áø±Î9 »â ØA;NÃéí½14…3Ú4hÅÜགྷág§"«É 9ŸðCºÝpÁÏó<
áÔñ+ØS;À<å‡<lT<6C>ò<EFBFBD>`‡zY…f/•´<E280A2>jÌOPN¯>=ˆ!Æò—9k³X7r
MÃC*È ~ÛÚ
4µ=º^·Á;ŸÇ¨½6ÈÌÏŒд®H³†CÙ<¯§ö2.“îÅðü"Â`ȱ†{Ï”JZÖ&2<>j5ê~$X£Œ%²š,ÄmÍÇîÈ€ÂáŸë³£xöZ7å<ÏcmŠ5÷ηGÖ¼ú1³‡ä
=6tí¤™Yø©`— \kÙ‰<C399><ç* vŠ Y™`‡zÍšà)LU  `øwøb+Ù½rë©°k• ηèÈc½…,¬U¥!¿AÛÁ·ÔµÛ'oýãñ3<ƒwÏh"hƒæ[¾è
ƒ&F`×Ã8G„&,¬<04>Mtå 'Ü9o<07>×OøAtAWèåé$H%a§«ü0<C3BC>yA<79>}.ÁYÃ!OpjB<17>ÄX"Íî8yÛ¸àžˆóZKœëé*˜<E282AC>šò¨4»ÃôŽ!à/ƒ„¬Õ`aa]RõкšK<x v*v½Sänådß
ð±ŸyçögÄÇúyl„ÝvMž<4D>Ù<E2809A>Ý3š%!¿XÜqÓÝ^º²?ƒ<>RIhé
†IQ,F„&ˆA<k8ÁÒ¡)Á,<2C>½vØ~¬¿éòhÇbíZ§Î§<C38E>vðØÏÖ\W™Ù»“F¢Q¬$„lrZKd¡œ=NK×ìJ±£fÖ¶`ce<63>v‡'h˜q'ÿØÊ»Áh8Ì®ò~Þ|¬?¡XºëuZ¾Æú³°°S¿Y`×4ƒÆˆøºù5<"¦y,ŒÆ3tÐHd#<23><6E> #âÖÜѪ,¬3lbËîl³p¨-Œ¶ƒ vxBŒ½¶ã.ˆ—’ ¥7¤ea
Öt 04;îHÈ
Ë"ÙÐÂ9N„3ékÒã\ BÁœ^ï]‡˜ü%d1â3 B© ­äîÅ,V•.zhD°ƒfÐñšì ivT€¹Ìn_<¬mòà„¦`ïvëC¨Sµ†¨)9`A`Ä0žeÉCx…²ê±#çyë5{|Ž3
†76k%ÕLT³¹ iÞ·è<02>Cc³ 8A[g <20>Ð Pƒ§³ËÕÊf8_ŸtðÂXõ<13>Jίö¦ ä´æ„U²æ¾ÖüÇúCúµ€ª €aGÄÎ ;…IhŠ'ü4]OøA=LÓp*·žÊèp†š¯ë:>¡Gˆ}y%”´3{ƒ»zqÑFšnMð ÁÆè1ðÄèH<> k ÖjŽ<>ÃCÀÙc1‡ß©à±³ãÍìSbßÝ&](•Ì Ì’¡€ ƒFB hv<68>œÐÜyzD]%ïL7EÓ<45><×?&pk¼cy<H4TÅbDÔÌB<(À’’»f¡½i"f $Œ]9³alpb„)ÌeR+ éMb³fìšWŸ®î¬^Ì"T˜<fÍõ…<C3B5>"ÙASBÅÈïñÕÃg†=×Ë Ç;:fj¦ïG¡fI¤PZžh.Œ@µ<>Ýá‰]à)žN%ê™óžç±Šq«ßM±æâé<C3A2>„l'ˆ…9Ž_µYóŽ<C3B3>UűP$ˆA¼Q³¶²5³ @€¡Âx«‰(8oj•ÓÊvOÙ­ä^m:[¯ËÎz6$ÇLwcÈÆh‰ì 4ˆAÄ Î ?cïýL¿eFÜøÌ{ïèD 6h+LdÑ:䮪ÏÓ<37>£hïÎõ\<ý˜”Š•»0)nÅM°† <ØÅ ‡¥CÓ<43>d¡$÷Èk“OäRû©æ«×­ñÔ
6ÄXˆa%ÜáĤÙAÆŽ ùY MG<>  ƒÆMŒE"+h¶üðŒì¾øXÁeh¨°  BÚà ú3¦×@š ZfS˜È"(ÉÔ¾æ¾é¾kçyëP<C3AB>Øá¹ÖI£?ÖçÏ@çS©$”´<02>È"Á<06> cwòLï.B/š]1¬“Æ¢BÓ©#ÔïÝH¨ßÁ#©X$äßáÁ.vÆc,4³°Djk®ØßÌ×u¯:ÏõÿŒà^ðØ £ŒUžTà„)ØÈÉšHd£Q,HŒÝ‡˜×ìj ÑëjfUËÞ¸ÖŸ^=šE¶óºd†Y ÉFšÅÐqcü#¾Fˆ<18>È<06>Ä[£‰ wv â'ˆ ‘¸Ùã1XäÐ…ßQg}P:x·¯/„Ùk”Áa(' ÑYgÂðüÙA;Ô«ÇɳøyãŽø¸Aüžh^¤n™5 nFHH³ƒ&vaxÐÒ0¹@˜ŽbÂÀ]ì°7Ó,F€![˜×D¦³ü^ÝV€ðø^vÖþ>_ËB$ŒÑ‰lÐH<­TAK"\v[`_œ§ˆçZ+:h8~6NÍÇBÍ-#<23>r6EÖÔ7Æ9â‰.”a,h²ƒfˆQèp´X5«œ`éó<<3C>WZsÆJ2”œÅˆùÙáÖ”' ùm®é,š5çw[­¹c@_¯5'Îó<î cAȃfÉüØ…€=bàqCè2/T®lM_l=Ùû#ˆôDBÐò?íðl4v<34> â ?7»“çic×tbìP“<05>ÄØáë&ò°1šh©Y+ODíàu]Ç6îØ°×âÛwÈ#[hš1Üšoi“‚Øñ.e½µ\"/RÚycCÁlH â ?F <20>ULr7š…; 1à1*žúi‡g3ä ]+x‡ôN¾½Öžõ „7»“çic×rü¦3i§ÅÅ/IÂ÷u´Ø¡¦®Ê†O k8;CÓDí‰<lŒ&°‹<18>¸YƒJ”d1i8fÞxîŽ×ßzÁkÜ©&XÓ((>ä¼!Ä  "Ò,ìbÇD¬5wOÝVÚa`ãZu†¢HÂ6±PäÜáǤǂÀ[±³{hs=e³Ž
ëþ:·³1ÔÔÅö3É(<28> ƒT!?°Ã­‰§'ÆŸÈ‚@" â ?ˆ<>ñŒ ‰ìofžfMŒø  vx@€,¸Å‡]д#öŻמçi¿bΞ<0F>ŸírËÊtÌDD¤³Oø`‡[sgºš7 ‡Êíp=ˆ—Žu2]+]žTÍ †½¹kh"Á>ÙsšÂDD¯ð<14>ÿ·€[7<>œÁoF¯/ÂV²òWć ?ˆ<>ñŒ ‰lH;xv“V˜3æo~|PìW<E280B9>·„ÓåŒÁðkýÚtüh;ÈHE°0Å[€Øáza^¨„çŠU°óêvê<¸Ç§(äÜáG<6E>½+ÍBr˜ ¦væ9Õy[óã…u¶)Â*ÒØ å4`ߢ+h$XHdwL GÅGPm­¤š;!;×z
ÇÛ‰²ÎASV*iv¸5±{hqcüc‡šlÐD¼Õ‰,ˆ ‡šÙ<C5A1>Ý“ÎbÄÏ3Cvì ƒ×j©k³®õ—Øçyë'®cIÌÆA<41><te14žâç1$h$X³³Šñ js;4Uëóç4:“¾/žÎ3ò + ˆ<>§gG/&-̪'h ~¨™Ý¹ynMì7i+àkâÓïâÇgÛÊ«$+ÃP“ #@ƒˆ·:ÁÊ"¬¿]°jÓô××NÏÇLJšo'­<,$ÿI  A€4A`hSƒPëYé˜ù³É¹þelÖÅÑeåYT B6Œøy ‰ÝÂÐT¤í¶æjp¼r•Ê¯B¥pAܽ4 i,¤ÚyzvôbÒlÐH°CMÖ¼¡eóø@X^Øy8^ç„رò=<)k8 i É~†ÞH<C39E>ÅM°A<07>;9³ƒfÐ1š4ñVÄèÄØAÄpk7¿¦½NÝÜtGÎöÙÄsû¹èUàÙ8;nã›…´ <20>`A€4vb¨É1Ôd¡hªÊSîŽsèSB;“,¿^5Óaàgˆa‡š,ˆØ54åa˜Žf¡IC4* âWÑVB˜¶ÆÙ&Oí<4F>¬7<>+€ Âp$Ø ƒFň<C385>œÙA3L´uP1x³<78>çy¬¿¸¸®ëXŸ¿6‰0„E²òš †[s¸ùkŽ»kb<>ûLÿX·çë¦ÀË<C380>U¿`Æ‚ CMˆ¡&;L“0šZ©¬#áþv#\mX|_÷Ý­w<*Õ(ˆg¨Éˆ]cš#pÓ @ …y¡¨ÙòªÙV¤R‡D ⺮cý{¬`C ¤!<21>´¡Éâ&X$XŒøŒX @ {s×CÎl¤w{ã g6ÒlÐ @ Ó܈Hgc44vËÂYbáÌ/+G¶kºÜÀ¡œ,Ù h ü @<04>] š BðÀ½p;úšhF‡MÙÎH5i1F ­7hŒÐ Â,0]äaëÍ4ˆaš7Á"Á<C381>-La.<2E>éa]C_ €f¡W ñ†‡&»rf#½Ûã4Q4µXoZåµA<C2B5>®ðS³×<…gß@D¤³nHhb <20>`A€éÉVI*d‡ó<<3C>õ/^ÚD½Õ?%Ù- @<04>] š vx@ÀŒÐ —š»¿®v/çî5ík"{©2€À¤Ù 1ÂXŒˆzßÚ AMŨ™p†-)¡`v¸®ëXx"¶ÕFórî7±ÛH<C39B><48>t6FOøA<04>¡ù–½ë­±Ã‰Q“41"jfÈÄ  +Éjzù¸]êó<<3C>…¦mõ¦ÝqÁ Qž±Î†)4#ÍÆha7t<37> :ÒY^I¡:»#<23>4pz:Â4³ÆFš<05>@ q³g˜œf´V 4sB k‹Ä[‹›Ø­< ³“6õö`£ †ïðƒ#¨ <C2A0>·š ‚ØNô¢8×_¯%Ø«Osj©0"jfA˜(  MÃÞL³ ` íà)h(˜õff?Ö¿p«~ºF…fH•<48>]Cä klÐ!r,Œ(2 ÅXó¹ |»çk¢—…!<21><><06>ÄÍ<61>S³H$²Hd´h¬šûFXR {¼¾ þ`â<ÏãõoˆtZ ü>Ùýô°7éHg`wò°AÇèéìN#BóÉøÙADÜ4vq#çX7r² +6ÂvØA¹ëàȹ#Ž\[É©‹h†T78C$ØÏЋ†³ <20>xkAÌX¸5ª%œCBÙ]e³Ñ¨±AÂ<>AsüHgA a‰`1iš´$¬˜Á¤ÁÏ>Ùýô`,´ÌMÑ“º_ <;»#˜•-è<18>éìN 4oɪ“° ¬<><C2AC> òBsÆ4áGí£Å IX7r²ò³ nädA„QŸ!ŒjB(ÉR+ÏL3<ûâãXýíBdß2a »ŸE „HT°æ­`Ö‚³\Ì Ã“ÝŸfM 3 Ù gŸä2¨GUüý¸õ.¢‡ký ü±zuâ<E28098>2“•<E2809C><00>ÄÓÀ¤³78A€:è'ã š7Þ:ŸÃ.° ؈/(`l¤Y$Ø¡¦Å·„;ºæ~q¹æ.låüÁ$ÄGÃA Á"!&´‰Þ¢@þ½ì<0E>xHBßð,lÏU€H£ž,F@˜øÐtVÙtÈz!ÍèþÂQ§9Aˆ<4) 7+F ÌMtC%õ”*èøY$ ºÁɾELL9QëÿêpêÚ/¨¹HC@@ž·€€l#Í"ÁÅË€Dö-ºB«jg§Zõ»/„_/,ÔÏŸ54ûD@aŠ'üõ†<C3B5>àaƒ¾!Œ5íx¸Ëþɵ5‡«Í3QXÈ6ÔefÃØzYè¥Y’Ü'aFS0ÛçÏŸJâZ_kîóÍ/Ò7Êp©@š±ë_D0ˆØõNþì<C3BE>ݹëàŸéà_h4ŸQoöÆî¤?ÖŽ°ðÅ÷C«óæ¤[Ó¯\m ‘ê-õfÃ(þzY<lбkÁ,JU©è¤iºû{~ìÃé ÝcAHŽœ „Âì¬7?ã1*hS=hFZäŽ$!!d†Yï5¯ˆ¦Ö+Ì<><C38C>
ücwp²0Šf!Ið€'hA˰ðF•GyΕ¿© á¼9u*§ Ɇ @ìÈ9èm” 7rê=4Jž± žð žáV[ýn‡;âµæí¼Yöv¿0ñ0ö É6ŠUêÀÃrŽâC‰,Ša ”<>V3 5³QÍFÆbh Éax8‡Q6nz0P‡„ö3ãÇüŸ-pÂSXs1S?LÁífw¾ðìlÐA#Á"ñ…<C3B1>þ² ÞùEOͱ vòdߢ Ägèmýí¸±/N—¦Ûa³¸ã<C2B8>C1CÓp$X PŒ! B~DÐŽ<C390>©…Á¨AªAXv¨™l,!¡úû‰è¤¹ï*—_/„A—QCͱ@I¬„HÈ~ÈÆ <´^j0uešˆ„!àA36û͆ð°F…$<ð˜3ËÌXW Ùh$X$XcMÁ6KX6ø!ây `,ˆCŒ-=ò®€×rË¥f½bDjf' ±ÃSkŒ•òCpB€0½YÙ†i @ˆ4òhZgesS¼Ùêàlcc4½4+g(O†PsÐÂt±ƒQc¥Â¤õF™¥ê`t»Yë¯ÉÏŠa³0<!œ0Jæ*,œFÔÔ+†€Q<E282AC>!x<>Èê+§$-²_¶Çú³IöZÿÁ@<40> iÍYÌ,<2C>m¸DÐAÿ<oãÇ9éÝš<18>º² F";Ôô ‰H<>ö& »hûì;ÛîøÑè°î8<C3AE>ƒ¡wöK¼<4B> YŒÐͬ!:28Rq†ƒ Z<Ã!á?žAÍ~èº)Ž¢¦Ì““…QOô†(<>ÊV' õîpB€'jR°U§.š†³¨ªPÀpk* œ0Ät“j“Ùß,AsZRâ<E28098> ©Ä³78õéf©f <1B>ee{¢++DHeYˆÖJÙæVšE60èAÅD5{jVfHMë£ "AŽ[Ú±<31>"•Zñ® sqêºÕƒ3<10>JR¿$„j• œºø…<C3B8>0vØÓB3ti²òjcáXªÙGÜ1…ÌrŠ <30>tb NZ©¬ h«Á*#Fif1 AC4¥0yTÛ‡#|MþÇúÏéûó¬¦Åa³ƒTcA€1ÔÌ@}¿ó“'»Ãƒ]|Áð™~K,,F »'½[Œøy ±oaw:pÍG £v;hÊB°„Nš#áPuÒø!mÖa&
£ 1%AiA쌇lˆ±4¼Q'yè…02èbƒÖ 4*žP^xpbzÁÉzS;ü´ó?¸¶êÑ%@W£¦Ú¨€,PTM󲊱¤dfMáý=³YÔ#ì6äÜÉÃÊù1£LÁB~ÈÌB~]¦ð f`”á %Œ]‡Fo<> £ÀÃFšÕËJBBæ<ÙTkAì, <0B>ã§K€0Á†@rb @@4\BÖæúñ,Ï¢þê ÃwøÐ B¼QåTž"ePŠg!ÿ¬¹xÃAH4ˆ C˜Y 4\~9+[—´œ{ÂH`D€lhäd-¸œ2§=‚äÕ,R<tÎJÅi©\"¹®ëx|Mà¹B—<dÏ4jf14v<34>]7<>š <20>Ø-Ü<37> #vnΚÙaoZðØûb÷<62> —‘§ó@웣‚' BW6„ÓArAˆvü=3ë„°~#¹ïh8!¤¥A€ˆ]£!¦S°¹œ@M²ç” š36  <0C>SÓS<>gè*}¬ÿ3_Ь¦‡õ¤U9eVÝt¡]/+¸z4=#ë<>ÐÒÉ4<5 C@.tк†)Ø°û¬'j×X˜¥IáÙ=Ѩ°eÆMèQU<51><55>õò¬fO'•^a0
ü» ½ b ½°,S³ I³œ*Ф†h¸g<‰bÄÃXÓ)›Çû“ÿÛœ‰›-@°!Z‡¤œÊ¶»cFE9Ù¡f³°MÔ~Yn
3D0v˜f]lÙ<¯§æ>Ö_I}||XyG…öPbDŠ7Š:´.ÐâÖf9x}M|J0_R€`É(Ég¼íå»ÞÙýtÐ;yX‘Îîð€@"»þa <20>"h$Ø_dÂ7 bÐŒ6"ÜJ¶“f³ìi¥š 4'Øp;îÓ¯wì¹8ÖöÇú×ÆÜt—ÅÁ㜃'UpJŽ§-F<¡TÖŒ®$hì yv G´x£`¸´ 2ƒpÇÍèoÏx¬Þ<>'p.<©.Ân<C382>IQ"Å«¤I!;+í±ÖÖ2º¿Ñö Ìo`Ð!çÏô¢ª°Ý—óÇ<C3B3>Šw—Á“ššðàls)<29>*d~.a0Ä\Ä<âA#ÁBW.<2E>š=µgw<67>„U3qžç±ýïAŽ…Í2©² Ô ÙØ0Ånc0ÐŒߌš°§<üJÒœ`c×zÁ#xê—¡l·ú•Í"§½ LªxÂ(ÃAËBòl¤uA¤¹xv,…u#êChö‰^è…”M(ÛyP³Ïó<Ö²ãX8H¦°Ú¦Pƒáà—-v ½ze<7A>ß:\Û×Ätº`5
3Ä@Hœ vÆ3»ÞáGb· <20>`ƒ~K]Ù<>ñŒÍ Ÿðƒj²AcÄNÎìpkîèt¤m‹öÅî;´pÂÛ,;ˆ`Ä IèóÃÂ-wÁapØçÎÎõ¦åœ7!mFg²Aò
€`E6©;Âzo³È)@ ѼY$X1a¤å„ª¤B—ÈÊ°ˆU¹Çtßr<>鹎=éùú<C3B9>8Áíè1eßDÑì ~a LmÆÍ\ÒZRVroK—ŽŠdRȆz!R1°üG×Çzýžk
<EFBFBD>àAs™šðȉuhº<68> ¦Øá¡ ¦Ó4æ-‰.!œcC—¦HHb¬õ´ªŽP5×cᤠè‹žš-!$Ï>á<>&%,”I­•óÖ"ȦWöœ;<º R¼Têç”Çöµ°ÖYÍYçº&î57<35>Ó±Hƒs·Ð•<Ó©œ<C2A9>T*!Ä°Þà!òhJÅAÙN…Rᜀ3¬¶CbÒâ! opB¯)”dˆ…å±2Ÿ<>ÄŒšf/@°!Êó´ ÆM°qÓH°±kÔÜ-O{cœ‰ì[êb`c×7êºY$ØŸçm<'ˆ°wš¬í°)Ž–&œ—”°§º@Ã<>Ý2@˜$ºœ×ͼc5q]ױΠ9ÇÃÛÀñÐTCI$ ÚHŒE½Ík í¦¸t =§."Á Ñ``‹ÉÂ@Àj@SZÏ(¿Ç¬lgÞËʳÐá‡žÔ Áš†ËF ¹L}³Ð…©¡»ƒ<C2BB>õÚ¼Öåe<C3A5>u£gQrcQ6v¨Éêeå/¾üÞìyžÇkŠN×ÙÛƒè»f¬‰ òHÉ¡+‹„® Å[cc2è* !Y$t…Hñ2¨ÙYµÝÖäܾ}œ¨l2“jJS@Fè¥a^ÖXÖjû
•H¥·HCM$êeEŠ7JS{÷cý<8^<05>í¹:Z³æ,äa%DS°æfä„]Âf¡—f‘Å]Q¤Ê<C2A4>u˜
«Î~½&j}xŒ•<C592><E280A2>Ùz…±0P©H\kg-…µÒ„HÀXȳÃb‡ÄÎÓó–ÏÂÆ¿  @DšéÝFšEbì  b‡#<23>fwÆC šo™®D¬Ð6ÄzI&r<16>…áAƒ?ƒâ<C692>u æŽ8œOò_×u¼0/ ©@Hk¢ÐÌ"¡"7?ñÙyãqC ÿn庉ÊéR{R5»2š>(ž×+Ë<>ï¡<`Ô„·<E2809E>xÊæ ~à‡0˜kVÕ<2ã\ÿÉA~½bÔ¦H£Œ…0©žðëFâY|¬ÿz9ñf4»é|W€.?Y]!Rš—U€šy €<>²¡ <2B j¾ÖÛš7r*›õŠó `R©Ê™5K ¶.,Ôl­6 MÅì©ŒºÁ ½Fb áH°Q<C2B0>78áéŽuÆXX4V* C3L"¦K…F9!¾€S<ÿä1ü7« ´Hv*wZÎÇ׬Z?Ÿ,µïŽY4! H{cº”d}”ªÎ¹ËÒ²ð<C2B2>¬*c! ø³A#<>t#bo¦Ç"1v¸­3t!<05>§xò¶F<™."h$X¸‰,vë¶CvÐî'æ @ÌŒJd¡k¬Íí¤ùÝâ,¹ãôƒ'º<×Ü Ñ„y%! 'L4p²ƒ^(Þ‰Ò厳?ÖQ$dÓ+Fd]ìPs¬06hÈͬœ2ƒGf=5{®ÿtOGƒðŒi‰ —GÚàoÞ AÔ%Ø\V†î}r½³2ÿXÿ¯¦š2”™nly<6C>ÈêŠ"m´äË%•JBη˜ŽE;ë,öIA˜èI]"{"ó ù]¡æüšCå×uëÏq^ƒÔ ¤Õ„Y° Œ€EfMÍ¢f½Â@£ù¡ <09>ßSKbÍþjûŒŠ?ÏóXkîs`RÅËc^i¡Ë,»¡ ÂÔF;<3B>æÕEóðëÕÆ6*´.f^V ­ü[|®ÿ¿àc¡Z³<10>rîðgÃD3 kËd“5-šGP¿0Áày<>`Aìð <05> wxvxÞÉ“ ‰ÝxK]ÙH³ Í<18>`wò°¸ ˆ'ü 0â-zí,ÛíhÙ _ðhr@ò„_¯ lseàqæYwüº®ãK:!ÞNÞÃýÑ”JBiiÖ,ÏX]„Hñª¥e`‡_UbP¼<50>Yšä§I!¹)zj³;ç„Æõzç_ëéx<>U<55>ü!§¹ˆ,ð£¹Ð}M 3ˆs]Û|ÇXÈ<AóGÉÕêMå6þ“Ây*¦KÍßÉ SÐY¡K3¬„J "y²¡‹Ö\°àþ¬¡fõ¨Šç3ôÂ.(ÛíeKHHn¢<6E>¡ „HÕz7ÚD/:t\9u‰ ž²H°Ð+@c­9{ýDý Oȉ¦Èšº`RÁêäl^Å·ÐË
O´.+I-£#ýEåz;VÌ(Ã%‰Ò Ÿ…HñF)r¾YÍbœöc}ˆ•!LIšÆBžÏ"hH° b×'v±Ã4F <20>}’ŸÅÐ1Œ'<>ÑO<C391>шáÖˆ9Yƒõ~›â|Ú ­l§ì¬f10*
\§ÝIëäw¾@ÎõJ<ÖûßÔ„„,$o.ö§^‘¦¦«Ü+ŽíB=ª Ls§ c‡[fádw$ÿ xÖ 2{<7B>Ow]×±þÉ[Œz4<7A>•$LñD „)Þ^ÐðteŹNfZfÁ†Èœ˜lø!Œ'\[enëdþŒ¼ÃY´ì!-³ìä©«yÕé¡<C3A9>Î)„àM@¯HñžTÓS[êsûáúÈgHÙH%!¿Ìf MvÐÔË¢<C2A2>µbDpJ"& ],èbE²(ÃÇë/‹ ù¶¦âý)XŠç”MZÐM´0h`ÅÀ(h²;º8eìa ô2Ÿ£øijœçy,<æÜhyBSÚü!Ò,bàkòãõçei MZÊ ”ÄBž± @½Ãi»j²H° @€xò™u± näÌîð€ˆÑD¤Ùá&<26>‹Ä¶õ:Ÿö;eg…A<,]&XÓ9g<39>4öº®ã'Ø LÝY6Lô]šZñûÔáÞ±%C7„… 7t<37>4c×<C397>­S˜H >¦®¿»v®¿ò±ÞÞ~„RY£ž<>äˆVrâñ„Ec¯×_€xRß)b˜´H° êžÙ[`#ø†sýÇ÷š×ƒKÅYfÖ,~Ð=i]!R“4ùƒ†Y_@«ªæVã¹ÖF(Uv1”Üt1šjæAS{I<49>DýzÅhf<68>Ú-ˆ°æÎIkþõ P¼¯‰çuŠ*Þ¼º¤<C2BA>fÉFZ—yAOñ2Ø÷è] aÍƆCÎ:áìղ͚«I oŠ¬ÌHŒ…^DÈ Ï,¡é¬Z=õ;½S¿^ÈtÐÃ4»<>§fÍ'ü;<1Œg 0â3 Ó$04F ͈ÑÄ<C391>ÝùóšE»`;lŠ£E‡°ï ½ ÀMž ~A±×u?Aç Û!1/=ÉaºH³CS{“ÓMíÞ±pìóCI+FìpÀÐ<> ¥Bre[·¦s X8ÿ8ן ¼ýÜb¥
6D/d<>tÐà‡¦`™ ÏÂv…-ZÐ^<5E>Þ0Í+˜äy¿cÁÝMBf[VÎ/âãõDžZ…žHÁqešº„ê44rFÁ,ˆ0<CB86>…H£Ìè5e¬!Õ󋳄åb¡  ÃÔ Së…§V€¥³šFe¨KV[s\׊?×):Öç€$‡YèÝÌËBñ,¿¯€Q'z_0Å.ÚõÉ<C3B5>i<EFBFBD>¡TejÃ% 9¡DÐ!R¼QÆjÚâ²™ˆÐì£5¦$ñrBèb?CoÐ;ã!vxMOÆ?"4‡šc1â Ä 5Ç~Í-fo¾Õ‰ì[tEš4­Ð6……=bmåì”& yÒX¶0‡“<E280A1><C5B8>‡„ö5qÅ°O-¹‰B‰¬^aÚOe¯VSC3Ê) ´<F<>©vx<76>ÈÀÐ<>j K·Õߥ?Ö?@Œc½ÄtY[‘†º$¼ÁazøGr^¯+|½þl²cˆ<63>¡ ÙvøÁ¯ kBx9ÈÜ×äø%L ó~¬ßÉ®¼'3˜n¬.CSÍ£«Á¡É­ "i3š—°¬zØ_DعÊöÔt$šš™Es±ƒ&„Aµ¦öá6»­t®t=“ <E2809C>,ˆº„ & l7­¹Ã¯0Î/p]×±¨xe°Êšæ4C—0T<<3C>ÊA„.11FX4Å;04ÏÞPm~Ÿ{ eRSkJÒîèbùÃDâ<44>2Ö;AnV<6E>Fëi$¡Ç‚qƒ3h»8AÜÈɈ]ßÐ…H³±ë'zƒ¦ù™ØmÐØÅÎxF€Âú‡íÓtNXG…µ•6”Ð%´Q78Û*ƒæ•îw÷¹CÅÿ5bÐÙÃ~Hfj¤uA˜`Só˜š5u ¬W<C2AC>ÛçXbæ¡`쎜ƒ˜l¤³ McYÈ©SXC3zÓrâ\T ”Á
6tyX tAX99Ãmš Îõz<2 6„6ò œ ø!Lp™gÙ¯×9Ïóxá´°J Ú,OtAo˜:”¡© „HÜ[¯Y iF|¼žýç±mÍ,W™³ÍÅ"ÁZdS;äöœƒ˜0dGË4£5WŒ³ú3k¾Ç4µb$”6xL”EÕ%Œ¯x§Né¾b²¡ÆÂ@_“î¸æ¥z(<28>F䦞áe“?Ò,?ˆù9ðçy+-‹ùšS?1¥Úá<01>ÄgˆA‰±Ã­ùÂ<>`‡·M6Òì/2aÄÏ0v¨ÉîÜ<š vløo8-ì¾SO?­.tTNÍÏr'͸~îÕ41N%”™5Yz!R<>¦vA8ýÙ„Åu]Çz÷*¦·‡çÖsNb´äob‡ÇBrnÜü²rï*L® !’…±ò < ø‰ÂÔÌvƒ<kÑ@àããÃgÔÊ{:M4VìPS/dVªÌEf¥ºþ^×u_b^œçy¼>ß!!+3Ìò„…55±£"ŸðÀÞž«%±_#ÆFìË¥ þ¦Ë0)*»©m"Û+½$õ
ƒQOø!tøÖœþÎõ¤s´aÎ,üF7¯ ö<>sІ¼E/ÀóÊàl{¥w?C¯$`j³ò€<C3B2>üà<C3BC>eh‰l_ÂÔN¬ú…U?'$¦9â &æ&Ø op¸1Nb'È"ÁFšÅMŒEÅM°f?C/ˆœƒ&Ú;ÖvØçÄ9*¬­œ<C2AD> †<©W˜`ÂX<6D>%MÄ8×l'Ç!ojþ2³f M vÏzK°Þ,œ=™<>õSÜA…y:Íò+XÙDÖDÙÁ,ÁÏÆè] ¥*¿gñÇ¢>"VƒÐäTª©9aHM ~H(XÂ}a=—ëv]×±¾&žŽ<C5BE>™ c!ÏÀ 2CfN¯ æe-²U%,ióÊÆÊl
"i] Dz(¢ÙÓ¨W0n cY3š×S{ösý«Uõ“øÁc=%¡%4/Ìò] RÁΧM´bPƒf]…±†„& "ôŠ,‰¦óÀž?Q¿^1ÇÂ!7/±#3Û\YùMjg^•³û
°Eò„_¯0Ih}Mg`ªÚQ-ûãõOáiSyPNƒ¦.â•j¬ ò8ðrB—óïʦ¤[Zö-º@` <20>`A ·&žpéì4c×_ 7Á~F½,ˆ<>§g§ÞÝîðX¶Ã¦6¨ƒJ8i9Q˜QuÉÀÚ\½ôœ×üXç
Ä\ë_Vñ²uM4ÍËJXfÖ,OêHu6JyÎí_¹tÙ<>õŸFôòQæ` dÙž˜k0dÐܺäT<C3A4>¥ðÖÒt\B^DºÌtü†?© -,á
w<EFBFBD>ÐÓá<Ïc!³ (XfMÃ!Oð°ÁœalKG_ïÞƒyA Syæ¥å„)Þ¢ Â`‹„š¡"wôf¡W˜¹¬¡yy,ˆÃ£æ[UO8 6ŸÂ:¦Š<E28093>„‰04ê5µ!„åò¢“Äe±æ<ºXa0*xØ uAd“¿X¿.(ÞAR¼cгË#œh
v‡G+L°â«v°Œüz…Á<E280A6>·è0C†V^UjÓÜááW­÷€¦IMMHrü,øÃDâ<44>Ò[&QZÂ"°×4<C397>•gÐé,ŸÙ AÄ®oèq#çnA ÁbDhšÃÞ|j6hŒÀ®1MÄ~?‰õ£l
kƒØ˜<EFBFBD>œFÅhþàqXw<58>íÕg€}ÂNˆÃãzV€yY9a
T(^µ®¶W"§CÎÊÌB~¶z¼ÏÍB4Q¬á<C2AC>‡FBæAæ D¤Y0\6Mxçôûóc}[Ínq\“g Mà§ec-l»Âìu]Ǟε5—Ì4Œ ZªüЕXɽ£$o#fénä¿Öןðh=Žò4S {“6¯'2)m^6xø "A³ƒ¦.0£šm¥ÿ±þU‡k[“'*·PÂ>Ö?o­$1)”¹‰‚](Ì=5mvB“Sý<ÂXCƽ˜<Õßš«<C5A1>Sì NçyO„lrBþàÜ-ô²UhR5%i & <09>²ƒ&„IÒøy¦ò뺎 »ÓDž—5E”slðkÂDâ-,-k}äaRæ¸zÅ(I¼±)F|†Ü»Þ:w
``#ÍعynÍÈ9 ök&&1#@#Ábqc÷Ó¶„Mam;p>w
tкB¤xÎ<EFBFBD>sý1<EFBFBD>xÂ?GÑ«ÞèI9iSdã¦!u®ækr®IA‡è5â6 ÷°²N/<08>¢´¨ & »"<‹›«$÷ÎÂì<f4…ü³ gwø!LpµÁ#°èéX÷ëÇúÇ<C3BA>^Mf)Þ(c9åy^Ì¢[¯H_
æB[f=UâF[FMIš1äçÜ-ø³"Å«Ó:ÍïWŒ!HŒE½aˆ‡%,/köÊöPÊÖôFe—žI³3WhB4ϼa7<61>"νþèW y ' Jewòœëßâè7yÅFiå<69>“Š7Jµš°Ýi~VF!ÁbÄ$¡áT+›ø û˜È“Z"ÃMr†0ˆo”±2˜eÇ>jšÚÙã<C399>Äy],FÜÈŸ<C388>ôXñkõÀ‰ `ƒÆŒ&pìL<00>;ÔÌâ&vû„DŒ¶þЄMa;`r»iO 1 ö”0Ä@¼9mº­ï$ ŽˆÁ©ëšÀY­È Â,ÃÞ¤a^C tË4ãZëk
Öï:oo]å îa¡`ÈõCB~HÎB% ÐÔcƒ†%ƒlf1£§óÀ¯WŒÈ0ºX$ø!L°TÈïr±Ô"Íz·³)Þ(Úpy@³‘ÖZ¤x%Y +S~\×u¼0ˆksÚ_ËH<g$pºB¤x3z¢<7A>A¿^ aÍzY„Qj&ªÙG<C399>½Ö_Ýà‡» lºI•Q*4ÅÍ0#ÁFëÁ½Íì¬×?&•H£nðŠÔï§Î±¸Ícm±{äüÜŠ§!!ÌÍ AèbÑš;<3B>v*oõ W ø1B¯0 T{­
o¨\Í?_]<5D>JÚ'u&Þ(ceðì²™HZ]añYX‡†ËBªA3èao¦Y$X~Z$ÆÞ¸9oÍõ²AƒÄNžì NOvÿèÄØHgA šCMë¢í€sâ¦Y{j³ˆÂÅšQ€<ÅËã$tøöv®õ[ѧ½l<C2BD> çœ6#»cA3h˜MÚMÑå<C391>ÁJ~.š£*aÊ v¨éËF+Ff&
ój²ƒfÐ<><C2B1><EFBFBD>n$—™(š¡RASU6¥§hIq]×±š“ÕlìØIÈêäâÕ#?§—¤í°†rjZFS€€ÅôŠ0—ï²Þýé¤ùA€½à1k ‡yéœñ;ü,tÑja ëÝÈÆyžÇz±¸Våœ=ÎÐ@IJ˜…)<29>È(@<«`áÄB3ª_£"<22><45>ÊšW†5÷δæôyžÇdÖ¼­ÓÉ [ȺÆB4a:VåËŒ¬À³§Ò4*FÐ+LðÔ¬$«jy5oÌú{.6 g!Ui³ ÀÍ°În«Óèñ5å„ µ šP¿b”$³<>à”g¨É¸37øƒ~­ 6F?ÒYŒ@:4ˆAÄPs,v=bgœ ë6…u»í “ÆÒm?DòzÅØV²°N<C2B0>Ÿ^ÞB“€óæd:ä4š®l¬Ì Ù<C2A0>˜™ÑéêOòΡWŸ)#@Ó)eÅg5A‡xvZ¯;¨¤¦€é`jh"Á ñFòXLÏHC%Ê0<C38A>´b¤âd Í,øC¤lŠá”<C3A1>õæéÑZIÚŸM<»Ê c!á?]!?<3F>f#¬•:y 3Û6ñXŸþþrÉ£5—á<E28094>‡ÎJŽ ëéYÌECÁò˜w<Â`4w ],ÊC„e±È…¶&H¨Š·>½<>MWÙ†K2 ÁP§jm¢E<C2A2>К[1~1²<31>hÔØAb¤"`¬E°æ4zg­yÓ™´âUÎS©JËFš…^V¤!*4\SͬcË&€cc×å<C397>`Cä) ®ë:xí÷•±)ÊöŒ4ä 9ƒ“ ~Í)Æ(ce°²ÉÉy­é¬I(FI† y¾`F <20>ň·ì½»þŒ‰I°H°Oò³CM6ÒÙØuäɽÃógWžÝšCͶÀv°¶†µƒNhž¯7k<D !Œuêd³ïÞN®3'œvšpœ†Ëšƒ)žˆ<C5BE>^3šNZfwÐ;$Íö,Š¸b9+ÀAÁ![CÄÈ?… ƒÙ#ÍňgÑì*a
sÉ) HV†·è R<>ª*¡·:k1AhzáLrñF½RÅ®¡7b ábL!!-§.³ì+Ã/;Èœ º¶‰hä±Îl“æƒFíð„Þ˜T†Ë&•“æåÃã ;+ãøé 9ÎÊÃJ»Û-Å{dÓIåSÒ\´Ù‰Š ÂÀœ¨7ÚVJ•VÍЬYfÍ#Cè•<nº^Á¬±2;œ²Eõ½<>§`øüYänYïùÞÿ,¬«là y gð°ÁaoYûc}M]ëÏ&vÖ†ò<ëg‡i ‚Æ.@¼eºFÄ4w<34> A€x2~Ä[Þv½uî<00>D6h‘Κ @ šOlAؾ6%Ú)؆ÚV½šÂX˜j
ͲÙkÖMq*¼…Jë&†fiHsA9 ö3Ä !æ2Ì.£¦£ÎjšºKd
ÜfÑ¿áÊ(ž<>ÕäÔ+F3ÌȆ²ƒ^È ÷à^>%•$3'!€Ì2° @„àj0VU¼Û-¦çõæa5ça© ÉbÀ†š-Ù?h*ž_ cÃCÂA§xSÈ`³ä„´j <>Ù ¡+hÙX —DNMØw«A ºl"e™CX¤Ù£i ų­<<¦Ì“V3klvÐÔbPá¥m…!-ˆ*×+¬) CÈ aìP“ ´ [Ú<>daÞrŠD£<44>`C—f†;ûÄkßûÿÇ»n"uA¼QDœ¯¯æzý§„<ˆ•QÌgõ5³1:±Û A ·æg Ÿ 6h$²<06>¡4<18>Ñ»ˆ]C3hŒÀ®ŸXˆ±¬­a;ÃmÝ÷ ò=hBÈJÛÁ°ûÎsåÔShš5äD :è" 4<.<2E>âç¦ÀIæÔe
1"9³H°ºS<12>!Uº£aÒf<07>]@€`*ÉׄÐäÔ%uÁð¡&«7hÁÊ ÔÉ*•Íµòf£áñU. HC¶Ïòƒ0Vy„:%×[°äAïÙh¨w0\B•ÓP§åŒ¾ å„xh†ÚÔLX„ Íũ˼;ˆ)çnM10
’ÈƾãÕ?N `l»(€…œJÒ”™•GÁMBñlˆ7
tÙØœ!L¼üËæxpúaâõÑ+F$Ë‚Zh(F*¾&×u¼ö}MüÉe¾&œ3 ät<>à‡0M³ÆÊðcý½Ÿœ,Î×¼úY?äÙÀbhìb˜&1hÞxëÜ™€ÄX±ë·LÀ¤³Ã4w‰,nblŒ&Ž4 [ÀÂ9±Aþ ëíä½ÇÎö‹†`Ÿèbõ,Ëv6 [D~1òMVªNvÐ!´ %ì¾(Þas;öÍb
V|Œ&Bò8üà«‹´Ùƒ´sÒX¸>%®¡)¹.ÂÃÀA‹„^ÐÏÂðêì=i>¸åçdß"XCcYIX0ˆä<>#@ ` 1V厙¦Õ¡øb‡&d<00>Dc+9+¤+;„¡QYL6b‡„HÐ<48>Ͳ[pgÌkÙ¶꯫ƒ€ác` È* ÆBSžœºÄ !<21>$F'²ÂBÓX¶5¯fö´>D6pàABkv©â-!‰ïýÄ›¿¯ +æ) /~)ž°°NÈ<4E>õ5<C3B5>´×u/ÔÏB|%‰t‰ìP“EE :hƒfÐo¹uÕdƒF"7Æ?bç欙ÅSìäH³Aƒ4AØÐÜω âGíÖ8 „^[F@¼±;œƒ^Œ0
†ó°ƒfèeEˆÉI<áÇ$T¼7y|JXׇ“ÐËÊ<C38B>7$<24>H8É!X<>E°H°P*”äFp† ÈB—B0;‰¬€Ð”™EÃ=`Ôƒ‡Iù <><E282AC>šAï4'x@É<C389>áIþX¥òà
®Z«á ë¤ùy¯©fV˜`5œ`oädÅ°0Š ³dƒ†ˆ‡&IB“ÅŒ&N@ýöÔBç)8a ÖØ<C396>4CXÖ@"ne‡˜àÇ$!vxŠa<C5A0>"¬9kÍÕL([ÍÖ¼ b.aaøŽ`„±†°<E280A0>„½¶Qsàñ5ñð-0/O˜…œº^³€°Î¬š}•d“SóZÓåhTŒøÆ=hÞÈy³ vx@€@ň<C385>§“fA ±[$ØϸõÖÌ~Í-FĈ$X[ú† >¶#G´e <65>ÍÆS Û1vG †áìN‰ì q#é-:G7áé2È<E280B9>ßØ y™5
„&¡ c#Í"11áëìÞyí MVµòƒ±Aš<>pàÌ*‰<>$¶ÉO5O šz C£dÈN1;<<3C>!訋<05>†ï 10êñàÊó*ëtýøñÃÁCÙšöiHHòD b4AÀØàºÀƒ„±AßÈ9¶xIP…UûññQý´ïãW¯0CÂð'b@€4 gwêbA4ŠbÐDRµæjsðæ¶ÀFП•dÐ?„ Ö„lþŒã=O<4F>µò]he +3ˆASèÁXß#³ô5çùúš8ð¬f 4!Uvбóô<yssîM Dбë<C2B1>ñ'²ƒæðE“4A 5³Hd‡¯­Ö¦8u˜cæ°ñزýš °k´xHµÃÄ rÐŒtf`E";h†Q¬ªÔF@µî5hg˜ axpfÁ?ç £ ‹Å.ô²hxëI¸w?ÖŸÖ Ö-Ð¥æÂÐ@IMC1ì`8ä±;Ö\_ ëí1S€S¼$¡É<06>:v=ægÈšC5 ÎÞ–Å©³,š S@³T,ÙASäpkÞÐ;4vìÎî¡ Thkk…} í©â½Ÿ»/° ö‚ 4#<23>…äA#Á"‘…!ÐŒ4 „0ÐU
ö××JU0,8§âÈFA   Âœ(çÊ
èD*âI¯}ëã[@̲H•34à‡Y  eË Ï|M®õg“cý[%‰70xÊ6»F <H°ÃÍŸÑÐqƒ3hAßx:?óÜìF OOðïðÜhñYÛaSœ“˜sb ×Ä<C397><C384>œÆŠI†š» Â0Ÿé~¨ªâ;äpi~½h.Cvv<76>€àdwò°ÂÍ M0#í9ðÞœÖSÓÕs<C395><73>õ5Ñå~ <03>!lFÂpHåayЗÔV€ß
hŠŒ°ñ…<hÁ4ˆ !2TÈ©E:K>ÎÏyžÞþI®e±J<jf§fvGNŒòˆ¿Á¿#æÉÓ?ž“ª±wÅ·­^zš·úƒ†<HdA B|Ü´Þ'ü M¢TìkÎïR+d¬Wµ" AØ]¬0Á†ô˜q]×ñÀkŸ5<C5B8>e±VVLÓðò°Ò<Y~V£Œ%àœÈçú<C3A7>­ë<>³Jo,4¥Âì5³HŒ »»Ž<7 âÆ×ÎDv¨™nÍøgÍÝ~<7E>±ëÖ?ÄØÖÁc66ü°§Î<C2A7>“)ÆÆqhv'ÏgÖ(‚¥ #0:Eb·  $…uÈ•êu
~v§!,$ÆBmÐ B tÕc¹h•øëðŸçy¬¿9„iBØ5jfAŸ +'!y¯¸<>·Y Ä<>¯<EFBFBD>‰ìP“5ZMñNóŠà<×<C397>Ê÷EÙŽŸš9 <0A>±Ð,ÛدQ“ýšbn6hy XOΞBÙô¹þ‡ö®ŒÒû­Á¬<C381>¬$Y|!0:Eb· @ ¦ þÖ|Çšgýµdž<C387>Reͱ ô²0Ä>Ú,[¦y]×ñÀ;¿Íõ5±Vö´5) +gh² øƒo”ë,ƒfÙb×bDªß¨(ûdüŒ<00>;hôgè<67>щ,ŒxK½»<C2BD>4{#gv‡n"]€q#§Åm;œ=çÄuÖœÃN§)˜m $_P@ » >leT¼Cîøi:Àì<6*4ÙH³H°ƒ&n" „ä ¬<>zˆÁÛ† w°õT°0Á<30>bå¹ÁÏ<00>„xÐ<78>‡õ¼Þf÷;Ä,DˆÃoè<6F>щ,ŒxK½cMHÏFxå²Ä;Á;Ç»çy¯NWÍžÈ@ð@žr²Hd  nìÎ]£&;˜Š<>¦uf{Ë<>/|YÔßäÕïy 60[Î,v,|&v ³@³©ÕP©ƒ5Ç|M*ÕRÓ „<ÙA`ëIY\ŸüSxvØË<C398>Òîð³ü¡þ)Þ:ûíq,¤5<C2A4>G°Úr1"Åòìˆac×g¤w âÆî¤A Á=h bg<q#é± vx°¡æn#ÍšA[ü°*Œí°Ù5öcûS¼ƒT˜x£@K…Dvx6`A <>ôXŒØQtAmŠ÷5ñ:Õ$X'­‚£`ÈbÄ þaš#<23>9¬Œ¦y<C2A6>s58óÓýíèÒäôÚ L< Œ²±±ë<C2B1>3ij†³R±ðøö fQ€]ã´Â@ÃÀòdƒ±33šºÚìëcêD<C3AA>Û@þZ+c‰è¾5V†…á <<3C>Ÿ1ÔÜm¤ÙA3vyž¶©3Ïba};|ékÕOØYV—5wkexhBÂ,$²‘OtAþ0£fuZóyíãZkn©ýIŠÐ˪³!ÆBÂþ¡040®•ó†×¾5ñÎw-ˆ UOÃM™ŸðCŒHñFÑÖÙ<C396>­‰ÊL8Þl1h äaA šHŒEbl¤Y$²;yX$² "<22> D¤³ @€<00>]€xK]Ù ƒ~òôó :hƒæÐ
tNÜ GŽhã4Û;ׄuüX4JÎÆè§ "žšÝÉ“Ý1»³§$Å+ÏŸJú a…. ƒ<>çÆîü Í<>˜Q1„ì.¸S-ãµ®C¯ ¨E£ ‰´¡‰ÄX$ÆFÜ,)¢w…÷¶¥àáw7y
ƒ‰ØÉ@üÏöî`WvãÊÒpÕ(É*¸aÀ¨ÁÔ¬‡õ|4>z}ˆg#Ä<÷–,ɲ%Ç?XX±ŒØ 2wf^Ëù 00223<32>Õð\¹#jÊp­ŸK”‡Ò{d ÅG9[Iƒ*ÄM´#L|ð`Í¡f
CM F>tnœ„eÞ% ;kCT?룿ü§ƒ1`ð`âÝÓ<C39D>"鎈‘Ó™TÜšSøäZó>Î÷G¶>Ñ=ºéï,A§Ã€<<3C>¼8x<38><78>ânz¶5¿F³XÏ¿ûÞjÌ,†¥ãt3&¬$5½ïûµx<*ž§0ÎÓ€t§ƒLú5æhf×<66>‰0`vŠP0`vDvöÈS„Fžf¨™š`Àùô<C3B9> ÝxÀhÜëãka<47>Fƒ>4òé§Ì¡1È<1B>?<šõIå#1&T-êQì1ŽÉw:E†âÝ  ˜šB&ÖǼf÷A„ç¿ÏWôYfä¬3ãDj3DR0ȤÐÎ…qŒfRGÝ,äµ6ê®Qä¦'œ%BÁÀh;E(0;";EŒžšÅt&ÕÜñ,Ñû¾_ÌÊ\ëû"ê•ËqºA4 ˜ÂDÐ&øȧÁ€y<1F>BD2RrsUHuÒµ¸ùÃ-¾®ëµÖÜëak®ðRK# †…Cc0ž3<š1Z
&šWóBÁ{),2-=†6,E¨n Ü×è1³IY<49>×f± Ö‡‡³nLÊ° E†ŠÃ,­³ˆK 0š1™ÔsÒg\þz:…w.…ÑÞ™ô<E284A2>÷ø{døô<C3B8>`ð`<60>IƒßA&3ÔL1»<>"Ý)B| ÌÐ-èv¸WÔsÒ<73>»×Óâöígœh„Á৳3AÌ Y¼xH@3ÊÍÇ<-[U¶õt
2 EwŠ¤Ã£ d´ñß¹®ëµ>`®U3¥ÇûTR”žq†™<2)ô ':]Ü•ÚM|ÊÔŠ>È_Ö·”þŒ4£ïëFƒ`ð`<60>Iƒß<11>AFa
™nÊU|\8s¯
Ì šP1ø/kì<39>¦0ïƒOƒƒ£Áƒ0íÐ(˜Ñ.ª+²¤>Œœe®ëZ7W¼g<C2BC>±Îr. AÚàØ 0CÍ4ò Í0-COrk.I¹½ò¼®ëµÞYôad(7
"O!<4D>)Ü”. ªý6ÝJ³8—1NðƧ`D¢YºŠð0»òÏ{°©dÙYÎ…¡àè°S„ÓdÀì<"5ß» þ<> ŒöæAÁ4x0ȤÃÞ̧ƒ&0ƒ&2V>ܸ)nMØ—õ‰Žnßµ>&j`Ïô§N„A#‡£E& ÞYß`:è|Ajvñ ’¿Š*Ò‡Úuñ9ª¿ÁS0ÆLƒ>vN„8ŒIJ·L<íÒx}&4}:¤'U«íÏ6œ9·1i˜èvšÝí0”yÝ#Móëº^ Õ¯u0—ˆS`|h¦<68><ŠÝÿ@FcX¸ù4—k¬¬ñ¸ß¶¹×÷]‡‡]c(jX˜šÔìàGM0`ga̧8ºŸ%SÃuuQèŠîu9ƒ¦•ï“â¨5§<35>â\#0FKÑDô¼wp¢à®a¡ÌeÑ,<2C>5‘‰%•ÕëÇ‹Ü“©›ÎŒÜ¨¡‚>xÉëf
O=½žaq4òŽˆYº•>ƒ>ŒÓ©Ñ>Å!˜…êéBœå¹µÎTÚÆt(\—ö´óà 2X«ŸÂÞ“4M0`À š`"ŸâaÒƒ<¦9&4#Ÿš1~7ÃÞäƒÇ<<¬üàº}žq(ƒô<C692>gãZÿÌÛQ·ØÇÄCÕ)NCA$<24>¹˜àÁ„n¡``œdF*yêsá鞇Üc AÍ=[=<3D>‡Œ0Ô”Ò Bc÷ºib hdÆ,> ¤A­öOúÈï
ÿ6zRèÌ>¹þföYy}ݵ×Â:X%ŸSK¡ §ü¨Ëd(2éŽncŒÆ·Ã\ãu]¯obqîõç}÷.$L1Úh¨)ðÁc #ÛAâƒñúHÀº¹ÅÑEUå62ò­5§Ê…KøtÍC„$24x0Ð<30>ƒ 5 5¾ÜÌ¥Ô—ž L¤$1hRôlHÏçÅã*7MƒÀ€!BƒwV ÎðÉ¢aLú @n24?4øN³è©ÿ¾Î]…)äO¡3u"……¢ñðOM0ÁƒyP0¦¹<Ì(2Lì~ø4ø5<1E>k¦ƒ&0±û¡`
™ýÆñÁwÜAωGw|œ= îàkáò"â•&UÖNo &4M# 5S0úÄxq˜ÚãäÙó¹<C3B3>Uµôº®×zAö0»œÙP\£Î9á)˜ÈÓϼA$rð õgov
«Gw¬$•'…<>?<3F>
¦ˆ|:h¢º(̧Ïæ½ïûµJ
µ2~¬1WwÍ)NdŒ3lf
 ¥ž¥<C5BE><C2A5>¥pGJ :|J‡î•³SœètƒhÂÈÆg¤ôAÁ vR ¾1ùàõ4!==?­žÄèµ³÷ú¤0°æ<14>åè`üáÓfŠ1/%ÈÊ°Û×Üc¯û.«
M0`’›Ï ïD#0aä4x³€ ³ø@9<>¿¶÷àþˆÏ¾#IŠFkpŒ<70>Ë¡áYu-œûc4ù»(Ÿ<>Àž|(wi<<˜È<E28098>âÝ<41fxD4wD0&Íaâcv
¦ÈŒÁ˜w:D1yw
™tЄ;ëº#*%WÁñ)wËh¸ƒ`=‡Œ­§Gw>&aŒFÃ3t¢A šŽÓd sO©d<{>G2¼®ëµ²¥"N
§PƒÏ %C?e1pVðƒu°zÌp}ü?2@sç¾ï×<C3AF>)7·@nàK<Ì>LºÑ}+е›Ú\×ú¿3ö©ì®‰æjc†ñ!BÍ0úGƒM&-…‚£Œô,]k+aäC¿M}®uaC1†5¸“¢4RŒAž"C<>§C3 
ñ]ã¹4ÆJþ×z¼ïûµQòÑš[[¼[£;]F3” »4eò·,ŒGÈî`¹<ðr¹Wn{Jq¯ø—<C3B8>ÿþJLJ†}§C&b`"WdþÜkW•‰²/±ÇåÓ<C3A5>]ŽuvsyÜ?~7é<37> «ÊÃYF°8¨î<C2A8>Án<C381>¡`"ŸFžbÌP„»<>‰ì &ƒÌ(2)0;EÒAÌ;{ük>¬­ ˜°æ<41> î ÛÍ çäþ¸}Áƒñ„ø˜xHx:'†<01>š&ŘpNá! 0<33>F<EFBFBD>·GT<47>aàÙ“ØõQ»Àxð|p|üÒ²uJƒàið2L‡â4œ¥ ã­r ¦¦-Ú;÷ú¬YI¹I[>A eØ®ÍeR0Q0 Ÿqj¨f¿×íDÌEC}ÖŸËPh
Æ…ƒÆ!d¨³œÎ -2µà=H.“ÊÌÿŠn`àÞÄE¹w%lµÅÍh^ðÒ^†á(ô‡sEÐPiÀ@ŸtpH‡zº:EXV¼K“*øA3Õt;êìñ˜K0š1 .1ð`&ç1ƒ£©³ÐôH3hÍ-ZSCtGä^Ï? ËËÊ€à ž† DÌå5qß÷ë+Üë<C39C>§NJŒGιfáa@#"´2Œ+ÒŸ<C392>Ë¡Ò¦;r¨<72>SŒ`YöSýÒA3vÿÎã¨&˜àƒ Æ`|&4‡OŸêƒG°f:¼7-oXê´ò`Âì÷PÙñéµþ®fzHzÝe<C39D>=Àž.çzß1éƒT<1A>ix\gÏž&Ä5=3Tgä³SOWÏ­ôÊ“ÖôH3чÚ,†mR™€)7:Ôt44<34>â\#ðFë©­áµf¤Íþ5•a=Ã_•T±z\ø`ÒÈKúè)wÁ½˜ŒÏ ÷šËmu׬žÍÅÀP.0øpíÁÃ!dJƒ—ƒ%e,/õ`¨¢–ÝŒ¯¿ÉCÂ<43>p¥†[g K³y% ”j:4è,ØŠ1aÀ2/8Æ&æ+æ6¹:þZ´Èš_ãZkî<]Ùà!CÈ™3h:”êïD~hÍÝV³¼ÖŒ÷âÓÄ!sw§þžj&+Ú,¡é<C2A1>=fšærQ&z­é;ÅÍBõ[mÆz64C<34>šH==¢­óú4³;Ú…IJ´Jj]¥<>"³ëΟIƒG& ~¨¹ë°7~ÐŒ<ƒ1ÄÁì¼Gb<47>ó§3ˆ<CB86>U<EFBFBD>9<!ƒ[wÐíóœ8êEÃô€y<l"n}ç¾ï×zNÜS>&<C2AC>‡a©ñM4ˆ€AFð;Æ¡ñÜz¨Œéé¡<®ÈçZŸq^ò`îRƒÅ¹F0Žˆ4 ¥A<C2A5>šÐ‡º:gAbšáÙþ¯U?Í¥ij0ß úËíµ>¡a\8•5K˜"4xù—C§Á56Ú§Üë[/}¤ª©0ÒÐPÍÒ5ºäxøA7<41>Á;—Âc f2¦0Ks<4B>!ø·â, Ï'•pW= cÒc†þRu®<ZÍ“@yG-¸C:è
qG=3hõ®U´__GþÐÇ%x0Dà¬c@#Ÿß)aúŽËt½ps<E28093> ¥Fk­¹_ßd²rºë` èJ<C3A8>l|A
MAMèFç1»¿9YÀ\럃z¶=oV[Ä€†58OÁˆH€qQÔ,ôú¸¢†£ùZèé,ËÒn¢Ü †ˆ1_C‡àÍàìÁÝGšÃ4ÇìL‡š»bÌNÁÌ Ÿú1;‚³°ÙR{yËÞ£n„Ûêº)žIϹ;è9Ñó^·üÁàÝh“NqºA Å“ššñS…r`|º;Q-òÔ!oL¦gXd¨‡Ÿ:äe “k{ö ˆ1ƒË¤;Ö<>ʇJ™Q‡ÀH¸«“€ë•dÙZ³ßë9Ÿñkì=¯Uçé¹pƒûèY×kÆA‡—ƒzjƵVÀ°!2Ô¼ךËg\Ä\ÔP´u\û0MÆR@gùHCžÖ<><C396>ƒY^“þîÅu]¯UÆ­ö$lvÆ ä)Ä¡¯¿³,—gÆ 6Ï’Å„ž.*ôq<>ÎÒÍ­ ÔÍÕ<C38D>ùu¸×j8šÂȦ`¬¤$*sðêÐ\-%R2éBäk¼m@e媙rÓ¤.ߊ1_ÖÓž1<C5BE>ŠCòy­žÝ>HØ,³ÔM®Hœ±>fq¯-—s b(ñá^ÃÊÙ)Œ²F¡è<C2A1>©ÿ`Ž~Ðü‡‚ÿ¦É€ó)JwŠ¤ÈìŠLº#“"CѪ¢­¤=Šž·nGω;(î9¹VIÄëÃ-ÖÍíæ¯õçÓ8=f¡Í A9P8ÑãÁÈÁSÂsõZG=‡ŒT=<3D>TSPºéÏÜ ‰AsØ×úIÚ)Fn+`d"a”˜<SMð¡'…¨q(Z˜ ÌOAOdhXÃJÏÈ°‰\µ<ÁS0׆¡À| G­®<>¥0qLáhhè`(,År—¥á^HRÆ1Zëp¯çÇDô—`„àq­M<C2AD>1£„9P)<29>Å$,®duQè¬/«$2y} ýòq<C3B2>rx-îõ¤1?{}X`CYsÁ&$¥Ê¸„p <0E>q½Ö\JR•Òkq­Ïìëc¹˜o£OtÖµë`@Y)ùГâ^ç~SP¸jê\Î|ùÈÜà`š4ôÑSV÷bŠ|ùòÅjX%^•£½tgª"»È<E28098>b x0Ø ŒyÇ¡àÁ 5S0x7ñÞ<˜ÈïŠÝD~4ø“z+aÐVâiT0=Š¼
à&ºƒ='nŸgiîàëã~ýDtvÆé¯5ša n
š×­£ 9P==fç¯uºqh\+r-ÕÇÈ å!a8ê ¼ó `z¯ÎÎ5óe{¶©„%ù@|øòÑÿZ‰Ýk´FN9×2ìzyf˜àB¨E óSг³Œ`
Ý8wÇ:8VÌ ýòƵx­Õ0,ó+Ò€Ôà¯$L%L%,[ðåIÁÀµÐ/κÖ3ƒdDZ*ÂDžŠë§ÈD$ý‰è<hâZËÕ­ì*ʳ«ˆ.¡ˆkœ5—|ŒP>˜a:Ns¯EÀµR2r4K*>Üý;ýÛÔ'•-f‡îèFáD0ƒ¦t°tší&þ`¦¡ô©„¡9
f¨™"CÁ CIñ0Š1ȧÁƒ‰wŸbÌŽ`ð5ÓàƒGÌ ‰1h+±È^{Aödö4º ž¯?$`~":7šzBÀDŸ|Yõ§¦ƒS ƒ âô¡÷:Á˜¯¡Cð÷Âh­@ÈMx­l!"[MZÎNéD¾¡0æç1§g(˜áZI6)2÷⵨úS¨çý±z×¢ t¥Ô…»üýÞ‰€×AOý_k„× Ìßãƒ<C3A3>Ë7;d¢‰/ëö‰@ž<u
s\ïýã´ïÕ„nØͽÝ'R0?ƒN¤0àk!7WFù?Ð<>¢¬vŒFÎ÷¯Í×<C38D>ù8™Ñ¸7^?>ô ¾¬ÿ%5*ž}üTB |jÀ|ƒO;¼'I‡š 0CÍtçÓÈ°7™t¨™Ö¶·?_·z+±ìß³z]×kݾ×ÒpûBð—`„ȹÇ>½ÖŸR}^«3ŒÙÿËiœû#ááZË%ÈPþ^¼<16>•þŠ¼(‚Ý€ þÒ £®44¯µÐƒ{-ÕY3ý h¢tç^wäú¸G`<60>qÎJLp î5¦˜Ÿ:N4ﵞ ×<>_·ú<0F>æÏækãLüÝü çj?hù)(kŠãÏf
TBA:L“jŽ¾óŸÈ˜¡HŠ1;ÈP0`0f(~ŠC`†½™Oƒƒ1ï8ÓªÚJ¬°ï¸|ïêû%ñ߆yæ^„]N™|Mñ0é?„ßfê}û£´ÒàÁüÃy¤¡üØOÙý߃÷ñ÷?hÂj¸¹þ6æg7QñÔ=ÕOJ"0ó)sôÝÄ4 ïŠ1ßæÝöC|ðƒ&24¾æCÌ`%aIamûŽË¯{¾qmåÿ±xèï…ßW¶¿Ì?3ÿüþtþH×òëbeì&¯õ[’ÝÄ×/|(€`ÔÃQ0ó xº#æSB†Lð`†O)˜àƒ>òéƒ fÒÈSìc¬aˆØJ¨EöbÂ|YßÁzS¦‡Ãáð»ÆnBá½ï?þQhE¯% 0`"OÁD>&xdFMd(˜á½9|ÚLwDÀ|}À ³ëÎD0`¬aðVÕ{~y÷br]×ëp8~ÿØMÀ(nóo¿Õ=UÂj#2tг³Gv<47>i2`À€A&<26>ÝÇᇚ4¾æwÄÁìL$“"CÁ <0C>&DÐêù Lßq1ø²þ<C2B2>Sïõbx8¿wl%´/ë[—ÿ»þÿˆÕO%Œ©<C592> ˜<>"4x0ïL| vÍà1æ<úL“f¨IIÁ€ÙyD¦9æAqëÞJ¢ Úw\¾Nüóú¯‰þ÷Ç?Œ<‡?ÿ±þ/ç_È(€Š!4ÕÆ¿¡æCßùZ|Ð!x0ƒæƒ=ȃ>m¦ƒfðÁƒöæî‡iZFØJÚ£[^?QùŽ¹î?n9BªfÿµþßáýåÌÛM@ePI„
ñ<>"£ñîÓ‚ȤÁ<07>Ìè ‰1àÁ šÃ4ǼãPäiäÓȧµbaa%½•PØJèµ¾ãjå‡Ãá<C383><C3A1>š¦²½ß}÷<>º·ï&P'c÷_£>éðh{|÷QäÛãÇ`|&EfסfŠÌ®§;+x«g o%ÔWˆ~–²•Øµ}Ç5k~8îUÙüo7a
#TÎ?_ó¨9
ãÇ`÷Ø›<Æ€ÿéìýy0x˜‡"C«A
ûf+¡¾à²•0ýüp8þHx7ã§á~ø¡tT1TG•Í4v<34>GóAGwöæø1Èï:<š‰ü§
Lì~§8 jY†š6&¼ßQo%ÔªÎ[ɽ¾à8$T6%îº.»‰¦ïdhÅpP?ñ5“âÝ ERdR0`b|†‚‰|<˜d<>¡ÁÄîQ3…]ƒ>ì#Ñ. ûˆ}ÙïPÿ±þs£øï<C3B8>ÿ4Üáp8ü+àk™ÿ³þKéP•JFM‡iŽÙÌÎ{<04>àÞ̧CM
 2t¨™ÆîCdØ­@ðȤˆii ¶z¶ËÈGÿËo%6næp8þØTôBaT!•JUWŠL
f¨Iãáƒ4a||ͤ:Š£»ƒLùtÇá˜àÁ€AÆÊËÕO$ö/w^IúvëËöߎ³¡‡?0Jœ/»ú*æûï¿WÕÆ}7WrÁàSó@cÀïipºói$ò)2c¾ÁÞg÷ÐÜÙ±),<0E>÷}ÄîÿoýßÞù¡äZÿ¯µÈôp8þØTëüt¢ªŠ|S!£òï<2t¨™âÝ„!HÁ(Ô ñàÁ<04> ÅðÈP0Áƒ4#OM=Ô¤-µƒ06¾Ôâ[1Kga0¯$÷úÙ<C3BA>I‡Ãáϵª_¿«“ÕÌPc&Þý®`À€j¦`Œ f*ö  ñ<>"´³MdÒA3œˆ1ØýŽÙ}`5ÀÄl"~ùî»ï,šïµìÅÞGzÅýöð‡Ãáð/Â=]ûº†Ù§š¬øƒ<C3B8><æS:DƒGÆ°àpì¥{Wñwôw”?Ô¤ñðÎ}GŒé^@˜Ô&ba†?ÿùϾԲ<C394>x<19>ˆ_ÛïÅ,éáp8ü«áïj…ÑO'¼BªºªÀ
~Â.<0ïLœ1&LÁK7iu;x0â`ôٙȘh4ˆcÌPĘÈPs<50><73><EFBFBD>ÿ¶h±ÉZ?¯ÛA4m"~d½×j¦‡Ãáð¯Æ}߯õÓ‰ïmÔÏêªRdÒAs§Èè;Åm%àM¡þ+é&U½}}$2aÔv
z>p(3<šÎ… <20>Æ755˜°}ø5ÄÌB¿¬MÄr-ziïH‡Ãá_•Pm|­/»¨2ëxE^©WóC<ì»F>ƒ } ø)ŠùÔp/¡ð‚´]™]¡C<1A>KÃOF†éàPX¿ƒ€·½Òð]VÈëcóµhôp8﨟
¬¿Ø½,Ìn"»dè§81lˆÃt¶0Æ¢ž{<11>IðâÊ»x¯ ;í;iRpJ'¡ÆÉÐÿ²x­ÿœ2ì<½®ëµ~ ¡÷bßAv8T¯U<wl(Ê>ì<æÁ#¸7y004a» ã Býg*éà©÷ФŽ2ïˆÛ¼ƒ¿Ö×<56>ãõc,˜Ãáp8|<7C>ê¤*ªÌöGû¼žðÊ~ð6üðhî8„Ì( Mo@¾X3ï ¨^" Ÿëº^ˤŠ<Á€¹Vç¸>x}p}øk3ñZƒ?‡ÃߊŠJ¿|ù¢ž{MàCµWöÁÃF<C383>"“âÝ„&˜As4>µ¡ø¡œ±¡PßqÙÚ®Åkq¯"¿›ôÞÞ#ŠD—CÁ\×z%I£ tƒ}œÃáp8ü Ôð^Oà}Ag*øê¿<C3AA> 4)2é  &Þ}jÀ0~ïA6”~F÷Ûµ§ø†êº®×ªöôþx=a^A0"”¿×¡k<C2A1>u-^뺨ь  l Înr8?%´: ¯'ÿþïÿî—qŠ:¯Ú«ùá¨<C3A1> ƒ1È<31>š`Í0>5͈bÞýåÈî¦æ3jþ;Þ/(ê@a× p¢ŸÚáÇwåw™ö©?-òâ:ÌžÒd5À‡Ãáo¢âY-U„[*<Uðm%àmÈP0ïE†"Ó€©)ÚPøöï)6µï?þ0l
ÔþBã(ìv Mê*œîÛ3ã´Cs´ˆ£º1;÷Ç÷i‡Ãápø[Q?UÑëº^«2Ó~ŽWá•zTöa#ƒ ÅŒó)ŽŒ)l(ŒÍËØSäPñ‡MAñg"OáP´G ³œnf°UÑA§ØtxÛ(½®ËR€?‡ÃÏà^_vùúˆ†R¬°Wä¡ìÛAäÓà¡ ˜¤6f¦k[Qÿ!“OMìF8—CÅxFÝœbCñ†bõ•—Ë·vès8‡¿õS!}ýÅN…‡²AˆÐao~êÇ Fƒ82³­„â?Øà( >4<>øPÊ/Ç|i放ñ;‡ÃϦ åº.¿eûIÂW@¾2ò¼CP„Á ö
&x0ïL| ÆÛG 94£þï¼GBçÐ<C3A7>`j¢ ÅeÒ°Üëõ„?‡ÃÏ@ …rÊÛP|ùÃÀ†RÕU„Á¨É° 숪;{„3S$øà›”~}'bÌqýÁôãg:Xz8‡ŸA%”¹ýZ­9(ÂäÔ.<2E>âaFÁ¿³Gx6D<44>ÝC™]‘¡Ã£ ¯3m”;÷}¿‡ÃáðË°›(§6?"øaúÏë?xbCñ†ÂÀnª1…-` ̧̡1ƒGGÁÿ)|\µ›¸´~:q½.\Ð"ÐÃáp8üB”Óêê—µ¡ÌAj¯¿ç¨Éƒ*ý@‡àÁ€öæø1Ï»ù)LOÙòÔnÞnBí&ôº®ûünr8¿j)Õ×ú7ÃÿùŸÿé+/~ÿµÚž¢«ÉФP«C„A†>؃ãÇ Ÿîˆ<04>Lº#ãy—?»‰w›¦/÷®³›‡Ã¯Jõº®6m(~¹öÂT<C382>g0P®Sd(˜¡fŠ1ñÞ³#Š‡¡²…ä퉚ðòe7apŸÝäp8~=¦¢ÎW^ùË_~øáÆkú{Þ†¢«ÉšŠ32ê60§`>¥Cïú  ù]1Ò AÈßÎÈ|·þÇõáÂÁ‡ÃáWAQ ®{CñEÐ_ÿú×~_Ø—W“í)à§PƒWÀC“îˆD~ÌŽÈÿJÝ(Œ²•¶¼pýéOêýËÖézÁ‡Ãá×¢ºz¯åu]×ký'­úÅKJÿcù^R DC­~ <>b¾3Ì×4x0ÈÐàw&€ 9@$•$$Ì»_s1‡Ãápø»bCsß÷kñe}ëÅÀö
²ï‹ì)šJ´Bͨ۱{…}Å»Dyú`ŒI#O¥fðbâ<62>±cÒ.öp8?îõ’Ò·^š¾õúþû、ð}ñeO±¡€Gu»2Ž £à#“âÝ<(NMdù]Ë$•$…­ÐVÒ«Öu]¯ÅÙM‡Ãá·Á†¢öÚSø^RÚS¼¤¼ï)
8(ìã Øv>m: <˜<>"´ÙS‰Á”T%,2ÿ”ë:ÿ6øp8~Cõöÿ¯ÿãBÞÿðÃß}÷]%*6”n{
sˆÓŠ<<3C>ÝC3xd(˜AÌÎDÌMjv’¡ö‘[ÿ­o%ÔÏ@m‡Ãápø‡p/¼§¨ÆöeYÞSh{^U¨ª¼"<Ôÿàc÷Ÿb¨Ð3L„¦¶¯ÉÄ>â«­ö»^I¾|üßøÞÇFy8‡ß€jï}߯õÝõcŠúüZµÚ«
SéVÆ©åê¼[x0§§x˜Ô8¡i|0¦k±µÙGDà·oRö>ûˆo·^ëºÃáp8ü©ßëÿåµþc,ö”¿þõ¯Š¶Òí ¥ù'¸
;Txu"Ê>ÞÍ7¨5BÆ7 l"mg½.AV¶<ûß(y0‡Ãápø‡£ ÆBUiÛŠ=Eݶ­¼ÞVl+LxSPê|^ñ‡]€§``wÝÑ Æ
Æ42l"~Ä1£í̦fIȵ~mÇk¥M‡ÃáðO…⌠<0C>m…·³ør‰±­¨ð¾ó¾à<C2BE>Tÿa/àm

¦;"p¢A Åï;È_þò3ÚD^ȇ¢ôÒÃáp8ü^¸×<C2B8>õŒ·ÛŠ
<EFBFBD>×Ç¿.†úo±Ø1h"CuÎ¥v:;ˆ‰ÚÈšú^¼‡Ãáð;¤¿ÿÓ{í)àû‹âßàû(ØbàP»CŒg <0A>E¾,^k(Ûøf¹×öav0‡Ãápø½3õœQäãµq}ü‚ŸÚ˜È{×`
\ë,{CùûÇÃÂŒôp8`”z0qo{Áýñ."¸¯eæÄ{E‡Ãápf<>ø:<04>1‡Ãápø ø£
endstream
endobj
10 0 obj
32389
endobj
11 0 obj
<< /ExtGState << /E1 << /SMask << /Type /Mask
/G 1 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
/XObject << /X2 5 0 R
/X1 9 0 R
>>
>>
endobj
12 0 obj
<< /Length 13 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
269.000000 0.000000 -0.000000 74.234528 0.000000 0.000000 cm
/X1 Do
Q
q
/E1 gs
/X2 Do
Q
endstream
endobj
13 0 obj
118
endobj
14 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 269.000000 75.000000 ]
/Resources 11 0 R
/Contents 12 0 R
/Parent 15 0 R
>>
endobj
15 0 obj
<< /Kids [ 14 0 R ]
/Count 1
/Type /Pages
>>
endobj
16 0 obj
<< /Pages 15 0 R
/Type /Catalog
>>
endobj
xref
0 17
0000000000 65535 f
0000000010 00000 n
0000000493 00000 n
0000000515 00000 n
0000001038 00000 n
0000001060 00000 n
0000012016 00000 n
0000012039 00000 n
0000038194 00000 n
0000038218 00000 n
0000070841 00000 n
0000070866 00000 n
0000071206 00000 n
0000071382 00000 n
0000071405 00000 n
0000071583 00000 n
0000071659 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 16 0 R
/Size 17
>>
startxref
71720
%%EOF

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -45,6 +45,10 @@ public enum Asset {
public static let searchCard = ColorAsset(name: "Colors/Border/searchCard")
public static let status = ColorAsset(name: "Colors/Border/status")
}
public enum Brand {
public static let blurple = ColorAsset(name: "Colors/Brand/Blurple")
public static let lightBlurple = ColorAsset(name: "Colors/Brand/Light Blurple")
}
public enum Button {
public static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
public static let disabled = ColorAsset(name: "Colors/Button/disabled")

View File

@ -13,9 +13,10 @@ extension APIService {
public func servers(
language: String? = nil,
category: String? = nil
category: String? = nil,
registrations: String? = nil
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error> {
let query = Mastodon.API.Onboarding.ServersQuery(language: language, category: category)
let query = Mastodon.API.Onboarding.ServersQuery(language: language, category: category, registrations: registrations)
return Mastodon.API.Onboarding.servers(session: session, query: query)
}
@ -28,5 +29,8 @@ extension APIService {
return Mastodon.Entity.Category(category: kind.rawValue, serversCount: 0)
}
}
public func languages() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Language]>, Error> {
return Mastodon.API.Onboarding.languages(session: session)
}
}

View File

@ -620,18 +620,26 @@ public enum L10n {
}
}
public enum ConfirmEmail {
/// Tap the link we emailed to you to verify your account.
public static let subtitle = L10n.tr("Localizable", "Scene.ConfirmEmail.Subtitle", fallback: "Tap the link we emailed to you to verify your account.")
/// Tap the link we emailed to you to verify your account
public static let tapTheLinkWeEmailedToYouToVerifyYourAccount = L10n.tr("Localizable", "Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount", fallback: "Tap the link we emailed to you to verify your account")
/// One last thing.
public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.Title", fallback: "One last thing.")
/// Tap the link we sent you to verify %@. We'll wait right here.
public static func tapTheLinkWeEmailedToYouToVerifyYourAccount(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount", String(describing: p1), fallback: "Tap the link we sent you to verify %@. We'll wait right here.")
}
/// Check Your Inbox
public static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.Title", fallback: "Check Your Inbox")
public enum Button {
/// Open Email App
public static let openEmailApp = L10n.tr("Localizable", "Scene.ConfirmEmail.Button.OpenEmailApp", fallback: "Open Email App")
/// Resend
public static let resend = L10n.tr("Localizable", "Scene.ConfirmEmail.Button.Resend", fallback: "Resend")
}
public enum DidntGetLink {
/// Didn't get a Link?
public static let `prefix` = L10n.tr("Localizable", "Scene.ConfirmEmail.DidntGetLink.Prefix", fallback: "Didn't get a Link?")
/// Resend (%@)
public static func resendIn(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.ConfirmEmail.DidntGetLink.ResendIn", String(describing: p1), fallback: "Resend (%@)")
}
/// Resend now.
public static let resendNow = L10n.tr("Localizable", "Scene.ConfirmEmail.DidntGetLink.ResendNow", fallback: "Resend now.")
}
public enum DontReceiveEmail {
/// Check if your email address is correct as well as your junk folder if you havent.
public static let description = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Description", fallback: "Check if your email address is correct as well as your junk folder if you havent.")
@ -791,6 +799,24 @@ public enum L10n {
public static let showPrevious = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowPrevious", fallback: "Show Previous")
}
}
public enum Privacy {
/// Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your servers privacy policy.
public static let description = L10n.tr("Localizable", "Scene.Privacy.Description", fallback: "Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your servers privacy policy.")
/// Privacy
public static let title = L10n.tr("Localizable", "Scene.Privacy.Title", fallback: "Privacy")
public enum Button {
/// I agree
public static let confirm = L10n.tr("Localizable", "Scene.Privacy.Button.confirm", fallback: "I agree")
}
public enum Policy {
/// Privacy Policy - Mastodon for iOS
public static let ios = L10n.tr("Localizable", "Scene.Privacy.Policy.Ios", fallback: "Privacy Policy - Mastodon for iOS")
/// Privacy Policy - %@
public static func server(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Privacy.Policy.Server", String(describing: p1), fallback: "Privacy Policy - %@")
}
}
}
public enum Profile {
public enum Accessibility {
/// Double tap to open the list
@ -906,14 +932,8 @@ public enum L10n {
public static let title = L10n.tr("Localizable", "Scene.RebloggedBy.Title", fallback: "Reblogged By")
}
public enum Register {
/// Lets get you set up on %@
public static func letsGetYouSetUpOnDomain(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Register.LetsGetYouSetUpOnDomain", String(describing: p1), fallback: "Lets get you set up on %@")
}
/// Lets get you set up on %@
public static func title(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Register.Title", String(describing: p1), fallback: "Lets get you set up on %@")
}
/// Create account
public static let title = L10n.tr("Localizable", "Scene.Register.Title", fallback: "Create account")
public enum Error {
public enum Item {
/// Agreement
@ -1002,10 +1022,12 @@ public enum L10n {
public enum Password {
/// 8 characters
public static let characterLimit = L10n.tr("Localizable", "Scene.Register.Input.Password.CharacterLimit", fallback: "8 characters")
/// Confirm password
public static let confirmationPlaceholder = L10n.tr("Localizable", "Scene.Register.Input.Password.ConfirmationPlaceholder", fallback: "Confirm password")
/// Your password needs at least eight characters
public static let hint = L10n.tr("Localizable", "Scene.Register.Input.Password.Hint", fallback: "Your password needs at least eight characters")
/// password
public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder", fallback: "password")
/// Password
public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Password.Placeholder", fallback: "Password")
/// Your password needs at least:
public static let require = L10n.tr("Localizable", "Scene.Register.Input.Password.Require", fallback: "Your password needs at least:")
public enum Accessibility {
@ -1020,6 +1042,10 @@ public enum L10n {
public static let duplicatePrompt = L10n.tr("Localizable", "Scene.Register.Input.Username.DuplicatePrompt", fallback: "This username is taken.")
/// username
public static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.Username.Placeholder", fallback: "username")
/// amazing_%@
public static func suggestion(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Register.Input.Username.Suggestion", String(describing: p1), fallback: "amazing_%@")
}
}
}
}
@ -1186,15 +1212,19 @@ public enum L10n {
}
}
public enum ServerPicker {
/// 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.
public static let subtitle = L10n.tr("Localizable", "Scene.ServerPicker.Subtitle", fallback: "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.")
/// Mastodon is made of users in different servers.
public static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title", fallback: "Mastodon is made of users in different servers.")
/// Well pick a server based on your language if you continue without making a selection.
public static let noServerSelectedHint = L10n.tr("Localizable", "Scene.ServerPicker.NoServerSelectedHint", fallback: "Well pick a server based on your language if you continue without making a selection.")
/// Pick server
public static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title", fallback: "Pick server")
public enum Button {
/// Language
public static let language = L10n.tr("Localizable", "Scene.ServerPicker.Button.Language", fallback: "Language")
/// See Less
public static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess", fallback: "See Less")
/// See More
public static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore", fallback: "See More")
/// Sign-up Speed
public static let signupSpeed = L10n.tr("Localizable", "Scene.ServerPicker.Button.SignupSpeed", fallback: "Sign-up Speed")
public enum Category {
/// academia
public static let academia = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Academia", fallback: "academia")
@ -1246,6 +1276,22 @@ public enum L10n {
/// USERS
public static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users", fallback: "USERS")
}
public enum Language {
/// All
public static let all = L10n.tr("Localizable", "Scene.ServerPicker.Language.All", fallback: "All")
}
public enum Search {
/// Search name or URL
public static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Search.Placeholder", fallback: "Search name or URL")
}
public enum SignupSpeed {
/// All
public static let all = L10n.tr("Localizable", "Scene.ServerPicker.SignupSpeed.All", fallback: "All")
/// Instant Sign-up
public static let instant = L10n.tr("Localizable", "Scene.ServerPicker.SignupSpeed.Instant", fallback: "Instant Sign-up")
/// Manual Review
public static let manuallyReviewed = L10n.tr("Localizable", "Scene.ServerPicker.SignupSpeed.ManuallyReviewed", fallback: "Manual Review")
}
}
public enum ServerRules {
/// privacy policy
@ -1383,6 +1429,30 @@ public enum L10n {
/// Social networking
/// back in your hands.
public static let slogan = L10n.tr("Localizable", "Scene.Welcome.Slogan", fallback: "Social networking\nback in your hands.")
public enum Education {
public enum HowDoIPickAServer {
/// 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.
///
/// You cant 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), youll never miss a beat anywhere.
public static let description = L10n.tr("Localizable", "Scene.Welcome.Education.HowDoIPickAServer.description", fallback: "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 cant 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), youll never miss a beat anywhere.")
/// How do I pick a server?
public static let title = L10n.tr("Localizable", "Scene.Welcome.Education.HowDoIPickAServer.title", fallback: "How do I pick a server?")
}
public enum MastodonIsLikeThat {
/// Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.
public static let description = L10n.tr("Localizable", "Scene.Welcome.Education.MastodonIsLikeThat.description", fallback: "Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.")
/// Mastodon is like that
public static let title = L10n.tr("Localizable", "Scene.Welcome.Education.MastodonIsLikeThat.title", fallback: "Mastodon is like that")
}
public enum WhatIsMastodon {
/// Imagine you have an email address that ends with @example.com.
///
/// You can still send and receive emails from anyone, even if their email ends in @gmail.com or @icloud.com or @example.com.
public static let description = L10n.tr("Localizable", "Scene.Welcome.Education.WhatIsMastodon.description", fallback: "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.")
/// What is
public static let title = L10n.tr("Localizable", "Scene.Welcome.Education.WhatIsMastodon.title", fallback: "What is")
}
}
}
public enum Wizard {
/// Double tap to dismiss this wizard

View File

@ -224,7 +224,6 @@ uploaded to Mastodon.";
"Scene.Compose.Visibility.Private" = "Followers only";
"Scene.Compose.Visibility.Public" = "Public";
"Scene.Compose.Visibility.Unlisted" = "Unlisted";
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
"Scene.ConfirmEmail.Button.Resend" = "Resend";
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you havent.";
"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email";
@ -233,9 +232,11 @@ uploaded to Mastodon.";
"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail";
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client";
"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox.";
"Scene.ConfirmEmail.Subtitle" = "Tap the link we emailed to you to verify your account.";
"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account";
"Scene.ConfirmEmail.Title" = "One last thing.";
"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we sent you to verify %@. We'll wait right here.";
"Scene.ConfirmEmail.Title" = "Check Your Inbox";
"Scene.ConfirmEmail.DidntGetLink.Prefix" = "Didn't get a Link?";
"Scene.ConfirmEmail.DidntGetLink.ResendIn" = "Resend (%@)";
"Scene.ConfirmEmail.DidntGetLink.ResendNow" = "Resend now.";
"Scene.Discovery.Intro" = "These are the posts gaining traction in your corner of Mastodon.";
"Scene.Discovery.Tabs.Community" = "Community";
"Scene.Discovery.Tabs.ForYou" = "For You";
@ -346,12 +347,13 @@ uploaded to Mastodon.";
"Scene.Register.Input.Password.Accessibility.Unchecked" = "unchecked";
"Scene.Register.Input.Password.CharacterLimit" = "8 characters";
"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters";
"Scene.Register.Input.Password.Placeholder" = "password";
"Scene.Register.Input.Password.Placeholder" = "Password";
"Scene.Register.Input.Password.ConfirmationPlaceholder" = "Confirm password";
"Scene.Register.Input.Password.Require" = "Your password needs at least:";
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
"Scene.Register.Input.Username.Placeholder" = "username";
"Scene.Register.LetsGetYouSetUpOnDomain" = "Lets get you set up on %@";
"Scene.Register.Title" = "Lets get you set up on %@";
"Scene.Register.Input.Username.Suggestion" = "amazing_%@";
"Scene.Register.Title" = "Create account";
"Scene.Report.Content1" = "Are there any other posts youd like to add to the report?";
"Scene.Report.Content2" = "Is there anything the moderators should know about this report?";
"Scene.Report.ReportSentTitle" = "Thanks for reporting, well look into this.";
@ -428,6 +430,8 @@ uploaded to Mastodon.";
"Scene.ServerPicker.Button.Category.Tech" = "tech";
"Scene.ServerPicker.Button.SeeLess" = "See Less";
"Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.Button.Language" = "Language";
"Scene.ServerPicker.Button.SignupSpeed" = "Sign-up Speed";
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading the data. Check your internet connection.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
@ -435,8 +439,13 @@ uploaded to Mastodon.";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"Scene.ServerPicker.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.";
"Scene.ServerPicker.Title" = "Mastodon is made of users in different servers.";
"Scene.ServerPicker.Title" = "Pick server";
"Scene.ServerPicker.NoServerSelectedHint" = "Well pick a server based on your language if you continue without making a selection.";
"Scene.ServerPicker.SignupSpeed.All" = "All";
"Scene.ServerPicker.SignupSpeed.Instant" = "Instant Sign-up";
"Scene.ServerPicker.SignupSpeed.ManuallyReviewed" = "Manual Review";
"Scene.ServerPicker.Language.All" = "All";
"Scene.ServerPicker.Search.Placeholder" = "Search name or URL";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, youre subject to the terms of service and privacy policy for %@.";
@ -486,6 +495,21 @@ uploaded to Mastodon.";
"Scene.Welcome.LogIn" = "Log In";
"Scene.Welcome.Slogan" = "Social networking
back in your hands.";
"Scene.Welcome.Education.WhatIsMastodon.title" = "What is";
"Scene.Welcome.Education.WhatIsMastodon.description" = "Imagine you have an email address that ends with @example.com.
You can still send and receive emails from anyone, even if their email ends in @gmail.com or @icloud.com or @example.com.";
"Scene.Welcome.Education.MastodonIsLikeThat.title" = "Mastodon is like that";
"Scene.Welcome.Education.MastodonIsLikeThat.description" = "Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.";
"Scene.Welcome.Education.HowDoIPickAServer.title" = "How do I pick a server?";
"Scene.Welcome.Education.HowDoIPickAServer.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.
You cant 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), youll never miss a beat anywhere.";
"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard";
"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button.";
"Scene.Wizard.NewInMastodon" = "New in Mastodon";
"Scene.Wizard.NewInMastodon" = "New in Mastodon";
"Scene.Privacy.Title" = "Privacy";
"Scene.Privacy.Description" = "Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your servers privacy policy.";
"Scene.Privacy.Button.confirm" = "I agree";
"Scene.Privacy.Policy.Ios" = "Privacy Policy - Mastodon for iOS";
"Scene.Privacy.Policy.Server" = "Privacy Policy - %@";

View File

@ -228,8 +228,11 @@ uploaded to Mastodon.";
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client";
"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox.";
"Scene.ConfirmEmail.Subtitle" = "Tap the link we emailed to you to verify your account.";
"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account";
"Scene.ConfirmEmail.Title" = "One last thing.";
"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we sent you to verify %@. We'll wait right here.";
"Scene.ConfirmEmail.Title" = "Check Your Inbox";
"Scene.ConfirmEmail.DidntGetLink.Prefix" = "Didn't get a Link?";
"Scene.ConfirmEmail.DidntGetLink.ResendIn" = "Resend (%@)";
"Scene.ConfirmEmail.DidntGetLink.ResendNow" = "Resend now.";
"Scene.Discovery.Intro" = "These are the posts gaining traction in your corner of Mastodon.";
"Scene.Discovery.Tabs.Community" = "Community";
"Scene.Discovery.Tabs.ForYou" = "For You";
@ -285,7 +288,7 @@ uploaded to Mastodon.";
"Scene.Profile.Dashboard.Following" = "following";
"Scene.Profile.Dashboard.Posts" = "posts";
"Scene.Profile.Fields.AddRow" = "Add Row";
"Scene.Profile.Fields.Joined" = "Liitytty";
"Scene.Profile.Fields.Joined" = "Joined";
"Scene.Profile.Fields.Placeholder.Content" = "Content";
"Scene.Profile.Fields.Placeholder.Label" = "Label";
"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@";
@ -338,11 +341,12 @@ uploaded to Mastodon.";
"Scene.Register.Input.Password.CharacterLimit" = "8 characters";
"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters";
"Scene.Register.Input.Password.Placeholder" = "password";
"Scene.Register.Input.Password.ConfirmationPlaceholder" = "Confirm password";
"Scene.Register.Input.Password.Require" = "Your password needs at least:";
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
"Scene.Register.Input.Username.Placeholder" = "username";
"Scene.Register.LetsGetYouSetUpOnDomain" = "Lets get you set up on %@";
"Scene.Register.Title" = "Lets get you set up on %@";
"Scene.Register.Input.Username.Suggestion" = "amazing_%@";
"Scene.Register.Title" = "Create account";
"Scene.Report.Content1" = "Are there any other posts youd like to add to the report?";
"Scene.Report.Content2" = "Is there anything the moderators should know about this report?";
"Scene.Report.ReportSentTitle" = "Thanks for reporting, well look into this.";
@ -419,6 +423,8 @@ uploaded to Mastodon.";
"Scene.ServerPicker.Button.Category.Tech" = "tech";
"Scene.ServerPicker.Button.SeeLess" = "See Less";
"Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.Button.Language" = "Language";
"Scene.ServerPicker.Button.SignupSpeed" = "Sign-up Speed";
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading the data. Check your internet connection.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
@ -426,8 +432,13 @@ uploaded to Mastodon.";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"Scene.ServerPicker.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.";
"Scene.ServerPicker.Title" = "Mastodon is made of users in different servers.";
"Scene.ServerPicker.Title" = "Pick server";
"Scene.ServerPicker.NoServerSelectedHint" = "Well pick a server based on your language if you continue without making a selection.";
"Scene.ServerPicker.SignupSpeed.All" = "All";
"Scene.ServerPicker.SignupSpeed.Instant" = "Instant Sign-up";
"Scene.ServerPicker.SignupSpeed.ManuallyReviewed" = "Manual Review";
"Scene.ServerPicker.Language.All" = "All";
"Scene.ServerPicker.Search.Placeholder" = "Search name or URL";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, youre subject to the terms of service and privacy policy for %@.";
@ -479,4 +490,9 @@ uploaded to Mastodon.";
back in your hands.";
"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard";
"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button.";
"Scene.Wizard.NewInMastodon" = "New in Mastodon";
"Scene.Wizard.NewInMastodon" = "New in Mastodon";
"Scene.Privacy.Title" = "Privacy";
"Scene.Privacy.Description" = "Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your servers privacy policy.";
"Scene.Privacy.Button.confirm" = "I agree";
"Scene.Privacy.Policy.Ios" = "Privacy Policy - Mastodon for iOS";
"Scene.Privacy.Policy.Server" = "Privacy Policy - %@";

View File

@ -12,6 +12,7 @@ extension Mastodon.API.Onboarding {
static let serversEndpointURL = Mastodon.API.joinMastodonEndpointURL.appendingPathComponent("servers")
static let categoriesEndpointURL = Mastodon.API.joinMastodonEndpointURL.appendingPathComponent("categories")
static let languagesEndpointURL = Mastodon.API.joinMastodonEndpointURL.appendingPathComponent("languages")
/// Fetch server list
///
@ -68,7 +69,33 @@ extension Mastodon.API.Onboarding {
}
.eraseToAnyPublisher()
}
/// Fetch server languages
///
/// Using this endpoint to fetch booked languages
///
/// # Last Update
/// 2022/12/19
/// # Reference
/// undocumented
/// - Parameters:
/// - session: `URLSession`
/// - Returns: `AnyPublisher` contains `Language` nested in the response
public static func languages(
session: URLSession
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Language]>, Error> {
let request = Mastodon.API.get(
url: languagesEndpointURL,
query: nil,
authorization: nil
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Language].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}
extension Mastodon.API.Onboarding {
@ -76,16 +103,20 @@ extension Mastodon.API.Onboarding {
public struct ServersQuery: GetQuery {
public let language: String?
public let category: String?
/// Check if registrations need to be manually approved or not (or it doesn't matter)
public let registrations: String?
public init(language: String?, category: String?) {
public init(language: String?, category: String?, registrations: String?) {
self.language = language
self.category = category
self.registrations = registrations
}
var queryItems: [URLQueryItem]? {
var items: [URLQueryItem] = []
language.flatMap { items.append(URLQueryItem(name: "language", value: $0)) }
category.flatMap { items.append(URLQueryItem(name: "category", value: $0)) }
registrations.flatMap { items.append(URLQueryItem(name: "registrations", value: $0)) }
guard !items.isEmpty else { return nil }
return items
}

View File

@ -0,0 +1,29 @@
//
// File.swift
//
//
// Created by Nathan Mattes on 19.12.22.
//
import Foundation
extension Mastodon.Entity {
public struct Language: Codable {
public let locale: String
public let serversCount: Int
public let language: String?
enum CodingKeys: String, CodingKey {
case locale
case serversCount = "servers_count"
case language
}
public init(locale: String, serversCount: Int, language: String?) {
self.locale = locale
self.serversCount = serversCount
self.language = language
}
}
}