From eb94461c04c365ce56b01daa870505e8656f14be Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Fri, 21 Aug 2026 18:44:40 +0700 Subject: [PATCH] =?UTF-8?q?feat/fix:=20=D0=B1=D0=B5=D0=B9=D0=B4=D0=B6?= =?UTF-8?q?=D0=B8=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D1=83=D1=82=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=B2=D0=BE=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0.=20=D0=A4=D0=B8=D0=BA=D1=81=20=D1=81=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B0=D0=B9=D0=BE=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/lottie/ic_mic_on_to_off.json | 1 + assets/lottie/ic_videocam_on_to_off.json | 1 + ios/Runner/AppDelegate.swift | 45 +- lib/core/calls/active_call.dart | 93 +++ lib/core/config/app_icon.dart | 49 +- lib/frontend/screens/calls/call_screen.dart | 56 +- .../screens/profile/app_icon_screen.dart | 4 +- lib/frontend/widgets/call_video_view.dart | 99 +++ .../widgets/draggable_floating_layer.dart | 245 ++++++ lib/frontend/widgets/floating_call_badge.dart | 711 ++++++++++++++++++ lib/frontend/widgets/floating_video_note.dart | 77 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_localizations.dart | 12 + lib/l10n/app_localizations_en.dart | 8 + lib/l10n/app_localizations_ru.dart | 8 + lib/l10n/app_ru.arb | 2 + lib/main.dart | 4 + test/app_icon_names_test.dart | 96 +++ test/floating_call_badge_test.dart | 128 ++++ test/floating_dock_geometry_test.dart | 94 +++ tool/make_morph_icons.py | 14 + 21 files changed, 1637 insertions(+), 112 deletions(-) create mode 100644 assets/lottie/ic_mic_on_to_off.json create mode 100644 assets/lottie/ic_videocam_on_to_off.json create mode 100644 lib/core/calls/active_call.dart create mode 100644 lib/frontend/widgets/call_video_view.dart create mode 100644 lib/frontend/widgets/draggable_floating_layer.dart create mode 100644 lib/frontend/widgets/floating_call_badge.dart create mode 100644 test/app_icon_names_test.dart create mode 100644 test/floating_call_badge_test.dart create mode 100644 test/floating_dock_geometry_test.dart diff --git a/assets/lottie/ic_mic_on_to_off.json b/assets/lottie/ic_mic_on_to_off.json new file mode 100644 index 0000000..e414c4e --- /dev/null +++ b/assets/lottie/ic_mic_on_to_off.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":24,"w":600,"h":600,"nm":"ic_mic_on_to_off","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"slashed","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300.0,300.0,0],"ix":2},"a":{"a":0,"k":[300.0,300.0,0],"ix":1},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.0],"y":[1.0]},"o":{"x":[0.2],"y":[0]}},{"t":11,"s":[92,92,100],"i":{"x":[0.25],"y":[1.0]},"o":{"x":[0.33],"y":[0]}},{"t":24,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.0,"y":1.0},"o":{"x":0.2,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[694.4,-578.4],[-578.4,694.4],[-1851.19,-578.4],[-578.4,-1851.19]],"c":true}]},{"t":24,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1178.4,-94.4],[-94.4,1178.4],[-1367.19,-94.4],[-94.4,-1367.19]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Wipe"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.9,-1.31],[2.04,2.04],[1.24,1.24],[1.17,1.17],[1.13,1.13],[1.11,1.11],[1.1,1.1],[1.08,1.08],[1.06,1.06],[1.02,1.02],[0.94,0.94],[0.0,0.0],[-0.76,1.36],[-0.71,1.39],[-0.65,1.41],[-0.6,1.44],[-0.54,1.46],[-0.48,1.49],[-0.42,1.51],[-0.36,1.53],[-0.3,1.54],[-0.24,1.55],[-0.18,1.56],[-0.12,1.57],[-0.06,1.57],[0.0,1.57],[-3.08,0.0],[-1.87,0.0],[-1.76,0.0],[-1.71,0.0],[-1.68,0.0],[-1.65,0.0],[-1.61,0.0],[-1.56,0.0],[-1.44,0.0],[0.0,0.0],[0.05,-1.76],[0.1,-1.76],[0.16,-1.75],[0.21,-1.75],[0.27,-1.74],[0.32,-1.73],[0.38,-1.72],[0.44,-1.71],[0.5,-1.7],[0.55,-1.68],[0.53,-1.49],[0.57,-1.47],[0.61,-1.46],[0.65,-1.44],[0.69,-1.43],[0.72,-1.41],[0.76,-1.39],[0.8,-1.37],[0.83,-1.35],[0.86,-1.33]],"o":[[0.0,0.0],[-0.94,-0.94],[-1.02,-1.02],[-1.06,-1.06],[-1.08,-1.08],[-1.1,-1.1],[-1.11,-1.11],[-1.13,-1.13],[-1.17,-1.17],[-1.24,-1.24],[-2.04,-2.04],[0.82,-1.34],[0.77,-1.37],[0.71,-1.4],[0.66,-1.42],[0.6,-1.45],[0.54,-1.47],[0.48,-1.49],[0.42,-1.51],[0.36,-1.52],[0.3,-1.53],[0.24,-1.54],[0.18,-1.55],[0.12,-1.56],[0.06,-1.56],[0.0,0.0],[1.44,0.0],[1.56,0.0],[1.61,0.0],[1.65,0.0],[1.68,0.0],[1.71,0.0],[1.76,0.0],[1.87,0.0],[3.08,0.0],[0.0,1.78],[-0.05,1.78],[-0.1,1.77],[-0.16,1.77],[-0.21,1.76],[-0.27,1.75],[-0.33,1.74],[-0.38,1.73],[-0.44,1.71],[-0.5,1.7],[-0.49,1.5],[-0.53,1.49],[-0.57,1.47],[-0.61,1.46],[-0.65,1.44],[-0.69,1.42],[-0.72,1.4],[-0.76,1.38],[-0.79,1.36],[-0.83,1.34],[-0.86,1.32]],"v":[[443.75,373.75],[440.46,370.46],[437.16,367.16],[433.86,363.86],[430.57,360.57],[427.27,357.27],[423.98,353.98],[420.68,350.68],[417.39,347.39],[414.09,344.09],[410.79,340.79],[407.5,337.5],[409.87,333.45],[412.08,329.31],[414.13,325.09],[416.02,320.79],[417.73,316.43],[419.27,311.99],[420.62,307.5],[421.8,302.95],[422.79,298.36],[423.59,293.73],[424.22,289.07],[424.65,284.39],[424.91,279.7],[425.0,275.0],[429.99,275.0],[435.0,275.0],[440.0,275.0],[445.0,275.0],[450.0,275.0],[455.0,275.0],[460.0,275.0],[465.0,275.0],[470.01,275.0],[475.0,275.0],[474.92,280.3],[474.69,285.6],[474.3,290.89],[473.75,296.17],[473.03,301.42],[472.14,306.65],[471.08,311.85],[469.85,317.01],[468.45,322.12],[466.88,327.19],[465.33,331.67],[463.68,336.12],[461.9,340.51],[460.02,344.86],[458.01,349.16],[455.9,353.41],[453.68,357.6],[451.35,361.73],[448.92,365.8],[446.38,369.81]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.29,0.0],[-2.05,-0.5],[-1.9,-1.09],[-1.6,-1.6],[-0.9,-1.34],[-0.61,-1.53],[-0.29,-1.65],[0.0,-1.7],[0.0,-4.07],[0.0,-2.45],[0.0,-2.31],[0.0,-2.25],[0.0,-2.22],[0.0,-2.2],[0.0,-2.18],[0.0,-2.17],[0.0,-2.16],[0.0,-2.15],[0.0,-2.14],[0.0,-2.13],[0.0,-2.12],[0.0,-2.11],[0.0,-2.09],[0.0,-2.07],[0.0,-2.03],[0.0,-1.97],[0.0,-1.82],[0.0,0.0],[2.81,2.81],[1.7,1.7],[1.61,1.61],[1.56,1.56],[1.53,1.53],[1.51,1.51],[1.49,1.49],[1.46,1.46],[1.41,1.41],[1.3,1.3],[0.0,0.0],[0.0,4.08],[0.0,2.47],[0.0,2.33],[0.0,2.26],[0.0,2.23],[0.0,2.2],[0.0,2.17],[0.0,2.14],[0.0,2.1],[0.0,2.03],[0.0,1.88],[0.0,0.0],[-0.5,2.05],[-1.09,1.9],[-1.6,1.6],[-1.86,1.07],[-2.16,0.53]],"o":[[2.29,0.0],[2.16,0.53],[1.86,1.07],[1.2,1.2],[0.92,1.38],[0.6,1.51],[0.28,1.57],[0.0,0.0],[0.0,1.82],[0.0,1.97],[0.0,2.03],[0.0,2.07],[0.0,2.09],[0.0,2.11],[0.0,2.12],[0.0,2.13],[0.0,2.14],[0.0,2.15],[0.0,2.16],[0.0,2.17],[0.0,2.18],[0.0,2.2],[0.0,2.22],[0.0,2.25],[0.0,2.31],[0.0,2.45],[0.0,4.07],[0.0,0.0],[-1.3,-1.3],[-1.41,-1.41],[-1.46,-1.46],[-1.49,-1.49],[-1.51,-1.51],[-1.53,-1.53],[-1.56,-1.56],[-1.61,-1.61],[-1.7,-1.7],[-2.81,-2.81],[0.0,0.0],[0.0,-1.88],[0.0,-2.03],[0.0,-2.1],[0.0,-2.14],[0.0,-2.17],[0.0,-2.2],[0.0,-2.23],[0.0,-2.26],[0.0,-2.33],[0.0,-2.47],[0.0,-4.08],[0.0,-2.29],[0.53,-2.16],[1.07,-1.86],[1.6,-1.6],[1.9,-1.09],[2.05,-0.5]],"v":[[300.0,100.0],[306.52,100.75],[312.62,103.18],[317.81,107.19],[320.95,110.99],[323.25,115.35],[324.58,120.09],[325.0,125.0],[325.0,131.42],[325.0,137.86],[325.0,144.3],[325.0,150.74],[325.0,157.18],[325.0,163.62],[325.0,170.06],[325.0,176.5],[325.0,182.94],[325.0,189.38],[325.0,195.81],[325.0,202.25],[325.0,208.69],[325.0,215.13],[325.0,221.57],[325.0,228.01],[325.0,234.45],[325.0,240.89],[325.0,247.33],[325.0,253.75],[320.46,249.21],[315.91,244.66],[311.36,240.11],[306.82,235.57],[302.27,231.02],[297.73,226.48],[293.18,221.93],[288.64,217.39],[284.09,212.84],[279.54,208.29],[275.0,203.75],[275.0,197.2],[275.0,190.63],[275.0,184.06],[275.0,177.5],[275.0,170.94],[275.0,164.37],[275.0,157.81],[275.0,151.25],[275.0,144.69],[275.0,138.12],[275.0,131.55],[275.0,125.0],[275.75,118.48],[278.18,112.38],[282.19,107.19],[287.38,103.18],[293.48,100.75]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.25,-3.75],[6.46,6.46],[3.91,3.91],[3.44,3.44],[0.0,0.0],[0.0,9.8],[0.0,5.96],[0.0,5.6],[0.0,5.42],[0.0,5.26],[0.0,5.07],[0.0,4.67],[0.0,0.0],[4.79,4.79],[7.08,0.0],[4.79,-4.79],[0.0,-7.08],[0.0,-9.25],[0.0,-5.64],[0.0,-5.19],[0.0,-4.7],[0.0,0.0],[5.88,5.88],[3.58,3.58],[3.29,3.29],[2.98,2.98],[0.0,0.0],[0.0,7.19],[0.0,0.0],[-0.87,4.71],[-1.85,4.51],[-2.81,4.12],[-3.59,3.59],[-3.96,2.7],[-4.53,1.86],[-4.92,0.91],[-5.08,0.0],[-4.71,-0.87],[-4.51,-1.85],[-4.12,-2.81],[-3.59,-3.59],[-2.7,-3.96],[-1.86,-4.53],[-0.91,-4.92],[0.0,-5.08],[0.0,-9.24],[0.0,-5.61],[0.0,-5.28],[0.0,-5.13],[0.0,-5.03],[0.0,-4.94],[0.0,-4.84],[0.0,-4.68],[0.0,-4.32],[0.0,0.0],[1.04,-3.75]],"o":[[0.0,0.0],[-3.44,-3.44],[-3.91,-3.91],[-6.46,-6.46],[0.0,0.0],[0.0,-4.67],[0.0,-5.07],[0.0,-5.26],[0.0,-5.42],[0.0,-5.6],[0.0,-5.96],[0.0,-9.8],[0.0,-7.08],[-4.79,-4.79],[-7.08,0.0],[-4.79,4.79],[0.0,0.0],[0.0,4.7],[0.0,5.19],[0.0,5.64],[0.0,9.25],[0.0,0.0],[-2.98,-2.98],[-3.29,-3.29],[-3.58,-3.58],[-5.88,-5.88],[0.0,0.0],[0.0,-7.19],[0.0,-5.08],[0.91,-4.92],[1.86,-4.53],[2.7,-3.96],[3.59,-3.59],[4.12,-2.81],[4.51,-1.85],[4.71,-0.87],[5.08,0.0],[4.92,0.91],[4.53,1.86],[3.96,2.7],[3.59,3.59],[2.81,4.12],[1.85,4.51],[0.87,4.71],[0.0,0.0],[0.0,4.32],[0.0,4.68],[0.0,4.84],[0.0,4.94],[0.0,5.03],[0.0,5.13],[0.0,5.28],[0.0,5.61],[0.0,9.24],[0.0,4.58],[-1.04,3.75]],"v":[[370.0,298.75],[358.75,287.5],[347.5,276.25],[336.25,265.0],[325.0,253.75],[325.0,237.66],[325.0,221.57],[325.0,205.47],[325.0,189.38],[325.0,173.28],[325.0,157.18],[325.0,141.09],[325.0,125.0],[317.81,107.19],[300.0,100.0],[282.19,107.19],[275.0,125.0],[275.0,140.74],[275.0,156.5],[275.0,172.25],[275.0,188.01],[275.0,203.75],[265.0,193.75],[255.0,183.75],[245.0,173.75],[235.0,163.75],[225.0,153.75],[225.0,139.38],[225.0,125.0],[226.3,110.31],[230.44,96.17],[237.44,83.2],[246.88,71.87],[258.2,62.44],[271.17,55.44],[285.31,51.3],[300.0,50.0],[314.69,51.3],[328.83,55.44],[341.8,62.44],[353.12,71.87],[362.56,83.2],[369.56,96.17],[373.7,110.31],[375.0,125.0],[375.0,139.98],[375.0,154.99],[375.0,169.99],[375.0,185.0],[375.0,200.0],[375.0,215.0],[375.0,230.01],[375.0,245.01],[375.0,260.02],[375.0,275.0],[373.44,287.5]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.0,0.0],[0.0,9.11],[0.0,5.55],[0.0,5.11],[0.0,4.63],[0.0,0.0],[5.02,1.1],[4.91,1.54],[4.77,2.0],[4.58,2.45],[4.34,2.9],[4.06,3.31],[3.76,3.67],[3.45,3.99],[3.41,4.79],[2.91,5.13],[2.36,5.45],[1.76,5.7],[1.15,5.89],[0.56,6.01],[0.0,6.05],[-9.18,0.0],[-5.36,0.0],[0.0,0.0],[-0.64,-5.32],[-1.34,-5.23],[-2.08,-5.03],[-2.79,-4.74],[-3.42,-4.36],[-3.95,-3.95],[-4.22,-3.31],[-4.65,-2.74],[-5.04,-2.08],[-5.33,-1.37],[-5.51,-0.66],[-5.59,0.0],[-6.63,1.06],[-6.44,2.22],[-5.75,3.05],[-5.24,3.99],[-6.54,-6.54],[-3.82,-3.82],[0.0,0.0],[4.31,-2.86],[4.55,-2.5],[4.75,-2.13],[5.06,-1.68],[5.23,-1.18],[5.34,-0.68],[0.0,-9.11],[0.0,-5.55],[0.0,-5.11],[0.0,-4.63],[0.0,0.0],[9.18,0.0],[5.36,0.0]],"o":[[0.0,0.0],[0.0,-4.63],[0.0,-5.11],[0.0,-5.55],[0.0,-9.11],[-5.23,-0.7],[-5.14,-1.13],[-5.01,-1.57],[-4.82,-2.02],[-4.58,-2.46],[-4.3,-2.88],[-4.0,-3.25],[-3.68,-3.59],[-3.96,-4.57],[-3.49,-4.91],[-2.95,-5.21],[-2.36,-5.47],[-1.75,-5.66],[-1.13,-5.78],[-0.54,-5.84],[0.0,0.0],[5.36,0.0],[9.18,0.0],[0.0,5.59],[0.66,5.51],[1.37,5.33],[2.08,5.04],[2.74,4.65],[3.31,4.22],[3.95,3.95],[4.36,3.42],[4.74,2.79],[5.03,2.08],[5.23,1.34],[5.32,0.64],[6.99,0.0],[6.81,-1.09],[6.31,-2.17],[5.82,-3.09],[0.0,0.0],[3.82,3.82],[6.54,6.54],[-4.05,3.21],[-4.29,2.85],[-4.5,2.48],[-4.85,2.18],[-5.02,1.67],[-5.14,1.16],[0.0,0.0],[0.0,4.63],[0.0,5.11],[0.0,5.55],[0.0,9.11],[0.0,0.0],[-5.36,0.0],[-9.18,0.0]],"v":[[275.0,525.0],[275.0,509.51],[275.0,494.0],[275.0,478.5],[275.0,462.99],[275.0,447.5],[259.63,444.8],[244.54,440.8],[229.87,435.46],[215.78,428.75],[202.4,420.72],[189.84,411.45],[178.2,401.05],[167.5,389.69],[156.45,375.65],[146.85,360.59],[138.88,344.6],[132.7,327.85],[128.35,310.53],[125.81,292.85],[125.0,275.0],[141.67,275.0],[158.33,275.0],[175.0,275.0],[175.95,291.36],[178.95,307.46],[184.12,323.01],[191.43,337.67],[200.68,351.19],[211.56,363.44],[223.81,374.32],[237.33,383.57],[251.99,390.88],[267.54,396.05],[283.64,399.05],[300.0,400.0],[320.43,398.4],[340.31,393.44],[358.4,385.61],[375.0,375.0],[386.87,386.87],[398.75,398.75],[410.62,410.62],[398.08,419.74],[384.82,427.77],[370.94,434.69],[356.08,440.47],[340.71,444.74],[325.0,447.5],[325.0,462.99],[325.0,478.5],[325.0,494.0],[325.0,509.51],[325.0,525.0],[308.33,525.0],[291.67,525.0]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.0,0.0],[11.25,11.25],[6.78,6.78],[6.37,6.37],[6.23,6.23],[6.13,6.13],[6.07,6.07],[6.03,6.03],[6.0,6.0],[5.98,5.98],[5.96,5.96],[5.93,5.93],[5.92,5.92],[5.9,5.9],[5.89,5.89],[5.87,5.87],[5.85,5.85],[5.84,5.84],[5.82,5.82],[5.79,5.79],[5.75,5.75],[5.71,5.71],[5.66,5.66],[5.57,5.57],[5.39,5.39],[4.99,4.99],[0.0,0.0],[-8.75,8.75],[0.0,0.0],[-11.25,-11.25],[-6.78,-6.78],[-6.37,-6.37],[-6.23,-6.23],[-6.13,-6.13],[-6.07,-6.07],[-6.03,-6.03],[-6.0,-6.0],[-5.98,-5.98],[-5.96,-5.96],[-5.93,-5.93],[-5.92,-5.92],[-5.9,-5.9],[-5.89,-5.89],[-5.87,-5.87],[-5.85,-5.85],[-5.84,-5.84],[-5.82,-5.82],[-5.79,-5.79],[-5.75,-5.75],[-5.71,-5.71],[-5.66,-5.66],[-5.57,-5.57],[-5.39,-5.39],[-4.99,-4.99],[0.0,0.0],[8.75,-8.75]],"o":[[0.0,0.0],[-4.99,-4.99],[-5.39,-5.39],[-5.57,-5.57],[-5.66,-5.66],[-5.71,-5.71],[-5.75,-5.75],[-5.79,-5.79],[-5.82,-5.82],[-5.84,-5.84],[-5.85,-5.85],[-5.87,-5.87],[-5.89,-5.89],[-5.9,-5.9],[-5.92,-5.92],[-5.93,-5.93],[-5.96,-5.96],[-5.98,-5.98],[-6.0,-6.0],[-6.03,-6.03],[-6.07,-6.07],[-6.13,-6.13],[-6.23,-6.23],[-6.37,-6.37],[-6.78,-6.78],[-11.25,-11.25],[0.0,0.0],[8.75,-8.75],[0.0,0.0],[4.99,4.99],[5.39,5.39],[5.57,5.57],[5.66,5.66],[5.71,5.71],[5.75,5.75],[5.79,5.79],[5.82,5.82],[5.84,5.84],[5.85,5.85],[5.87,5.87],[5.89,5.89],[5.9,5.9],[5.92,5.92],[5.93,5.93],[5.96,5.96],[5.98,5.98],[6.0,6.0],[6.03,6.03],[6.07,6.07],[6.13,6.13],[6.23,6.23],[6.37,6.37],[6.78,6.78],[11.25,11.25],[0.0,0.0],[-8.75,8.75]],"v":[[495.0,565.0],[477.37,547.37],[459.64,529.64],[441.97,511.97],[424.25,494.25],[406.54,476.54],[388.85,458.85],[371.17,441.17],[353.48,423.48],[335.78,405.78],[318.08,388.08],[300.39,370.39],[282.7,352.7],[265.0,335.0],[247.3,317.3],[229.61,299.61],[211.92,281.92],[194.22,264.22],[176.52,246.52],[158.83,228.83],[141.15,211.15],[123.46,193.46],[105.75,175.75],[88.03,158.03],[70.36,140.36],[52.63,122.63],[35.0,105.0],[52.5,87.5],[70.0,70.0],[87.63,87.63],[105.36,105.36],[123.03,123.03],[140.75,140.75],[158.46,158.46],[176.15,176.15],[193.83,193.83],[211.52,211.52],[229.22,229.22],[246.92,246.92],[264.61,264.61],[282.3,282.3],[300.0,300.0],[317.7,317.7],[335.39,335.39],[353.08,353.08],[370.78,370.78],[388.48,388.48],[406.17,406.17],[423.85,423.85],[441.54,441.54],[459.25,459.25],[476.97,476.97],[494.64,494.64],[512.37,512.37],[530.0,530.0],[512.5,547.5]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"slashed","np":7,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plain","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300.0,300.0,0],"ix":2},"a":{"a":0,"k":[300.0,300.0,0],"ix":1},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.0],"y":[1.0]},"o":{"x":[0.2],"y":[0]}},{"t":11,"s":[92,92,100],"i":{"x":[0.25],"y":[1.0]},"o":{"x":[0.33],"y":[0]}},{"t":24,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.0,"y":1.0},"o":{"x":0.2,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[694.4,-578.4],[-578.4,694.4],[694.4,1967.19],[1967.19,694.4]],"c":true}]},{"t":24,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1178.4,-94.4],[-94.4,1178.4],[1178.4,2451.19],[2451.19,1178.4]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Wipe"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.08,0.0],[4.71,0.87],[4.51,1.85],[4.12,2.81],[3.59,3.59],[2.7,3.96],[1.86,4.53],[0.91,4.92],[0.0,5.08],[0.0,8.44],[0.0,5.11],[0.0,4.82],[0.0,4.69],[0.0,4.6],[0.0,4.53],[0.0,4.46],[0.0,4.38],[0.0,4.24],[0.0,3.91],[0.0,0.0],[-0.55,3.81],[-1.17,3.71],[-1.8,3.51],[-2.38,3.21],[-2.85,2.85],[-3.96,2.7],[-4.53,1.86],[-4.92,0.91],[-5.08,0.0],[-4.71,-0.87],[-4.51,-1.85],[-4.12,-2.81],[-3.59,-3.59],[-2.3,-3.09],[-1.78,-3.47],[-1.19,-3.77],[-0.57,-3.96],[0.0,-4.04],[0.0,-8.44],[0.0,-5.11],[0.0,-4.82],[0.0,-4.69],[0.0,-4.6],[0.0,-4.53],[0.0,-4.46],[0.0,-4.38],[0.0,-4.24],[0.0,-3.91],[0.0,0.0],[0.87,-4.71],[1.85,-4.51],[2.81,-4.12],[3.59,-3.59],[3.96,-2.7],[4.53,-1.86],[4.92,-0.91]],"o":[[-5.08,0.0],[-4.92,-0.91],[-4.53,-1.86],[-3.96,-2.7],[-3.59,-3.59],[-2.81,-4.12],[-1.85,-4.51],[-0.87,-4.71],[0.0,0.0],[0.0,-3.91],[0.0,-4.24],[0.0,-4.38],[0.0,-4.46],[0.0,-4.53],[0.0,-4.6],[0.0,-4.69],[0.0,-4.82],[0.0,-5.11],[0.0,-8.44],[0.0,-4.04],[0.57,-3.96],[1.19,-3.77],[1.78,-3.47],[2.3,-3.09],[3.59,-3.59],[4.12,-2.81],[4.51,-1.85],[4.71,-0.87],[5.08,0.0],[4.92,0.91],[4.53,1.86],[3.96,2.7],[2.85,2.85],[2.38,3.21],[1.8,3.51],[1.17,3.71],[0.55,3.81],[0.0,0.0],[0.0,3.91],[0.0,4.24],[0.0,4.38],[0.0,4.46],[0.0,4.53],[0.0,4.6],[0.0,4.69],[0.0,4.82],[0.0,5.11],[0.0,8.44],[0.0,5.08],[-0.91,4.92],[-1.86,4.53],[-2.7,3.96],[-3.59,3.59],[-4.12,2.81],[-4.51,1.85],[-4.71,0.87]],"v":[[300.0,350.0],[285.31,348.7],[271.17,344.56],[258.2,337.56],[246.88,328.12],[237.44,316.8],[230.44,303.83],[226.3,289.69],[225.0,275.0],[225.0,261.37],[225.0,247.74],[225.0,234.09],[225.0,220.46],[225.0,206.82],[225.0,193.18],[225.0,179.54],[225.0,165.91],[225.0,152.26],[225.0,138.63],[225.0,125.0],[225.82,113.23],[228.43,101.72],[232.91,90.81],[239.15,80.8],[246.88,71.87],[258.2,62.44],[271.17,55.44],[285.31,51.3],[300.0,50.0],[314.69,51.3],[328.83,55.44],[341.8,62.44],[353.12,71.87],[360.85,80.8],[367.09,90.81],[371.57,101.72],[374.18,113.23],[375.0,125.0],[375.0,138.63],[375.0,152.26],[375.0,165.91],[375.0,179.54],[375.0,193.18],[375.0,206.82],[375.0,220.46],[375.0,234.09],[375.0,247.74],[375.0,261.37],[375.0,275.0],[373.7,289.69],[369.56,303.83],[362.56,316.8],[353.12,328.12],[341.8,337.56],[328.83,344.56],[314.69,348.7]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-3.5,0.0],[-2.94,-1.17],[-2.43,-2.43],[-1.07,-1.86],[-0.53,-2.16],[0.0,-2.29],[0.0,-5.26],[0.0,-3.17],[0.0,-2.99],[0.0,-2.91],[0.0,-2.87],[0.0,-2.84],[0.0,-2.82],[0.0,-2.8],[0.0,-2.78],[0.0,-2.77],[0.0,-2.75],[0.0,-2.73],[0.0,-2.71],[0.0,-2.68],[0.0,-2.64],[0.0,-2.56],[0.0,-2.36],[0.0,0.0],[1.17,-2.94],[2.43,-2.43],[1.86,-1.07],[2.16,-0.53],[2.29,0.0],[2.94,1.17],[2.43,2.43],[1.07,1.86],[0.53,2.16],[0.0,2.29],[0.0,5.26],[0.0,3.17],[0.0,2.99],[0.0,2.91],[0.0,2.87],[0.0,2.84],[0.0,2.82],[0.0,2.8],[0.0,2.78],[0.0,2.77],[0.0,2.75],[0.0,2.73],[0.0,2.71],[0.0,2.68],[0.0,2.64],[0.0,2.56],[0.0,2.36],[0.0,0.0],[-0.5,2.05],[-1.09,1.9],[-1.6,1.6],[-3.01,1.2]],"o":[[3.5,0.0],[3.01,1.2],[1.6,1.6],[1.09,1.9],[0.5,2.05],[0.0,0.0],[0.0,2.36],[0.0,2.56],[0.0,2.64],[0.0,2.68],[0.0,2.71],[0.0,2.73],[0.0,2.75],[0.0,2.77],[0.0,2.78],[0.0,2.8],[0.0,2.82],[0.0,2.84],[0.0,2.87],[0.0,2.91],[0.0,2.99],[0.0,3.17],[0.0,5.26],[0.0,3.5],[-1.2,3.01],[-1.6,1.6],[-1.9,1.09],[-2.05,0.5],[-3.5,0.0],[-3.01,-1.2],[-1.6,-1.6],[-1.09,-1.9],[-0.5,-2.05],[0.0,0.0],[0.0,-2.36],[0.0,-2.56],[0.0,-2.64],[0.0,-2.68],[0.0,-2.71],[0.0,-2.73],[0.0,-2.75],[0.0,-2.77],[0.0,-2.78],[0.0,-2.8],[0.0,-2.82],[0.0,-2.84],[0.0,-2.87],[0.0,-2.91],[0.0,-2.99],[0.0,-3.17],[0.0,-5.26],[0.0,-2.29],[0.53,-2.16],[1.07,-1.86],[2.43,-2.43],[2.94,-1.17]],"v":[[300.0,100.0],[309.65,101.75],[317.81,107.19],[321.82,112.38],[324.25,118.48],[325.0,125.0],[325.0,133.32],[325.0,141.65],[325.0,149.99],[325.0,158.32],[325.0,166.66],[325.0,175.0],[325.0,183.33],[325.0,191.66],[325.0,200.0],[325.0,208.34],[325.0,216.67],[325.0,225.0],[325.0,233.34],[325.0,241.68],[325.0,250.01],[325.0,258.35],[325.0,266.68],[325.0,275.0],[323.25,284.65],[317.81,292.81],[312.62,296.82],[306.52,299.25],[300.0,300.0],[290.35,298.25],[282.19,292.81],[278.18,287.62],[275.75,281.52],[275.0,275.0],[275.0,266.68],[275.0,258.35],[275.0,250.01],[275.0,241.68],[275.0,233.34],[275.0,225.0],[275.0,216.67],[275.0,208.34],[275.0,200.0],[275.0,191.66],[275.0,183.33],[275.0,175.0],[275.0,166.66],[275.0,158.32],[275.0,149.99],[275.0,141.65],[275.0,133.32],[275.0,125.0],[275.75,118.48],[278.18,112.38],[282.19,107.19],[290.35,101.75]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.0,0.0],[0.0,11.03],[0.0,6.68],[0.0,5.87],[0.0,0.0],[6.63,1.66],[6.41,2.46],[6.09,3.29],[5.67,4.08],[5.16,4.78],[4.62,5.36],[4.47,6.88],[3.45,7.54],[2.3,8.06],[1.12,8.4],[0.0,8.54],[-12.5,0.0],[0.0,0.0],[-0.92,-6.34],[-1.96,-6.18],[-3.01,-5.84],[-3.98,-5.34],[-4.76,-4.76],[-5.14,-3.83],[-5.76,-2.97],[-6.27,-1.99],[-6.59,-0.96],[-6.73,0.0],[-6.34,0.92],[-6.18,1.96],[-5.84,3.01],[-5.34,3.98],[-4.76,4.76],[-3.83,5.14],[-2.97,5.76],[-1.99,6.27],[-0.96,6.59],[0.0,6.73],[-12.5,0.0],[0.0,0.0],[1.08,-8.13],[2.27,-7.96],[3.48,-7.61],[4.61,-7.09],[5.57,-6.47],[5.02,-4.64],[5.58,-4.02],[6.1,-3.29],[6.53,-2.51],[6.83,-1.71],[7.02,-0.95],[0.0,-11.03],[0.0,-6.68],[0.0,-5.87],[0.0,0.0],[12.5,0.0]],"o":[[0.0,0.0],[0.0,-5.87],[0.0,-6.68],[0.0,-11.03],[-7.02,-0.95],[-6.83,-1.71],[-6.53,-2.51],[-6.1,-3.29],[-5.58,-4.02],[-5.02,-4.64],[-5.57,-6.47],[-4.61,-7.09],[-3.48,-7.61],[-2.27,-7.96],[-1.08,-8.13],[0.0,0.0],[12.5,0.0],[0.0,6.73],[0.96,6.59],[1.99,6.27],[2.97,5.76],[3.83,5.14],[4.76,4.76],[5.34,3.98],[5.84,3.01],[6.18,1.96],[6.34,0.92],[6.73,0.0],[6.59,-0.96],[6.27,-1.99],[5.76,-2.97],[5.14,-3.83],[4.76,-4.76],[3.98,-5.34],[3.01,-5.84],[1.96,-6.18],[0.92,-6.34],[0.0,0.0],[12.5,0.0],[0.0,8.54],[-1.12,8.4],[-2.3,8.06],[-3.45,7.54],[-4.47,6.88],[-4.62,5.36],[-5.16,4.78],[-5.67,4.08],[-6.09,3.29],[-6.41,2.46],[-6.63,1.66],[0.0,0.0],[0.0,5.87],[0.0,6.68],[0.0,11.03],[0.0,0.0],[-12.5,0.0]],"v":[[275.0,525.0],[275.0,505.78],[275.0,486.56],[275.0,467.34],[275.0,448.12],[254.53,444.22],[234.66,437.97],[215.72,429.27],[198.07,418.21],[181.95,405.01],[167.5,390.0],[152.44,369.98],[140.36,348.03],[131.69,324.53],[126.62,300.0],[125.0,275.0],[150.0,275.0],[175.0,275.0],[176.38,294.61],[180.76,313.77],[188.26,331.93],[198.69,348.59],[211.56,363.44],[226.41,376.31],[243.07,386.74],[261.23,394.24],[280.39,398.62],[300.0,400.0],[319.61,398.62],[338.77,394.24],[356.93,386.74],[373.59,376.31],[388.44,363.44],[401.31,348.59],[411.74,331.93],[419.24,313.77],[423.62,294.61],[425.0,275.0],[450.0,275.0],[475.0,275.0],[473.38,300.0],[468.31,324.53],[459.64,348.03],[447.56,369.98],[432.5,390.0],[418.05,405.01],[401.93,418.21],[384.28,429.27],[365.34,437.97],[345.47,444.22],[325.0,448.12],[325.0,467.34],[325.0,486.56],[325.0,505.78],[325.0,525.0],[300.0,525.0]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-2.29,0.0],[-2.94,1.17],[-2.43,2.43],[-1.07,1.86],[-0.53,2.16],[0.0,2.29],[0.0,5.26],[0.0,3.17],[0.0,2.99],[0.0,2.91],[0.0,2.87],[0.0,2.84],[0.0,2.82],[0.0,2.8],[0.0,2.78],[0.0,2.77],[0.0,2.75],[0.0,2.73],[0.0,2.71],[0.0,2.68],[0.0,2.64],[0.0,2.56],[0.0,2.36],[0.0,0.0],[0.5,2.05],[1.09,1.9],[1.6,1.6],[3.01,1.2],[3.5,0.0],[2.94,-1.17],[2.43,-2.43],[1.07,-1.86],[0.53,-2.16],[0.0,-2.29],[0.0,-5.26],[0.0,-3.17],[0.0,-2.99],[0.0,-2.91],[0.0,-2.87],[0.0,-2.84],[0.0,-2.82],[0.0,-2.8],[0.0,-2.78],[0.0,-2.77],[0.0,-2.75],[0.0,-2.73],[0.0,-2.71],[0.0,-2.68],[0.0,-2.64],[0.0,-2.56],[0.0,-2.36],[0.0,0.0],[-1.17,-2.94],[-2.43,-2.43],[-1.86,-1.07],[-2.16,-0.53]],"o":[[3.5,0.0],[3.01,-1.2],[1.6,-1.6],[1.09,-1.9],[0.5,-2.05],[0.0,0.0],[0.0,-2.36],[0.0,-2.56],[0.0,-2.64],[0.0,-2.68],[0.0,-2.71],[0.0,-2.73],[0.0,-2.75],[0.0,-2.77],[0.0,-2.78],[0.0,-2.8],[0.0,-2.82],[0.0,-2.84],[0.0,-2.87],[0.0,-2.91],[0.0,-2.99],[0.0,-3.17],[0.0,-5.26],[0.0,-2.29],[-0.53,-2.16],[-1.07,-1.86],[-2.43,-2.43],[-2.94,-1.17],[-3.5,0.0],[-3.01,1.2],[-1.6,1.6],[-1.09,1.9],[-0.5,2.05],[0.0,0.0],[0.0,2.36],[0.0,2.56],[0.0,2.64],[0.0,2.68],[0.0,2.71],[0.0,2.73],[0.0,2.75],[0.0,2.77],[0.0,2.78],[0.0,2.8],[0.0,2.82],[0.0,2.84],[0.0,2.87],[0.0,2.91],[0.0,2.99],[0.0,3.17],[0.0,5.26],[0.0,3.5],[1.2,3.01],[1.6,1.6],[1.9,1.09],[2.05,0.5]],"v":[[300.0,300.0],[309.65,298.25],[317.81,292.81],[321.82,287.62],[324.25,281.52],[325.0,275.0],[325.0,266.68],[325.0,258.35],[325.0,250.01],[325.0,241.68],[325.0,233.34],[325.0,225.0],[325.0,216.67],[325.0,208.34],[325.0,200.0],[325.0,191.66],[325.0,183.33],[325.0,175.0],[325.0,166.66],[325.0,158.32],[325.0,149.99],[325.0,141.65],[325.0,133.32],[325.0,125.0],[324.25,118.48],[321.82,112.38],[317.81,107.19],[309.65,101.75],[300.0,100.0],[290.35,101.75],[282.19,107.19],[278.18,112.38],[275.75,118.48],[275.0,125.0],[275.0,133.32],[275.0,141.65],[275.0,149.99],[275.0,158.32],[275.0,166.66],[275.0,175.0],[275.0,183.33],[275.0,191.66],[275.0,200.0],[275.0,208.34],[275.0,216.67],[275.0,225.0],[275.0,233.34],[275.0,241.68],[275.0,250.01],[275.0,258.35],[275.0,266.68],[275.0,275.0],[276.75,284.65],[282.19,292.81],[287.38,296.82],[293.48,299.25]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"plain","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/lottie/ic_videocam_on_to_off.json b/assets/lottie/ic_videocam_on_to_off.json new file mode 100644 index 0000000..e759f3f --- /dev/null +++ b/assets/lottie/ic_videocam_on_to_off.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":24,"w":600,"h":600,"nm":"ic_videocam_on_to_off","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"slashed","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300.0,300.0,0],"ix":2},"a":{"a":0,"k":[300.0,300.0,0],"ix":1},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.0],"y":[1.0]},"o":{"x":[0.2],"y":[0]}},{"t":11,"s":[92,92,100],"i":{"x":[0.25],"y":[1.0]},"o":{"x":[0.33],"y":[0]}},{"t":24,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.0,"y":1.0},"o":{"x":0.2,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[675.65,-597.15],[-597.15,675.65],[-1869.94,-597.15],[-597.15,-1869.94]],"c":true}]},{"t":24,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1197.15,-75.65],[-75.65,1197.15],[-1348.44,-75.65],[-75.65,-1348.44]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Wipe"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.0,0.0],[11.75,11.75],[7.16,7.16],[6.59,6.59],[5.97,5.97],[0.0,0.0],[0.0,-10.47],[0.0,0.0],[9.18,9.18],[5.36,5.36],[0.0,0.0],[0.0,15.47],[0.0,9.42],[0.0,8.83],[0.0,8.49],[0.0,8.14],[0.0,7.48],[0.0,0.0],[0.0,0.0],[0.0,0.0],[15.47,0.0],[9.42,0.0],[8.83,0.0],[8.49,0.0],[8.14,0.0],[7.48,0.0],[0.0,0.0],[9.18,9.18],[5.36,5.36],[0.0,0.0],[-17.46,0.0],[-10.62,0.0],[-9.98,0.0],[-9.65,0.0],[-9.38,0.0],[-9.03,0.0],[-8.32,0.0],[0.0,0.0],[-9.79,-9.79],[0.0,-13.75],[0.0,-16.14],[0.0,-9.77],[0.0,-8.59],[0.0,0.0],[-11.75,11.75],[-7.16,7.16],[-6.59,6.59],[-5.97,5.97],[0.0,0.0],[0.0,-20.93],[0.0,-12.73],[0.0,-11.96],[0.0,-11.57],[0.0,-11.24],[0.0,-10.82],[0.0,-9.97]],"o":[[0.0,0.0],[-5.97,-5.97],[-6.59,-6.59],[-7.16,-7.16],[-11.75,-11.75],[0.0,0.0],[0.0,10.47],[0.0,0.0],[-5.36,-5.36],[-9.18,-9.18],[0.0,0.0],[0.0,-7.48],[0.0,-8.14],[0.0,-8.49],[0.0,-8.83],[0.0,-9.42],[0.0,-15.47],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-7.48,0.0],[-8.14,0.0],[-8.49,0.0],[-8.83,0.0],[-9.42,0.0],[-15.47,0.0],[0.0,0.0],[-5.36,-5.36],[-9.18,-9.18],[0.0,0.0],[8.32,0.0],[9.03,0.0],[9.38,0.0],[9.65,0.0],[9.98,0.0],[10.62,0.0],[17.46,0.0],[13.75,0.0],[9.79,9.79],[0.0,0.0],[0.0,8.59],[0.0,9.77],[0.0,16.14],[0.0,0.0],[5.97,-5.97],[6.59,-6.59],[7.16,-7.16],[11.75,-11.75],[0.0,0.0],[0.0,9.97],[0.0,10.82],[0.0,11.24],[0.0,11.57],[0.0,11.96],[0.0,12.73],[0.0,20.93]],"v":[[550.0,437.5],[530.01,417.51],[510.0,397.5],[490.0,377.5],[469.99,357.49],[450.0,337.5],[450.0,358.44],[450.0,379.38],[433.33,362.71],[416.67,346.04],[400.0,329.38],[400.0,303.76],[400.0,278.13],[400.0,252.5],[400.0,226.87],[400.0,201.24],[400.0,175.61],[400.0,150.0],[400.0,150.0],[400.0,150.0],[374.39,150.0],[348.76,150.0],[323.13,150.0],[297.5,150.0],[271.87,150.0],[246.24,150.0],[220.63,150.0],[203.96,133.33],[187.29,116.67],[170.62,100.0],[199.29,100.0],[227.96,100.0],[256.63,100.0],[285.31,100.0],[313.99,100.0],[342.66,100.0],[371.34,100.0],[400.0,100.0],[435.31,114.69],[450.0,150.0],[450.0,178.12],[450.0,206.25],[450.0,234.38],[450.0,262.5],[469.99,242.51],[490.0,222.5],[510.0,202.5],[530.01,182.49],[550.0,162.5],[550.0,196.86],[550.0,231.24],[550.0,265.62],[550.0,300.0],[550.0,334.38],[550.0,368.76],[550.0,403.14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.0,0.0],[12.17,12.17],[7.34,7.34],[6.89,6.89],[6.73,6.73],[6.63,6.63],[6.56,6.56],[6.52,6.52],[6.49,6.49],[6.46,6.46],[6.44,6.44],[6.42,6.42],[6.4,6.4],[6.39,6.39],[6.37,6.37],[6.35,6.35],[6.33,6.33],[6.32,6.32],[6.29,6.29],[6.26,6.26],[6.22,6.22],[6.18,6.18],[6.12,6.12],[6.03,6.03],[5.83,5.83],[5.4,5.4],[0.0,0.0],[-8.75,8.75],[0.0,0.0],[-12.17,-12.17],[-7.34,-7.34],[-6.89,-6.89],[-6.73,-6.73],[-6.63,-6.63],[-6.56,-6.56],[-6.52,-6.52],[-6.49,-6.49],[-6.46,-6.46],[-6.44,-6.44],[-6.42,-6.42],[-6.4,-6.4],[-6.39,-6.39],[-6.37,-6.37],[-6.35,-6.35],[-6.33,-6.33],[-6.32,-6.32],[-6.29,-6.29],[-6.26,-6.26],[-6.22,-6.22],[-6.18,-6.18],[-6.12,-6.12],[-6.03,-6.03],[-5.83,-5.83],[-5.4,-5.4],[0.0,0.0],[8.75,-8.75]],"o":[[0.0,0.0],[-5.4,-5.4],[-5.83,-5.83],[-6.03,-6.03],[-6.12,-6.12],[-6.18,-6.18],[-6.22,-6.22],[-6.26,-6.26],[-6.29,-6.29],[-6.32,-6.32],[-6.33,-6.33],[-6.35,-6.35],[-6.37,-6.37],[-6.39,-6.39],[-6.4,-6.4],[-6.42,-6.42],[-6.44,-6.44],[-6.46,-6.46],[-6.49,-6.49],[-6.52,-6.52],[-6.56,-6.56],[-6.63,-6.63],[-6.73,-6.73],[-6.89,-6.89],[-7.34,-7.34],[-12.17,-12.17],[0.0,0.0],[8.75,-8.75],[0.0,0.0],[5.4,5.4],[5.83,5.83],[6.03,6.03],[6.12,6.12],[6.18,6.18],[6.22,6.22],[6.26,6.26],[6.29,6.29],[6.32,6.32],[6.33,6.33],[6.35,6.35],[6.37,6.37],[6.39,6.39],[6.4,6.4],[6.42,6.42],[6.44,6.44],[6.46,6.46],[6.49,6.49],[6.52,6.52],[6.56,6.56],[6.63,6.63],[6.73,6.73],[6.89,6.89],[7.34,7.34],[12.17,12.17],[0.0,0.0],[-8.75,8.75]],"v":[[513.75,583.75],[494.68,564.68],[475.51,545.51],[456.4,526.4],[437.23,507.23],[418.08,488.08],[398.95,468.95],[379.83,449.83],[360.7,430.7],[341.56,411.56],[322.41,392.41],[303.28,373.28],[284.14,354.14],[265.0,335.0],[245.86,315.86],[226.72,296.72],[207.59,277.59],[188.44,258.44],[169.3,239.3],[150.17,220.17],[131.05,201.05],[111.92,181.92],[92.77,162.77],[73.6,143.6],[54.49,124.49],[35.32,105.32],[16.25,86.25],[33.75,68.75],[51.25,51.25],[70.32,70.32],[89.49,89.49],[108.6,108.6],[127.77,127.77],[146.92,146.92],[166.05,166.05],[185.17,185.17],[204.3,204.3],[223.44,223.44],[242.59,242.59],[261.72,261.72],[280.86,280.86],[300.0,300.0],[319.14,319.14],[338.28,338.28],[357.41,357.41],[376.56,376.56],[395.7,395.7],[414.83,414.83],[433.95,433.95],[453.08,453.08],[472.23,472.23],[491.4,491.4],[510.51,510.51],[529.68,529.68],[548.75,548.75],[531.25,566.25]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.0,0.0],[-7.04,0.0],[-4.25,0.0],[-4.01,0.0],[-3.91,0.0],[-3.84,0.0],[-3.8,0.0],[-3.77,0.0],[-3.75,0.0],[-3.72,0.0],[-3.69,0.0],[-3.66,0.0],[-3.62,0.0],[-3.56,0.0],[-3.45,0.0],[-3.19,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-7.04],[0.0,-4.25],[0.0,-4.01],[0.0,-3.91],[0.0,-3.84],[0.0,-3.8],[0.0,-3.77],[0.0,-3.75],[0.0,-3.72],[0.0,-3.69],[0.0,-3.66],[0.0,-3.62],[0.0,-3.56],[0.0,-3.45],[0.0,-3.19],[0.0,0.0],[5.17,5.17],[3.11,3.11],[2.93,2.93],[2.86,2.86],[2.82,2.82],[2.79,2.79],[2.77,2.77],[2.75,2.75],[2.74,2.74],[2.73,2.73],[2.72,2.72],[2.71,2.71],[2.7,2.7],[2.69,2.69],[2.68,2.68],[2.66,2.66],[2.64,2.64],[2.61,2.61],[2.57,2.57],[2.49,2.49],[2.31,2.31]],"o":[[0.0,0.0],[3.19,0.0],[3.45,0.0],[3.56,0.0],[3.62,0.0],[3.66,0.0],[3.69,0.0],[3.72,0.0],[3.75,0.0],[3.77,0.0],[3.8,0.0],[3.84,0.0],[3.91,0.0],[4.01,0.0],[4.25,0.0],[7.04,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,3.19],[0.0,3.45],[0.0,3.56],[0.0,3.62],[0.0,3.66],[0.0,3.69],[0.0,3.72],[0.0,3.75],[0.0,3.77],[0.0,3.8],[0.0,3.84],[0.0,3.91],[0.0,4.01],[0.0,4.25],[0.0,7.04],[0.0,0.0],[-2.31,-2.31],[-2.49,-2.49],[-2.57,-2.57],[-2.61,-2.61],[-2.64,-2.64],[-2.66,-2.66],[-2.68,-2.68],[-2.69,-2.69],[-2.7,-2.7],[-2.71,-2.71],[-2.72,-2.72],[-2.73,-2.73],[-2.74,-2.74],[-2.75,-2.75],[-2.77,-2.77],[-2.79,-2.79],[-2.82,-2.82],[-2.86,-2.86],[-2.93,-2.93],[-3.11,-3.11],[-5.17,-5.17]],"v":[[220.63,150.0],[231.82,150.0],[243.04,150.0],[254.25,150.0],[265.46,150.0],[276.67,150.0],[287.89,150.0],[299.1,150.0],[310.31,150.0],[321.53,150.0],[332.74,150.0],[343.95,150.0],[355.16,150.0],[366.38,150.0],[377.59,150.0],[388.81,150.0],[400.0,150.0],[400.0,150.0],[400.0,150.0],[400.0,161.19],[400.0,172.41],[400.0,183.62],[400.0,194.84],[400.0,206.05],[400.0,217.26],[400.0,228.47],[400.0,239.69],[400.0,250.9],[400.0,262.11],[400.0,273.33],[400.0,284.54],[400.0,295.75],[400.0,306.96],[400.0,318.18],[400.0,329.38],[391.86,321.24],[383.7,313.08],[375.55,304.92],[367.4,296.78],[359.24,288.62],[351.08,280.46],[342.93,272.3],[334.78,264.15],[326.62,256.0],[318.47,247.84],[310.31,239.69],[302.16,231.53],[294.0,223.38],[285.85,215.22],[277.7,207.07],[269.54,198.92],[261.38,190.76],[253.22,182.6],[245.08,174.45],[236.92,166.3],[228.76,158.14]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,10.99],[0.0,6.64],[0.0,6.25],[0.0,6.1],[0.0,6.0],[0.0,5.94],[0.0,5.89],[0.0,5.85],[0.0,5.81],[0.0,5.76],[0.0,5.71],[0.0,5.65],[0.0,5.55],[0.0,5.38],[0.0,4.97],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-9.81,-9.81],[-5.91,-5.91],[-5.57,-5.57],[-5.43,-5.43],[-5.35,-5.35],[-5.3,-5.3],[-5.26,-5.26],[-5.22,-5.22],[-5.2,-5.2],[-5.17,-5.17],[-5.14,-5.14],[-5.1,-5.1],[-5.06,-5.06],[-5.01,-5.01],[-4.92,-4.92],[-4.77,-4.77],[-4.41,-4.41],[0.0,0.0],[0.0,0.0],[0.0,0.0],[10.99,0.0],[6.64,0.0],[6.25,0.0],[6.1,0.0],[6.0,0.0],[5.94,0.0],[5.89,0.0],[5.85,0.0],[5.81,0.0],[5.76,0.0],[5.71,0.0],[5.65,0.0],[5.55,0.0],[5.38,0.0],[4.97,0.0]],"o":[[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-4.97],[0.0,-5.38],[0.0,-5.55],[0.0,-5.65],[0.0,-5.71],[0.0,-5.76],[0.0,-5.81],[0.0,-5.85],[0.0,-5.89],[0.0,-5.94],[0.0,-6.0],[0.0,-6.1],[0.0,-6.25],[0.0,-6.64],[0.0,-10.99],[0.0,0.0],[0.0,0.0],[0.0,0.0],[4.41,4.41],[4.77,4.77],[4.92,4.92],[5.01,5.01],[5.06,5.06],[5.1,5.1],[5.14,5.14],[5.17,5.17],[5.2,5.2],[5.22,5.22],[5.26,5.26],[5.3,5.3],[5.35,5.35],[5.43,5.43],[5.57,5.57],[5.91,5.91],[9.81,9.81],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-4.97,0.0],[-5.38,0.0],[-5.55,0.0],[-5.65,0.0],[-5.71,0.0],[-5.76,0.0],[-5.81,0.0],[-5.85,0.0],[-5.89,0.0],[-5.94,0.0],[-6.0,0.0],[-6.1,0.0],[-6.25,0.0],[-6.64,0.0],[-10.99,0.0]],"v":[[100.0,450.0],[100.0,450.0],[100.0,450.0],[100.0,432.53],[100.0,415.01],[100.0,397.52],[100.0,380.01],[100.0,362.51],[100.0,345.01],[100.0,327.5],[100.0,310.0],[100.0,292.5],[100.0,274.99],[100.0,257.49],[100.0,239.99],[100.0,222.48],[100.0,204.99],[100.0,187.47],[100.0,170.0],[100.0,170.0],[100.0,170.0],[115.54,185.54],[131.09,201.09],[146.64,216.64],[162.2,232.2],[177.77,247.77],[193.32,263.32],[208.88,278.88],[224.44,294.44],[240.0,310.0],[255.56,325.56],[271.12,341.12],[286.68,356.68],[302.23,372.23],[317.8,387.8],[333.36,403.36],[348.91,418.91],[364.46,434.46],[380.0,450.0],[380.0,450.0],[380.0,450.0],[362.53,450.0],[345.01,450.0],[327.52,450.0],[310.01,450.0],[292.51,450.0],[275.01,450.0],[257.5,450.0],[240.0,450.0],[222.5,450.0],[204.99,450.0],[187.49,450.0],[169.99,450.0],[152.48,450.0],[134.99,450.0],[117.47,450.0]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.0,0.0],[-12.5,-12.5],[0.0,0.0],[12.5,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-22.83],[0.0,-13.89],[0.0,-13.05],[0.0,-12.62],[0.0,-12.26],[0.0,-11.81],[0.0,-10.88],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-22.83,0.0],[-13.89,0.0],[-13.05,0.0],[-12.62,0.0],[-12.26,0.0],[-11.81,0.0],[-10.88,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,12.5],[0.0,0.0],[-12.5,-12.5],[0.0,0.0],[0.0,0.0],[9.79,-9.79],[13.75,0.0],[20.43,0.0],[12.41,0.0],[11.68,0.0],[11.33,0.0],[11.07,0.0],[10.81,0.0],[10.44,0.0],[9.62,0.0],[0.0,0.0],[9.79,9.79],[0.0,13.75],[0.0,20.43],[0.0,12.41],[0.0,11.68],[0.0,11.33],[0.0,11.07],[0.0,10.81],[0.0,10.44],[0.0,9.62],[0.0,0.0],[-9.79,9.79],[-13.75,0.0]],"o":[[0.0,0.0],[12.5,12.5],[0.0,0.0],[-12.5,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,10.88],[0.0,11.81],[0.0,12.26],[0.0,12.62],[0.0,13.05],[0.0,13.89],[0.0,22.83],[0.0,0.0],[0.0,0.0],[0.0,0.0],[10.88,0.0],[11.81,0.0],[12.26,0.0],[12.62,0.0],[13.05,0.0],[13.89,0.0],[22.83,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-12.5],[0.0,0.0],[12.5,12.5],[0.0,0.0],[0.0,13.75],[-9.79,9.79],[0.0,0.0],[-9.62,0.0],[-10.44,0.0],[-10.81,0.0],[-11.07,0.0],[-11.33,0.0],[-11.68,0.0],[-12.41,0.0],[-20.43,0.0],[-13.75,0.0],[-9.79,-9.79],[0.0,0.0],[0.0,-9.62],[0.0,-10.44],[0.0,-10.81],[0.0,-11.07],[0.0,-11.33],[0.0,-11.68],[0.0,-12.41],[0.0,-20.43],[0.0,-13.75],[9.79,-9.79],[0.0,0.0]],"v":[[100.0,100.0],[125.0,125.0],[150.0,150.0],[125.0,150.0],[100.0,150.0],[100.0,150.0],[100.0,150.0],[100.0,187.49],[100.0,224.99],[100.0,262.49],[100.0,300.0],[100.0,337.51],[100.0,375.01],[100.0,412.51],[100.0,450.0],[100.0,450.0],[100.0,450.0],[137.49,450.0],[174.99,450.0],[212.49,450.0],[250.0,450.0],[287.51,450.0],[325.01,450.0],[362.51,450.0],[400.0,450.0],[400.0,450.0],[400.0,450.0],[400.0,425.0],[400.0,400.0],[425.0,425.0],[450.0,450.0],[450.0,450.0],[435.31,485.31],[400.0,500.0],[366.69,500.0],[333.35,500.0],[300.01,500.0],[266.67,500.0],[233.33,500.0],[199.99,500.0],[166.65,500.0],[133.31,500.0],[100.0,500.0],[64.69,485.31],[50.0,450.0],[50.0,416.69],[50.0,383.35],[50.0,350.01],[50.0,316.67],[50.0,283.33],[50.0,249.99],[50.0,216.65],[50.0,183.31],[50.0,150.0],[64.69,114.69],[100.0,100.0]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"slashed","np":7,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plain","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300.0,300.0,0],"ix":2},"a":{"a":0,"k":[300.0,300.0,0],"ix":1},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.0],"y":[1.0]},"o":{"x":[0.2],"y":[0]}},{"t":11,"s":[92,92,100],"i":{"x":[0.25],"y":[1.0]},"o":{"x":[0.33],"y":[0]}},{"t":24,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.0,"y":1.0},"o":{"x":0.2,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[675.65,-597.15],[-597.15,675.65],[675.65,1948.44],[1948.44,675.65]],"c":true}]},{"t":24,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1197.15,-75.65],[-75.65,1197.15],[1197.15,2469.94],[2469.94,1197.15]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Wipe"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.0,0.0],[9.79,9.79],[0.0,13.75],[0.0,22.83],[0.0,13.89],[0.0,13.05],[0.0,12.62],[0.0,12.26],[0.0,11.81],[0.0,10.88],[0.0,0.0],[-9.79,9.79],[-5.87,2.45],[-6.89,0.0],[-22.83,0.0],[-13.89,0.0],[-13.05,0.0],[-12.62,0.0],[-12.26,0.0],[-11.81,0.0],[-10.88,0.0],[0.0,0.0],[-9.79,-9.79],[0.0,-13.75],[0.0,-20.65],[0.0,-12.07],[0.0,0.0],[-14.35,14.35],[-8.68,8.68],[-7.64,7.64],[0.0,0.0],[0.0,-20.93],[0.0,-12.73],[0.0,-11.96],[0.0,-11.57],[0.0,-11.24],[0.0,-10.82],[0.0,-9.97],[0.0,0.0],[14.35,14.35],[8.68,8.68],[7.64,7.64],[0.0,0.0],[0.0,-20.65],[0.0,-12.07],[0.0,0.0],[2.46,-5.9],[4.89,-4.89],[13.75,0.0],[22.83,0.0],[13.89,0.0],[13.05,0.0],[12.62,0.0],[12.26,0.0],[11.81,0.0],[10.88,0.0]],"o":[[-13.75,0.0],[-9.79,-9.79],[0.0,0.0],[0.0,-10.88],[0.0,-11.81],[0.0,-12.26],[0.0,-12.62],[0.0,-13.05],[0.0,-13.89],[0.0,-22.83],[0.0,-13.75],[4.89,-4.89],[5.9,-2.46],[0.0,0.0],[10.88,0.0],[11.81,0.0],[12.26,0.0],[12.62,0.0],[13.05,0.0],[13.89,0.0],[22.83,0.0],[13.75,0.0],[9.79,9.79],[0.0,0.0],[0.0,12.07],[0.0,20.65],[0.0,0.0],[7.64,-7.64],[8.68,-8.68],[14.35,-14.35],[0.0,0.0],[0.0,9.97],[0.0,10.82],[0.0,11.24],[0.0,11.57],[0.0,11.96],[0.0,12.73],[0.0,20.93],[0.0,0.0],[-7.64,-7.64],[-8.68,-8.68],[-14.35,-14.35],[0.0,0.0],[0.0,12.07],[0.0,20.65],[0.0,6.89],[-2.45,5.87],[-9.79,9.79],[0.0,0.0],[-10.88,0.0],[-11.81,0.0],[-12.26,0.0],[-12.62,0.0],[-13.05,0.0],[-13.89,0.0],[-22.83,0.0]],"v":[[100.0,500.0],[64.69,485.31],[50.0,450.0],[50.0,412.51],[50.0,375.01],[50.0,337.51],[50.0,300.0],[50.0,262.49],[50.0,224.99],[50.0,187.49],[50.0,150.0],[64.69,114.69],[80.82,103.69],[100.0,100.0],[137.49,100.0],[174.99,100.0],[212.49,100.0],[250.0,100.0],[287.51,100.0],[325.01,100.0],[362.51,100.0],[400.0,100.0],[435.31,114.69],[450.0,150.0],[450.0,187.5],[450.0,225.0],[450.0,262.5],[475.0,237.5],[500.0,212.5],[525.0,187.5],[550.0,162.5],[550.0,196.86],[550.0,231.24],[550.0,265.62],[550.0,300.0],[550.0,334.38],[550.0,368.76],[550.0,403.14],[550.0,437.5],[525.0,412.5],[500.0,387.5],[475.0,362.5],[450.0,337.5],[450.0,375.0],[450.0,412.5],[450.0,450.0],[446.31,469.18],[435.31,485.31],[400.0,500.0],[362.51,500.0],[325.01,500.0],[287.51,500.0],[250.0,500.0],[212.49,500.0],[174.99,500.0],[137.49,500.0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.0,0.0],[-15.53,0.0],[-9.41,0.0],[-8.87,0.0],[-8.63,0.0],[-8.48,0.0],[-8.37,0.0],[-8.27,0.0],[-8.15,0.0],[-7.99,0.0],[-7.75,0.0],[-7.15,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,15.53],[0.0,9.41],[0.0,8.87],[0.0,8.63],[0.0,8.48],[0.0,8.37],[0.0,8.27],[0.0,8.15],[0.0,7.99],[0.0,7.75],[0.0,7.15],[0.0,0.0],[0.0,0.0],[0.0,0.0],[15.53,0.0],[9.41,0.0],[8.87,0.0],[8.63,0.0],[8.48,0.0],[8.37,0.0],[8.27,0.0],[8.15,0.0],[7.99,0.0],[7.75,0.0],[7.15,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-15.53],[0.0,-9.41],[0.0,-8.87],[0.0,-8.63],[0.0,-8.48],[0.0,-8.37],[0.0,-8.27],[0.0,-8.15],[0.0,-7.99],[0.0,-7.75],[0.0,-7.15],[0.0,0.0],[0.0,0.0]],"o":[[0.0,0.0],[7.15,0.0],[7.75,0.0],[7.99,0.0],[8.15,0.0],[8.27,0.0],[8.37,0.0],[8.48,0.0],[8.63,0.0],[8.87,0.0],[9.41,0.0],[15.53,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-7.15],[0.0,-7.75],[0.0,-7.99],[0.0,-8.15],[0.0,-8.27],[0.0,-8.37],[0.0,-8.48],[0.0,-8.63],[0.0,-8.87],[0.0,-9.41],[0.0,-15.53],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-7.15,0.0],[-7.75,0.0],[-7.99,0.0],[-8.15,0.0],[-8.27,0.0],[-8.37,0.0],[-8.48,0.0],[-8.63,0.0],[-8.87,0.0],[-9.41,0.0],[-15.53,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,7.15],[0.0,7.75],[0.0,7.99],[0.0,8.15],[0.0,8.27],[0.0,8.37],[0.0,8.48],[0.0,8.63],[0.0,8.87],[0.0,9.41],[0.0,15.53],[0.0,0.0],[0.0,0.0]],"v":[[100.0,450.0],[124.97,450.0],[149.97,450.0],[174.99,450.0],[199.99,450.0],[224.99,450.0],[250.0,450.0],[275.01,450.0],[300.01,450.0],[325.01,450.0],[350.03,450.0],[375.03,450.0],[400.0,450.0],[400.0,450.0],[400.0,450.0],[400.0,425.03],[400.0,400.03],[400.0,375.01],[400.0,350.01],[400.0,325.01],[400.0,300.0],[400.0,274.99],[400.0,249.99],[400.0,224.99],[400.0,199.97],[400.0,174.97],[400.0,150.0],[400.0,150.0],[400.0,150.0],[375.03,150.0],[350.03,150.0],[325.01,150.0],[300.01,150.0],[275.01,150.0],[250.0,150.0],[224.99,150.0],[199.99,150.0],[174.99,150.0],[149.97,150.0],[124.97,150.0],[100.0,150.0],[100.0,150.0],[100.0,150.0],[100.0,174.97],[100.0,199.97],[100.0,224.99],[100.0,249.99],[100.0,274.99],[100.0,300.0],[100.0,325.01],[100.0,350.01],[100.0,375.01],[100.0,400.03],[100.0,425.03],[100.0,450.0],[100.0,450.0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,15.53],[0.0,9.41],[0.0,8.87],[0.0,8.63],[0.0,8.48],[0.0,8.37],[0.0,8.27],[0.0,8.15],[0.0,7.99],[0.0,7.75],[0.0,7.15],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-15.53,0.0],[-9.41,0.0],[-8.87,0.0],[-8.63,0.0],[-8.48,0.0],[-8.37,0.0],[-8.27,0.0],[-8.15,0.0],[-7.99,0.0],[-7.75,0.0],[-7.15,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-15.53],[0.0,-9.41],[0.0,-8.87],[0.0,-8.63],[0.0,-8.48],[0.0,-8.37],[0.0,-8.27],[0.0,-8.15],[0.0,-7.99],[0.0,-7.75],[0.0,-7.15],[0.0,0.0],[0.0,0.0],[0.0,0.0],[15.53,0.0],[9.41,0.0],[8.87,0.0],[8.63,0.0],[8.48,0.0],[8.37,0.0],[8.27,0.0],[8.15,0.0],[7.99,0.0],[7.75,0.0],[7.15,0.0]],"o":[[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,-7.15],[0.0,-7.75],[0.0,-7.99],[0.0,-8.15],[0.0,-8.27],[0.0,-8.37],[0.0,-8.48],[0.0,-8.63],[0.0,-8.87],[0.0,-9.41],[0.0,-15.53],[0.0,0.0],[0.0,0.0],[0.0,0.0],[7.15,0.0],[7.75,0.0],[7.99,0.0],[8.15,0.0],[8.27,0.0],[8.37,0.0],[8.48,0.0],[8.63,0.0],[8.87,0.0],[9.41,0.0],[15.53,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,7.15],[0.0,7.75],[0.0,7.99],[0.0,8.15],[0.0,8.27],[0.0,8.37],[0.0,8.48],[0.0,8.63],[0.0,8.87],[0.0,9.41],[0.0,15.53],[0.0,0.0],[0.0,0.0],[0.0,0.0],[-7.15,0.0],[-7.75,0.0],[-7.99,0.0],[-8.15,0.0],[-8.27,0.0],[-8.37,0.0],[-8.48,0.0],[-8.63,0.0],[-8.87,0.0],[-9.41,0.0],[-15.53,0.0]],"v":[[100.0,450.0],[100.0,450.0],[100.0,450.0],[100.0,425.03],[100.0,400.03],[100.0,375.01],[100.0,350.01],[100.0,325.01],[100.0,300.0],[100.0,274.99],[100.0,249.99],[100.0,224.99],[100.0,199.97],[100.0,174.97],[100.0,150.0],[100.0,150.0],[100.0,150.0],[124.97,150.0],[149.97,150.0],[174.99,150.0],[199.99,150.0],[224.99,150.0],[250.0,150.0],[275.01,150.0],[300.01,150.0],[325.01,150.0],[350.03,150.0],[375.03,150.0],[400.0,150.0],[400.0,150.0],[400.0,150.0],[400.0,174.97],[400.0,199.97],[400.0,224.99],[400.0,249.99],[400.0,274.99],[400.0,300.0],[400.0,325.01],[400.0,350.01],[400.0,375.01],[400.0,400.03],[400.0,425.03],[400.0,450.0],[400.0,450.0],[400.0,450.0],[375.03,450.0],[350.03,450.0],[325.01,450.0],[300.01,450.0],[275.01,450.0],[250.0,450.0],[224.99,450.0],[199.99,450.0],[174.99,450.0],[149.97,450.0],[124.97,450.0]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"plain","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index fecc9b1..64183c3 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -65,26 +65,35 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler { private func registerAppIcon(_ messenger: FlutterBinaryMessenger) { method("ru.komet.app/app_icon", messenger) { call, result in - guard call.method == "setAppIcon" else { - result(FlutterMethodNotImplemented) - return - } - let name = (call.arguments as? [String: Any])?["name"] as? String - let iconName: String? = (name == "DefaultIcon") ? nil : name - guard UIApplication.shared.supportsAlternateIcons else { - result(FlutterError(code: "UNSUPPORTED", - message: "Alternate icons are not supported", - details: nil)) - return - } - UIApplication.shared.setAlternateIconName(iconName) { error in - if let error = error { - result(FlutterError(code: "APPLY_FAILED", - message: error.localizedDescription, + switch call.method { + case "getAppIcon": + result(UIApplication.shared.alternateIconName) + case "setAppIcon": + let requested = (call.arguments as? [String: Any])?["name"] as? String + let iconName: String? = (requested?.isEmpty ?? true) ? nil : requested + guard UIApplication.shared.supportsAlternateIcons else { + result(FlutterError(code: "UNSUPPORTED", + message: "Alternate icons are not supported", details: nil)) - } else { - result(nil) + return } + guard UIApplication.shared.alternateIconName != iconName else { + result(nil) + return + } + UIApplication.shared.setAlternateIconName(iconName) { error in + DispatchQueue.main.async { + if let error = error { + result(FlutterError(code: "APPLY_FAILED", + message: error.localizedDescription, + details: nil)) + } else { + result(nil) + } + } + } + default: + result(FlutterMethodNotImplemented) } } } diff --git a/lib/core/calls/active_call.dart b/lib/core/calls/active_call.dart new file mode 100644 index 0000000..1705aeb --- /dev/null +++ b/lib/core/calls/active_call.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; + +import 'call_session.dart'; + +class ActiveCallPresentation { + const ActiveCallPresentation({ + required this.session, + required this.name, + required this.avatarUrl, + required this.isGroup, + }); + + final CallSession session; + final String name; + final String? avatarUrl; + final bool isGroup; + + bool sameAs(ActiveCallPresentation other) => + identical(session, other.session) && + name == other.name && + avatarUrl == other.avatarUrl && + isGroup == other.isGroup; +} + +class ActiveCall { + ActiveCall._(); + + static final ActiveCall instance = ActiveCall._(); + + final ValueNotifier current = ValueNotifier(null); + + final ValueNotifier screenVisible = ValueNotifier(false); + + int _openScreens = 0; + StreamSubscription? _stateSub; + + void attach({ + required CallSession session, + required String name, + String? avatarUrl, + bool isGroup = false, + }) { + if (session.currentState == CallSessionState.ended) return; + final next = ActiveCallPresentation( + session: session, + name: name, + avatarUrl: avatarUrl, + isGroup: isGroup, + ); + final previous = current.value; + if (previous != null && previous.sameAs(next)) return; + if (previous == null || !identical(previous.session, session)) { + _stateSub?.cancel(); + _stateSub = session.stateStream.listen((state) { + if (state == CallSessionState.ended) detach(session); + }); + } + _publish(current, next); + } + + void detach([CallSession? session]) { + final active = current.value; + if (active == null) return; + if (session != null && !identical(active.session, session)) return; + _stateSub?.cancel(); + _stateSub = null; + _publish(current, null); + } + + void enterScreen() { + _openScreens++; + _publish(screenVisible, true); + } + + void leaveScreen() { + if (_openScreens > 0) _openScreens--; + _publish(screenVisible, _openScreens > 0); + } + + void _publish(ValueNotifier notifier, T value) { + if (SchedulerBinding.instance.schedulerPhase == + SchedulerPhase.persistentCallbacks) { + SchedulerBinding.instance.addPostFrameCallback((_) { + notifier.value = value; + }); + return; + } + notifier.value = value; + } +} diff --git a/lib/core/config/app_icon.dart b/lib/core/config/app_icon.dart index 8c503fa..bd360bd 100644 --- a/lib/core/config/app_icon.dart +++ b/lib/core/config/app_icon.dart @@ -5,15 +5,34 @@ import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum AppIcon { - defaultIcon('default', 'Default', 'assets/komet_icon.png', 'MainActivity'), - minimal('minimal', 'Minimal', 'assets/meteor_icon.png', 'MinimalIcon'); + defaultIcon( + 'default', + 'Default', + 'assets/komet_icon.png', + 'MainActivity', + null, + ), + minimal( + 'minimal', + 'Minimal', + 'assets/meteor_icon.png', + 'MinimalIcon', + 'MinimalIcon', + ); final String id; final String title; final String previewAsset; - final String platformName; + final String androidAlias; + final String? iosAlternateName; - const AppIcon(this.id, this.title, this.previewAsset, this.platformName); + const AppIcon( + this.id, + this.title, + this.previewAsset, + this.androidAlias, + this.iosAlternateName, + ); } class AppIconConfig { @@ -29,21 +48,37 @@ class AppIconConfig { static Future load() async { if (!isSupported) return; final prefs = await SharedPreferences.getInstance(); - final id = prefs.getString(prefKey); - current.value = _parse(id); + var icon = _parse(prefs.getString(prefKey)); + final applied = await _appliedIcon(); + if (applied != null && applied != icon) { + icon = applied; + await prefs.setString(prefKey, icon.id); + } + current.value = icon; } static Future apply(AppIcon icon) async { if (!isSupported) return; if (current.value == icon) return; await _channel.invokeMethod('setAppIcon', { - 'name': icon.platformName, + 'name': Platform.isIOS ? icon.iosAlternateName : icon.androidAlias, }); final prefs = await SharedPreferences.getInstance(); await prefs.setString(prefKey, icon.id); current.value = icon; } + static Future _appliedIcon() async { + if (!Platform.isIOS) return null; + try { + final name = await _channel.invokeMethod('getAppIcon'); + for (final icon in AppIcon.values) { + if (icon.iosAlternateName == name) return icon; + } + } catch (_) {} + return null; + } + static AppIcon _parse(String? val) { for (final icon in AppIcon.values) { if (icon.id == val) return icon; diff --git a/lib/frontend/screens/calls/call_screen.dart b/lib/frontend/screens/calls/call_screen.dart index 67ebf9f..467f40a 100644 --- a/lib/frontend/screens/calls/call_screen.dart +++ b/lib/frontend/screens/calls/call_screen.dart @@ -9,18 +9,19 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' MediaStream, RTCVideoRenderer, RTCVideoValue, - RTCVideoView, RTCVideoViewObjectFit; import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/messages.dart' show ContactCache; import '../../../core/cache/info_cache.dart'; +import '../../../core/calls/active_call.dart'; import '../../../core/calls/call_controller.dart'; import '../../../core/calls/call_info.dart'; import '../../../core/calls/call_session.dart'; import '../../../core/config/app_colors.dart'; import '../../../core/utils/format.dart'; import '../../../l10n/app_localizations.dart'; +import '../../widgets/call_video_view.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/animated_slash_icon.dart'; @@ -133,6 +134,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); + ActiveCall.instance.enterScreen(); _dotsController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1400), @@ -179,6 +181,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { if (name != null && name.isNotEmpty) _name = name; if (avatar != null && avatar.isNotEmpty) _avatarUrl = avatar; }); + _publishActiveCall(); } Future _initRenderer() async { @@ -245,6 +248,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { _syncLocalPreview(); _isSpeaker = session.isSpeaker; setState(() {}); + _publishActiveCall(); }); _remoteStreamSub = session.remoteStreamStream.listen(_attachStream); _tileStreamSub = session.participantStreamUpdates.listen(_onTileStream); @@ -258,6 +262,18 @@ class _CallScreenState extends State with TickerProviderStateMixin { _resolveParticipants(); _syncVideo(); _isSpeaker = session.isSpeaker; + _publishActiveCall(); + } + + void _publishActiveCall() { + final session = _session; + if (session == null) return; + ActiveCall.instance.attach( + session: session, + name: _name, + avatarUrl: _avatarUrl, + isGroup: _isGroup, + ); } void _showKometBadge() { @@ -368,12 +384,15 @@ class _CallScreenState extends State with TickerProviderStateMixin { Future _toggleVideo() async { final session = _session; if (session == null || _videoBusy) return; + final l10n = AppLocalizations.of(context)!; setState(() => _videoBusy = true); await WidgetsBinding.instance.endOfFrame; try { await session.setVideoEnabled(!session.localVideo); } catch (e) { - if (mounted) showCustomNotification(context, 'Камера недоступна: $e'); + if (mounted) { + showCustomNotification(context, l10n.callCameraUnavailable(e)); + } } finally { _syncLocalPreview(); if (mounted) setState(() => _videoBusy = false); @@ -404,6 +423,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { @override void dispose() { + ActiveCall.instance.leaveScreen(); _stateSub?.cancel(); _canceledSub?.cancel(); _infoSub?.cancel(); @@ -528,25 +548,26 @@ class _CallScreenState extends State with TickerProviderStateMixin { border: Border.all(color: cs.outlineVariant, width: 1), ), child: _localRendererReady && _localRenderer.srcObject != null - ? RTCVideoView( - _localRenderer, + ? CallVideoView( + renderer: _localRenderer, mirror: _session?.localScreen != true, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + placeholder: _localPreviewIcon(cs), ) - : Center( - child: Icon( - _session?.localScreen == true - ? Symbols.screen_share - : Symbols.videocam, - color: cs.onSurfaceVariant, - size: 28, - ), - ), + : _localPreviewIcon(cs), ), ), ); } + Widget _localPreviewIcon(ColorScheme cs) => Center( + child: Icon( + _session?.localScreen == true ? Symbols.screen_share : Symbols.videocam, + color: cs.onSurfaceVariant, + size: 28, + ), + ); + Widget _buildGroupBody(ColorScheme cs) { final l10n = AppLocalizations.of(context)!; final participants = _session?.participants ?? const []; @@ -764,9 +785,10 @@ class _CallScreenState extends State with TickerProviderStateMixin { child: Stack( fit: StackFit.expand, children: [ - RTCVideoView( - renderer, + CallVideoView( + renderer: renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + placeholder: ColoredBox(color: cs.surfaceContainerHighest), ), Positioned( left: 8, @@ -872,8 +894,8 @@ class _CallScreenState extends State with TickerProviderStateMixin { child: AspectRatio( aspectRatio: ar, child: RepaintBoundary( - child: RTCVideoView( - _remoteRenderer, + child: CallVideoView( + renderer: _remoteRenderer, objectFit: RTCVideoViewObjectFit .RTCVideoViewObjectFitCover, ), diff --git a/lib/frontend/screens/profile/app_icon_screen.dart b/lib/frontend/screens/profile/app_icon_screen.dart index 7db13fe..b41c5a6 100644 --- a/lib/frontend/screens/profile/app_icon_screen.dart +++ b/lib/frontend/screens/profile/app_icon_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../../widgets/connection_status.dart'; @@ -38,7 +39,8 @@ class _AppIconScreenState extends State { showCustomNotification(context, 'Иконка изменена на «${icon.title}»'); } catch (e) { if (!mounted) return; - showCustomNotification(context, 'Не удалось сменить иконку: $e'); + final reason = e is PlatformException ? (e.message ?? e.code) : '$e'; + showCustomNotification(context, 'Не удалось сменить иконку: $reason'); } } diff --git a/lib/frontend/widgets/call_video_view.dart b/lib/frontend/widgets/call_video_view.dart new file mode 100644 index 0000000..6bd135c --- /dev/null +++ b/lib/frontend/widgets/call_video_view.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart' + show RTCVideoRenderer, RTCVideoView, RTCVideoViewObjectFit; + +class CallVideoView extends StatefulWidget { + const CallVideoView({ + super.key, + required this.renderer, + this.objectFit = RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + this.mirror = false, + this.placeholder, + }); + + final RTCVideoRenderer renderer; + final RTCVideoViewObjectFit objectFit; + final bool mirror; + final Widget? placeholder; + + @override + State createState() => _CallVideoViewState(); +} + +class _CallVideoViewState extends State { + static const Duration _switchCooldown = Duration(milliseconds: 200); + + bool _armed = false; + String? _sourceId; + Timer? _cooldown; + + @override + void initState() { + super.initState(); + _bind(widget.renderer); + } + + @override + void didUpdateWidget(CallVideoView old) { + super.didUpdateWidget(old); + if (identical(old.renderer, widget.renderer)) return; + old.renderer.removeListener(_onRenderer); + _cooldown?.cancel(); + _cooldown = null; + _bind(widget.renderer); + } + + @override + void dispose() { + _cooldown?.cancel(); + widget.renderer.removeListener(_onRenderer); + super.dispose(); + } + + void _bind(RTCVideoRenderer renderer) { + _sourceId = renderer.srcObject?.id; + _armed = _hasFrames; + renderer.addListener(_onRenderer); + } + + bool get _hasFrames { + final renderer = widget.renderer; + return renderer.textureId != null && + renderer.srcObject != null && + renderer.value.width > 0; + } + + void _onRenderer() { + final id = widget.renderer.srcObject?.id; + if (id != _sourceId) { + _sourceId = id; + _cooldown?.cancel(); + _cooldown = Timer(_switchCooldown, () { + _cooldown = null; + _sync(); + }); + if (_armed && mounted) setState(() => _armed = false); + return; + } + if (_cooldown != null) return; + _sync(); + } + + void _sync() { + if (!mounted) return; + final next = _hasFrames; + if (next != _armed) setState(() => _armed = next); + } + + @override + Widget build(BuildContext context) { + if (!_armed) return widget.placeholder ?? const SizedBox.expand(); + return RTCVideoView( + widget.renderer, + objectFit: widget.objectFit, + mirror: widget.mirror, + ); + } +} diff --git a/lib/frontend/widgets/draggable_floating_layer.dart b/lib/frontend/widgets/draggable_floating_layer.dart new file mode 100644 index 0000000..a820214 --- /dev/null +++ b/lib/frontend/widgets/draggable_floating_layer.dart @@ -0,0 +1,245 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; + +import '../../core/utils/haptics.dart'; + +@immutable +class FloatingDockGeometry { + const FloatingDockGeometry({ + required this.bounds, + required this.size, + required this.safeArea, + this.edge = 12, + this.restingBottomGap = 96, + }); + + final Size bounds; + final Size size; + final EdgeInsets safeArea; + final double edge; + final double restingBottomGap; + + static const double flingSeconds = 0.09; + static const double flingThreshold = 320; + + double get minX => edge; + double get maxX => math.max(minX, bounds.width - size.width - edge); + double get minY => safeArea.top + edge; + double get maxY => + math.max(minY, bounds.height - size.height - safeArea.bottom - edge); + + Offset get resting => clamp( + Offset( + maxX, + bounds.height - size.height - safeArea.bottom - restingBottomGap, + ), + ); + + Offset clamp(Offset value) => + Offset(value.dx.clamp(minX, maxX), value.dy.clamp(minY, maxY)); + + Offset snap(Offset value, Offset velocity) { + final center = value.dx + size.width / 2; + final toRight = velocity.dx.abs() > flingThreshold + ? velocity.dx > 0 + : center >= bounds.width / 2; + return clamp( + Offset(toRight ? maxX : minX, value.dy + velocity.dy * flingSeconds), + ); + } +} + +class DraggableFloatingLayer extends StatefulWidget { + const DraggableFloatingLayer({ + super.key, + required this.storageKey, + required this.size, + required this.child, + this.onTap, + this.onDragStart, + this.onDragEnd, + this.edge = 12, + this.restingBottomGap = 96, + }); + + final String storageKey; + final Size size; + final Widget child; + final VoidCallback? onTap; + final VoidCallback? onDragStart; + final VoidCallback? onDragEnd; + final double edge; + final double restingBottomGap; + + @override + State createState() => _DraggableFloatingLayerState(); +} + +class _DraggableFloatingLayerState extends State + with TickerProviderStateMixin { + static final Map _remembered = {}; + + static final SpringDescription _spring = SpringDescription.withDampingRatio( + mass: 1, + stiffness: 520, + ratio: 1.0, + ); + + final ValueNotifier _offset = ValueNotifier(null); + + late final AnimationController _settle = + AnimationController.unbounded(vsync: this) + ..addListener(_onSettle) + ..addStatusListener(_onSettleStatus); + + late final AnimationController _lift = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 160), + reverseDuration: const Duration(milliseconds: 220), + ); + + FloatingDockGeometry _dock = const FloatingDockGeometry( + bounds: Size.zero, + size: Size.zero, + safeArea: EdgeInsets.zero, + ); + + Offset _from = Offset.zero; + Offset _to = Offset.zero; + Timer? _resizeSettle; + bool _dragging = false; + + @override + void initState() { + super.initState(); + _offset.value = _remembered[widget.storageKey]; + } + + @override + void didUpdateWidget(DraggableFloatingLayer old) { + super.didUpdateWidget(old); + if (widget.size == old.size || _dragging) return; + _resizeSettle?.cancel(); + _resizeSettle = Timer(const Duration(milliseconds: 90), _redock); + } + + @override + void dispose() { + _resizeSettle?.cancel(); + _settle.dispose(); + _lift.dispose(); + _offset.dispose(); + super.dispose(); + } + + Offset get _current => _dock.clamp(_offset.value ?? _dock.resting); + + void _redock() { + if (!mounted || _dragging || _dock.bounds.isEmpty) return; + _animateTo(_dock.snap(_current, Offset.zero), Offset.zero); + } + + void _animateTo(Offset target, Offset velocity) { + final start = _current; + final delta = target - start; + final distance = delta.distance; + if (distance < 0.5) { + _apply(target); + return; + } + final along = + (velocity.dx * delta.dx + velocity.dy * delta.dy) / + (distance * distance); + _from = start; + _to = target; + _settle.stop(); + _settle.value = 0; + _settle.animateWith( + SpringSimulation(_spring, 0, 1, along.clamp(-12.0, 12.0)), + ); + } + + void _onSettle() => _apply(Offset.lerp(_from, _to, _settle.value)!); + + void _onSettleStatus(AnimationStatus status) { + if (status.isAnimating) return; + _apply(_to); + } + + void _apply(Offset value) { + final position = _dock.clamp(value); + _offset.value = position; + _remembered[widget.storageKey] = position; + } + + void _onPanStart(DragStartDetails details) { + _settle.stop(); + _resizeSettle?.cancel(); + _dragging = true; + _lift.forward(); + Haptics.selection(); + widget.onDragStart?.call(); + } + + void _onPanUpdate(DragUpdateDetails details) => + _apply(_current + details.delta); + + void _onPanEnd(DragEndDetails details) { + final velocity = details.velocity.pixelsPerSecond; + _dragging = false; + _lift.reverse(); + widget.onDragEnd?.call(); + _animateTo(_dock.snap(_current, velocity), velocity); + } + + void _onPanCancel() { + if (!_dragging) return; + _dragging = false; + _lift.reverse(); + widget.onDragEnd?.call(); + _redock(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + _dock = FloatingDockGeometry( + bounds: constraints.biggest, + size: widget.size, + safeArea: MediaQuery.paddingOf(context), + edge: widget.edge, + restingBottomGap: widget.restingBottomGap, + ); + return ValueListenableBuilder( + valueListenable: _offset, + child: RepaintBoundary( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onTap, + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onPanCancel: _onPanCancel, + child: ScaleTransition( + scale: Tween(begin: 1, end: 1.06).animate( + CurvedAnimation(parent: _lift, curve: Curves.easeOutCubic), + ), + child: widget.child, + ), + ), + ), + builder: (context, offset, child) { + final position = _dock.clamp(offset ?? _dock.resting); + return Stack( + children: [Transform.translate(offset: position, child: child)], + ); + }, + ); + }, + ); + } +} diff --git a/lib/frontend/widgets/floating_call_badge.dart b/lib/frontend/widgets/floating_call_badge.dart new file mode 100644 index 0000000..b48ecca --- /dev/null +++ b/lib/frontend/widgets/floating_call_badge.dart @@ -0,0 +1,711 @@ +import 'dart:async'; +import 'dart:ui' show lerpDouble; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart' + show MediaStream, RTCVideoRenderer, RTCVideoViewObjectFit; +import 'package:material_symbols_icons/symbols.dart'; + +import '../../core/calls/active_call.dart'; +import '../../core/calls/call_session.dart'; +import '../../core/config/app_colors.dart'; +import '../../core/config/app_fonts.dart'; +import '../../core/utils/format.dart'; +import '../../core/utils/haptics.dart'; +import '../../l10n/app_localizations.dart'; +import '../../main.dart' show KometApp; +import '../screens/calls/call_screen.dart'; +import 'call_video_view.dart'; +import 'custom_notification.dart'; +import 'draggable_floating_layer.dart'; +import 'glossy_pill.dart'; +import 'lottie_slash_icon.dart'; +import 'small_spinner.dart'; + +class FloatingCallBadgeLayer extends StatelessWidget { + const FloatingCallBadgeLayer({super.key}); + + @override + Widget build(BuildContext context) { + final call = ActiveCall.instance; + return ValueListenableBuilder( + valueListenable: call.current, + builder: (context, active, _) { + if (active == null) return const SizedBox.shrink(); + return ValueListenableBuilder( + valueListenable: call.screenVisible, + builder: (context, onScreen, _) => onScreen + ? const SizedBox.shrink() + : _CallBadge(key: ObjectKey(active.session), call: active), + ); + }, + ); + } +} + +typedef _BadgeSnapshot = ({ + bool muted, + bool video, + bool speaking, + bool hasVideo, + bool reconnecting, + CallSessionState state, +}); + +class _CallBadge extends StatefulWidget { + const _CallBadge({super.key, required this.call}); + + final ActiveCallPresentation call; + + @override + State<_CallBadge> createState() => _CallBadgeState(); +} + +class _CallBadgeState extends State<_CallBadge> + with SingleTickerProviderStateMixin { + static const double _collapsedWidth = 120; + static const double _expandedWidth = 152; + static const double _collapsedHeight = 112; + static const double _expandedHeight = 160; + static const double _avatarSize = 56; + static const double _buttonSize = 38; + static const double _controlsWidth = _expandedWidth - 20; + static const Duration _autoCollapse = Duration(seconds: 4); + + late final AnimationController _reveal = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 260), + reverseDuration: const Duration(milliseconds: 200), + ); + + StreamSubscription? _stateSub; + StreamSubscription? _infoSub; + StreamSubscription? _remoteStreamSub; + StreamSubscription? _participantStreamSub; + Timer? _collapseTimer; + bool _chromeMounted = false; + _BadgeSnapshot? _rendered; + + RTCVideoRenderer? _renderer; + MediaStream? _videoStream; + bool _rendererPending = false; + bool _videoBusy = false; + + CallSession get _session => widget.call.session; + + @override + void initState() { + super.initState(); + _stateSub = _session.stateStream.listen((_) => _sync()); + _infoSub = _session.infoUpdates.listen((_) => _sync()); + _remoteStreamSub = _session.remoteStreamStream.listen((_) => _sync()); + _participantStreamSub = _session.participantStreamUpdates.listen( + (_) => _sync(), + ); + _reveal.addStatusListener(_onRevealStatus); + _attachVideo(); + _rendered = _snapshot(); + } + + @override + void dispose() { + _stateSub?.cancel(); + _infoSub?.cancel(); + _remoteStreamSub?.cancel(); + _participantStreamSub?.cancel(); + _collapseTimer?.cancel(); + _renderer?.srcObject = null; + _renderer?.dispose(); + _reveal.dispose(); + super.dispose(); + } + + void _sync() { + if (!mounted) return; + _attachVideo(); + final next = _snapshot(); + if (next == _rendered) return; + _rendered = next; + setState(() {}); + } + + void _redraw() { + if (!mounted) return; + _rendered = _snapshot(); + setState(() {}); + } + + _BadgeSnapshot _snapshot() => ( + muted: _session.isMuted, + video: _session.localVideo, + speaking: _peerSpeaking, + hasVideo: _videoStream != null, + reconnecting: _session.isReconnecting, + state: _session.currentState, + ); + + MediaStream? _pickVideoStream() { + final session = _session; + for (final participant in session.participants) { + if (participant.isSelf) continue; + if (!participant.videoEnabled && !participant.screenSharing) continue; + final stream = session.streamOf(participant.id); + if (stream != null && stream.getVideoTracks().isNotEmpty) return stream; + } + final remote = session.remoteStream; + if (session.peerVideo && + remote != null && + remote.getVideoTracks().isNotEmpty) { + return remote; + } + return null; + } + + void _attachVideo() { + final next = _pickVideoStream(); + if (identical(next, _videoStream)) return; + _videoStream = next; + final renderer = _renderer; + if (renderer != null) { + renderer.srcObject = next; + return; + } + if (next != null && !_rendererPending) { + _rendererPending = true; + unawaited(_createRenderer()); + } + } + + Future _createRenderer() async { + final renderer = RTCVideoRenderer(); + try { + await renderer.initialize(); + } catch (_) { + _rendererPending = false; + return; + } + _rendererPending = false; + if (!mounted || _videoStream == null) { + await renderer.dispose(); + return; + } + renderer.srcObject = _videoStream; + _renderer = renderer; + _redraw(); + } + + void _restartCollapseTimer() { + _collapseTimer?.cancel(); + _collapseTimer = Timer(_autoCollapse, _collapse); + } + + void _mountChrome() { + if (_chromeMounted) return; + setState(() => _chromeMounted = true); + } + + void _onRevealStatus(AnimationStatus status) { + if (status != AnimationStatus.dismissed || !_chromeMounted) return; + setState(() => _chromeMounted = false); + } + + void _expandControls() { + _mountChrome(); + _reveal.forward(); + _restartCollapseTimer(); + } + + void _collapse() { + _collapseTimer?.cancel(); + if (mounted) _reveal.reverse(); + } + + void _toggleControls() { + Haptics.tap(); + if (_reveal.value > 0.5) { + _collapse(); + } else { + _expandControls(); + } + } + + void _onDragStart() { + _collapseTimer?.cancel(); + _mountChrome(); + _reveal.forward(); + } + + void _onDragEnd() => _restartCollapseTimer(); + + Future _toggleMute() async { + Haptics.tap(); + _restartCollapseTimer(); + await _session.setMuted(!_session.isMuted); + _redraw(); + } + + Future _toggleVideo() async { + if (_videoBusy) return; + Haptics.tap(); + _restartCollapseTimer(); + final l10n = AppLocalizations.of(context)!; + setState(() => _videoBusy = true); + await WidgetsBinding.instance.endOfFrame; + try { + await _session.setVideoEnabled(!_session.localVideo); + } catch (e) { + _notify(l10n.callCameraUnavailable(e)); + } finally { + _videoBusy = false; + _redraw(); + } + } + + void _notify(String message) { + final overlay = KometApp.navigatorKey.currentState?.overlay; + if (overlay == null) return; + showCustomNotificationOnOverlay(overlay, message); + } + + Future _hangup() async { + Haptics.medium(); + _collapseTimer?.cancel(); + await _session.hangup(); + } + + void _openCall() { + Haptics.tap(); + _collapse(); + final navigator = KometApp.navigatorKey.currentState; + if (navigator == null) return; + navigator.push( + MaterialPageRoute( + builder: (_) => CallScreen( + name: widget.call.name, + avatarUrl: widget.call.avatarUrl, + session: _session, + isGroup: widget.call.isGroup, + ), + ), + ); + } + + bool get _peerSpeaking => + _session.participants.any((p) => !p.isSelf && _session.isSpeaking(p.id)); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final cs = theme.colorScheme; + final l10n = AppLocalizations.of(context)!; + final text = (theme.textTheme.bodyMedium ?? const TextStyle()).copyWith( + color: cs.onSurface, + ); + final title = _title(cs, l10n); + final face = _face(cs); + final expand = _chromeMounted ? _expandButton(cs, l10n) : null; + final controls = _chromeMounted ? _controls(cs, l10n) : null; + return AnimatedBuilder( + animation: _reveal, + builder: (context, _) { + final t = Curves.easeOutCubic.transform(_reveal.value); + final size = Size( + lerpDouble(_collapsedWidth, _expandedWidth, t)!, + lerpDouble(_collapsedHeight, _expandedHeight, t)!, + ); + return DraggableFloatingLayer( + storageKey: 'call_badge', + size: size, + onTap: _toggleControls, + onDragStart: _onDragStart, + onDragEnd: _onDragEnd, + child: SizedBox.fromSize( + size: size, + child: _card( + cs, + t, + text: text, + title: title, + face: face, + expand: expand, + controls: controls, + ), + ), + ); + }, + ); + } + + Widget _card( + ColorScheme cs, + double t, { + required TextStyle text, + required Widget title, + required Widget face, + required Widget? expand, + required Widget? controls, + }) { + final radius = BorderRadius.circular(26); + return DefaultTextStyle( + style: text, + child: GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: radius, + depth: 10, + child: ClipRRect( + borderRadius: radius, + child: Stack( + children: [ + Positioned( + left: 12, + right: lerpDouble(12, 44, t)!, + top: 10, + child: title, + ), + if (expand != null) + Positioned(top: 7, right: 7, child: _fade(t, expand)), + Positioned( + left: 0, + right: 0, + top: 44, + bottom: lerpDouble(12, 58, t)!, + child: Center(child: face), + ), + if (controls != null) + Positioned( + left: 0, + right: 0, + bottom: 10, + child: _fade( + t, + SizedBox( + height: _buttonSize, + child: OverflowBox( + minWidth: _controlsWidth, + maxWidth: _controlsWidth, + minHeight: _buttonSize, + maxHeight: _buttonSize, + alignment: Alignment.center, + child: controls, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _fade(double t, Widget child) => IgnorePointer( + ignoring: t < 0.5, + child: Opacity( + opacity: t.clamp(0.0, 1.0), + child: Transform.scale(scale: lerpDouble(0.82, 1, t)!, child: child), + ), + ); + + Widget _title(ColorScheme cs, AppLocalizations l10n) { + final name = widget.call.name.isEmpty + ? l10n.callUnknownName + : widget.call.name; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.onSurface, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + _CallStatusLine(session: _session, color: cs.onSurfaceVariant), + ], + ); + } + + Widget _expandButton(ColorScheme cs, AppLocalizations l10n) { + return Semantics( + label: l10n.callTooltipExpand, + button: true, + child: SizedBox.square( + dimension: 30, + child: GlossyPill( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(15), + depth: 6, + onTap: _openCall, + child: Center( + child: Icon( + Symbols.open_in_full, + size: 16, + weight: 600, + color: cs.onSurface, + ), + ), + ), + ), + ); + } + + Widget _face(ColorScheme cs) { + final muted = _session.isMuted; + final renderer = _renderer; + final showVideo = _videoStream != null && renderer != null; + return SizedBox.square( + dimension: _avatarSize, + child: Stack( + clipBehavior: Clip.none, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 220), + width: _avatarSize, + height: _avatarSize, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cs.surfaceContainerHighest, + border: Border.all( + color: _peerSpeaking + ? kSuccessGreen + : Colors.white.withValues(alpha: 0.10), + width: _peerSpeaking ? 2.5 : 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.32), + blurRadius: 16, + offset: const Offset(0, 6), + ), + ], + ), + child: showVideo + ? CallVideoView( + renderer: renderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + placeholder: _avatar(cs), + ) + : _avatar(cs), + ), + if (muted) + Positioned( + right: -2, + bottom: -2, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + shape: BoxShape.circle, + border: Border.all(color: cs.surfaceContainerHigh, width: 2), + ), + child: Icon( + Symbols.mic_off, + size: 12, + fill: 1, + color: cs.onSurfaceVariant, + ), + ), + ), + ], + ), + ); + } + + Widget _avatar(ColorScheme cs) { + final url = widget.call.avatarUrl; + if (url == null || url.isEmpty) return _avatarFallback(cs); + return CachedNetworkImage( + imageUrl: url, + fit: BoxFit.cover, + memCacheWidth: 192, + memCacheHeight: 192, + errorWidget: (_, _, _) => _avatarFallback(cs), + ); + } + + Widget _avatarFallback(ColorScheme cs) { + final name = widget.call.name; + final letter = (name.isEmpty ? '?' : name[0]).toUpperCase(); + return ColoredBox( + color: cs.primaryContainer, + child: Center( + child: Text( + letter, + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: _avatarSize * 0.38, + fontWeight: FontWeight.w600, + fontFamily: displayFontOf(context), + ), + ), + ), + ); + } + + Widget _controls(ColorScheme cs, AppLocalizations l10n) { + final muted = _session.isMuted; + final video = _session.localVideo; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _BadgeButton( + size: _buttonSize, + background: muted ? cs.primary : cs.surfaceContainerHighest, + label: muted ? l10n.callUnmute : l10n.callMute, + onTap: _toggleMute, + child: LottieSlashIcon( + asset: 'assets/lottie/ic_mic_on_to_off.json', + slashed: muted, + color: muted ? cs.onPrimary : cs.onSurface, + size: 20, + ), + ), + _BadgeButton( + size: _buttonSize, + background: video ? cs.primary : cs.surfaceContainerHighest, + label: l10n.callVideoLabel, + onTap: _toggleVideo, + child: _videoBusy + ? SmallSpinner( + size: 16, + color: video ? cs.onPrimary : cs.onSurface, + ) + : LottieSlashIcon( + asset: 'assets/lottie/ic_videocam_on_to_off.json', + slashed: !video, + color: video ? cs.onPrimary : cs.onSurface, + size: 20, + ), + ), + _BadgeButton( + size: _buttonSize, + background: kDangerRed, + label: l10n.callEndButton, + onTap: _hangup, + child: const Icon( + Symbols.call_end, + size: 20, + fill: 1, + color: Colors.white, + ), + ), + ], + ); + } +} + +class _BadgeButton extends StatelessWidget { + const _BadgeButton({ + required this.size, + required this.background, + required this.label, + required this.onTap, + required this.child, + }); + + final double size; + final Color background; + final String label; + final VoidCallback onTap; + final Widget child; + + @override + Widget build(BuildContext context) { + return Semantics( + label: label, + button: true, + child: SizedBox.square( + dimension: size, + child: GlossyPill( + color: background, + borderRadius: BorderRadius.circular(size / 2), + depth: 7, + onTap: onTap, + child: Center(child: child), + ), + ), + ); + } +} + +class _CallStatusLine extends StatefulWidget { + const _CallStatusLine({required this.session, required this.color}); + + final CallSession session; + final Color color; + + @override + State<_CallStatusLine> createState() => _CallStatusLineState(); +} + +class _CallStatusLineState extends State<_CallStatusLine> { + Timer? _ticker; + + @override + void initState() { + super.initState(); + _syncTicker(); + } + + @override + void didUpdateWidget(_CallStatusLine old) { + super.didUpdateWidget(old); + _syncTicker(); + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + void _syncTicker() { + final counting = + widget.session.currentState == CallSessionState.active && + !widget.session.isReconnecting; + if (counting == (_ticker != null)) return; + if (!counting) { + _ticker?.cancel(); + _ticker = null; + return; + } + _ticker = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) setState(() {}); + }); + } + + String _label(AppLocalizations l10n) { + final session = widget.session; + if (session.isReconnecting) return l10n.callStatusConnecting; + return switch (session.currentState) { + CallSessionState.connecting => l10n.callStatusConnecting, + CallSessionState.ringing => l10n.callStatusRinging, + CallSessionState.ended => l10n.callStatusEnded, + CallSessionState.active => formatSecondsMmSs( + session.elapsedSeconds, + padMinutes: true, + ), + }; + } + + @override + Widget build(BuildContext context) { + return Text( + _label(AppLocalizations.of(context)!), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: widget.color, + fontSize: 11, + fontWeight: FontWeight.w500, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ); + } +} diff --git a/lib/frontend/widgets/floating_video_note.dart b/lib/frontend/widgets/floating_video_note.dart index f53d5bd..af22734 100644 --- a/lib/frontend/widgets/floating_video_note.dart +++ b/lib/frontend/widgets/floating_video_note.dart @@ -5,6 +5,7 @@ import 'package:video_player/video_player.dart'; import '../../core/media/media_playback.dart'; import '../../core/utils/haptics.dart'; +import 'draggable_floating_layer.dart'; class FloatingVideoNoteLayer extends StatelessWidget { const FloatingVideoNoteLayer({super.key}); @@ -27,45 +28,16 @@ class FloatingVideoNoteLayer extends StatelessWidget { } } -class _DraggableNote extends StatefulWidget { +class _DraggableNote extends StatelessWidget { const _DraggableNote({required this.track}); final VideoNoteTrack track; - @override - State<_DraggableNote> createState() => _DraggableNoteState(); -} - -class _DraggableNoteState extends State<_DraggableNote> { static const double _size = 96; - static const double _edge = 12; - - static Offset? _saved; - - final ValueNotifier _offset = ValueNotifier(null); - - @override - void dispose() { - _offset.dispose(); - super.dispose(); - } - - Offset _clamp(Offset value, Size bounds, EdgeInsets safe) { - final minX = _edge; - final maxX = math.max(minX, bounds.width - _size - _edge); - final minY = safe.top + _edge; - final maxY = math.max(minY, bounds.height - _size - safe.bottom - _edge); - return Offset(value.dx.clamp(minX, maxX), value.dy.clamp(minY, maxY)); - } - - Offset _initial(Size bounds, EdgeInsets safe) => Offset( - bounds.width - _size - _edge, - bounds.height - _size - safe.bottom - 96, - ); void _toggle() { Haptics.tap(); - final controller = widget.track.controller; + final controller = track.controller; if (controller.value.isPlaying) { controller.pause(); } else { @@ -73,46 +45,13 @@ class _DraggableNoteState extends State<_DraggableNote> { } } - void _drag(Offset delta, Size bounds, EdgeInsets safe) { - final current = _clamp( - _offset.value ?? _saved ?? _initial(bounds, safe), - bounds, - safe, - ); - final next = _clamp(current + delta, bounds, safe); - _offset.value = next; - _saved = next; - } - @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final bounds = constraints.biggest; - final safe = MediaQuery.paddingOf(context); - return ValueListenableBuilder( - valueListenable: _offset, - child: RepaintBoundary( - child: GestureDetector( - onTap: _toggle, - onPanUpdate: (details) => _drag(details.delta, bounds, safe), - child: _NoteCircle(track: widget.track, size: _size), - ), - ), - builder: (context, offset, child) { - final position = _clamp( - offset ?? _saved ?? _initial(bounds, safe), - bounds, - safe, - ); - return Stack( - children: [ - Positioned(left: position.dx, top: position.dy, child: child!), - ], - ); - }, - ); - }, + return DraggableFloatingLayer( + storageKey: 'video_note', + size: const Size(_size, _size), + onTap: _toggle, + child: _NoteCircle(track: track, size: _size), ); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2405fd6..55087b4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -389,6 +389,7 @@ "callParticipantYou": "You", "callParticipantFallback": "Participant", "callTooltipMinimize": "Minimize", + "callTooltipExpand": "Expand", "callTooltipKometHub": "Komet", "callInfoTitle": "About call", "callPeerMicOff": "Microphone off", @@ -405,6 +406,7 @@ "callUnmute": "Unmute", "callMute": "Mute", "callEndButton": "End", + "callCameraUnavailable": "Camera unavailable: {error}", "callInfoClient": "Client", "callInfoPlatform": "Platform", "callInfoCountry": "Country", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a0db0ca..0b021b1 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2072,6 +2072,12 @@ abstract class AppLocalizations { /// **'Minimize'** String get callTooltipMinimize; + /// No description provided for @callTooltipExpand. + /// + /// In en, this message translates to: + /// **'Expand'** + String get callTooltipExpand; + /// No description provided for @callTooltipKometHub. /// /// In en, this message translates to: @@ -2168,6 +2174,12 @@ abstract class AppLocalizations { /// **'End'** String get callEndButton; + /// No description provided for @callCameraUnavailable. + /// + /// In en, this message translates to: + /// **'Camera unavailable: {error}'** + String callCameraUnavailable(Object error); + /// No description provided for @callInfoClient. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 15f141e..f78702c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1051,6 +1051,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get callTooltipMinimize => 'Minimize'; + @override + String get callTooltipExpand => 'Expand'; + @override String get callTooltipKometHub => 'Komet'; @@ -1099,6 +1102,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get callEndButton => 'End'; + @override + String callCameraUnavailable(Object error) { + return 'Camera unavailable: $error'; + } + @override String get callInfoClient => 'Client'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ae9364b..7874df6 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1053,6 +1053,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get callTooltipMinimize => 'Свернуть'; + @override + String get callTooltipExpand => 'Развернуть'; + @override String get callTooltipKometHub => 'Komet'; @@ -1101,6 +1104,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get callEndButton => 'Завершить'; + @override + String callCameraUnavailable(Object error) { + return 'Камера недоступна: $error'; + } + @override String get callInfoClient => 'Клиент'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 53f2603..6ba9e40 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -347,6 +347,7 @@ "callParticipantYou": "Вы", "callParticipantFallback": "Участник", "callTooltipMinimize": "Свернуть", + "callTooltipExpand": "Развернуть", "callTooltipKometHub": "Komet", "callInfoTitle": "О звонке", "callPeerMicOff": "Микрофон выключен", @@ -363,6 +364,7 @@ "callUnmute": "Вкл. звук", "callMute": "Выкл. звук", "callEndButton": "Завершить", + "callCameraUnavailable": "Камера недоступна: {error}", "callInfoClient": "Клиент", "callInfoPlatform": "Платформа", "callInfoCountry": "Страна", diff --git a/lib/main.dart b/lib/main.dart index 96c2841..8caa05c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -91,6 +91,7 @@ import 'frontend/widgets/custom_notification.dart'; import 'frontend/widgets/liquid_glass.dart'; import 'frontend/widgets/small_spinner.dart'; import 'frontend/widgets/theme_reveal.dart'; +import 'frontend/widgets/floating_call_badge.dart'; import 'frontend/widgets/floating_video_note.dart'; final api = Api(); @@ -1033,6 +1034,9 @@ class KometAppState extends State const Positioned.fill( child: FloatingVideoNoteLayer(), ), + const Positioned.fill( + child: FloatingCallBadgeLayer(), + ), if (fpsOn) const FpsOverlayLayer(), ], ); diff --git a/test/app_icon_names_test.dart b/test/app_icon_names_test.dart new file mode 100644 index 0000000..aa682ff --- /dev/null +++ b/test/app_icon_names_test.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:komet/core/config/app_icon.dart'; + +Set _iosAlternateIcons() { + final plist = File('ios/Runner/Info.plist').readAsStringSync(); + final anchor = plist.indexOf('CFBundleAlternateIcons'); + expect( + anchor, + isNonNegative, + reason: 'в Info.plist нет альтернативных иконок', + ); + final tokens = RegExp( + r'||([^<]*)', + ).allMatches(plist.substring(anchor)); + + final names = {}; + var depth = 0; + var entered = false; + for (final token in tokens) { + final text = token.group(0)!; + if (text == '') { + depth++; + entered = true; + } else if (text == '') { + depth--; + if (entered && depth == 0) break; + } else if (entered && depth == 1) { + names.add(token.group(1)!); + } + } + return names; +} + +Set _androidComponents() { + final manifest = File( + 'android/app/src/main/AndroidManifest.xml', + ).readAsStringSync(); + return RegExp( + r'android:name="ru\.komet\.app\.(\w+)"', + ).allMatches(manifest).map((m) => m.group(1)!).toSet(); +} + +Set _androidIconKeys() { + final source = File( + 'android/app/src/main/kotlin/ru/komet/app/MainActivity.kt', + ).readAsStringSync(); + final start = source.indexOf('iconComponents = mapOf('); + expect(start, isNonNegative, reason: 'в MainActivity нет карты иконок'); + final body = source.substring(start, source.indexOf(')', start)); + return RegExp(r'"(\w+)" to').allMatches(body).map((m) => m.group(1)!).toSet(); +} + +void main() { + test('дефолтная иконка на iOS — это primary, а не альтернативная', () { + expect( + AppIcon.defaultIcon.iosAlternateName, + isNull, + reason: 'setAlternateIconName ждёт nil, любое имя тут — APPLY_FAILED', + ); + }); + + test('каждая альтернативная иконка объявлена в Info.plist', () { + final declared = _iosAlternateIcons(); + for (final icon in AppIcon.values) { + final name = icon.iosAlternateName; + if (name == null) continue; + expect( + declared, + contains(name), + reason: 'иконка ${icon.id} шлёт в iOS имя $name', + ); + } + }); + + test('каждый android alias объявлен в манифесте и в MainActivity', () { + final components = _androidComponents(); + final keys = _androidIconKeys(); + for (final icon in AppIcon.values) { + expect( + components, + contains(icon.androidAlias), + reason: 'иконка ${icon.id} шлёт в Android имя ${icon.androidAlias}', + ); + expect(keys, contains(icon.androidAlias)); + } + }); + + test('имена иконок не пересекаются между платформами по смыслу', () { + final ids = AppIcon.values.map((i) => i.id).toSet(); + expect(ids.length, AppIcon.values.length); + final aliases = AppIcon.values.map((i) => i.androidAlias).toSet(); + expect(aliases.length, AppIcon.values.length); + }); +} diff --git a/test/floating_call_badge_test.dart b/test/floating_call_badge_test.dart new file mode 100644 index 0000000..62576cd --- /dev/null +++ b/test/floating_call_badge_test.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:komet/core/calls/active_call.dart'; +import 'package:komet/core/calls/call_session.dart'; +import 'package:komet/core/calls/ws2_signaling.dart'; +import 'package:komet/frontend/widgets/floating_call_badge.dart'; +import 'package:komet/l10n/app_localizations.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +const _name = 'Тестовый собеседник'; + +CallSession _session() => CallSession( + ws2Config: Ws2Config(uri: Uri.parse('wss://calls.invalid/ws2'), userId: 42), + role: CallRole.caller, +); + +Widget _host() => MaterialApp( + locale: const Locale('ru'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + builder: (context, child) => Stack( + children: [ + child!, + const Positioned.fill(child: FloatingCallBadgeLayer()), + ], + ), + home: const Scaffold(body: SizedBox.expand()), +); + +final Finder _hangup = find.byIcon(Symbols.call_end); + +double _controlsOpacity(WidgetTester tester) => tester + .widget( + find.ancestor(of: _hangup, matching: find.byType(Opacity)).first, + ) + .opacity; + +Finder get _badge => find.descendant( + of: find.byType(FloatingCallBadgeLayer), + matching: find.byType(ScaleTransition), +); + +Offset _badgeCorner(WidgetTester tester) => tester.getTopLeft(_badge); + +void main() { + tearDown(() { + ActiveCall.instance.detach(); + while (ActiveCall.instance.screenVisible.value) { + ActiveCall.instance.leaveScreen(); + } + }); + + testWidgets('бейджик виден только пока экран звонка закрыт', (tester) async { + await tester.pumpWidget(_host()); + expect(find.text(_name), findsNothing); + + ActiveCall.instance.attach(session: _session(), name: _name); + await tester.pump(); + expect(find.text(_name), findsOneWidget); + + ActiveCall.instance.enterScreen(); + await tester.pump(); + expect(find.text(_name), findsNothing); + + ActiveCall.instance.leaveScreen(); + await tester.pump(); + expect(find.text(_name), findsOneWidget); + + ActiveCall.instance.detach(); + await tester.pump(); + expect(find.text(_name), findsNothing); + }); + + testWidgets('тап раскрывает кнопки и повторный тап их прячет', ( + tester, + ) async { + ActiveCall.instance.attach(session: _session(), name: _name); + await tester.pumpWidget(_host()); + + expect( + _hangup, + findsNothing, + reason: 'свёрнутый бейджик не держит кнопки в дереве', + ); + + await tester.tap(find.text(_name)); + await tester.pumpAndSettle(); + expect(_hangup, findsOneWidget); + expect(_controlsOpacity(tester), 1); + + await tester.tap(find.text(_name)); + await tester.pumpAndSettle(); + expect(_hangup, findsNothing); + }); + + testWidgets('кнопки прячутся сами, если бейджик не трогают', (tester) async { + ActiveCall.instance.attach(session: _session(), name: _name); + await tester.pumpWidget(_host()); + + await tester.tap(find.text(_name)); + await tester.pumpAndSettle(); + expect(_controlsOpacity(tester), 1); + + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + expect(_hangup, findsNothing); + }); + + testWidgets('перетаскивание липнет к краю экрана', (tester) async { + ActiveCall.instance.attach(session: _session(), name: _name); + await tester.pumpWidget(_host()); + + final screen = tester.getSize(find.byType(FloatingCallBadgeLayer)).width; + final start = _badgeCorner(tester); + final width = tester.getSize(_badge).width; + expect(start.dx + width, moreOrLessEquals(screen - 12)); + + await tester.timedDrag( + find.text(_name), + const Offset(-260, -120), + const Duration(milliseconds: 300), + ); + await tester.pumpAndSettle(); + + expect(_badgeCorner(tester).dx, moreOrLessEquals(12)); + expect(_badgeCorner(tester).dy, lessThan(start.dy)); + }); +} diff --git a/test/floating_dock_geometry_test.dart b/test/floating_dock_geometry_test.dart new file mode 100644 index 0000000..b8be72a --- /dev/null +++ b/test/floating_dock_geometry_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:komet/frontend/widgets/draggable_floating_layer.dart'; + +const _screen = Size(400, 800); +const _safe = EdgeInsets.only(top: 48, bottom: 24); + +FloatingDockGeometry _dockOf(Size size) => + FloatingDockGeometry(bounds: _screen, size: size, safeArea: _safe); + +final _badge = _dockOf(const Size(120, 118)); + +void main() { + test('бейджик стартует у правого края над нижней панелью', () { + final resting = _badge.resting; + expect(resting.dx, _screen.width - 120 - 12); + expect(resting.dy, _screen.height - 118 - 24 - 96); + }); + + test('позиция зажата в безопасной зоне', () { + expect(_badge.clamp(const Offset(-500, -500)), Offset(12, 48 + 12)); + expect( + _badge.clamp(const Offset(9999, 9999)), + Offset(_screen.width - 120 - 12, _screen.height - 118 - 24 - 12), + ); + }); + + test('без броска липнет к ближнему краю', () { + final left = _badge.snap(const Offset(40, 300), Offset.zero); + expect(left.dx, 12); + expect( + left.dy, + 300, + reason: 'высота не меняется без вертикальной скорости', + ); + + final right = _badge.snap(const Offset(250, 300), Offset.zero); + expect(right.dx, _screen.width - 120 - 12); + }); + + test('бросок перебивает ближний край', () { + final flungRight = _badge.snap(const Offset(40, 300), const Offset(900, 0)); + expect(flungRight.dx, _screen.width - 120 - 12); + + final flungLeft = _badge.snap( + const Offset(250, 300), + const Offset(-900, 0), + ); + expect(flungLeft.dx, 12); + }); + + test('слабый рывок не считается броском', () { + final weak = _badge.snap(const Offset(40, 300), const Offset(200, 0)); + expect(weak.dx, 12); + }); + + test('вертикальный бросок продолжает движение, но не за экран', () { + final down = _badge.snap(const Offset(250, 300), const Offset(0, 1000)); + expect(down.dy, 300 + 1000 * FloatingDockGeometry.flingSeconds); + + final overshoot = _badge.snap( + const Offset(250, 600), + const Offset(0, 5000), + ); + expect(overshoot.dy, _screen.height - 118 - 24 - 12); + + final up = _badge.snap(const Offset(250, 100), const Offset(0, -5000)); + expect(up.dy, 48 + 12); + }); + + test('раскрытие бейджика удерживает правый край на месте', () { + final collapsed = _badge; + final expanded = _dockOf(const Size(152, 166)); + + final docked = collapsed.resting; + final grown = expanded.clamp(docked); + + expect( + grown.dx + 152, + docked.dx + 120, + reason: 'кнопки должны вырастать влево, а не выезжать за экран', + ); + }); + + test('узкий экран не переворачивает границы', () { + final tiny = FloatingDockGeometry( + bounds: const Size(100, 200), + size: const Size(120, 118), + safeArea: EdgeInsets.zero, + ); + expect(tiny.maxX, tiny.minX); + expect(tiny.clamp(const Offset(50, 50)).dx, 12); + }); +} diff --git a/tool/make_morph_icons.py b/tool/make_morph_icons.py index f468c8f..ae06090 100644 --- a/tool/make_morph_icons.py +++ b/tool/make_morph_icons.py @@ -657,7 +657,9 @@ def pair_glyphs(from_cp, to_cp, count, fill=0.0): MIC = 0xE31D +MIC_OFF = 0xE02B CAM = 0xE04B +CAM_OFF = 0xE04C SEND = 0xE163 VOLUME_UP = 0xE050 VOLUME_OFF = 0xE04F @@ -993,6 +995,18 @@ SLASH_SPECS = [ fill=1.0, scale=[(0, 100), (11, 92), (DUR, 100)], ), + dict( + name='ic_mic_on_to_off', + plain_cp=MIC, slashed_cp=MIC_OFF, + fill=1.0, + scale=[(0, 100), (11, 92), (DUR, 100)], + ), + dict( + name='ic_videocam_on_to_off', + plain_cp=CAM, slashed_cp=CAM_OFF, + fill=1.0, + scale=[(0, 100), (11, 92), (DUR, 100)], + ), ]