forked from zelo72/mastodon-ios
feat: draw welcome illustration
This commit is contained in:
parent
25c3d6e74d
commit
47abbadaba
|
@ -69,6 +69,19 @@ internal enum Asset {
|
||||||
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||||
}
|
}
|
||||||
internal enum Welcome {
|
internal enum Welcome {
|
||||||
|
internal enum Illustration {
|
||||||
|
internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
|
||||||
|
internal static let cloudBase = ImageAsset(name: "Welcome/illustration/cloud.base")
|
||||||
|
internal static let cloudFirst = ImageAsset(name: "Welcome/illustration/cloud.first")
|
||||||
|
internal static let cloudSecond = ImageAsset(name: "Welcome/illustration/cloud.second")
|
||||||
|
internal static let cloudThird = ImageAsset(name: "Welcome/illustration/cloud.third")
|
||||||
|
internal static let elephantFourOnGrassWithTreeTwo = ImageAsset(name: "Welcome/illustration/elephant.four.on.grass.with.tree.two")
|
||||||
|
internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Welcome/illustration/elephant.on.airplane.with.contrail")
|
||||||
|
internal static let elephantThreeOnGrass = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass")
|
||||||
|
internal static let elephantThreeOnGrassWithTreeFour = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.four")
|
||||||
|
internal static let elephantTwo = ImageAsset(name: "Welcome/illustration/elephant.two")
|
||||||
|
internal static let lineDashTwo = ImageAsset(name: "Welcome/illustration/line.dash.two")
|
||||||
|
}
|
||||||
internal static let mastodonLogo = ImageAsset(name: "Welcome/mastodon.logo")
|
internal static let mastodonLogo = ImageAsset(name: "Welcome/mastodon.logo")
|
||||||
internal static let mastodonLogoLarge = ImageAsset(name: "Welcome/mastodon.logo.large")
|
internal static let mastodonLogoLarge = ImageAsset(name: "Welcome/mastodon.logo.large")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Untitled-1_0008_Group-3.png",
|
"filename" : "Untitled-1_0010_Group-5.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Untitled-1_0010_Group-5.png",
|
"filename" : "Untitled-1_0009_Group-4.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
|
@ -9,6 +9,31 @@ import UIKit
|
||||||
|
|
||||||
final class WelcomeIllustrationView: UIView {
|
final class WelcomeIllustrationView: UIView {
|
||||||
|
|
||||||
|
static let artworkImageSize = CGSize(width: 870, height: 2000)
|
||||||
|
let artworkImageView = UIImageView()
|
||||||
|
|
||||||
|
// layout outside
|
||||||
|
let elephantOnAirplaneWithContrailImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: Asset.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
let cloudFirstImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: Asset.Welcome.Illustration.cloudFirst.image)
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
let cloudSecondImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: Asset.Welcome.Illustration.cloudSecond.image)
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
let cloudThirdImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: Asset.Welcome.Illustration.cloudThird.image)
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -23,7 +48,77 @@ final class WelcomeIllustrationView: UIView {
|
||||||
|
|
||||||
extension WelcomeIllustrationView {
|
extension WelcomeIllustrationView {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
|
backgroundColor = Asset.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),
|
||||||
|
])
|
||||||
|
|
||||||
|
artworkImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(artworkImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
artworkImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||||
|
artworkImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
artworkImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
artworkImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
artworkImageView.widthAnchor.constraint(equalTo: artworkImageView.heightAnchor, multiplier: WelcomeIllustrationView.artworkImageSize.width / WelcomeIllustrationView.artworkImageSize.height),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
artworkImageView.image = WelcomeIllustrationView.bottomPartImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func bottomPartImage() -> UIImage {
|
||||||
|
let size = artworkImageSize
|
||||||
|
let width = artworkImageSize.width
|
||||||
|
let height = artworkImageSize.height
|
||||||
|
let image = UIGraphicsImageRenderer(size: size).image { context in
|
||||||
|
// clear background
|
||||||
|
UIColor.clear.setFill()
|
||||||
|
context.fill(CGRect(origin: .zero, size: artworkImageSize))
|
||||||
|
|
||||||
|
// draw cloud
|
||||||
|
let cloudBaseImage = Asset.Welcome.Illustration.cloudBase.image
|
||||||
|
cloudBaseImage.draw(at: CGPoint(x: 0, y: height - cloudBaseImage.size.height))
|
||||||
|
|
||||||
|
let elephantFourOnGrassWithTreeTwoImage = Asset.Welcome.Illustration.elephantFourOnGrassWithTreeTwo.image
|
||||||
|
let elephantThreeOnGrassWithTreeFourImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeFour.image
|
||||||
|
let elephantThreeOnGrassImage = Asset.Welcome.Illustration.elephantThreeOnGrass.image
|
||||||
|
let elephantTwoImage = Asset.Welcome.Illustration.elephantTwo.image
|
||||||
|
let ineDashTwoImage = Asset.Welcome.Illustration.lineDashTwo.image
|
||||||
|
|
||||||
|
let elephantOnAirplaneWithContrailImageView = Asset.Welcome.Illustration.elephantOnAirplaneWithContrail.image
|
||||||
|
|
||||||
|
// draw elephantFourOnGrassWithTreeTwo
|
||||||
|
// elephantFourOnGrassWithTreeTwo.bottomY + 40 align to elephantThreeOnGrassImage.centerY
|
||||||
|
elephantFourOnGrassWithTreeTwoImage.draw(at: CGPoint(x: 0, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantFourOnGrassWithTreeTwoImage.size.height - 40))
|
||||||
|
|
||||||
|
// draw elephantThreeOnGrassWithTreeFour
|
||||||
|
// elephantThreeOnGrassWithTreeFour.bottomY + 40 align to elephantThreeOnGrassImage.centerY
|
||||||
|
elephantThreeOnGrassWithTreeFourImage.draw(at: CGPoint(x: width - elephantThreeOnGrassWithTreeFourImage.size.width, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeFourImage.size.height - 40))
|
||||||
|
|
||||||
|
// draw elephantThreeOnGrass
|
||||||
|
elephantThreeOnGrassImage.draw(at: CGPoint(x: 0, y: height - elephantThreeOnGrassImage.size.height))
|
||||||
|
|
||||||
|
// darw ineDashTwoImage
|
||||||
|
ineDashTwoImage.draw(at: CGPoint(x: 0.5 * elephantThreeOnGrassImage.size.width + 60, y: height - elephantThreeOnGrassImage.size.height - 50))
|
||||||
|
|
||||||
|
// draw elephantTwo.image
|
||||||
|
elephantTwoImage.draw(at: CGPoint(x: 0, y: height - elephantTwoImage.size.height - 125))
|
||||||
|
|
||||||
|
// draw elephantOnAirplaneWithContrailImageView
|
||||||
|
elephantOnAirplaneWithContrailImageView.draw(at: CGPoint(x: 0, y: height - cloudBaseImage.size.height - 0.5 * elephantOnAirplaneWithContrailImageView.size.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
return image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,10 +128,20 @@ import SwiftUI
|
||||||
struct WelcomeIllustrationView_Previews: PreviewProvider {
|
struct WelcomeIllustrationView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
UIViewPreview(width: 375) {
|
Group {
|
||||||
WelcomeIllustrationView()
|
UIViewPreview(width: 870) {
|
||||||
|
WelcomeIllustrationView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 870, height: 2000))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
WelcomeIllustrationView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 812))
|
||||||
|
UIViewPreview(width: 428) {
|
||||||
|
WelcomeIllustrationView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 428, height: 926))
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 812))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
let welcomeIllustrationView = WelcomeIllustrationView()
|
||||||
|
var welcomeIllustrationViewBottomAnchorLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
private(set) lazy var logoImageView: UIImageView = {
|
private(set) lazy var logoImageView: UIImageView = {
|
||||||
let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Welcome.mastodonLogo.image : Asset.Welcome.mastodonLogoLarge.image
|
let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Welcome.mastodonLogo.image : Asset.Welcome.mastodonLogoLarge.image
|
||||||
let imageView = UIImageView(image: image)
|
let imageView = UIImageView(image: image)
|
||||||
|
@ -42,7 +45,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||||
let button = UIButton(type: .system)
|
let button = UIButton(type: .system)
|
||||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||||
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||||
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
button.setTitleColor(UIColor.white.withAlphaComponent(0.8), for: .normal)
|
||||||
button.setInsets(forContentPadding: UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0), imageTitlePadding: 0)
|
button.setInsets(forContentPadding: UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0), imageTitlePadding: 0)
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return button
|
return button
|
||||||
|
@ -60,6 +63,16 @@ extension WelcomeViewController {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
setupOnboardingAppearance()
|
setupOnboardingAppearance()
|
||||||
|
view.backgroundColor = Asset.Welcome.Illustration.backgroundCyan.color
|
||||||
|
|
||||||
|
welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(welcomeIllustrationView)
|
||||||
|
welcomeIllustrationViewBottomAnchorLayoutConstraint = welcomeIllustrationView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
welcomeIllustrationView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||||
|
welcomeIllustrationView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
||||||
|
welcomeIllustrationViewBottomAnchorLayoutConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
view.addSubview(logoImageView)
|
view.addSubview(logoImageView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -76,6 +89,19 @@ extension WelcomeViewController {
|
||||||
sloganLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 168),
|
sloganLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 168),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
welcomeIllustrationView.cloudFirstImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
welcomeIllustrationView.cloudSecondImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
welcomeIllustrationView.cloudFirstImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
// welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// view.addSubview(welcomeIllustrationView.elephantOnAirplaneWithContrailImageView)
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||||
|
// welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.bottomAnchor.constraint(equalTo: sloganLabel.topAnchor),
|
||||||
|
// ])
|
||||||
|
// welcomeIllustrationView.welcomeIllustrationView.sca
|
||||||
|
// view.bringSubviewToFront(sloganLabel)
|
||||||
|
|
||||||
view.addSubview(signInButton)
|
view.addSubview(signInButton)
|
||||||
view.addSubview(signUpButton)
|
view.addSubview(signUpButton)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -94,7 +120,13 @@ extension WelcomeViewController {
|
||||||
signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside)
|
signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var preferredStatusBarStyle: UIStatusBarStyle { return .darkContent }
|
override func viewSafeAreaInsetsDidChange() {
|
||||||
|
super.viewSafeAreaInsetsDidChange()
|
||||||
|
|
||||||
|
// make illustration bottom over the bleeding
|
||||||
|
let overlap: CGFloat = 100
|
||||||
|
welcomeIllustrationViewBottomAnchorLayoutConstraint.constant = overlap - view.safeAreaInsets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -287,11 +287,11 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let image = images[3]
|
let image = images[3]
|
||||||
let imageView = view.setupImageView(
|
let artworkImageView = view.setupImageView(
|
||||||
aspectRatio: image.size,
|
aspectRatio: image.size,
|
||||||
maxSize: CGSize(width: 375, height: 400)
|
maxSize: CGSize(width: 375, height: 400)
|
||||||
)
|
)
|
||||||
imageView.image = image
|
artworkImageView.image = image
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
@ -299,14 +299,14 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let image = images[1]
|
let image = images[1]
|
||||||
let imageView = view.setupImageView(
|
let artworkImageView = view.setupImageView(
|
||||||
aspectRatio: image.size,
|
aspectRatio: image.size,
|
||||||
maxSize: CGSize(width: 375, height: 400)
|
maxSize: CGSize(width: 375, height: 400)
|
||||||
)
|
)
|
||||||
imageView.layer.masksToBounds = true
|
artworkImageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = 8
|
artworkImageView.layer.cornerRadius = 8
|
||||||
imageView.contentMode = .scaleAspectFill
|
artworkImageView.contentMode = .scaleAspectFill
|
||||||
imageView.image = image
|
artworkImageView.image = image
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
@ -315,8 +315,8 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(2)
|
let images = self.images.prefix(2)
|
||||||
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, artworkImageView) in imageViews.enumerated() {
|
||||||
imageView.image = images[i]
|
artworkImageView.image = images[i]
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -326,8 +326,8 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(3)
|
let images = self.images.prefix(3)
|
||||||
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, artworkImageView) in imageViews.enumerated() {
|
||||||
imageView.image = images[i]
|
artworkImageView.image = images[i]
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -337,8 +337,8 @@ struct MosaicImageView_Previews: PreviewProvider {
|
||||||
let view = MosaicImageViewContainer()
|
let view = MosaicImageViewContainer()
|
||||||
let images = self.images.prefix(4)
|
let images = self.images.prefix(4)
|
||||||
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, artworkImageView) in imageViews.enumerated() {
|
||||||
imageView.image = images[i]
|
artworkImageView.image = images[i]
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -358,8 +358,8 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
statusView.updateContentWarningDisplay(isHidden: false)
|
statusView.updateContentWarningDisplay(isHidden: false)
|
||||||
let images = MosaicImageView_Previews.images
|
let images = MosaicImageView_Previews.images
|
||||||
let imageViews = statusView.statusMosaicImageView.setupImageViews(count: 4, maxHeight: 162)
|
let imageViews = statusView.statusMosaicImageView.setupImageViews(count: 4, maxHeight: 162)
|
||||||
for (i, imageView) in imageViews.enumerated() {
|
for (i, artworkImageView) in imageViews.enumerated() {
|
||||||
imageView.image = images[i]
|
artworkImageView.image = images[i]
|
||||||
}
|
}
|
||||||
statusView.statusMosaicImageView.isHidden = false
|
statusView.statusMosaicImageView.isHidden = false
|
||||||
return statusView
|
return statusView
|
||||||
|
|
Loading…
Reference in New Issue