diff --git a/apps/web-client/dist-manifest.json b/apps/web-client/dist-manifest.json index 85a0380..967d38c 100644 --- a/apps/web-client/dist-manifest.json +++ b/apps/web-client/dist-manifest.json @@ -1 +1,472 @@ -{"schemaVersion":1,"packageVersion":"0.11.2","generatedBy":"apps/web-client/tools/build.mjs","files":[{"path":"assets/api.d.ts","bytes":7867,"sha256":"0de04eb0eb1f794c7dc509a73dbd6cfe73636a3301951b1e7b591d6b81f8228f"},{"path":"assets/api.js","bytes":10545,"sha256":"46558e1f056e2976f827b3db89b31399bb99a67fa5d6a3bdb5df3d1fe2fbe3a2"},{"path":"assets/api.js.map","bytes":9625,"sha256":"ecb4a621a9ea74f3f99ef8a42ec5f6b517c69acee8e07218cc67c5d73a13e840"},{"path":"assets/audio-player.d.ts","bytes":847,"sha256":"3929bbdb74475c04416a0ee226eca7b091bca29c79fc1f2a4e3f98a276b17829"},{"path":"assets/audio-player.js","bytes":4861,"sha256":"1b88f6fbe635c9a782fd045399715a17a6d2c59ee36b9ca75a83517ab8a5d852"},{"path":"assets/audio-player.js.map","bytes":4963,"sha256":"fdc6daf346924fa61aca418d40a1225cf5759b52475277a238741f8d2b7d6088"},{"path":"assets/auth-controller.d.ts","bytes":1467,"sha256":"53fe1f6196b1e68800c547ef522f0deb02104a783fd4f63164a17d69c9f27fc4"},{"path":"assets/auth-controller.js","bytes":8041,"sha256":"34e6bb0595008cbe7786dc7f634da286379e46e16e4c1494aa24744c3b9156c0"},{"path":"assets/auth-controller.js.map","bytes":8076,"sha256":"752523af0fa93013ab6dbf78459b3027b4e29997b89af67b1f51e78d60c3a93c"},{"path":"assets/auto-download-controller.d.ts","bytes":1393,"sha256":"ed32f90b79f8c9ff43af33934e273a7040449f9e4ea02e41009d7e3426efcc66"},{"path":"assets/auto-download-controller.js","bytes":11291,"sha256":"57337ebf7a023afa1c12f6788cb27779aef2d3644bdd9116a29e0a462eef0598"},{"path":"assets/auto-download-controller.js.map","bytes":11320,"sha256":"4f5f221906ec6494312293b56d3a12a8a8b3b626531e2956c8777dfc187dcd91"},{"path":"assets/browser-support.d.ts","bytes":766,"sha256":"224c57a68b358da8fcb76277d3202738e70fa5ee1946fba0bb8d91077fb4e4f8"},{"path":"assets/browser-support.js","bytes":671,"sha256":"010a0b76cbe8bb6ae9e20ff5a60836b4fcfe94010678ee09fcaf349380675529"},{"path":"assets/browser-support.js.map","bytes":719,"sha256":"412b951eb2bbf8b3614024506c288498b5033e62c07a49385cd754101f0e8d74"},{"path":"assets/controller.d.ts","bytes":4252,"sha256":"9d4db1417756dee4ba4d7681428a0b0e0be01a042f317b67d879aeab3e6ded4a"},{"path":"assets/controller.js","bytes":54718,"sha256":"928926e0f48f04769c21edeb6347ed2751f456b55c4aa48da582d4af3cbfb486"},{"path":"assets/controller.js.map","bytes":48223,"sha256":"40ca9fb61bfd194b5d9fc80c27c88c3a6d1839872f8e9ebdd51e82712ce13fb7"},{"path":"assets/display-config.d.ts","bytes":1412,"sha256":"37e0c88b13717cd04ea6ea5c1412a01df5f14ac23ea9c085d5baa72a7d09ad1c"},{"path":"assets/display-config.js","bytes":5470,"sha256":"8f5d3062eaec98823cd14c0f113c44a70397fd41e777800c0e8afa92f2381261"},{"path":"assets/display-config.js.map","bytes":4879,"sha256":"93640726bf114c650354fc0024fd63f520f2b00facd3ba35c5aa828f0949ad6f"},{"path":"assets/input.d.ts","bytes":1204,"sha256":"a3b75762c1552ba294591b821d0c333851b14afd0297b1a0f0e3ddef12f5f4b2"},{"path":"assets/input.js","bytes":3522,"sha256":"095f6bd62c75597776522c9045faa51fe8ab2fb74653d3ce17c5589bbd6ff10f"},{"path":"assets/input.js.map","bytes":3991,"sha256":"e742a1a23316541d5817125104daa75f12bb3a9b40b0b5e04db117411ab3b9fe"},{"path":"assets/main.d.ts","bytes":11,"sha256":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"path":"assets/main.js","bytes":14522,"sha256":"746ee71599bdc6625c5eccaecef8869ebd3b17daf434a490e7830635f7b59b3f"},{"path":"assets/main.js.map","bytes":8360,"sha256":"7b55a69966b32924b33d62ac7fd7d468fff3c81e211f82b4976db57716e391f7"},{"path":"assets/network-controller.d.ts","bytes":1313,"sha256":"1148e42730c2d3dc367aec36a74cfb52dbd833d1c1c158e2f6a6dd20967e8447"},{"path":"assets/network-controller.js","bytes":8287,"sha256":"738a787baf0e3820ae11b6c022a0fbfb65563fbc115524fd3218e04495085b13"},{"path":"assets/network-controller.js.map","bytes":8582,"sha256":"713bd634c35efb9acd3b64488f6b16b35e384d5c942e8ed39ba8d1a1547e13df"},{"path":"assets/running-app-controller.d.ts","bytes":863,"sha256":"97907288db5c5327f06052ce31c47a7779cb53809323cf9486b59603c4b1e40e"},{"path":"assets/running-app-controller.js","bytes":13136,"sha256":"20dfb262c7ba35e32daf1a8dc439a1bdcdcc7a0cf6f26fd7adf85f1ded9e76c1"},{"path":"assets/running-app-controller.js.map","bytes":12847,"sha256":"52b65b0ee2d86aff2b8c5e86441648961aa1d21c88e663250a2c43df848e5980"},{"path":"assets/transfer-controller.d.ts","bytes":2919,"sha256":"af2de615137481a9eae6d8d7a5998835a2d848bfcb003e5431fd1d921823bd7f"},{"path":"assets/transfer-controller.js","bytes":39191,"sha256":"3d068756e035e7598800ed12a7269a8bc862f699529295d56dda2724f048bb56"},{"path":"assets/transfer-controller.js.map","bytes":36404,"sha256":"afc1fbd63898f171f8dd4eff2eed9c145ec30d5286ff692043949e6bd54e4d2c"},{"path":"assets/types.d.ts","bytes":10197,"sha256":"20bfe1bec434dbd8bbd9e0948271f771a3871064e08d258a78b2fb8942985045"},{"path":"assets/types.js","bytes":44,"sha256":"01ae2a5b120382f9a648ced7ee8507493a134f216d100fc61600c6c9738235d2"},{"path":"assets/types.js.map","bytes":105,"sha256":"17307f7387fbcf31645bdc6c61dd25c80edefb6fbacbb2909f503b879227b631"},{"path":"assets/video-renderer.d.ts","bytes":1419,"sha256":"af3401568c03f92f23eb240a17dc27f5458db4f25726ec1f989b860dfb734480"},{"path":"assets/video-renderer.js","bytes":7848,"sha256":"ed82e5181cad5f2cbd65ebc14414d6611501a20f00feae0ed023e6be8d06c14e"},{"path":"assets/video-renderer.js.map","bytes":7412,"sha256":"22697668c013202784c14a38d5dc6c0d17392302e19d91d6dd337e06b1773fc8"},{"path":"assets/websocket-transport.d.ts","bytes":706,"sha256":"3bf5358eab85fac59bd836cdb334f2a8c25a53fa5e9868a8db8879dff1c155e5"},{"path":"assets/websocket-transport.js","bytes":5490,"sha256":"81dae6f375c11bce01ac585e61e7bf75fef44b703ea8e41b8426fd6cb2a6b7ec"},{"path":"assets/websocket-transport.js.map","bytes":5366,"sha256":"f420b77d19cd66926a9e8f0aa3df4450eee1de9dcf923da5ff3bb55d414325fa"},{"path":"droidwebdisplay-main-drawer-core.css","bytes":27104,"sha256":"8bc304be85b8fcdd8f21d51ab708b5a24e1102938dc16c651ec01bcb6fb6a3c7"},{"path":"droidwebdisplay-main-drawer.css","bytes":5670,"sha256":"fdf37a0bcb9ad994d2771f5ae8c6aa8f1871f401a206d28e76e62df1fa4bc0aa"},{"path":"droidwebdisplay-main-drawer.js","bytes":26484,"sha256":"57291e2c349f95159d731dd90010f42bf0748fc019a4c32e922d6fed72af097d"},{"path":"favicon.svg","bytes":328,"sha256":"0f7ee6fcc369edd2ca0fcfbe571c41e034677102d6b5b619cdaf878fdb279301"},{"path":"index.html","bytes":33335,"sha256":"f138aeee999e7765ae1c415d80ad89f6acab48548911f16234f7dc739ad6e897"},{"path":"styles.css","bytes":31432,"sha256":"0bc546956b467f8d5d072409732a5e86154dd67f7a5d441e229a6967cabfd701"},{"path":"vendor/scrcpy-protocol/src/common/async-byte-reader.d.ts","bytes":504,"sha256":"079350d7c949cb0a9580d6fa9d84bb5ccac01b26f9f3596861371fbca69c83fb"},{"path":"vendor/scrcpy-protocol/src/common/async-byte-reader.js","bytes":2561,"sha256":"6dfe495ec309ebf8775e5a19d7c69318d753cb8cb736e076e41b6a0c099823c4"},{"path":"vendor/scrcpy-protocol/src/common/async-byte-reader.js.map","bytes":2720,"sha256":"f6184e646d46fd24d91cde7ba9d6a1df6ffa09e0fc03a6bdfb7b840fa14752ba"},{"path":"vendor/scrcpy-protocol/src/common/binary.d.ts","bytes":1095,"sha256":"0d5cb1f303300fd88b5eb6ddb2c6dc70f84c983ff30f09bd2c1957cb049f948d"},{"path":"vendor/scrcpy-protocol/src/common/binary.js","bytes":4566,"sha256":"40706609995a47dcbb1f2bf20da3dcb3696efa081dac54c033229a3c067c3474"},{"path":"vendor/scrcpy-protocol/src/common/binary.js.map","bytes":5496,"sha256":"63d5024d0699d5c41410161cc5369fd0802dd2a8b24d5dab660aa032cee19f72"},{"path":"vendor/scrcpy-protocol/src/common/errors.d.ts","bytes":580,"sha256":"92d70ebbca76d09f48732f2f098ab103d26f264708a6e6261ff0d7ae2462c0b3"},{"path":"vendor/scrcpy-protocol/src/common/errors.js","bytes":840,"sha256":"4af7bf2aa4fc805e64528813746253c154ce05c2272b5fb2329aa63e1c56f3fb"},{"path":"vendor/scrcpy-protocol/src/common/errors.js.map","bytes":633,"sha256":"98941e677dd3bd055903a6bfc2e589cdb94679a9b9808f8e6f9a9b7a4dcc6fea"},{"path":"vendor/scrcpy-protocol/src/common/transport.d.ts","bytes":759,"sha256":"93f3bae3bb94e6b194e4df91910aa276c9ef69e8e34f2e47298563a7dee32612"},{"path":"vendor/scrcpy-protocol/src/common/transport.js","bytes":48,"sha256":"b7bb187ea1bf4d71015d2996c266e05de2320643320e9812e482925ecf334011"},{"path":"vendor/scrcpy-protocol/src/common/transport.js.map","bytes":123,"sha256":"3babf01144639d913594870ced9d6899006705414836a75e798d07518fc699a1"},{"path":"vendor/scrcpy-protocol/src/common/types.d.ts","bytes":634,"sha256":"33272a2438a6a2a16f26fe4ad52647cfec047f9010360c4bf0f9ea3594e004ac"},{"path":"vendor/scrcpy-protocol/src/common/types.js","bytes":44,"sha256":"01ae2a5b120382f9a648ced7ee8507493a134f216d100fc61600c6c9738235d2"},{"path":"vendor/scrcpy-protocol/src/common/types.js.map","bytes":115,"sha256":"bbabd0ed1cc20d31fe44374fd8b0d830dab18c6de2b695a0dcac5026bf4e41d3"},{"path":"vendor/scrcpy-protocol/src/index.d.ts","bytes":235,"sha256":"81b5042459b96bd118b00aa51d894b9219186f76331558070468ae564f69c89d"},{"path":"vendor/scrcpy-protocol/src/index.js","bytes":268,"sha256":"e9ca80e46655bd1a830210ec2495a3ff6445ac816cbac03b76b427f228df279a"},{"path":"vendor/scrcpy-protocol/src/index.js.map","bytes":241,"sha256":"b74a1039b8a04948cc823be75849defdd823f61a102dcfe8a4d3f46f8c877f30"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/adapter.d.ts","bytes":1021,"sha256":"6c55edebbf07b87952ce9fa65ae61aa35e6ec5360b5c82eb46fe468755e3c6fe"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/adapter.js","bytes":5078,"sha256":"7dc0b972037f817fa3b8e3ec983646f0d3b50f06bc075e9f0c80d336a2d60142"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/adapter.js.map","bytes":4236,"sha256":"58d319f6384c4158b0cbc0c97db68130abc22a0d14f55ee22447c0351b90bb57"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/constants.d.ts","bytes":1319,"sha256":"e42eaf42e4a1aa6876990d1c8d383692e6ea473c5bb93d94daf3be539a46e768"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/constants.js","bytes":1743,"sha256":"b17dcda45c891b25e1eb382151048e37d220e25fa8fd2a208ee9a81fb0e6b22f"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/constants.js.map","bytes":1578,"sha256":"8b9720613fb8edbc34f78287a4f5645cf0bab5575e93c9eb20b600f14757a2b6"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/control.d.ts","bytes":3110,"sha256":"8f9ad2937ecfa76333da5e27df5a15f40b03f2e9b7140ca7d45fe5f41123337e"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/control.js","bytes":7524,"sha256":"0d2d46b93174b841f0ca69df212d97c781b0045004834805b4e1393a62427dc6"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/control.js.map","bytes":5786,"sha256":"5745603c2460d596a390e16e9a0687d4cac8366d92d67a3f0511d0303eff9aaa"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/device.d.ts","bytes":690,"sha256":"1d7d9a195a14d9be22319c800e22fb13a67402f74c7662612e2e26a8bb80844c"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/device.js","bytes":1914,"sha256":"8abf7d414d26ba21b38709b9016a6c5fb89e0ecb0908b01a9bf3e497dd99e731"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/device.js.map","bytes":1684,"sha256":"0eaf2239c4b19bfac8aaed54080231716cbb9440207a9bc3086865c2d02d0ae5"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/h264.d.ts","bytes":400,"sha256":"6d3ca9b66d344a200f7f1d50801cfe75506f0f47bb25b7d3b632523b1fdc8459"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/h264.js","bytes":2213,"sha256":"918ba61e5ac1c7f186102e8fae149d0855b171bd1f1d126df27eee027492a43d"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/h264.js.map","bytes":2617,"sha256":"04bc2c0a25445c7fad35fad73e091ebe91d669bd185f465041c03c6acb512d65"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/handshake.d.ts","bytes":291,"sha256":"e0b28c022282702b68fe05f6122032e24440825857afc057dabd66886ffd360b"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/handshake.js","bytes":689,"sha256":"dfc2d0038117c8821715fcc442abc93dcd2b117cba862b000fa988b90c254ace"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/handshake.js.map","bytes":834,"sha256":"5c39d2d343d6d72f8895819527f0de54265e91ccaebc749049255643d263a556"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/index.d.ts","bytes":208,"sha256":"83bb34a9a93cc19775845e021cb04ebbab146486f8e55a9e69e0b1a80a0ddf9b"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/index.js","bytes":241,"sha256":"9470acdc6865f9237ad6c1385d21a2a9ad2752ecd62bdd137e0dd296cb3ff252"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/index.js.map","bytes":274,"sha256":"18572ffc879b1367e1584db05f41e3f3940a816d06089d132a482cb1f3d3990b"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/media.d.ts","bytes":836,"sha256":"7cbb9223f6fcfa665361f3d15450774baa920667fc93a21ff42cd8cca91b1748"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/media.js","bytes":4595,"sha256":"21aae13478490fcc5311f4442fc5bcb4e5a820c263d35b0b853948970f8772f0"},{"path":"vendor/scrcpy-protocol/src/versions/v4_1/media.js.map","bytes":4275,"sha256":"a7b8922f316ec12b1729f53fa5bf900fae02839a4a9a27de40549e9341a584c7"}]} +{ + "schemaVersion": 1, + "packageVersion": "0.11.2", + "generatedBy": "apps/web-client/tools/build.mjs", + "files": [ + { + "path": "assets/api.d.ts", + "bytes": 7867, + "sha256": "0de04eb0eb1f794c7dc509a73dbd6cfe73636a3301951b1e7b591d6b81f8228f" + }, + { + "path": "assets/api.js", + "bytes": 10545, + "sha256": "46558e1f056e2976f827b3db89b31399bb99a67fa5d6a3bdb5df3d1fe2fbe3a2" + }, + { + "path": "assets/api.js.map", + "bytes": 9625, + "sha256": "ecb4a621a9ea74f3f99ef8a42ec5f6b517c69acee8e07218cc67c5d73a13e840" + }, + { + "path": "assets/audio-player.d.ts", + "bytes": 847, + "sha256": "3929bbdb74475c04416a0ee226eca7b091bca29c79fc1f2a4e3f98a276b17829" + }, + { + "path": "assets/audio-player.js", + "bytes": 4861, + "sha256": "1b88f6fbe635c9a782fd045399715a17a6d2c59ee36b9ca75a83517ab8a5d852" + }, + { + "path": "assets/audio-player.js.map", + "bytes": 4963, + "sha256": "fdc6daf346924fa61aca418d40a1225cf5759b52475277a238741f8d2b7d6088" + }, + { + "path": "assets/auth-controller.d.ts", + "bytes": 1467, + "sha256": "53fe1f6196b1e68800c547ef522f0deb02104a783fd4f63164a17d69c9f27fc4" + }, + { + "path": "assets/auth-controller.js", + "bytes": 8041, + "sha256": "34e6bb0595008cbe7786dc7f634da286379e46e16e4c1494aa24744c3b9156c0" + }, + { + "path": "assets/auth-controller.js.map", + "bytes": 8076, + "sha256": "752523af0fa93013ab6dbf78459b3027b4e29997b89af67b1f51e78d60c3a93c" + }, + { + "path": "assets/auto-download-controller.d.ts", + "bytes": 1393, + "sha256": "ed32f90b79f8c9ff43af33934e273a7040449f9e4ea02e41009d7e3426efcc66" + }, + { + "path": "assets/auto-download-controller.js", + "bytes": 11291, + "sha256": "57337ebf7a023afa1c12f6788cb27779aef2d3644bdd9116a29e0a462eef0598" + }, + { + "path": "assets/auto-download-controller.js.map", + "bytes": 11320, + "sha256": "4f5f221906ec6494312293b56d3a12a8a8b3b626531e2956c8777dfc187dcd91" + }, + { + "path": "assets/browser-support.d.ts", + "bytes": 766, + "sha256": "224c57a68b358da8fcb76277d3202738e70fa5ee1946fba0bb8d91077fb4e4f8" + }, + { + "path": "assets/browser-support.js", + "bytes": 671, + "sha256": "010a0b76cbe8bb6ae9e20ff5a60836b4fcfe94010678ee09fcaf349380675529" + }, + { + "path": "assets/browser-support.js.map", + "bytes": 719, + "sha256": "412b951eb2bbf8b3614024506c288498b5033e62c07a49385cd754101f0e8d74" + }, + { + "path": "assets/controller.d.ts", + "bytes": 4252, + "sha256": "9d4db1417756dee4ba4d7681428a0b0e0be01a042f317b67d879aeab3e6ded4a" + }, + { + "path": "assets/controller.js", + "bytes": 57121, + "sha256": "35577dbe1abb009a81530d785c6243c7f79947c8b5d0bc3b81d4c0f3cc7ee230" + }, + { + "path": "assets/controller.js.map", + "bytes": 48223, + "sha256": "40ca9fb61bfd194b5d9fc80c27c88c3a6d1839872f8e9ebdd51e82712ce13fb7" + }, + { + "path": "assets/display-config.d.ts", + "bytes": 1412, + "sha256": "37e0c88b13717cd04ea6ea5c1412a01df5f14ac23ea9c085d5baa72a7d09ad1c" + }, + { + "path": "assets/display-config.js", + "bytes": 5470, + "sha256": "8f5d3062eaec98823cd14c0f113c44a70397fd41e777800c0e8afa92f2381261" + }, + { + "path": "assets/display-config.js.map", + "bytes": 4879, + "sha256": "93640726bf114c650354fc0024fd63f520f2b00facd3ba35c5aa828f0949ad6f" + }, + { + "path": "assets/input.d.ts", + "bytes": 1204, + "sha256": "a3b75762c1552ba294591b821d0c333851b14afd0297b1a0f0e3ddef12f5f4b2" + }, + { + "path": "assets/input.js", + "bytes": 3522, + "sha256": "095f6bd62c75597776522c9045faa51fe8ab2fb74653d3ce17c5589bbd6ff10f" + }, + { + "path": "assets/input.js.map", + "bytes": 3991, + "sha256": "e742a1a23316541d5817125104daa75f12bb3a9b40b0b5e04db117411ab3b9fe" + }, + { + "path": "assets/main.d.ts", + "bytes": 11, + "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881" + }, + { + "path": "assets/main.js", + "bytes": 14522, + "sha256": "746ee71599bdc6625c5eccaecef8869ebd3b17daf434a490e7830635f7b59b3f" + }, + { + "path": "assets/main.js.map", + "bytes": 8360, + "sha256": "7b55a69966b32924b33d62ac7fd7d468fff3c81e211f82b4976db57716e391f7" + }, + { + "path": "assets/network-controller.d.ts", + "bytes": 1313, + "sha256": "1148e42730c2d3dc367aec36a74cfb52dbd833d1c1c158e2f6a6dd20967e8447" + }, + { + "path": "assets/network-controller.js", + "bytes": 8287, + "sha256": "738a787baf0e3820ae11b6c022a0fbfb65563fbc115524fd3218e04495085b13" + }, + { + "path": "assets/network-controller.js.map", + "bytes": 8582, + "sha256": "713bd634c35efb9acd3b64488f6b16b35e384d5c942e8ed39ba8d1a1547e13df" + }, + { + "path": "assets/running-app-controller.d.ts", + "bytes": 863, + "sha256": "97907288db5c5327f06052ce31c47a7779cb53809323cf9486b59603c4b1e40e" + }, + { + "path": "assets/running-app-controller.js", + "bytes": 13136, + "sha256": "20dfb262c7ba35e32daf1a8dc439a1bdcdcc7a0cf6f26fd7adf85f1ded9e76c1" + }, + { + "path": "assets/running-app-controller.js.map", + "bytes": 12847, + "sha256": "52b65b0ee2d86aff2b8c5e86441648961aa1d21c88e663250a2c43df848e5980" + }, + { + "path": "assets/transfer-controller.d.ts", + "bytes": 2919, + "sha256": "af2de615137481a9eae6d8d7a5998835a2d848bfcb003e5431fd1d921823bd7f" + }, + { + "path": "assets/transfer-controller.js", + "bytes": 39191, + "sha256": "3d068756e035e7598800ed12a7269a8bc862f699529295d56dda2724f048bb56" + }, + { + "path": "assets/transfer-controller.js.map", + "bytes": 36404, + "sha256": "afc1fbd63898f171f8dd4eff2eed9c145ec30d5286ff692043949e6bd54e4d2c" + }, + { + "path": "assets/types.d.ts", + "bytes": 10197, + "sha256": "20bfe1bec434dbd8bbd9e0948271f771a3871064e08d258a78b2fb8942985045" + }, + { + "path": "assets/types.js", + "bytes": 44, + "sha256": "01ae2a5b120382f9a648ced7ee8507493a134f216d100fc61600c6c9738235d2" + }, + { + "path": "assets/types.js.map", + "bytes": 105, + "sha256": "17307f7387fbcf31645bdc6c61dd25c80edefb6fbacbb2909f503b879227b631" + }, + { + "path": "assets/video-renderer.d.ts", + "bytes": 1419, + "sha256": "af3401568c03f92f23eb240a17dc27f5458db4f25726ec1f989b860dfb734480" + }, + { + "path": "assets/video-renderer.js", + "bytes": 7848, + "sha256": "ed82e5181cad5f2cbd65ebc14414d6611501a20f00feae0ed023e6be8d06c14e" + }, + { + "path": "assets/video-renderer.js.map", + "bytes": 7412, + "sha256": "22697668c013202784c14a38d5dc6c0d17392302e19d91d6dd337e06b1773fc8" + }, + { + "path": "assets/websocket-transport.d.ts", + "bytes": 706, + "sha256": "3bf5358eab85fac59bd836cdb334f2a8c25a53fa5e9868a8db8879dff1c155e5" + }, + { + "path": "assets/websocket-transport.js", + "bytes": 5490, + "sha256": "81dae6f375c11bce01ac585e61e7bf75fef44b703ea8e41b8426fd6cb2a6b7ec" + }, + { + "path": "assets/websocket-transport.js.map", + "bytes": 5366, + "sha256": "f420b77d19cd66926a9e8f0aa3df4450eee1de9dcf923da5ff3bb55d414325fa" + }, + { + "path": "droidwebdisplay-main-drawer-core.css", + "bytes": 27104, + "sha256": "8bc304be85b8fcdd8f21d51ab708b5a24e1102938dc16c651ec01bcb6fb6a3c7" + }, + { + "path": "droidwebdisplay-main-drawer.css", + "bytes": 5670, + "sha256": "fdf37a0bcb9ad994d2771f5ae8c6aa8f1871f401a206d28e76e62df1fa4bc0aa" + }, + { + "path": "droidwebdisplay-main-drawer.js", + "bytes": 26484, + "sha256": "57291e2c349f95159d731dd90010f42bf0748fc019a4c32e922d6fed72af097d" + }, + { + "path": "favicon.svg", + "bytes": 328, + "sha256": "0f7ee6fcc369edd2ca0fcfbe571c41e034677102d6b5b619cdaf878fdb279301" + }, + { + "path": "index.html", + "bytes": 33335, + "sha256": "f138aeee999e7765ae1c415d80ad89f6acab48548911f16234f7dc739ad6e897" + }, + { + "path": "styles.css", + "bytes": 31432, + "sha256": "0bc546956b467f8d5d072409732a5e86154dd67f7a5d441e229a6967cabfd701" + }, + { + "path": "vendor/scrcpy-protocol/src/common/async-byte-reader.d.ts", + "bytes": 504, + "sha256": "079350d7c949cb0a9580d6fa9d84bb5ccac01b26f9f3596861371fbca69c83fb" + }, + { + "path": "vendor/scrcpy-protocol/src/common/async-byte-reader.js", + "bytes": 2561, + "sha256": "6dfe495ec309ebf8775e5a19d7c69318d753cb8cb736e076e41b6a0c099823c4" + }, + { + "path": "vendor/scrcpy-protocol/src/common/async-byte-reader.js.map", + "bytes": 2720, + "sha256": "f6184e646d46fd24d91cde7ba9d6a1df6ffa09e0fc03a6bdfb7b840fa14752ba" + }, + { + "path": "vendor/scrcpy-protocol/src/common/binary.d.ts", + "bytes": 1095, + "sha256": "0d5cb1f303300fd88b5eb6ddb2c6dc70f84c983ff30f09bd2c1957cb049f948d" + }, + { + "path": "vendor/scrcpy-protocol/src/common/binary.js", + "bytes": 4566, + "sha256": "40706609995a47dcbb1f2bf20da3dcb3696efa081dac54c033229a3c067c3474" + }, + { + "path": "vendor/scrcpy-protocol/src/common/binary.js.map", + "bytes": 5496, + "sha256": "63d5024d0699d5c41410161cc5369fd0802dd2a8b24d5dab660aa032cee19f72" + }, + { + "path": "vendor/scrcpy-protocol/src/common/errors.d.ts", + "bytes": 580, + "sha256": "92d70ebbca76d09f48732f2f098ab103d26f264708a6e6261ff0d7ae2462c0b3" + }, + { + "path": "vendor/scrcpy-protocol/src/common/errors.js", + "bytes": 840, + "sha256": "4af7bf2aa4fc805e64528813746253c154ce05c2272b5fb2329aa63e1c56f3fb" + }, + { + "path": "vendor/scrcpy-protocol/src/common/errors.js.map", + "bytes": 633, + "sha256": "98941e677dd3bd055903a6bfc2e589cdb94679a9b9808f8e6f9a9b7a4dcc6fea" + }, + { + "path": "vendor/scrcpy-protocol/src/common/transport.d.ts", + "bytes": 759, + "sha256": "93f3bae3bb94e6b194e4df91910aa276c9ef69e8e34f2e47298563a7dee32612" + }, + { + "path": "vendor/scrcpy-protocol/src/common/transport.js", + "bytes": 48, + "sha256": "b7bb187ea1bf4d71015d2996c266e05de2320643320e9812e482925ecf334011" + }, + { + "path": "vendor/scrcpy-protocol/src/common/transport.js.map", + "bytes": 123, + "sha256": "3babf01144639d913594870ced9d6899006705414836a75e798d07518fc699a1" + }, + { + "path": "vendor/scrcpy-protocol/src/common/types.d.ts", + "bytes": 634, + "sha256": "33272a2438a6a2a16f26fe4ad52647cfec047f9010360c4bf0f9ea3594e004ac" + }, + { + "path": "vendor/scrcpy-protocol/src/common/types.js", + "bytes": 44, + "sha256": "01ae2a5b120382f9a648ced7ee8507493a134f216d100fc61600c6c9738235d2" + }, + { + "path": "vendor/scrcpy-protocol/src/common/types.js.map", + "bytes": 115, + "sha256": "bbabd0ed1cc20d31fe44374fd8b0d830dab18c6de2b695a0dcac5026bf4e41d3" + }, + { + "path": "vendor/scrcpy-protocol/src/index.d.ts", + "bytes": 235, + "sha256": "81b5042459b96bd118b00aa51d894b9219186f76331558070468ae564f69c89d" + }, + { + "path": "vendor/scrcpy-protocol/src/index.js", + "bytes": 268, + "sha256": "e9ca80e46655bd1a830210ec2495a3ff6445ac816cbac03b76b427f228df279a" + }, + { + "path": "vendor/scrcpy-protocol/src/index.js.map", + "bytes": 241, + "sha256": "b74a1039b8a04948cc823be75849defdd823f61a102dcfe8a4d3f46f8c877f30" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/adapter.d.ts", + "bytes": 1021, + "sha256": "6c55edebbf07b87952ce9fa65ae61aa35e6ec5360b5c82eb46fe468755e3c6fe" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/adapter.js", + "bytes": 5078, + "sha256": "7dc0b972037f817fa3b8e3ec983646f0d3b50f06bc075e9f0c80d336a2d60142" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/adapter.js.map", + "bytes": 4236, + "sha256": "58d319f6384c4158b0cbc0c97db68130abc22a0d14f55ee22447c0351b90bb57" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/constants.d.ts", + "bytes": 1319, + "sha256": "e42eaf42e4a1aa6876990d1c8d383692e6ea473c5bb93d94daf3be539a46e768" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/constants.js", + "bytes": 1743, + "sha256": "b17dcda45c891b25e1eb382151048e37d220e25fa8fd2a208ee9a81fb0e6b22f" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/constants.js.map", + "bytes": 1578, + "sha256": "8b9720613fb8edbc34f78287a4f5645cf0bab5575e93c9eb20b600f14757a2b6" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/control.d.ts", + "bytes": 3110, + "sha256": "8f9ad2937ecfa76333da5e27df5a15f40b03f2e9b7140ca7d45fe5f41123337e" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/control.js", + "bytes": 7524, + "sha256": "0d2d46b93174b841f0ca69df212d97c781b0045004834805b4e1393a62427dc6" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/control.js.map", + "bytes": 5786, + "sha256": "5745603c2460d596a390e16e9a0687d4cac8366d92d67a3f0511d0303eff9aaa" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/device.d.ts", + "bytes": 690, + "sha256": "1d7d9a195a14d9be22319c800e22fb13a67402f74c7662612e2e26a8bb80844c" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/device.js", + "bytes": 1914, + "sha256": "8abf7d414d26ba21b38709b9016a6c5fb89e0ecb0908b01a9bf3e497dd99e731" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/device.js.map", + "bytes": 1684, + "sha256": "0eaf2239c4b19bfac8aaed54080231716cbb9440207a9bc3086865c2d02d0ae5" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/h264.d.ts", + "bytes": 400, + "sha256": "6d3ca9b66d344a200f7f1d50801cfe75506f0f47bb25b7d3b632523b1fdc8459" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/h264.js", + "bytes": 2213, + "sha256": "918ba61e5ac1c7f186102e8fae149d0855b171bd1f1d126df27eee027492a43d" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/h264.js.map", + "bytes": 2617, + "sha256": "04bc2c0a25445c7fad35fad73e091ebe91d669bd185f465041c03c6acb512d65" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/handshake.d.ts", + "bytes": 291, + "sha256": "e0b28c022282702b68fe05f6122032e24440825857afc057dabd66886ffd360b" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/handshake.js", + "bytes": 689, + "sha256": "dfc2d0038117c8821715fcc442abc93dcd2b117cba862b000fa988b90c254ace" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/handshake.js.map", + "bytes": 834, + "sha256": "5c39d2d343d6d72f8895819527f0de54265e91ccaebc749049255643d263a556" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/index.d.ts", + "bytes": 208, + "sha256": "83bb34a9a93cc19775845e021cb04ebbab146486f8e55a9e69e0b1a80a0ddf9b" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/index.js", + "bytes": 241, + "sha256": "9470acdc6865f9237ad6c1385d21a2a9ad2752ecd62bdd137e0dd296cb3ff252" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/index.js.map", + "bytes": 274, + "sha256": "18572ffc879b1367e1584db05f41e3f3940a816d06089d132a482cb1f3d3990b" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/media.d.ts", + "bytes": 836, + "sha256": "7cbb9223f6fcfa665361f3d15450774baa920667fc93a21ff42cd8cca91b1748" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/media.js", + "bytes": 4595, + "sha256": "21aae13478490fcc5311f4442fc5bcb4e5a820c263d35b0b853948970f8772f0" + }, + { + "path": "vendor/scrcpy-protocol/src/versions/v4_1/media.js.map", + "bytes": 4275, + "sha256": "a7b8922f316ec12b1729f53fa5bf900fae02839a4a9a27de40549e9341a584c7" + } + ] +} diff --git a/apps/web-client/dist/assets/controller.js b/apps/web-client/dist/assets/controller.js index 15525a7..74652e6 100644 --- a/apps/web-client/dist/assets/controller.js +++ b/apps/web-client/dist/assets/controller.js @@ -6,6 +6,16 @@ import { WebCodecsVideoRenderer } from "./video-renderer.js"; import { WebSocketBridgeTransport } from "./websocket-transport.js"; import { WebCodecsAudioPlayer } from "./audio-player.js"; const DEVICE_DROPDOWN_REFRESH_STALE_MS = 1500; +// textInjectionMessages chunks at 300 UTF-8 bytes and sendMessages awaits every +// chunk, so injection cost grows linearly with the text: ~875 sequential round +// trips at the 256 KiB clipboard limit. Above this size the clipboard is still +// synchronized and the user pastes on the device. +const MAX_INJECTED_BYTES = 8 * 1024; +// An unacknowledged automatic sync is deliberately retryable, but the poller +// runs every 1800ms and only skips text it has recorded as sent, so without a +// cap the same text is re-sent every tick for as long as it stays on the PC +// clipboard. Give up after this many attempts and stop asking. +const MAX_UNACKNOWLEDGED_SYNC_ATTEMPTS = 3; export class DroidWebDisplayController { elements; #api = new BridgeApi(); @@ -26,6 +36,7 @@ export class DroidWebDisplayController { #lastConnectValues = null; #lastAndroidClipboard = ""; #lastSentClipboard = ""; + #unacknowledgedSync = null; #clipboardPollTimer = null; #clipboardReadAllowed = false; #clipboardPollBusy = false; @@ -573,8 +584,12 @@ export class DroidWebDisplayController { if (!text) throw new Error("Enter or paste text into the fallback box first"); const maximum = Math.max(1, Math.min(256, Number(this.elements.clipboardMaxKib.value) || 256)) * 1024; - if (new TextEncoder().encode(text).byteLength > maximum) + const bytes = new TextEncoder().encode(text).byteLength; + if (bytes > maximum) throw new Error(`Text exceeds the configured ${maximum / 1024} KiB limit`); + if (bytes > MAX_INJECTED_BYTES) { + throw new Error(`Text is too large to type into Android (${Math.ceil(bytes / 1024)} KiB). Use Paste, which synchronizes the clipboard instead of typing.`); + } this.setStatus("Typing", "Injecting the text box directly into the focused Android input field…"); await this.sendMessages(textInjectionMessages(text)); this.setStatus("Text typed", "Text box content was injected directly into Android without using the clipboard."); @@ -582,12 +597,16 @@ export class DroidWebDisplayController { async pasteText(text, source) { const session = this.#protocolSession; const maximum = Math.max(1, Math.min(256, Number(this.elements.clipboardMaxKib.value) || 256)) * 1024; - if (new TextEncoder().encode(text).byteLength > maximum) + const bytes = new TextEncoder().encode(text).byteLength; + if (bytes > maximum) throw new Error(`Clipboard text exceeds the configured ${maximum / 1024} KiB limit`); if (!session) return; + const inject = bytes <= MAX_INJECTED_BYTES; const sequence = this.#clipboardSequence++; - this.setStatus("Pasting", `Synchronizing ${source} and injecting it into the focused Android input field…`); + this.setStatus("Pasting", inject + ? `Synchronizing ${source} and injecting it into the focused Android input field…` + : `Synchronizing ${source} with the Android clipboard…`); const acknowledgement = this.waitForClipboardAcknowledgement(sequence); try { // SetClipboard(paste=true) only proves that Android processed the clipboard @@ -595,13 +614,18 @@ export class DroidWebDisplayController { // clipboard synchronized with paste=false, then use scrcpy InjectText as the // deterministic insertion path (the same strategy as scrcpy legacy paste). await session.sendControl(clipboardMessage(text, sequence, false)); - await this.sendMessages(textInjectionMessages(text)); + if (inject) + await this.sendMessages(textInjectionMessages(text)); if (await acknowledgement) { this.#lastSentClipboard = text; - this.setStatus("Text pasted", `${source} was injected directly and the Android clipboard synchronization was acknowledged.`); + this.setStatus(inject ? "Text pasted" : "Clipboard synchronized", inject + ? `${source} was injected directly and the Android clipboard synchronization was acknowledged.` + : `${source} is on the Android clipboard. It is too large to type, so paste it on the device.`); } else { - this.setStatus("Text sent", `${source} was injected directly, but Android clipboard synchronization was not acknowledged.`); + this.setStatus("Text sent", inject + ? `${source} was injected directly, but Android clipboard synchronization was not acknowledged.` + : `${source} was sent to the Android clipboard but not acknowledged. If it arrived, paste it on the device.`); } } catch (error) { @@ -811,9 +835,12 @@ export class DroidWebDisplayController { return pending; } resetClipboardSessionState() { + // Cached device clipboard state must not leak across sessions. The visible + // text box is user input, not cached state, so it is deliberately left + // alone: clearing it discards text typed while disconnected. this.#lastAndroidClipboard = ""; this.#lastSentClipboard = ""; - this.elements.clipboardText.value = ""; + this.#unacknowledgedSync = null; this.completeAndroidCopyRequest(); } async startClipboardPolling(requestPermission) { @@ -918,10 +945,20 @@ export class DroidWebDisplayController { await session.sendControl(clipboardMessage(text, sequence, false)); if (await acknowledgement) { this.#lastSentClipboard = text; + this.#unacknowledgedSync = null; this.setStatus("Clipboard synchronized", "PC clipboard was acknowledged by Android without pasting into the focused field."); } else { - this.setStatus("Clipboard sync not confirmed", "PC clipboard update was sent, but Android did not acknowledge it."); + const attempts = this.#unacknowledgedSync?.text === text ? this.#unacknowledgedSync.attempts + 1 : 1; + this.#unacknowledgedSync = { text, attempts }; + if (attempts >= MAX_UNACKNOWLEDGED_SYNC_ATTEMPTS) { + // Record it as sent so the poller stops retrying this text. + this.#lastSentClipboard = text; + this.setStatus("Clipboard sync gave up", `Android did not acknowledge the PC clipboard after ${attempts} attempts. Copy again or use Paste to retry.`); + } + else { + this.setStatus("Clipboard sync not confirmed", "PC clipboard update was sent, but Android did not acknowledge it. It will be retried."); + } } } catch (error) { diff --git a/apps/web-client/src/controller.ts b/apps/web-client/src/controller.ts index 3cd61cd..6b2aa94 100644 --- a/apps/web-client/src/controller.ts +++ b/apps/web-client/src/controller.ts @@ -20,6 +20,16 @@ import { WebSocketBridgeTransport } from "./websocket-transport.js"; import { WebCodecsAudioPlayer, type AudioStatistics } from "./audio-player.js"; const DEVICE_DROPDOWN_REFRESH_STALE_MS = 1500; +// textInjectionMessages chunks at 300 UTF-8 bytes and sendMessages awaits every +// chunk, so injection cost grows linearly with the text: ~875 sequential round +// trips at the 256 KiB clipboard limit. Above this size the clipboard is still +// synchronized and the user pastes on the device. +const MAX_INJECTED_BYTES = 8 * 1024; +// An unacknowledged automatic sync is deliberately retryable, but the poller +// runs every 1800ms and only skips text it has recorded as sent, so without a +// cap the same text is re-sent every tick for as long as it stays on the PC +// clipboard. Give up after this many attempts and stop asking. +const MAX_UNACKNOWLEDGED_SYNC_ATTEMPTS = 3; interface ClipboardAckWaiter { readonly resolve: (acknowledged: boolean) => void; @@ -101,6 +111,7 @@ export class DroidWebDisplayController { #lastConnectValues: DisplayFormValues | null = null; #lastAndroidClipboard = ""; #lastSentClipboard = ""; + #unacknowledgedSync: { readonly text: string; readonly attempts: number } | null = null; #clipboardPollTimer: number | null = null; #clipboardReadAllowed = false; #clipboardPollBusy = false; @@ -631,7 +642,11 @@ export class DroidWebDisplayController { const text = this.elements.clipboardText.value; if (!text) throw new Error("Enter or paste text into the fallback box first"); const maximum = Math.max(1, Math.min(256, Number(this.elements.clipboardMaxKib.value) || 256)) * 1024; - if (new TextEncoder().encode(text).byteLength > maximum) throw new Error(`Text exceeds the configured ${maximum / 1024} KiB limit`); + const bytes = new TextEncoder().encode(text).byteLength; + if (bytes > maximum) throw new Error(`Text exceeds the configured ${maximum / 1024} KiB limit`); + if (bytes > MAX_INJECTED_BYTES) { + throw new Error(`Text is too large to type into Android (${Math.ceil(bytes / 1024)} KiB). Use Paste, which synchronizes the clipboard instead of typing.`); + } this.setStatus("Typing", "Injecting the text box directly into the focused Android input field…"); await this.sendMessages(textInjectionMessages(text)); this.setStatus("Text typed", "Text box content was injected directly into Android without using the clipboard."); @@ -640,10 +655,14 @@ export class DroidWebDisplayController { private async pasteText(text: string, source: string): Promise { const session = this.#protocolSession; const maximum = Math.max(1, Math.min(256, Number(this.elements.clipboardMaxKib.value) || 256)) * 1024; - if (new TextEncoder().encode(text).byteLength > maximum) throw new Error(`Clipboard text exceeds the configured ${maximum / 1024} KiB limit`); + const bytes = new TextEncoder().encode(text).byteLength; + if (bytes > maximum) throw new Error(`Clipboard text exceeds the configured ${maximum / 1024} KiB limit`); if (!session) return; + const inject = bytes <= MAX_INJECTED_BYTES; const sequence = this.#clipboardSequence++; - this.setStatus("Pasting", `Synchronizing ${source} and injecting it into the focused Android input field…`); + this.setStatus("Pasting", inject + ? `Synchronizing ${source} and injecting it into the focused Android input field…` + : `Synchronizing ${source} with the Android clipboard…`); const acknowledgement = this.waitForClipboardAcknowledgement(sequence); try { // SetClipboard(paste=true) only proves that Android processed the clipboard @@ -651,12 +670,16 @@ export class DroidWebDisplayController { // clipboard synchronized with paste=false, then use scrcpy InjectText as the // deterministic insertion path (the same strategy as scrcpy legacy paste). await session.sendControl(clipboardMessage(text, sequence, false)); - await this.sendMessages(textInjectionMessages(text)); + if (inject) await this.sendMessages(textInjectionMessages(text)); if (await acknowledgement) { this.#lastSentClipboard = text; - this.setStatus("Text pasted", `${source} was injected directly and the Android clipboard synchronization was acknowledged.`); + this.setStatus(inject ? "Text pasted" : "Clipboard synchronized", inject + ? `${source} was injected directly and the Android clipboard synchronization was acknowledged.` + : `${source} is on the Android clipboard. It is too large to type, so paste it on the device.`); } else { - this.setStatus("Text sent", `${source} was injected directly, but Android clipboard synchronization was not acknowledged.`); + this.setStatus("Text sent", inject + ? `${source} was injected directly, but Android clipboard synchronization was not acknowledged.` + : `${source} was sent to the Android clipboard but not acknowledged. If it arrived, paste it on the device.`); } } catch (error) { this.resolveClipboardAcknowledgement(sequence, false); @@ -864,9 +887,12 @@ export class DroidWebDisplayController { } private resetClipboardSessionState(): void { + // Cached device clipboard state must not leak across sessions. The visible + // text box is user input, not cached state, so it is deliberately left + // alone: clearing it discards text typed while disconnected. this.#lastAndroidClipboard = ""; this.#lastSentClipboard = ""; - this.elements.clipboardText.value = ""; + this.#unacknowledgedSync = null; this.completeAndroidCopyRequest(); } @@ -971,9 +997,18 @@ export class DroidWebDisplayController { await session.sendControl(clipboardMessage(text, sequence, false)); if (await acknowledgement) { this.#lastSentClipboard = text; + this.#unacknowledgedSync = null; this.setStatus("Clipboard synchronized", "PC clipboard was acknowledged by Android without pasting into the focused field."); } else { - this.setStatus("Clipboard sync not confirmed", "PC clipboard update was sent, but Android did not acknowledge it."); + const attempts = this.#unacknowledgedSync?.text === text ? this.#unacknowledgedSync.attempts + 1 : 1; + this.#unacknowledgedSync = { text, attempts }; + if (attempts >= MAX_UNACKNOWLEDGED_SYNC_ATTEMPTS) { + // Record it as sent so the poller stops retrying this text. + this.#lastSentClipboard = text; + this.setStatus("Clipboard sync gave up", `Android did not acknowledge the PC clipboard after ${attempts} attempts. Copy again or use Paste to retry.`); + } else { + this.setStatus("Clipboard sync not confirmed", "PC clipboard update was sent, but Android did not acknowledge it. It will be retried."); + } } } catch (error) { this.resolveClipboardAcknowledgement(sequence, false); diff --git a/droid_web_display/adb/client.py b/droid_web_display/adb/client.py index 5046387..01b6f30 100644 --- a/droid_web_display/adb/client.py +++ b/droid_web_display/adb/client.py @@ -4,11 +4,11 @@ import os import re import shutil -import subprocess from dataclasses import dataclass from pathlib import Path from typing import Mapping, Protocol, Sequence +from droid_web_display.process_utils import subprocess_creation_kwargs from droid_web_display.errors import AdbCommandError, AdbUnavailableError from droid_web_display.models import AndroidDevice @@ -34,11 +34,7 @@ async def _terminate(process: asyncio.subprocess.Process) -> None: def _subprocess_creation_kwargs(platform_name: str | None = None) -> dict[str, int]: """Return platform-specific flags for invisible background child processes.""" - if (platform_name or os.name) != "nt": - return {} - # CREATE_NO_WINDOW is 0x08000000. Keep the literal fallback so tests and - # alternate Python runtimes can still validate the Windows launch contract. - return {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)} + return subprocess_creation_kwargs(platform_name) @dataclass(frozen=True) diff --git a/droid_web_display/desktop/controller.py b/droid_web_display/desktop/controller.py index d7d46d5..3f63eb5 100644 --- a/droid_web_display/desktop/controller.py +++ b/droid_web_display/desktop/controller.py @@ -16,6 +16,7 @@ import psutil +from droid_web_display.process_utils import subprocess_creation_kwargs from droid_web_display.adb.devices import parse_adb_devices from droid_web_display.network_access import LAN_HTTPS, NetworkConfigStore @@ -250,8 +251,7 @@ def _device_summary(self) -> str: "timeout": 1.25, "check": False, } - if os.name == "nt": - kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0) + kwargs.update(subprocess_creation_kwargs()) try: result = subprocess.run(command, **kwargs) except (OSError, subprocess.SubprocessError): diff --git a/droid_web_display/desktop/support.py b/droid_web_display/desktop/support.py index a45df9e..292a040 100644 --- a/droid_web_display/desktop/support.py +++ b/droid_web_display/desktop/support.py @@ -11,6 +11,7 @@ import zipfile from droid_web_display import __version__ +from droid_web_display.process_utils import subprocess_creation_kwargs from droid_web_display.desktop.controller import DesktopPaths, ServerSnapshot from droid_web_display.diagnostics import redact_text @@ -125,8 +126,7 @@ def _run_version(command: list[str]) -> str: "timeout": 2.0, "check": False, } - if os.name == "nt": - kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0) + kwargs.update(subprocess_creation_kwargs()) try: result = subprocess.run(command, **kwargs) except (OSError, subprocess.SubprocessError): diff --git a/droid_web_display/network_access.py b/droid_web_display/network_access.py index 4bc100e..7c94ab2 100644 --- a/droid_web_display/network_access.py +++ b/droid_web_display/network_access.py @@ -9,6 +9,8 @@ import socket import ssl import subprocess + +from droid_web_display.process_utils import subprocess_creation_kwargs import tempfile from typing import Any, Iterable @@ -449,7 +451,16 @@ def apply(self, config: NetworkAccessConfig, *, remove: bool = False) -> dict[st if os.name != "nt": return {"applied": False, "reason": "Windows-only", "command": argv} try: - result = subprocess.run(argv, capture_output=True, text=True, timeout=30, check=False) + # Windows-only path: without the creation flags this pops a visible + # PowerShell window every time LAN access is applied or removed. + result = subprocess.run( + argv, + capture_output=True, + text=True, + timeout=30, + check=False, + **subprocess_creation_kwargs(), + ) except OSError as exc: return {"applied": False, "reason": str(exc), "command": argv} return { diff --git a/droid_web_display/process_utils.py b/droid_web_display/process_utils.py new file mode 100644 index 0000000..88a09d5 --- /dev/null +++ b/droid_web_display/process_utils.py @@ -0,0 +1,25 @@ +"""Shared subprocess launch policy. + +Every child process this app spawns is a background helper the user never +interacts with. On Windows each one would otherwise pop a console window, so +the CREATE_NO_WINDOW flag has to be applied consistently. It previously lived +inline in three modules with two different fallbacks, and the firewall call +had none at all, so it is centralised here. +""" + +from __future__ import annotations + +import os +import subprocess + +# subprocess.CREATE_NO_WINDOW exists on Windows CPython 3.7+. The literal +# keeps the contract testable from non-Windows runtimes, where the attribute +# is absent. +CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) + + +def subprocess_creation_kwargs(platform_name: str | None = None) -> dict[str, int]: + """Return platform-specific flags for invisible background child processes.""" + if (platform_name or os.name) != "nt": + return {} + return {"creationflags": CREATE_NO_WINDOW} diff --git a/tests/packaging/test_clipboard_sync_regression.py b/tests/packaging/test_clipboard_sync_regression.py index 67f5a89..a524e9c 100644 --- a/tests/packaging/test_clipboard_sync_regression.py +++ b/tests/packaging/test_clipboard_sync_regression.py @@ -1,3 +1,4 @@ +import re from pathlib import Path @@ -94,8 +95,11 @@ def test_clipboard_session_state_is_reset_before_use_and_on_cleanup() -> None: for reset in (source[src_reset_start:src_reset_end], dist[built_reset_start:built_reset_end]): assert '#lastAndroidClipboard = ""' in reset assert '#lastSentClipboard = ""' in reset - assert 'clipboardText.value = ""' in reset + assert "#unacknowledgedSync = null" in reset assert "completeAndroidCopyRequest()" in reset + # The visible text box is user input, not cached device state. Clearing + # it on connect discards text typed while disconnected. + assert 'clipboardText.value = ""' not in reset def test_failed_pc_clipboard_sync_remains_retryable() -> None: @@ -109,6 +113,13 @@ def test_failed_pc_clipboard_sync_remains_retryable() -> None: assert "clipboardMessage(text, sequence, false)" in block assert "pasteText(text" not in block assert "clipboardMessage(text, sequence, true)" not in block + # Retryable, but bounded: pollPcClipboard runs every 1800ms and skips + # only text recorded as sent, so an unbounded retry re-sends the same + # text every tick for as long as it stays on the PC clipboard. + assert "MAX_UNACKNOWLEDGED_SYNC_ATTEMPTS" in block + assert "#unacknowledgedSync" in block + gave_up = block.index("Clipboard sync gave up") + assert block.index("this.#lastSentClipboard = text", gave_up - 400) < gave_up def test_clipboard_permission_prompt_keeps_user_activation_and_drawer_does_not_force_textarea_focus() -> None: @@ -123,3 +134,21 @@ def test_clipboard_permission_prompt_keeps_user_activation_and_drawer_does_not_f assert "this.elements.clipboardText.focus()" not in source assert "this.elements.clipboardText.focus()" not in dist + + +def test_oversized_text_is_synchronized_rather_than_typed_chunk_by_chunk() -> None: + source, dist = _controller_blocks() + src = source[source.index("\n private async pasteText"):source.index("\n private waitForClipboardAcknowledgement")] + built = dist[dist.index("\n async pasteText"):dist.index("\n waitForClipboardAcknowledgement")] + for block in (src, built): + # textInjectionMessages chunks at 300 UTF-8 bytes and each chunk is an + # awaited control message, so injecting a 256 KiB clipboard would be + # roughly 875 sequential round trips. + assert "MAX_INJECTED_BYTES" in block + assert re.search(r"if \(inject\)\s+await this\.sendMessages\(textInjectionMessages\(text\)\)", block) + + src_type = source[source.index("\n private async pasteTypedText"):source.index("\n private async pasteText")] + built_type = dist[dist.index("\n async pasteTypedText"):dist.index("\n async pasteText")] + for block in (src_type, built_type): + assert "MAX_INJECTED_BYTES" in block + assert "too large to type into Android" in block