on master help sign in
Extract UI component constants and create reusable text field components with button variants
b10cab7iko
client/App.fs+37−26
@@ -16,6 +16,7 @@ module App =
16 open Crypto
16 open Crypto
17 open ApiClient
17 open ApiClient
18 open Buttons
18 open Buttons
19+ open TextFields
19
20
20 // ── Domain Types ──
21 // ── Domain Types ──
21
22
@@ -273,8 +274,7 @@ module App =
273 Error = None
274 Error = None
274 PollStatus = "polling..."
275 PollStatus = "polling..."
275 FlushingOutbox = true },
276 FlushingOutbox = true },
276− [ CmdPoll(session, model.PollCursor)
277+ [ CmdPoll(session, model.PollCursor); CmdFlushOutbox session ]
277− CmdFlushOutbox session ]
278 | _ -> model, []
278 | _ -> model, []
279
279
280 | AuthErr err ->
280 | AuthErr err ->
@@ -320,9 +320,7 @@ module App =
320 model, []
320 model, []
321 else
321 else
322 match trySession model with
322 match trySession model with
323− | Some session ->
324− { model with FlushingOutbox = true },
323+ | Some session -> { model with FlushingOutbox = true }, [ CmdFlushOutbox session ]
325− [ CmdFlushOutbox session ]
326 | None -> model, []
324 | None -> model, []
327
325
328 | FlushResult sent ->
326 | FlushResult sent ->
@@ -556,7 +554,10 @@ module App =
556 let x = (skBmp.Width - size) / 2
554 let x = (skBmp.Width - size) / 2
557 let y = (skBmp.Height - size) / 2
555 let y = (skBmp.Height - size) / 2
558 use cropped = new SkiaSharp.SKBitmap(size, size)
556 use cropped = new SkiaSharp.SKBitmap(size, size)
557+
559− skBmp.ExtractSubset(cropped, SkiaSharp.SKRectI(x, y, x + size, y + size)) |> ignore
558+ skBmp.ExtractSubset(cropped, SkiaSharp.SKRectI(x, y, x + size, y + size))
559+ |> ignore
560+
560 use img = SkiaSharp.SKImage.FromBitmap(cropped)
561 use img = SkiaSharp.SKImage.FromBitmap(cropped)
561 use data = img.Encode(SkiaSharp.SKEncodedImageFormat.Png, 90)
562 use data = img.Encode(SkiaSharp.SKEncodedImageFormat.Png, 90)
562 data.ToArray()
563 data.ToArray()
@@ -630,7 +631,10 @@ module App =
630 | Some item ->
631 | Some item ->
631 try
632 try
632 let blob =
633 let blob =
634+ Crypto.encrypt
635+ session.Identity.PrivKey
633− Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex item.RecipientHex) item.EnvelopeJson
636+ (Crypto.fromHex item.RecipientHex)
637+ item.EnvelopeJson
634
638
635 let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
639 let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
636 do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
640 do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
@@ -640,8 +644,7 @@ module App =
640 log $"outbox send error: {ex.Message}"
644 log $"outbox send error: {ex.Message}"
641 do! Async.Sleep 3000
645 do! Async.Sleep 3000
642 dispatch (FlushResult true)
646 dispatch (FlushResult true)
643− | None ->
644− dispatch (FlushResult false)
647+ | None -> dispatch (FlushResult false)
645 }
648 }
646 |> Async.Start)
649 |> Async.Start)
647
650
@@ -780,7 +783,7 @@ module App =
780
783
781 TextBlock("").height (50.)
784 TextBlock("").height (50.)
782
785
783− button "Next" GenIdentity
786+ button Primary "Next" GenIdentity
784 })
787 })
785 .margin(30.)
788 .margin(30.)
786 .centerVertical ()
789 .centerVertical ()
@@ -936,29 +939,37 @@ module App =
936 .margin (20.)
939 .margin (20.)
937
940
938 let private viewEditProfile model displayName bio (photo: byte[] option) =
941 let private viewEditProfile model displayName bio (photo: byte[] option) =
939− ScrollViewer(
942+ (Grid([], [ Dimension.Star; Dimension.Auto ]) {
943+ ScrollViewer(
940− (VStack(16.) {
944+ (VStack(16.) {
941− HStack(8.) {
945+ viewErrorBanner model.Error
942− Button("< Back", SetPage Settings)
943− TextBlock("Edit Profile").fontSize (20.)
944− }
945
946
946− viewErrorBanner model.Error
947+ imageButton photo DoPickPhoto
947
948
948− imageButton photo DoPickPhoto
949+ TextBlock("Name").fontFamily(Constants.fontFamily).fontWeight(FontWeight.Bold).margin (0, 24, 0, -8)
950+ (textField displayName (fun text -> SetPage(EditProfile(text, bio, photo)))).margin (0, 0, 0, 8)
949
951
950− TextBlock("Display Name:")
951− TextBox(displayName, fun text -> SetPage(EditProfile(text, bio, photo))).watermark ("Your name")
952
952
953− TextBlock("Bio:")
953+ TextBlock("Bio").fontFamily(Constants.fontFamily).fontWeight(FontWeight.Bold).margin (0, 0, 0, -8)
954
954
955− TextBox(bio, fun text -> SetPage(EditProfile(displayName, text, photo)))
955+ (multilineTextField bio (fun text -> SetPage(EditProfile(displayName, text, photo))))
956− .watermark ("Tell something about yourself")
956+ .margin (0, 0, 0, 8)
957+ })
958+ .margin (20.)
959+ )
960+ .gridRow (0)
961+
962+ (Grid() {
963+ Rectangle().stroke(SolidColorBrush(Colors.Black)).strokeThickness(4.).fill (Constants.accentColor)
964+
965+ HStack(8.) {
966+ (button Secondary "back" (SetPage Settings)).margin (8.)
957
967
958− Button("Save Profile", DoSaveProfile).centerHorizontal ()
968+ (button Secondary "save" DoSaveProfile).margin (8.)
969+ }
959 })
970 })
960− .margin (20.)
971+ .gridRow (1)
961− )
972+ })
962
973
963 let private pageContent model =
974 let private pageContent model =
964 let page =
975 let page =
client/Buttons.fs+24−13
@@ -9,18 +9,17 @@ open type Fabulous.Avalonia.View
9module Buttons =
9module Buttons =
10 open Avalonia.Media.Transformation
10 open Avalonia.Media.Transformation
11 open System
11 open System
12− open Avalonia.Animation.Easings
13−
14− let private animationDuration: int64 = 60
15− let private easing = CubicEaseOut()
16
12
13+ type ButtonPriority =
14+ | Primary
15+ | Secondary
17
16
18 let private translate x y =
17 let private translate x y =
19 let b = TransformOperations.CreateBuilder 1
18 let b = TransformOperations.CreateBuilder 1
20 b.AppendTranslate(x, y)
19 b.AppendTranslate(x, y)
21 b.Build() :> ITransform
20 b.Build() :> ITransform
22
21
23− let button (text: string) (msg: 'msg) =
22+ let button priority (text: string) (msg: 'msg) =
24 Component(text) {
23 Component(text) {
25 let! pressed = Context.State(false)
24 let! pressed = Context.State(false)
26 let offset = if pressed.Current then 4. else 8.
25 let offset = if pressed.Current then 4. else 8.
@@ -32,17 +31,27 @@ module Buttons =
32 .transition (
31 .transition (
33 TransformOperationsTransition(
32 TransformOperationsTransition(
34 Avalonia.Visual.RenderTransformProperty,
33 Avalonia.Visual.RenderTransformProperty,
35− TimeSpan.FromMilliseconds animationDuration
34+ TimeSpan.FromMilliseconds Constants.animationDuration
36 )
35 )
37− .easing (easing)
36+ .easing (Constants.easing)
38 )
37 )
39
38
39+ Rectangle()
40+ .fill (
41+ match priority with
40− Rectangle().fill (SolidColorBrush(Constants.accentColor))
42+ | Primary -> SolidColorBrush(Constants.accentColor)
43+ | Secondary -> SolidColorBrush(Colors.White)
44+ )
41
45
42 TextBlock(text)
46 TextBlock(text)
47+ .foreground(
48+ match priority with
43− .foreground(SolidColorBrush(Colors.White))
49+ | Primary -> SolidColorBrush(Colors.White)
50+ | Secondary -> SolidColorBrush(Colors.Black)
51+
52+ )
44 .fontSize(24.)
53 .fontSize(24.)
45− .fontFamily(FontFamily("avares://Skrepka/Assets/Fonts#Unbounded"))
54+ .fontFamily(FontFamily(Constants.fontFamily))
46 .fontWeight(FontWeight.Bold)
55 .fontWeight(FontWeight.Bold)
47 .center()
56 .center()
48 .margin (8.)
57 .margin (8.)
@@ -53,6 +62,7 @@ module Buttons =
53 .onPointerPressed(fun _ -> pressed.Set true)
62 .onPointerPressed(fun _ -> pressed.Set true)
54 .onPointerReleased(fun _ -> pressed.Set false)
63 .onPointerReleased(fun _ -> pressed.Set false)
55 .onPointerExited(fun _ -> pressed.Set false)
64 .onPointerExited(fun _ -> pressed.Set false)
65+ .margin(16.)
56 .onTapped (fun _ -> msg)
66 .onTapped (fun _ -> msg)
57 }
67 }
58
68
@@ -73,9 +83,9 @@ module Buttons =
73 .transition (
83 .transition (
74 TransformOperationsTransition(
84 TransformOperationsTransition(
75 Avalonia.Visual.RenderTransformProperty,
85 Avalonia.Visual.RenderTransformProperty,
76− TimeSpan.FromMilliseconds animationDuration
86+ TimeSpan.FromMilliseconds Constants.animationDuration
77 )
87 )
78− .easing (easing)
88+ .easing (Constants.easing)
79 )
89 )
80
90
81 Rectangle().fill (SolidColorBrush(Colors.White))
91 Rectangle().fill (SolidColorBrush(Colors.White))
@@ -89,7 +99,7 @@ module Buttons =
89 .margin (0., 20., 0., 0.)
99 .margin (0., 20., 0., 0.)
90
100
91 Label("Upload")
101 Label("Upload")
92− .fontFamily(FontFamily("avares://Skrepka/Assets/Fonts#Unbounded"))
102+ .fontFamily(FontFamily(Constants.fontFamily))
93 .fontWeight(FontWeight.Bold)
103 .fontWeight(FontWeight.Bold)
94 .fontSize(24.)
104 .fontSize(24.)
95 .centerContentHorizontal ()
105 .centerContentHorizontal ()
@@ -109,5 +119,6 @@ module Buttons =
109 .onPointerExited(fun _ -> pressed.Set false)
119 .onPointerExited(fun _ -> pressed.Set false)
110 .width(200.)
120 .width(200.)
111 .height(200.)
121 .height(200.)
122+ .margin(16.)
112 .onTapped (fun _ -> msg)
123 .onTapped (fun _ -> msg)
113 }
124 }
client/Constants.fs+3
@@ -4,3 +4,6 @@ open Avalonia.Media
4
4
5module Constants =
5module Constants =
6 let accentColor = Color.FromRgb(98uy, 54uy, 255uy)
6 let accentColor = Color.FromRgb(98uy, 54uy, 255uy)
7+ let animationDuration: int64 = 60
8+ let easing = Avalonia.Animation.Easings.CubicEaseOut()
9+ let fontFamily = "avares://Skrepka/Assets/Fonts#Unbounded"
client/Skrepka.fsproj+1
@@ -33,6 +33,7 @@
33 <Compile Include="ApiClient.fs" />
33 <Compile Include="ApiClient.fs" />
34 <Compile Include="Phonemic.fs" />
34 <Compile Include="Phonemic.fs" />
35 <Compile Include="Store.fs" />
35 <Compile Include="Store.fs" />
36+ <Compile Include="TextFields.fs" />
36 <Compile Include="Buttons.fs" />
37 <Compile Include="Buttons.fs" />
37 <Compile Include="$(iOSPlatformFolder)PhotoPicker.fs" Condition="$(AvaloniaPlatform) == 'iOS'" />
38 <Compile Include="$(iOSPlatformFolder)PhotoPicker.fs" Condition="$(AvaloniaPlatform) == 'iOS'" />
38 <Compile Include="App.fs" />
39 <Compile Include="App.fs" />
client/TextFields.fsnew file+20
@@ -0,0 +1,20 @@
1+namespace Skrepka
2+
3+open Avalonia.Media
4+open Fabulous
5+open Fabulous.Avalonia
6+
7+open type Fabulous.Avalonia.View
8+
9+module TextFields =
10+ let textField value msg =
11+ Grid() {
12+ TextBox(value, msg).fontFamily (Constants.fontFamily)
13+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
14+ }
15+
16+ let multilineTextField value msg =
17+ Grid() {
18+ TextBox(value, msg).fontFamily(Constants.fontFamily).acceptsReturn(true).height (150)
19+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
20+ }