Skip to content

Commit 7c85999

Browse files
committed
paste clipboard widget
1 parent 328ef64 commit 7c85999

File tree

8 files changed

+193
-25
lines changed

8 files changed

+193
-25
lines changed

ghcjs/lightning-verifier/package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ghcjs/lightning-verifier/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"dependencies": {
1010
"@capacitor/android": "^6.0.0",
1111
"@capacitor/browser": "^6.0.1",
12+
"@capacitor/clipboard": "^6.0.1",
1213
"@capacitor/core": "^6.0.0",
1314
"@capacitor/ios": "^6.0.0",
1415
"@capacitor/preferences": "^6.0.1",

ghcjs/lightning-verifier/src/App/Widgets/Bolt11.hs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ pair x =
127127
. DynamicFieldText
128128

129129
pairs :: [FieldPair DynamicField f] -> [View Action]
130-
pairs xs =
130+
pairs raw =
131131
FieldPairs.fieldPairsViewer
132132
FieldPairs.Args
133133
{ FieldPairs.argsModel = xs,
@@ -137,6 +137,14 @@ pairs xs =
137137
void $ fun xs
138138
pure next
139139
}
140+
where
141+
xs =
142+
filter
143+
( \x ->
144+
inspectDynamicField (x ^. #fieldPairValue . #fieldOutput)
145+
/= mempty
146+
)
147+
raw
140148

141149
defShow :: (Show a) => a -> MisoString
142150
defShow =

ghcjs/lightning-verifier/src/App/Widgets/Main.hs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
module App.Widgets.Main (mainWidget) where
22

3+
import qualified App.Misc as Misc
34
import App.Types
45
import qualified App.Widgets.Bolt11 as Bolt11
56
import qualified App.Widgets.Decrypt as Decrypt
67
import qualified App.Widgets.Menu as Menu
78
import qualified Functora.Miso.Css as Css
9+
import qualified Functora.Miso.Jsm as Jsm
810
import Functora.Miso.Prelude
911
import qualified Functora.Miso.Widgets.BrowserLink as BrowserLink
1012
import qualified Functora.Miso.Widgets.Field as Field
@@ -121,33 +123,54 @@ screenWidget st@Model {modelState = St {stScreen = Converter}} =
121123
FieldPairs.argsOptic = #modelState . #stDoc . #stDocFieldPairs,
122124
FieldPairs.argsAction = PushUpdate . Instant
123125
}
124-
<> [ Field.textField
126+
<> [ Field.textField @Model @Action
125127
Field.Args
126128
{ Field.argsModel = st,
127129
Field.argsOptic = #modelState . #stDoc . #stDocLnInvoice,
128130
Field.argsAction = PushUpdate . Delayed 300
129131
}
130-
( Field.defOpts @Model @Action
131-
& #optsFilledOrOutlined
132-
.~ Outlined
133-
& #optsPlaceholder
134-
.~ "Invoice"
132+
( Field.defOpts
133+
{ Field.optsFilledOrOutlined = Outlined,
134+
Field.optsPlaceholder = "Invoice",
135+
Field.optsLeadingWidget =
136+
pasteWidget $ #modelState . #stDoc . #stDocLnInvoice
137+
}
135138
),
136139
Field.textField
137140
Field.Args
138141
{ Field.argsModel = st,
139142
Field.argsOptic = #modelState . #stDoc . #stDocLnPreimage,
140143
Field.argsAction = PushUpdate . Delayed 300
141144
}
142-
( Field.defOpts @Model @Action
143-
& #optsFilledOrOutlined
144-
.~ Outlined
145-
& #optsPlaceholder
146-
.~ "Preimage"
147-
)
145+
Field.defOpts
146+
{ Field.optsFilledOrOutlined = Outlined,
147+
Field.optsPlaceholder = "Preimage",
148+
Field.optsLeadingWidget =
149+
pasteWidget $ #modelState . #stDoc . #stDocLnPreimage
150+
}
148151
]
149152
<> Bolt11.bolt11 st
150153

154+
pasteWidget ::
155+
ATraversal' Model (Field MisoString Unique) ->
156+
Maybe (Field.OptsWidget Model Action)
157+
pasteWidget optic =
158+
Just
159+
. Field.ActionWidget "content_paste" mempty
160+
. PushUpdate
161+
. Instant
162+
$ \prev -> do
163+
Jsm.selectClipboard $ \case
164+
Nothing ->
165+
Jsm.popupText @MisoString "Failed to paste!"
166+
Just clip ->
167+
Misc.pushActionQueue prev
168+
. Instant
169+
$ pure
170+
. (& cloneTraversal optic . #fieldOutput .~ clip)
171+
. (& cloneTraversal optic . #fieldInput . #uniqueValue .~ clip)
172+
pure prev
173+
151174
tosWidget :: View Action
152175
tosWidget =
153176
LayoutGrid.cell

ghcjs/lightning-verifier/static/cap.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineCustomElements } from "@ionic/pwa-elements/loader";
22
import { WebviewPrint } from "capacitor-webview-print";
33
import { Preferences } from "@capacitor/preferences";
4+
import { Clipboard } from "@capacitor/clipboard";
45
import { Browser } from "@capacitor/browser";
56
import { Share } from "@capacitor/share";
67
import { Toast } from "@capacitor/toast";
@@ -18,6 +19,11 @@ async function insertStorage(key, value) {
1819
return await Preferences.set({ key: key, value: value });
1920
}
2021

22+
async function selectClipboard() {
23+
const { value } = await Clipboard.read();
24+
return value;
25+
}
26+
2127
async function openBrowserPage(url) {
2228
try {
2329
return await Browser.open({ url: url, windowName: "_blank" });
@@ -43,6 +49,7 @@ defineCustomElements(window);
4349
globalThis.printCurrentPage = printCurrentPage;
4450
globalThis.selectStorage = selectStorage;
4551
globalThis.insertStorage = insertStorage;
52+
globalThis.selectClipboard = selectClipboard;
4653
globalThis.openBrowserPage = openBrowserPage;
4754
globalThis.shareText = shareText;
4855
globalThis.popupText = popupText;

ghcjs/lightning-verifier/static/main.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,6 +1814,83 @@ const WebviewPrint = registerPlugin("WebviewPrint", {
18141814
const Preferences = registerPlugin("Preferences", {
18151815
web: () => __vitePreload(() => import("./web2.js"), true ? [] : void 0).then((m) => new m.PreferencesWeb())
18161816
});
1817+
class ClipboardWeb extends WebPlugin {
1818+
async write(options) {
1819+
if (typeof navigator === "undefined" || !navigator.clipboard) {
1820+
throw this.unavailable("Clipboard API not available in this browser");
1821+
}
1822+
if (options.string !== void 0) {
1823+
await this.writeText(options.string);
1824+
} else if (options.url) {
1825+
await this.writeText(options.url);
1826+
} else if (options.image) {
1827+
if (typeof ClipboardItem !== "undefined") {
1828+
try {
1829+
const blob = await (await fetch(options.image)).blob();
1830+
const clipboardItemInput = new ClipboardItem({ [blob.type]: blob });
1831+
await navigator.clipboard.write([clipboardItemInput]);
1832+
} catch (err) {
1833+
throw new Error("Failed to write image");
1834+
}
1835+
} else {
1836+
throw this.unavailable("Writing images to the clipboard is not supported in this browser");
1837+
}
1838+
} else {
1839+
throw new Error("Nothing to write");
1840+
}
1841+
}
1842+
async read() {
1843+
if (typeof navigator === "undefined" || !navigator.clipboard) {
1844+
throw this.unavailable("Clipboard API not available in this browser");
1845+
}
1846+
if (typeof ClipboardItem !== "undefined") {
1847+
try {
1848+
const clipboardItems = await navigator.clipboard.read();
1849+
const type = clipboardItems[0].types[0];
1850+
const clipboardBlob = await clipboardItems[0].getType(type);
1851+
const data = await this._getBlobData(clipboardBlob, type);
1852+
return { value: data, type };
1853+
} catch (err) {
1854+
return this.readText();
1855+
}
1856+
} else {
1857+
return this.readText();
1858+
}
1859+
}
1860+
async readText() {
1861+
if (typeof navigator === "undefined" || !navigator.clipboard || !navigator.clipboard.readText) {
1862+
throw this.unavailable("Reading from clipboard not supported in this browser");
1863+
}
1864+
const text = await navigator.clipboard.readText();
1865+
return { value: text, type: "text/plain" };
1866+
}
1867+
async writeText(text) {
1868+
if (typeof navigator === "undefined" || !navigator.clipboard || !navigator.clipboard.writeText) {
1869+
throw this.unavailable("Writting to clipboard not supported in this browser");
1870+
}
1871+
await navigator.clipboard.writeText(text);
1872+
}
1873+
_getBlobData(clipboardBlob, type) {
1874+
return new Promise((resolve, reject) => {
1875+
const reader = new FileReader();
1876+
if (type.includes("image")) {
1877+
reader.readAsDataURL(clipboardBlob);
1878+
} else {
1879+
reader.readAsText(clipboardBlob);
1880+
}
1881+
reader.onloadend = () => {
1882+
const r = reader.result;
1883+
resolve(r);
1884+
};
1885+
reader.onerror = (e) => {
1886+
reject(e);
1887+
};
1888+
});
1889+
}
1890+
}
1891+
const Clipboard = registerPlugin("Clipboard", {
1892+
web: () => new ClipboardWeb()
1893+
});
18171894
const Browser = registerPlugin("Browser", {
18181895
web: () => __vitePreload(() => import("./web3.js"), true ? [] : void 0).then((m) => new m.BrowserWeb())
18191896
});
@@ -1833,6 +1910,10 @@ async function selectStorage(key) {
18331910
async function insertStorage(key, value) {
18341911
return await Preferences.set({ key, value });
18351912
}
1913+
async function selectClipboard() {
1914+
const { value } = await Clipboard.read();
1915+
return value;
1916+
}
18361917
async function openBrowserPage(url) {
18371918
try {
18381919
return await Browser.open({ url, windowName: "_blank" });
@@ -1855,6 +1936,7 @@ defineCustomElements();
18551936
globalThis.printCurrentPage = printCurrentPage;
18561937
globalThis.selectStorage = selectStorage;
18571938
globalThis.insertStorage = insertStorage;
1939+
globalThis.selectClipboard = selectClipboard;
18581940
globalThis.openBrowserPage = openBrowserPage;
18591941
globalThis.shareText = shareText;
18601942
globalThis.popupText = popupText;

ghcjs/miso-widgets/src/Functora/Miso/Jsm.hs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ module Functora.Miso.Jsm
1212
enterOrEscapeBlur,
1313
insertStorage,
1414
selectStorage,
15+
selectClipboard,
1516
)
1617
where
1718

1819
import qualified Data.Generics as Syb
20+
import qualified Data.Text as T
1921
import qualified Data.Text.Lazy.Encoding as TL
2022
import Functora.Miso.Prelude
2123
import Functora.Miso.Types
@@ -169,3 +171,27 @@ selectStorage key after = do
169171
void
170172
$ prom
171173
^. JS.js2 @MisoString "then" success failure
174+
175+
selectClipboard :: (Maybe MisoString -> JSM ()) -> JSM ()
176+
selectClipboard after = do
177+
success <- JS.function $ \_ _ ->
178+
handleAny (\e -> consoleLog e >> after Nothing) . \case
179+
[val] -> do
180+
valExist <- ghcjsPure $ JS.isTruthy val
181+
if not valExist
182+
then after Nothing
183+
else do
184+
raw <- JS.fromJSVal @Prelude.Text val
185+
str <- maybe (throwString @MisoString "Clipboard bad type!") pure raw
186+
popupText @MisoString "Inserted!"
187+
after . Just . from @Prelude.Text @MisoString $ T.strip str
188+
_ ->
189+
throwString @MisoString "Clipboard bad argv!"
190+
failure <-
191+
JS.function $ \_ _ _ ->
192+
popupText @MisoString "Failed to paste!"
193+
prom <-
194+
JS.global ^. JS.js0 @MisoString "selectClipboard"
195+
void
196+
$ prom
197+
^. JS.js2 @MisoString "then" success failure

ghcjs/miso-widgets/src/Functora/Miso/Widgets/Field.hs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import qualified Functora.Miso.Widgets.Grid as Grid
2424
import qualified Functora.Miso.Widgets.Qr as Qr
2525
import qualified Language.Javascript.JSaddle as JS
2626
import qualified Material.Button as Button
27+
import qualified Material.IconButton as IconButton
2728
import qualified Material.LayoutGrid as LayoutGrid
2829
import qualified Material.Select as Select
2930
import qualified Material.Select.Item as SelectItem
@@ -708,12 +709,12 @@ dynamicFieldViewer ::
708709
[View action]
709710
dynamicFieldViewer action value =
710711
case value ^. #fieldType of
711-
FieldTypeNumber -> plain out text
712-
FieldTypePercent -> plain out $ text . (<> "%")
713-
FieldTypeText -> plain out text
712+
FieldTypeNumber -> plain action value text
713+
FieldTypePercent -> plain action value $ text . (<> "%")
714+
FieldTypeText -> plain action value text
714715
FieldTypeTitle -> header out
715-
FieldTypeHtml -> plain out rawHtml
716-
FieldTypePassword -> plain out $ const "*****"
716+
FieldTypeHtml -> plain action value rawHtml
717+
FieldTypePassword -> plain action value $ const "*****"
717718
FieldTypeQrCode ->
718719
Qr.qr
719720
Qr.Args
@@ -728,14 +729,11 @@ dynamicFieldViewer action value =
728729
allowCopy = value ^. #fieldAllowCopy
729730

730731
plain ::
731-
( Eq a,
732-
Monoid a,
733-
ToMisoString a
734-
) =>
735-
a ->
732+
((model -> JSM model) -> action) ->
733+
Field DynamicField f ->
736734
(MisoString -> View action) ->
737735
[View action]
738-
plain out widget =
736+
plain action value widget =
739737
if out == mempty
740738
then mempty
741739
else
@@ -757,8 +755,22 @@ plain out widget =
757755
("line-height", "150%")
758756
]
759757
]
760-
[widget $ toMisoString out]
758+
$ [ widget $ toMisoString out
759+
]
760+
<> ( if not allowCopy
761+
then mempty
762+
else
763+
[ IconButton.iconButton
764+
( IconButton.config
765+
& IconButton.setOnClick (action $ Jsm.shareText out)
766+
)
767+
"content_copy"
768+
]
769+
)
761770
]
771+
where
772+
out = inspectDynamicField $ value ^. #fieldOutput
773+
allowCopy = value ^. #fieldAllowCopy
762774

763775
header :: MisoString -> [View action]
764776
header txt =

0 commit comments

Comments
 (0)