diff --git a/README.md b/README.md index acc001fc..d17cf354 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a Kotlin MultiPlatform library that provides declarative UI and applicat in common code. You can implement full application for Android and iOS only from common code with it. ## Current status -Current version - `0.1.0-dev-13`. Dev version is not tested in production tasks yet, API can be changed and +Current version - `0.1.0-dev-14`. Dev version is not tested in production tasks yet, API can be changed and bugs may be found. But dev version is chance to test limits of API and concepts to feedback and improve lib. We open for any feedback and ideas (go to issues or #moko at [kotlinlang.slack.com](https://kotlinlang.slack.com))! @@ -220,6 +220,7 @@ val loginScreen = Theme(baseTheme) { - 0.1.0-dev-11 - 0.1.0-dev-12 - 0.1.0-dev-13 + - 0.1.0-dev-14 ## Installation root build.gradle @@ -234,7 +235,7 @@ allprojects { project build.gradle ```groovy dependencies { - commonMainApi("dev.icerock.moko:widgets:0.1.0-dev-13") + commonMainApi("dev.icerock.moko:widgets:0.1.0-dev-14") } ``` @@ -252,7 +253,7 @@ buildscript { } dependencies { - classpath "dev.icerock.moko.widgets:gradle-plugin:0.1.0-dev-13" + classpath "dev.icerock.moko.widgets:gradle-plugin:0.1.0-dev-14" } } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 7f81bc62..fca88f72 100755 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -10,7 +10,7 @@ object Versions { } const val kotlin = "1.3.61" - private const val mokoWidgets = "0.1.0-dev-13" + private const val mokoWidgets = "0.1.0-dev-14" private const val mokoResources = "0.8.0" object Plugins { diff --git a/sample/mpp-library/src/commonMain/kotlin/App.kt b/sample/mpp-library/src/commonMain/kotlin/App.kt index e596b7bd..aef8e01c 100644 --- a/sample/mpp-library/src/commonMain/kotlin/App.kt +++ b/sample/mpp-library/src/commonMain/kotlin/App.kt @@ -40,6 +40,7 @@ import dev.icerock.moko.widgets.sample.InputWidgetGalleryScreen import dev.icerock.moko.widgets.sample.ProductsSearchScreen import dev.icerock.moko.widgets.sample.ScrollContentScreen import dev.icerock.moko.widgets.sample.SelectGalleryScreen +import dev.icerock.moko.widgets.sample.TabsSampleScreen import dev.icerock.moko.widgets.screen.Args import dev.icerock.moko.widgets.screen.BaseApplication import dev.icerock.moko.widgets.screen.Screen @@ -106,6 +107,7 @@ class App() : BaseApplication() { routes = listOf( buildInputGalleryRouteInfo(theme, router), buildSearchRouteInfo(theme, router), + buildTabsRouteInfo(theme, router), SelectGalleryScreen.RouteInfo( name = "Old Demo".desc(), route = router.createPushRoute(oldDemo(router)) @@ -158,6 +160,23 @@ class App() : BaseApplication() { ) } + private fun buildTabsRouteInfo( + theme: Theme, + router: NavigationScreen.Router + ): SelectGalleryScreen.RouteInfo { + val tabsTheme = Theme(theme) { + TabsSampleScreen.configureDefaultTheme(this) + } + val tabsScreen = registerScreen(TabsSampleScreen::class) { + TabsSampleScreen(tabsTheme) + } + + return SelectGalleryScreen.RouteInfo( + name = "Tabs".desc(), + route = router.createPushRoute(tabsScreen) + ) + } + private fun oldDemo( router: NavigationScreen.Router ): TypedScreenDesc { diff --git a/sample/mpp-library/src/commonMain/kotlin/dev/icerock/moko/widgets/sample/TabsSampleScreen.kt b/sample/mpp-library/src/commonMain/kotlin/dev/icerock/moko/widgets/sample/TabsSampleScreen.kt new file mode 100644 index 00000000..57f9509c --- /dev/null +++ b/sample/mpp-library/src/commonMain/kotlin/dev/icerock/moko/widgets/sample/TabsSampleScreen.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.sample + +import com.icerockdev.library.units.UserUnitWidget +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.resources.desc.desc +import dev.icerock.moko.units.TableUnitItem +import dev.icerock.moko.widgets.ContainerWidget +import dev.icerock.moko.widgets.ListWidget +import dev.icerock.moko.widgets.TabsWidget +import dev.icerock.moko.widgets.constraint +import dev.icerock.moko.widgets.container +import dev.icerock.moko.widgets.core.Theme +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.factory.ContainerViewFactory +import dev.icerock.moko.widgets.factory.SystemTabsViewFactory +import dev.icerock.moko.widgets.linear +import dev.icerock.moko.widgets.list +import dev.icerock.moko.widgets.screen.Args +import dev.icerock.moko.widgets.screen.WidgetScreen +import dev.icerock.moko.widgets.screen.navigation.NavigationBar +import dev.icerock.moko.widgets.screen.navigation.NavigationItem +import dev.icerock.moko.widgets.style.background.Background +import dev.icerock.moko.widgets.style.background.Direction +import dev.icerock.moko.widgets.style.background.Fill +import dev.icerock.moko.widgets.style.state.SelectableState +import dev.icerock.moko.widgets.style.view.Colors +import dev.icerock.moko.widgets.style.view.PaddingValues +import dev.icerock.moko.widgets.style.view.SizeSpec +import dev.icerock.moko.widgets.style.view.TextStyle +import dev.icerock.moko.widgets.style.view.WidgetSize +import dev.icerock.moko.widgets.tabs +import dev.icerock.moko.widgets.utils.platformSpecific + +class TabsSampleScreen( + private val theme: Theme +) : WidgetScreen(), NavigationItem { + + override val navigationBar: NavigationBar = NavigationBar.Normal( + title = "Tabs sample".desc(), + styles = NavigationBar.Styles( + backgroundColor = backgroundColor, + tintColor = tintColor, + textStyle = TextStyle( + color = textColor + ), + isShadowEnabled = false + ) + ) + + override fun createContentWidget(): Widget> { + return with(theme) { + constraint(size = WidgetSize.AsParent) { + val tabs = +tabs(size = WidgetSize.Const(SizeSpec.MatchConstraint, SizeSpec.MatchConstraint)) { + tab( + title = const("Active".desc()), + body = buildContent() + ) + + tab( + title = const("Done".desc()), + body = buildContent() + ) + } + + constraints { + tabs topToTop root.safeArea + tabs leftRightToLeftRight root + tabs bottomToBottom root.safeArea + } + } + } + } + + private fun Theme.buildContent() = { + val items = List(20) { + UserUnitWidget.TableUnitItem( + theme = this@buildContent, + itemId = it.toLong(), + data = UserUnitWidget.Data( + name = "item $it", + avatarUrl = "https://i.imgur.com/cVDadwb.png", + onClick = {} + ) + ) as TableUnitItem + } + + linear(size = WidgetSize.AsParent) { + +container( + size = WidgetSize.Const( + width = SizeSpec.AsParent, + height = SizeSpec.Exact(platformSpecific(android = 4f, ios = 2f)) + ) + ) {} + + +list( + size = WidgetSize.AsParent, + id = Ids.List, + items = const(items) + ) + } + }() + + object Ids { + object List : ListWidget.Id + } + + companion object { + val tintColor = Color(0xD20C0AFF) + val backgroundColor = Color(0xFFFFFFFF) + val textColor = Color(0x151515FF) + + fun configureDefaultTheme(theme: Theme.Builder) = with(theme) { + factory[TabsWidget.DefaultCategory] = SystemTabsViewFactory( + tabsTintColor = tintColor, + tabsBackground = Background( + fill = Fill.Solid(backgroundColor) + ), + tabsPadding = platformSpecific( + android = null, + ios = PaddingValues(start = 16f, end = 16f, bottom = 16f) + ), + titleColor = SelectableState( + selected = platformSpecific(android = null, ios = Colors.white), + unselected = null + ) + ) + factory[ContainerWidget.DefaultCategory] = ContainerViewFactory( + background = Background( + fill = Fill.Gradient( + colors = listOf(Color(0x00000000), Color(0x00000010)), + direction = Direction.BOTTOM_TOP + ) + ) + ) + } + } +} diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt new file mode 100644 index 00000000..c08e1c7d --- /dev/null +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +import android.content.Intent +import android.net.Uri + +actual fun Screen<*>.dialPhone(phone: String) { + val context = context ?: return + + val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phone")) + if (intent.resolveActivity(context.packageManager) == null) { + println("activity for $intent not found") + return + } + + startActivity(intent) +} diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt new file mode 100644 index 00000000..7fbe5155 --- /dev/null +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +import android.content.Intent +import android.net.Uri + +actual fun Screen<*>.sendEmail( + email: String, + subject: String, + body: String +) { + val context = context ?: return + + val intent = Intent( + Intent.ACTION_SENDTO, + Uri.fromParts( + "mailto", + email, + null + ) + ) + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + intent.putExtra(Intent.EXTRA_TEXT, body) + if (intent.resolveActivity(context.packageManager) == null) { + println("email clients not found") + return + } + + startActivity(Intent.createChooser(intent, email)) +} diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt index 04ea6401..af6c340f 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt @@ -15,10 +15,12 @@ import android.view.View import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.ViewCompat import androidx.fragment.app.FragmentManager import dev.icerock.moko.widgets.screen.navigation.NavigationBar import dev.icerock.moko.widgets.style.view.FontStyle import dev.icerock.moko.widgets.utils.ThemeAttrs +import dev.icerock.moko.widgets.utils.dp import dev.icerock.moko.widgets.utils.sp @@ -29,6 +31,8 @@ fun NavigationBar.Normal.apply( ) { toolbar.visibility = View.VISIBLE + styles?.apply(toolbar, context) + val title = title.toString(context) toolbar.title = SpannableString(title).apply { val size = styles?.textStyle?.size?.toFloat()?.sp(context) @@ -48,21 +52,9 @@ fun NavigationBar.Normal.apply( } } - val bgColor = styles?.backgroundColor?.argb?.toInt() - ?: ThemeAttrs.getPrimaryColor(context) - - toolbar.setBackgroundColor(bgColor) - val fallbackTintColor = ThemeAttrs.getControlNormalColor(context) - val tintColor = styles?.tintColor?.argb?.toInt() ?: fallbackTintColor - toolbar.setTitleTextColor(tintColor) - toolbar.overflowIcon?.also { DrawableCompat.setTint(it, tintColor) } - - val textColor = styles?.textStyle?.color?.argb?.toInt() ?: fallbackTintColor - toolbar.setTitleTextColor(textColor) - val backBtn = backButton if (backBtn != null) { toolbar.navigationIcon = ContextCompat.getDrawable(context, backBtn.icon.drawableResId) diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt index 60703dcf..0a53f4f1 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt @@ -33,6 +33,8 @@ fun NavigationBar.Search.apply( ) { toolbar.visibility = View.VISIBLE + styles?.apply(toolbar, context) + val title = title.toString(context) toolbar.title = SpannableString(title).apply { val size = styles?.textStyle?.size?.toFloat()?.sp(context) @@ -52,21 +54,9 @@ fun NavigationBar.Search.apply( } } - val bgColor = styles?.backgroundColor?.argb?.toInt() - ?: ThemeAttrs.getPrimaryColor(context) - - toolbar.setBackgroundColor(bgColor) - val fallbackTintColor = ThemeAttrs.getControlNormalColor(context) - val tintColor = styles?.tintColor?.argb?.toInt() ?: fallbackTintColor - toolbar.setTitleTextColor(tintColor) - toolbar.overflowIcon?.also { DrawableCompat.setTint(it, tintColor) } - - val textColor = styles?.textStyle?.color?.argb?.toInt() ?: fallbackTintColor - toolbar.setTitleTextColor(textColor) - val backBtn = backButton if (backBtn != null) { toolbar.navigationIcon = ContextCompat.getDrawable(context, backBtn.icon.drawableResId) diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarStyles.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarStyles.kt new file mode 100644 index 00000000..a923c3b5 --- /dev/null +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarStyles.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.style + +import android.content.Context +import androidx.appcompat.widget.Toolbar +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.ViewCompat +import dev.icerock.moko.widgets.screen.navigation.NavigationBar +import dev.icerock.moko.widgets.utils.ThemeAttrs +import dev.icerock.moko.widgets.utils.dp + + +fun NavigationBar.Styles.apply( + toolbar: Toolbar, + context: Context +) { + val bgColor = backgroundColor?.argb?.toInt() + ?: ThemeAttrs.getPrimaryColor(context) + + toolbar.setBackgroundColor(bgColor) + + val fallbackTintColor = ThemeAttrs.getControlNormalColor(context) + + val tintColor = tintColor?.argb?.toInt() ?: fallbackTintColor + + toolbar.setTitleTextColor(tintColor) + toolbar.overflowIcon?.also { DrawableCompat.setTint(it, tintColor) } + + val textColor = textStyle?.color?.argb?.toInt() ?: fallbackTintColor + toolbar.setTitleTextColor(textColor) + + if (isShadowEnabled == false) { + ViewCompat.setElevation(toolbar, 0f) + } else { + // 4 points https://material.io/design/environment/elevation.html#elevation-shadows-elevation-android + ViewCompat.setElevation(toolbar, 4f.dp(context)) + } +} diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt new file mode 100644 index 00000000..bd2cc322 --- /dev/null +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +expect fun Screen<*>.dialPhone(phone: String) diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/navigation/NavigationScreen.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/navigation/NavigationScreen.kt index c80aa1e3..144c80fe 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/navigation/NavigationScreen.kt +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/navigation/NavigationScreen.kt @@ -124,7 +124,8 @@ sealed class NavigationBar { data class Styles( val backgroundColor: Color? = null, val textStyle: TextStyle? = null, - val tintColor: Color? = null + val tintColor: Color? = null, + val isShadowEnabled: Boolean? = null ) data class BarButton( diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt new file mode 100644 index 00000000..0e6a8e61 --- /dev/null +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +expect fun Screen<*>.sendEmail( + email: String, + subject: String, + body: String +) diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt new file mode 100644 index 00000000..bed602ad --- /dev/null +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/dialPhoneExt.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +import platform.Foundation.NSURL +import platform.UIKit.UIApplication + +actual fun Screen<*>.dialPhone(phone: String) { + val application = UIApplication.sharedApplication + val url = NSURL(string = "tel://${phone.filter { it in '0'..'9' }}") + if (!application.canOpenURL(url)) { + println("can't open url $url") + return + } + + application.openURL(url) +} diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt new file mode 100644 index 00000000..322ffcd4 --- /dev/null +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/sendEmailExt.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +import dev.icerock.moko.widgets.objc.setAssociatedObject +import platform.Foundation.NSError +import platform.MessageUI.MFMailComposeResult +import platform.MessageUI.MFMailComposeViewController +import platform.MessageUI.MFMailComposeViewControllerDelegateProtocol +import platform.darwin.NSObject + +actual fun Screen<*>.sendEmail( + email: String, + subject: String, + body: String +) { + if (!MFMailComposeViewController.canSendMail()) { + println("MailCompose can't send mail") + return + } + + val mail = MFMailComposeViewController() + val manager = EmailManager() + mail.mailComposeDelegate = manager + setAssociatedObject(mail, manager) + mail.setToRecipients(listOf(email)) + mail.setSubject(subject) + mail.setMessageBody(body, false) + + viewController.presentViewController(mail, true, null) +} + +private class EmailManager : NSObject(), MFMailComposeViewControllerDelegateProtocol { + override fun mailComposeController( + controller: MFMailComposeViewController, + didFinishWithResult: MFMailComposeResult, + error: NSError? + ) { + controller.dismissViewControllerAnimated(true, null) + } +} diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarExt.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarExt.kt index abd4a38c..21c8535a 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarExt.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarExt.kt @@ -13,9 +13,9 @@ import platform.UIKit.NSFontAttributeName import platform.UIKit.NSForegroundColorAttributeName import platform.UIKit.UIApplication import platform.UIKit.UIBarButtonItem -import platform.UIKit.UIDevice +import platform.UIKit.UIBarMetricsDefault +import platform.UIKit.UIImage import platform.UIKit.UINavigationBar -import platform.UIKit.UINavigationBarAppearance import platform.UIKit.UINavigationController import platform.UIKit.UISearchController import platform.UIKit.UISearchResultsUpdatingProtocol @@ -39,13 +39,16 @@ fun UINavigationBar.applyNavigationBarStyle(style: NavigationBar.Styles?) { val backgroundColor = style?.backgroundColor?.toUIColor() val tintColor = style?.tintColor?.toUIColor() ?: UIApplication.sharedApplication.keyWindow?.rootViewController()?.view?.tintColor!! - + val shadowImage = if (style?.isShadowEnabled == false) UIImage() else null + val backgroundImage = if (style?.isShadowEnabled == false) UIImage() else null // TODO uncomment when kotlin-native will fix linking to newest api // if (UIDevice.currentDevice.systemVersion.compareTo("13.0") < 0) { - this.barTintColor = backgroundColor - this.titleTextAttributes = textAttributes - this.tintColor = tintColor + this.barTintColor = backgroundColor + this.titleTextAttributes = textAttributes + this.tintColor = tintColor + this.shadowImage = shadowImage + this.setBackgroundImage(backgroundImage, forBarMetrics = UIBarMetricsDefault) // } else { // val appearance = UINavigationBarAppearance().apply { // configureWithDefaultBackground()