feat: view polls
chore: remove unused import in send_file_dialog
|
|
@ -0,0 +1 @@
|
|||
assets/banner.png
assetassets/banner.pngassets/banner_transparent.png
assetassets/banner_transparent.pngassets/favicon.png
assetassets/favicon.pngassets/info-logo.png
assetassets/info-logo.pngassets/js/olm.zip
assetassets/js/olm.zipassets/logo.png
assetassets/logo.pngassets/logo.svg
assetassets/logo.svgassets/logo_transparent.png
assetassets/logo_transparent.pngassets/sas-emoji.json
assetassets/sas-emoji.jsonassets/sounds/call.ogg
assetassets/sounds/call.oggassets/sounds/notification.ogg
assetassets/sounds/notification.oggassets/sounds/phone.ogg
assetassets/sounds/phone.ogg2packages/cupertino_icons/assets/CupertinoIcons.ttf
asset2packages/cupertino_icons/assets/CupertinoIcons.ttf4packages/flutter_map/lib/assets/flutter_map_logo.png
asset4packages/flutter_map/lib/assets/flutter_map_logo.png2packages/handy_window/assets/handy-window-dark.css
asset2packages/handy_window/assets/handy-window-dark.css-packages/handy_window/assets/handy-window.css
asset-packages/handy_window/assets/handy-window.css(packages/material/lib/fonts/material.ttf
asset(packages/material/lib/fonts/material.ttf7packages/record_web/assets/js/record.fixwebmduration.js
asset7packages/record_web/assets/js/record.fixwebmduration.js/packages/record_web/assets/js/record.worklet.js
asset/packages/record_web/assets/js/record.worklet.js)packages/wakelock_plus/assets/no_sleep.js
asset)packages/wakelock_plus/assets/no_sleep.js
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"assets/banner.png":["assets/banner.png"],"assets/banner_transparent.png":["assets/banner_transparent.png"],"assets/favicon.png":["assets/favicon.png"],"assets/info-logo.png":["assets/info-logo.png"],"assets/js/olm.zip":["assets/js/olm.zip"],"assets/logo.png":["assets/logo.png"],"assets/logo.svg":["assets/logo.svg"],"assets/logo_transparent.png":["assets/logo_transparent.png"],"assets/sas-emoji.json":["assets/sas-emoji.json"],"assets/sounds/call.ogg":["assets/sounds/call.ogg"],"assets/sounds/notification.ogg":["assets/sounds/notification.ogg"],"assets/sounds/phone.ogg":["assets/sounds/phone.ogg"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"],"packages/flutter_map/lib/assets/flutter_map_logo.png":["packages/flutter_map/lib/assets/flutter_map_logo.png"],"packages/handy_window/assets/handy-window-dark.css":["packages/handy_window/assets/handy-window-dark.css"],"packages/handy_window/assets/handy-window.css":["packages/handy_window/assets/handy-window.css"],"packages/material/lib/fonts/material.ttf":["packages/material/lib/fonts/material.ttf"],"packages/record_web/assets/js/record.fixwebmduration.js":["packages/record_web/assets/js/record.fixwebmduration.js"],"packages/record_web/assets/js/record.worklet.js":["packages/record_web/assets/js/record.worklet.js"],"packages/wakelock_plus/assets/no_sleep.js":["packages/wakelock_plus/assets/no_sleep.js"]}
|
||||
|
|
@ -0,0 +1 @@
|
|||
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]},{"family":"packages/material/Material","fonts":[{"asset":"packages/material/lib/fonts/material.ttf"}]}]
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"format-version":[1,0,0],"native-assets":{}}
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1 @@
|
|||
Not Found
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70.82 81.78">
|
||||
<defs>
|
||||
<linearGradient id="a" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#406FBF" />
|
||||
<stop offset="100%" stop-color="#23509D" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill-rule="evenodd"
|
||||
d="M70.82 44.98v16.36l-17.7 10.22-17.71 10.22-2.59-1.5L0 61.34V20.45l17.7-10.23L35.41 0l28.33 16.36 4.49 2.59 2.59 1.5v8.17L35.41 49.07l-7.08-4.09V36.8l21.25-12.27-14.17-8.17-21.25 12.26v24.54l21.25 12.26.03.02.02-.05z"
|
||||
style="fill:url(#a)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1 @@
|
|||
window{background-color:#252525}window.csd.unified decoration-overlay{box-shadow:none}window.csd.unified:not(.solid-csd):not(.fullscreen):not(.maximized) decoration-overlay{box-shadow:inset 0 0 0 1px rgba(255,255,255,.07)}window.csd.unified decoration{box-shadow:0 3px 9px 1px rgba(0,0,0,.5)}window.csd.unified decoration:backdrop{box-shadow:0 3px 9px 1px rgba(0,0,0,0),0 2px 6px 2px rgba(0,0,0,.2)}headerbar{min-height:47px;background:#303030;box-shadow:inset 0 -1px rgba(0,0,0,.36);border:none}headerbar:backdrop{background:#242424}button.titlebutton{padding:4px;margin:1px}
|
||||
|
|
@ -0,0 +1 @@
|
|||
window{background-color:#fff}window.csd.unified decoration-overlay{box-shadow:none}window.csd.unified:not(.solid-csd):not(.fullscreen):not(.maximized) decoration-overlay{box-shadow:inset 0 0 0 1px rgba(255,255,255,.07)}window.csd.unified decoration{box-shadow:0 3px 9px 1px rgba(0,0,0,.5)}window.csd.unified decoration:backdrop{box-shadow:0 3px 9px 1px rgba(0,0,0,0),0 2px 6px 2px rgba(0,0,0,.2)}headerbar{min-height:47px;background:#ebebeb;box-shadow:inset 0 -1px rgba(0,0,0,.07);border:none}headerbar:backdrop{background:#fafafa}button.titlebutton{padding:4px;margin:1px}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
(function (name, definition) {
|
||||
window.jsFixWebmDuration = definition();
|
||||
})('fix-webm-duration', function () {
|
||||
/*
|
||||
* This is the list of possible WEBM file sections by their IDs.
|
||||
* Possible types: Container, Binary, Uint, Int, String, Float, Date
|
||||
*/
|
||||
var sections = {
|
||||
0xa45dfa3: { name: 'EBML', type: 'Container' },
|
||||
0x286: { name: 'EBMLVersion', type: 'Uint' },
|
||||
0x2f7: { name: 'EBMLReadVersion', type: 'Uint' },
|
||||
0x2f2: { name: 'EBMLMaxIDLength', type: 'Uint' },
|
||||
0x2f3: { name: 'EBMLMaxSizeLength', type: 'Uint' },
|
||||
0x282: { name: 'DocType', type: 'String' },
|
||||
0x287: { name: 'DocTypeVersion', type: 'Uint' },
|
||||
0x285: { name: 'DocTypeReadVersion', type: 'Uint' },
|
||||
0x6c: { name: 'Void', type: 'Binary' },
|
||||
0x3f: { name: 'CRC-32', type: 'Binary' },
|
||||
0xb538667: { name: 'SignatureSlot', type: 'Container' },
|
||||
0x3e8a: { name: 'SignatureAlgo', type: 'Uint' },
|
||||
0x3e9a: { name: 'SignatureHash', type: 'Uint' },
|
||||
0x3ea5: { name: 'SignaturePublicKey', type: 'Binary' },
|
||||
0x3eb5: { name: 'Signature', type: 'Binary' },
|
||||
0x3e5b: { name: 'SignatureElements', type: 'Container' },
|
||||
0x3e7b: { name: 'SignatureElementList', type: 'Container' },
|
||||
0x2532: { name: 'SignedElement', type: 'Binary' },
|
||||
0x8538067: { name: 'Segment', type: 'Container' },
|
||||
0x14d9b74: { name: 'SeekHead', type: 'Container' },
|
||||
0xdbb: { name: 'Seek', type: 'Container' },
|
||||
0x13ab: { name: 'SeekID', type: 'Binary' },
|
||||
0x13ac: { name: 'SeekPosition', type: 'Uint' },
|
||||
0x549a966: { name: 'Info', type: 'Container' },
|
||||
0x33a4: { name: 'SegmentUID', type: 'Binary' },
|
||||
0x3384: { name: 'SegmentFilename', type: 'String' },
|
||||
0x1cb923: { name: 'PrevUID', type: 'Binary' },
|
||||
0x1c83ab: { name: 'PrevFilename', type: 'String' },
|
||||
0x1eb923: { name: 'NextUID', type: 'Binary' },
|
||||
0x1e83bb: { name: 'NextFilename', type: 'String' },
|
||||
0x444: { name: 'SegmentFamily', type: 'Binary' },
|
||||
0x2924: { name: 'ChapterTranslate', type: 'Container' },
|
||||
0x29fc: { name: 'ChapterTranslateEditionUID', type: 'Uint' },
|
||||
0x29bf: { name: 'ChapterTranslateCodec', type: 'Uint' },
|
||||
0x29a5: { name: 'ChapterTranslateID', type: 'Binary' },
|
||||
0xad7b1: { name: 'TimecodeScale', type: 'Uint' },
|
||||
0x489: { name: 'Duration', type: 'Float' },
|
||||
0x461: { name: 'DateUTC', type: 'Date' },
|
||||
0x3ba9: { name: 'Title', type: 'String' },
|
||||
0xd80: { name: 'MuxingApp', type: 'String' },
|
||||
0x1741: { name: 'WritingApp', type: 'String' },
|
||||
// 0xf43b675: { name: 'Cluster', type: 'Container' },
|
||||
0x67: { name: 'Timecode', type: 'Uint' },
|
||||
0x1854: { name: 'SilentTracks', type: 'Container' },
|
||||
0x18d7: { name: 'SilentTrackNumber', type: 'Uint' },
|
||||
0x27: { name: 'Position', type: 'Uint' },
|
||||
0x2b: { name: 'PrevSize', type: 'Uint' },
|
||||
0x23: { name: 'SimpleBlock', type: 'Binary' },
|
||||
0x20: { name: 'BlockGroup', type: 'Container' },
|
||||
0x21: { name: 'Block', type: 'Binary' },
|
||||
0x22: { name: 'BlockVirtual', type: 'Binary' },
|
||||
0x35a1: { name: 'BlockAdditions', type: 'Container' },
|
||||
0x26: { name: 'BlockMore', type: 'Container' },
|
||||
0x6e: { name: 'BlockAddID', type: 'Uint' },
|
||||
0x25: { name: 'BlockAdditional', type: 'Binary' },
|
||||
0x1b: { name: 'BlockDuration', type: 'Uint' },
|
||||
0x7a: { name: 'ReferencePriority', type: 'Uint' },
|
||||
0x7b: { name: 'ReferenceBlock', type: 'Int' },
|
||||
0x7d: { name: 'ReferenceVirtual', type: 'Int' },
|
||||
0x24: { name: 'CodecState', type: 'Binary' },
|
||||
0x35a2: { name: 'DiscardPadding', type: 'Int' },
|
||||
0xe: { name: 'Slices', type: 'Container' },
|
||||
0x68: { name: 'TimeSlice', type: 'Container' },
|
||||
0x4c: { name: 'LaceNumber', type: 'Uint' },
|
||||
0x4d: { name: 'FrameNumber', type: 'Uint' },
|
||||
0x4b: { name: 'BlockAdditionID', type: 'Uint' },
|
||||
0x4e: { name: 'Delay', type: 'Uint' },
|
||||
0x4f: { name: 'SliceDuration', type: 'Uint' },
|
||||
0x48: { name: 'ReferenceFrame', type: 'Container' },
|
||||
0x49: { name: 'ReferenceOffset', type: 'Uint' },
|
||||
0x4a: { name: 'ReferenceTimeCode', type: 'Uint' },
|
||||
0x2f: { name: 'EncryptedBlock', type: 'Binary' },
|
||||
0x654ae6b: { name: 'Tracks', type: 'Container' },
|
||||
0x2e: { name: 'TrackEntry', type: 'Container' },
|
||||
0x57: { name: 'TrackNumber', type: 'Uint' },
|
||||
0x33c5: { name: 'TrackUID', type: 'Uint' },
|
||||
0x3: { name: 'TrackType', type: 'Uint' },
|
||||
0x39: { name: 'FlagEnabled', type: 'Uint' },
|
||||
0x8: { name: 'FlagDefault', type: 'Uint' },
|
||||
0x15aa: { name: 'FlagForced', type: 'Uint' },
|
||||
0x1c: { name: 'FlagLacing', type: 'Uint' },
|
||||
0x2de7: { name: 'MinCache', type: 'Uint' },
|
||||
0x2df8: { name: 'MaxCache', type: 'Uint' },
|
||||
0x3e383: { name: 'DefaultDuration', type: 'Uint' },
|
||||
0x34e7a: { name: 'DefaultDecodedFieldDuration', type: 'Uint' },
|
||||
0x3314f: { name: 'TrackTimecodeScale', type: 'Float' },
|
||||
0x137f: { name: 'TrackOffset', type: 'Int' },
|
||||
0x15ee: { name: 'MaxBlockAdditionID', type: 'Uint' },
|
||||
0x136e: { name: 'Name', type: 'String' },
|
||||
0x2b59c: { name: 'Language', type: 'String' },
|
||||
0x6: { name: 'CodecID', type: 'String' },
|
||||
0x23a2: { name: 'CodecPrivate', type: 'Binary' },
|
||||
0x58688: { name: 'CodecName', type: 'String' },
|
||||
0x3446: { name: 'AttachmentLink', type: 'Uint' },
|
||||
0x1a9697: { name: 'CodecSettings', type: 'String' },
|
||||
0x1b4040: { name: 'CodecInfoURL', type: 'String' },
|
||||
0x6b240: { name: 'CodecDownloadURL', type: 'String' },
|
||||
0x2a: { name: 'CodecDecodeAll', type: 'Uint' },
|
||||
0x2fab: { name: 'TrackOverlay', type: 'Uint' },
|
||||
0x16aa: { name: 'CodecDelay', type: 'Uint' },
|
||||
0x16bb: { name: 'SeekPreRoll', type: 'Uint' },
|
||||
0x2624: { name: 'TrackTranslate', type: 'Container' },
|
||||
0x26fc: { name: 'TrackTranslateEditionUID', type: 'Uint' },
|
||||
0x26bf: { name: 'TrackTranslateCodec', type: 'Uint' },
|
||||
0x26a5: { name: 'TrackTranslateTrackID', type: 'Binary' },
|
||||
0x60: { name: 'Video', type: 'Container' },
|
||||
0x1a: { name: 'FlagInterlaced', type: 'Uint' },
|
||||
0x13b8: { name: 'StereoMode', type: 'Uint' },
|
||||
0x13c0: { name: 'AlphaMode', type: 'Uint' },
|
||||
0x13b9: { name: 'OldStereoMode', type: 'Uint' },
|
||||
0x30: { name: 'PixelWidth', type: 'Uint' },
|
||||
0x3a: { name: 'PixelHeight', type: 'Uint' },
|
||||
0x14aa: { name: 'PixelCropBottom', type: 'Uint' },
|
||||
0x14bb: { name: 'PixelCropTop', type: 'Uint' },
|
||||
0x14cc: { name: 'PixelCropLeft', type: 'Uint' },
|
||||
0x14dd: { name: 'PixelCropRight', type: 'Uint' },
|
||||
0x14b0: { name: 'DisplayWidth', type: 'Uint' },
|
||||
0x14ba: { name: 'DisplayHeight', type: 'Uint' },
|
||||
0x14b2: { name: 'DisplayUnit', type: 'Uint' },
|
||||
0x14b3: { name: 'AspectRatioType', type: 'Uint' },
|
||||
0xeb524: { name: 'ColourSpace', type: 'Binary' },
|
||||
0xfb523: { name: 'GammaValue', type: 'Float' },
|
||||
0x383e3: { name: 'FrameRate', type: 'Float' },
|
||||
0x61: { name: 'Audio', type: 'Container' },
|
||||
0x35: { name: 'SamplingFrequency', type: 'Float' },
|
||||
0x38b5: { name: 'OutputSamplingFrequency', type: 'Float' },
|
||||
0x1f: { name: 'Channels', type: 'Uint' },
|
||||
0x3d7b: { name: 'ChannelPositions', type: 'Binary' },
|
||||
0x2264: { name: 'BitDepth', type: 'Uint' },
|
||||
0x62: { name: 'TrackOperation', type: 'Container' },
|
||||
0x63: { name: 'TrackCombinePlanes', type: 'Container' },
|
||||
0x64: { name: 'TrackPlane', type: 'Container' },
|
||||
0x65: { name: 'TrackPlaneUID', type: 'Uint' },
|
||||
0x66: { name: 'TrackPlaneType', type: 'Uint' },
|
||||
0x69: { name: 'TrackJoinBlocks', type: 'Container' },
|
||||
0x6d: { name: 'TrackJoinUID', type: 'Uint' },
|
||||
0x40: { name: 'TrickTrackUID', type: 'Uint' },
|
||||
0x41: { name: 'TrickTrackSegmentUID', type: 'Binary' },
|
||||
0x46: { name: 'TrickTrackFlag', type: 'Uint' },
|
||||
0x47: { name: 'TrickMasterTrackUID', type: 'Uint' },
|
||||
0x44: { name: 'TrickMasterTrackSegmentUID', type: 'Binary' },
|
||||
0x2d80: { name: 'ContentEncodings', type: 'Container' },
|
||||
0x2240: { name: 'ContentEncoding', type: 'Container' },
|
||||
0x1031: { name: 'ContentEncodingOrder', type: 'Uint' },
|
||||
0x1032: { name: 'ContentEncodingScope', type: 'Uint' },
|
||||
0x1033: { name: 'ContentEncodingType', type: 'Uint' },
|
||||
0x1034: { name: 'ContentCompression', type: 'Container' },
|
||||
0x254: { name: 'ContentCompAlgo', type: 'Uint' },
|
||||
0x255: { name: 'ContentCompSettings', type: 'Binary' },
|
||||
0x1035: { name: 'ContentEncryption', type: 'Container' },
|
||||
0x7e1: { name: 'ContentEncAlgo', type: 'Uint' },
|
||||
0x7e2: { name: 'ContentEncKeyID', type: 'Binary' },
|
||||
0x7e3: { name: 'ContentSignature', type: 'Binary' },
|
||||
0x7e4: { name: 'ContentSigKeyID', type: 'Binary' },
|
||||
0x7e5: { name: 'ContentSigAlgo', type: 'Uint' },
|
||||
0x7e6: { name: 'ContentSigHashAlgo', type: 'Uint' },
|
||||
0xc53bb6b: { name: 'Cues', type: 'Container' },
|
||||
0x3b: { name: 'CuePoint', type: 'Container' },
|
||||
0x33: { name: 'CueTime', type: 'Uint' },
|
||||
0x37: { name: 'CueTrackPositions', type: 'Container' },
|
||||
0x77: { name: 'CueTrack', type: 'Uint' },
|
||||
0x71: { name: 'CueClusterPosition', type: 'Uint' },
|
||||
0x70: { name: 'CueRelativePosition', type: 'Uint' },
|
||||
0x32: { name: 'CueDuration', type: 'Uint' },
|
||||
0x1378: { name: 'CueBlockNumber', type: 'Uint' },
|
||||
0x6a: { name: 'CueCodecState', type: 'Uint' },
|
||||
0x5b: { name: 'CueReference', type: 'Container' },
|
||||
0x16: { name: 'CueRefTime', type: 'Uint' },
|
||||
0x17: { name: 'CueRefCluster', type: 'Uint' },
|
||||
0x135f: { name: 'CueRefNumber', type: 'Uint' },
|
||||
0x6b: { name: 'CueRefCodecState', type: 'Uint' },
|
||||
0x941a469: { name: 'Attachments', type: 'Container' },
|
||||
0x21a7: { name: 'AttachedFile', type: 'Container' },
|
||||
0x67e: { name: 'FileDescription', type: 'String' },
|
||||
0x66e: { name: 'FileName', type: 'String' },
|
||||
0x660: { name: 'FileMimeType', type: 'String' },
|
||||
0x65c: { name: 'FileData', type: 'Binary' },
|
||||
0x6ae: { name: 'FileUID', type: 'Uint' },
|
||||
0x675: { name: 'FileReferral', type: 'Binary' },
|
||||
0x661: { name: 'FileUsedStartTime', type: 'Uint' },
|
||||
0x662: { name: 'FileUsedEndTime', type: 'Uint' },
|
||||
0x43a770: { name: 'Chapters', type: 'Container' },
|
||||
0x5b9: { name: 'EditionEntry', type: 'Container' },
|
||||
0x5bc: { name: 'EditionUID', type: 'Uint' },
|
||||
0x5bd: { name: 'EditionFlagHidden', type: 'Uint' },
|
||||
0x5db: { name: 'EditionFlagDefault', type: 'Uint' },
|
||||
0x5dd: { name: 'EditionFlagOrdered', type: 'Uint' },
|
||||
0x36: { name: 'ChapterAtom', type: 'Container' },
|
||||
0x33c4: { name: 'ChapterUID', type: 'Uint' },
|
||||
0x1654: { name: 'ChapterStringUID', type: 'String' },
|
||||
0x11: { name: 'ChapterTimeStart', type: 'Uint' },
|
||||
0x12: { name: 'ChapterTimeEnd', type: 'Uint' },
|
||||
0x18: { name: 'ChapterFlagHidden', type: 'Uint' },
|
||||
0x598: { name: 'ChapterFlagEnabled', type: 'Uint' },
|
||||
0x2e67: { name: 'ChapterSegmentUID', type: 'Binary' },
|
||||
0x2ebc: { name: 'ChapterSegmentEditionUID', type: 'Uint' },
|
||||
0x23c3: { name: 'ChapterPhysicalEquiv', type: 'Uint' },
|
||||
0xf: { name: 'ChapterTrack', type: 'Container' },
|
||||
0x9: { name: 'ChapterTrackNumber', type: 'Uint' },
|
||||
0x0: { name: 'ChapterDisplay', type: 'Container' },
|
||||
0x5: { name: 'ChapString', type: 'String' },
|
||||
0x37c: { name: 'ChapLanguage', type: 'String' },
|
||||
0x37e: { name: 'ChapCountry', type: 'String' },
|
||||
0x2944: { name: 'ChapProcess', type: 'Container' },
|
||||
0x2955: { name: 'ChapProcessCodecID', type: 'Uint' },
|
||||
0x50d: { name: 'ChapProcessPrivate', type: 'Binary' },
|
||||
0x2911: { name: 'ChapProcessCommand', type: 'Container' },
|
||||
0x2922: { name: 'ChapProcessTime', type: 'Uint' },
|
||||
0x2933: { name: 'ChapProcessData', type: 'Binary' },
|
||||
0x254c367: { name: 'Tags', type: 'Container' },
|
||||
0x3373: { name: 'Tag', type: 'Container' },
|
||||
0x23c0: { name: 'Targets', type: 'Container' },
|
||||
0x28ca: { name: 'TargetTypeValue', type: 'Uint' },
|
||||
0x23ca: { name: 'TargetType', type: 'String' },
|
||||
0x23c5: { name: 'TagTrackUID', type: 'Uint' },
|
||||
0x23c9: { name: 'TagEditionUID', type: 'Uint' },
|
||||
0x23c4: { name: 'TagChapterUID', type: 'Uint' },
|
||||
0x23c6: { name: 'TagAttachmentUID', type: 'Uint' },
|
||||
0x27c8: { name: 'SimpleTag', type: 'Container' },
|
||||
0x5a3: { name: 'TagName', type: 'String' },
|
||||
0x47a: { name: 'TagLanguage', type: 'String' },
|
||||
0x484: { name: 'TagDefault', type: 'Uint' },
|
||||
0x487: { name: 'TagString', type: 'String' },
|
||||
0x485: { name: 'TagBinary', type: 'Binary' }
|
||||
};
|
||||
|
||||
function doInherit(newClass, baseClass) {
|
||||
newClass.prototype = Object.create(baseClass.prototype);
|
||||
newClass.prototype.constructor = newClass;
|
||||
}
|
||||
|
||||
function WebmBase(name, type) {
|
||||
this.name = name || 'Unknown';
|
||||
this.type = type || 'Unknown';
|
||||
}
|
||||
WebmBase.prototype.updateBySource = function () { };
|
||||
WebmBase.prototype.setSource = function (source) {
|
||||
this.source = source;
|
||||
this.updateBySource();
|
||||
};
|
||||
WebmBase.prototype.updateByData = function () { };
|
||||
WebmBase.prototype.setData = function (data) {
|
||||
this.data = data;
|
||||
this.updateByData();
|
||||
};
|
||||
|
||||
function WebmUint(name, type) {
|
||||
WebmBase.call(this, name, type || 'Uint');
|
||||
}
|
||||
doInherit(WebmUint, WebmBase);
|
||||
function padHex(hex) {
|
||||
return hex.length % 2 === 1 ? '0' + hex : hex;
|
||||
}
|
||||
WebmUint.prototype.updateBySource = function () {
|
||||
// use hex representation of a number instead of number value
|
||||
this.data = '';
|
||||
for (var i = 0; i < this.source.length; i++) {
|
||||
var hex = this.source[i].toString(16);
|
||||
this.data += padHex(hex);
|
||||
}
|
||||
};
|
||||
WebmUint.prototype.updateByData = function () {
|
||||
var length = this.data.length / 2;
|
||||
this.source = new Uint8Array(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
var hex = this.data.substr(i * 2, 2);
|
||||
this.source[i] = parseInt(hex, 16);
|
||||
}
|
||||
};
|
||||
WebmUint.prototype.getValue = function () {
|
||||
return parseInt(this.data, 16);
|
||||
};
|
||||
WebmUint.prototype.setValue = function (value) {
|
||||
this.setData(padHex(value.toString(16)));
|
||||
};
|
||||
|
||||
function WebmFloat(name, type) {
|
||||
WebmBase.call(this, name, type || 'Float');
|
||||
}
|
||||
doInherit(WebmFloat, WebmBase);
|
||||
WebmFloat.prototype.getFloatArrayType = function () {
|
||||
return this.source && this.source.length === 4 ? Float32Array : Float64Array;
|
||||
};
|
||||
WebmFloat.prototype.updateBySource = function () {
|
||||
var byteArray = this.source.reverse();
|
||||
var floatArrayType = this.getFloatArrayType();
|
||||
var floatArray = new floatArrayType(byteArray.buffer);
|
||||
this.data = floatArray[0];
|
||||
};
|
||||
WebmFloat.prototype.updateByData = function () {
|
||||
var floatArrayType = this.getFloatArrayType();
|
||||
var floatArray = new floatArrayType([this.data]);
|
||||
var byteArray = new Uint8Array(floatArray.buffer);
|
||||
this.source = byteArray.reverse();
|
||||
};
|
||||
WebmFloat.prototype.getValue = function () {
|
||||
return this.data;
|
||||
};
|
||||
WebmFloat.prototype.setValue = function (value) {
|
||||
this.setData(value);
|
||||
};
|
||||
|
||||
function WebmContainer(name, type) {
|
||||
WebmBase.call(this, name, type || 'Container');
|
||||
}
|
||||
doInherit(WebmContainer, WebmBase);
|
||||
WebmContainer.prototype.readByte = function () {
|
||||
return this.source[this.offset++];
|
||||
};
|
||||
WebmContainer.prototype.readUint = function () {
|
||||
var firstByte = this.readByte();
|
||||
var bytes = 8 - firstByte.toString(2).length;
|
||||
var value = firstByte - (1 << (7 - bytes));
|
||||
for (var i = 0; i < bytes; i++) {
|
||||
// don't use bit operators to support x86
|
||||
value *= 256;
|
||||
value += this.readByte();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
WebmContainer.prototype.updateBySource = function () {
|
||||
this.data = [];
|
||||
for (this.offset = 0; this.offset < this.source.length; this.offset = end) {
|
||||
var id = this.readUint();
|
||||
var len = this.readUint();
|
||||
var end = Math.min(this.offset + len, this.source.length);
|
||||
var data = this.source.slice(this.offset, end);
|
||||
|
||||
var info = sections[id] || { name: 'Unknown', type: 'Unknown' };
|
||||
var ctr = WebmBase;
|
||||
switch (info.type) {
|
||||
case 'Container':
|
||||
ctr = WebmContainer;
|
||||
break;
|
||||
case 'Uint':
|
||||
ctr = WebmUint;
|
||||
break;
|
||||
case 'Float':
|
||||
ctr = WebmFloat;
|
||||
break;
|
||||
}
|
||||
var section = new ctr(info.name, info.type);
|
||||
section.setSource(data);
|
||||
this.data.push({
|
||||
id: id,
|
||||
idHex: id.toString(16),
|
||||
data: section
|
||||
});
|
||||
}
|
||||
};
|
||||
WebmContainer.prototype.writeUint = function (x, draft) {
|
||||
for (var bytes = 1, flag = 0x80; x >= flag && bytes < 8; bytes++, flag *= 0x80) { }
|
||||
|
||||
if (!draft) {
|
||||
var value = flag + x;
|
||||
for (var i = bytes - 1; i >= 0; i--) {
|
||||
// don't use bit operators to support x86
|
||||
var c = value % 256;
|
||||
this.source[this.offset + i] = c;
|
||||
value = (value - c) / 256;
|
||||
}
|
||||
}
|
||||
|
||||
this.offset += bytes;
|
||||
};
|
||||
WebmContainer.prototype.writeSections = function (draft) {
|
||||
this.offset = 0;
|
||||
for (var i = 0; i < this.data.length; i++) {
|
||||
var section = this.data[i],
|
||||
content = section.data.source,
|
||||
contentLength = content.length;
|
||||
this.writeUint(section.id, draft);
|
||||
this.writeUint(contentLength, draft);
|
||||
if (!draft) {
|
||||
this.source.set(content, this.offset);
|
||||
}
|
||||
this.offset += contentLength;
|
||||
}
|
||||
return this.offset;
|
||||
};
|
||||
WebmContainer.prototype.updateByData = function () {
|
||||
// run without accessing this.source to determine total length - need to know it to create Uint8Array
|
||||
var length = this.writeSections('draft');
|
||||
this.source = new Uint8Array(length);
|
||||
// now really write data
|
||||
this.writeSections();
|
||||
};
|
||||
WebmContainer.prototype.getSectionById = function (id) {
|
||||
for (var i = 0; i < this.data.length; i++) {
|
||||
var section = this.data[i];
|
||||
if (section.id === id) {
|
||||
return section.data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function WebmFile(source) {
|
||||
WebmContainer.call(this, 'File', 'File');
|
||||
this.setSource(source);
|
||||
}
|
||||
doInherit(WebmFile, WebmContainer);
|
||||
WebmFile.prototype.fixDuration = function (duration, options) {
|
||||
var logger = options && options.logger;
|
||||
if (logger === undefined) {
|
||||
logger = function (message) {
|
||||
console.log(message);
|
||||
};
|
||||
} else if (!logger) {
|
||||
logger = function () { };
|
||||
}
|
||||
|
||||
var segmentSection = this.getSectionById(0x8538067);
|
||||
if (!segmentSection) {
|
||||
logger('[fix-webm-duration] Segment section is missing');
|
||||
return false;
|
||||
}
|
||||
|
||||
var infoSection = segmentSection.getSectionById(0x549a966);
|
||||
if (!infoSection) {
|
||||
logger('[fix-webm-duration] Info section is missing');
|
||||
return false;
|
||||
}
|
||||
|
||||
var timeScaleSection = infoSection.getSectionById(0xad7b1);
|
||||
if (!timeScaleSection) {
|
||||
logger('[fix-webm-duration] TimecodeScale section is missing');
|
||||
return false;
|
||||
}
|
||||
|
||||
var durationSection = infoSection.getSectionById(0x489);
|
||||
if (durationSection) {
|
||||
if (durationSection.getValue() <= 0) {
|
||||
logger('[fix-webm-duration] Duration section is present, but the value is empty. Applying ' + duration.toLocaleString() + ' ms.');
|
||||
durationSection.setValue(duration);
|
||||
} else {
|
||||
logger('[fix-webm-duration] Duration section is present');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
logger('[fix-webm-duration] Duration section is missing. Applying ' + duration.toLocaleString() + ' ms.');
|
||||
// append Duration section
|
||||
durationSection = new WebmFloat('Duration', 'Float');
|
||||
durationSection.setValue(duration);
|
||||
infoSection.data.push({
|
||||
id: 0x489,
|
||||
data: durationSection
|
||||
});
|
||||
}
|
||||
|
||||
// set default time scale to 1 millisecond (1000000 nanoseconds)
|
||||
timeScaleSection.setValue(1000000);
|
||||
infoSection.updateByData();
|
||||
segmentSection.updateByData();
|
||||
this.updateByData();
|
||||
|
||||
return true;
|
||||
};
|
||||
WebmFile.prototype.toBlob = function (mimeType) {
|
||||
return new Blob([this.source.buffer], { type: mimeType || 'audio/webm' });
|
||||
};
|
||||
|
||||
function fixWebmDuration(blob, duration, callback, options) {
|
||||
// The callback may be omitted - then the third argument is options
|
||||
if (typeof callback === "object") {
|
||||
options = callback;
|
||||
callback = undefined;
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
return new Promise(function (resolve) {
|
||||
fixWebmDuration(blob, duration, resolve, options);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
try {
|
||||
var file = new WebmFile(new Uint8Array(reader.result));
|
||||
if (file.fixDuration(duration, options)) {
|
||||
blob = file.toBlob(blob.type);
|
||||
}
|
||||
} catch (ex) {
|
||||
// ignore
|
||||
}
|
||||
callback(blob);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
} catch (ex) {
|
||||
callback(blob);
|
||||
}
|
||||
}
|
||||
|
||||
// Support AMD import default
|
||||
fixWebmDuration.default = fixWebmDuration;
|
||||
|
||||
return fixWebmDuration;
|
||||
});
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
class RecorderProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'numChannels',
|
||||
defaultValue: 1,
|
||||
minValue: 1,
|
||||
maxValue: 16
|
||||
},
|
||||
{
|
||||
name: 'sampleRate',
|
||||
defaultValue: 48000,
|
||||
minValue: 8000,
|
||||
maxValue: 96000
|
||||
},
|
||||
{
|
||||
name: 'streamBufferSize',
|
||||
defaultValue: 2048,
|
||||
minValue: 256,
|
||||
maxValue: 8192
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Buffer size compromise between size and process call frequency
|
||||
_bufferSize = 2048
|
||||
// The current buffer fill level
|
||||
_bytesWritten = 0
|
||||
// Buffer per channel
|
||||
_buffers = []
|
||||
// Resampler (passthrough, down or up)
|
||||
_resampler = null
|
||||
// Config
|
||||
_numChannels = 1
|
||||
_sampleRate = 48000
|
||||
|
||||
constructor(options) {
|
||||
super(options)
|
||||
|
||||
this._numChannels = options.parameterData.numChannels
|
||||
this._sampleRate = options.parameterData.sampleRate
|
||||
this._bufferSize = options.parameterData.streamBufferSize
|
||||
|
||||
// Resampler(current context sample rate, desired sample rate, num channels, buffer size)
|
||||
// num channels is always 1 since we resample after interleaving channels
|
||||
this._resampler = new Resampler(sampleRate, this._sampleRate, 1, this._bufferSize * this._numChannels)
|
||||
|
||||
this.initBuffers()
|
||||
}
|
||||
|
||||
initBuffers() {
|
||||
this._bytesWritten = 0
|
||||
this._buffers = []
|
||||
|
||||
for (let channel = 0; channel < this._numChannels; channel++) {
|
||||
this._buffers[channel] = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBufferEmpty() {
|
||||
return this._bytesWritten === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBufferFull() {
|
||||
return this._bytesWritten >= this._bufferSize
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Float32Array[][]} inputs
|
||||
* @returns {boolean}
|
||||
*/
|
||||
process(inputs) {
|
||||
if (this.isBufferFull()) {
|
||||
this.flush()
|
||||
}
|
||||
|
||||
const input = inputs[0]
|
||||
|
||||
if (input.length == 0) {
|
||||
// Sometimes, Firefox doesn't give any input. Skip this frame to not fail.
|
||||
return true
|
||||
}
|
||||
|
||||
for (let channel = 0; channel < this._numChannels; channel++) {
|
||||
// Push a copy of the array.
|
||||
// The underlying implementation may reuse it which will break the recording.
|
||||
this._buffers[channel].push([...input[channel % input.length]])
|
||||
}
|
||||
|
||||
this._bytesWritten += input[0].length
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
flush() {
|
||||
let channels = []
|
||||
for (let channel = 0; channel < this._numChannels; channel++) {
|
||||
channels.push(this.mergeFloat32Arrays(this._buffers[channel], this._bytesWritten))
|
||||
}
|
||||
|
||||
let interleaved = this.interleave(channels)
|
||||
|
||||
let resampled = this._resampler.resample(interleaved)
|
||||
|
||||
this.port.postMessage(this.floatTo16BitPCM(resampled))
|
||||
|
||||
this.initBuffers()
|
||||
}
|
||||
|
||||
mergeFloat32Arrays(arrays, bytesWritten) {
|
||||
let result = new Float32Array(bytesWritten)
|
||||
var offset = 0
|
||||
|
||||
for (let i = 0; i < arrays.length; i++) {
|
||||
result.set(arrays[i], offset)
|
||||
offset += arrays[i].length
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Interleave data from channels from LLLLRRRR to LRLRLRLR
|
||||
interleave(channels) {
|
||||
if (channels === 1) {
|
||||
return channels[0]
|
||||
}
|
||||
|
||||
var length = 0
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
length += channels[i].length
|
||||
}
|
||||
|
||||
let result = new Float32Array(length)
|
||||
|
||||
var index = 0
|
||||
var inputIndex = 0
|
||||
|
||||
while (index < length) {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
result[index] = channels[i][inputIndex]
|
||||
index++
|
||||
}
|
||||
|
||||
inputIndex++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
floatTo16BitPCM(input) {
|
||||
let output = new DataView(new ArrayBuffer(input.length * 2))
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
let s = Math.max(-1, Math.min(1, input[i]))
|
||||
let s16 = s < 0 ? s * 0x8000 : s * 0x7FFF
|
||||
output.setInt16(i * 2, s16, true)
|
||||
}
|
||||
|
||||
return new Int16Array(output.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
class Resampler {
|
||||
constructor(fromSampleRate, toSampleRate, channels, inputBufferSize) {
|
||||
|
||||
if (!fromSampleRate || !toSampleRate || !channels) {
|
||||
throw (new Error("Invalid settings specified for the resampler."));
|
||||
}
|
||||
this.resampler = null;
|
||||
this.fromSampleRate = fromSampleRate;
|
||||
this.toSampleRate = toSampleRate;
|
||||
this.channels = channels || 0;
|
||||
this.inputBufferSize = inputBufferSize;
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (this.fromSampleRate == this.toSampleRate) {
|
||||
|
||||
// Setup resampler bypass - Resampler just returns what was passed through
|
||||
this.resampler = (buffer) => {
|
||||
return buffer
|
||||
};
|
||||
this.ratioWeight = 1;
|
||||
|
||||
} else {
|
||||
if (this.fromSampleRate < this.toSampleRate) {
|
||||
|
||||
// Use generic linear interpolation if upsampling,
|
||||
// as linear interpolation produces a gradient that we want
|
||||
// and works fine with two input sample points per output in this case.
|
||||
this.linearInterpolation();
|
||||
this.lastWeight = 1;
|
||||
|
||||
} else {
|
||||
|
||||
// Custom resampler I wrote that doesn't skip samples
|
||||
// like standard linear interpolation in high downsampling.
|
||||
// This is more accurate than linear interpolation on downsampling.
|
||||
this.multiTap();
|
||||
this.tailExists = false;
|
||||
this.lastWeight = 0;
|
||||
}
|
||||
|
||||
// Initialize the internal buffer:
|
||||
this.initializeBuffers();
|
||||
this.ratioWeight = this.fromSampleRate / this.toSampleRate;
|
||||
}
|
||||
}
|
||||
|
||||
bufferSlice(sliceAmount) {
|
||||
|
||||
//Typed array and normal array buffer section referencing:
|
||||
try {
|
||||
return this.outputBuffer.subarray(0, sliceAmount);
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
//Regular array pass:
|
||||
this.outputBuffer.length = sliceAmount;
|
||||
return this.outputBuffer;
|
||||
}
|
||||
catch (error) {
|
||||
//Nightly Firefox 4 used to have the subarray function named as slice:
|
||||
return this.outputBuffer.slice(0, sliceAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initializeBuffers() {
|
||||
this.outputBufferSize = (Math.ceil(this.inputBufferSize * this.toSampleRate / this.fromSampleRate / this.channels * 1.000000476837158203125) + this.channels) + this.channels;
|
||||
try {
|
||||
this.outputBuffer = new Float32Array(this.outputBufferSize);
|
||||
this.lastOutput = new Float32Array(this.channels);
|
||||
}
|
||||
catch (error) {
|
||||
this.outputBuffer = [];
|
||||
this.lastOutput = [];
|
||||
}
|
||||
}
|
||||
|
||||
linearInterpolation() {
|
||||
this.resampler = (buffer) => {
|
||||
let bufferLength = buffer.length,
|
||||
channels = this.channels,
|
||||
outLength,
|
||||
ratioWeight,
|
||||
weight,
|
||||
firstWeight,
|
||||
secondWeight,
|
||||
sourceOffset,
|
||||
outputOffset,
|
||||
outputBuffer,
|
||||
channel;
|
||||
|
||||
if ((bufferLength % channels) !== 0) {
|
||||
throw (new Error("Buffer was of incorrect sample length."));
|
||||
}
|
||||
if (bufferLength <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
outLength = this.outputBufferSize;
|
||||
ratioWeight = this.ratioWeight;
|
||||
weight = this.lastWeight;
|
||||
firstWeight = 0;
|
||||
secondWeight = 0;
|
||||
sourceOffset = 0;
|
||||
outputOffset = 0;
|
||||
outputBuffer = this.outputBuffer;
|
||||
|
||||
for (; weight < 1; weight += ratioWeight) {
|
||||
secondWeight = weight % 1;
|
||||
firstWeight = 1 - secondWeight;
|
||||
this.lastWeight = weight % 1;
|
||||
for (channel = 0; channel < this.channels; ++channel) {
|
||||
outputBuffer[outputOffset++] = (this.lastOutput[channel] * firstWeight) + (buffer[channel] * secondWeight);
|
||||
}
|
||||
}
|
||||
weight -= 1;
|
||||
for (bufferLength -= channels, sourceOffset = Math.floor(weight) * channels; outputOffset < outLength && sourceOffset < bufferLength;) {
|
||||
secondWeight = weight % 1;
|
||||
firstWeight = 1 - secondWeight;
|
||||
for (channel = 0; channel < this.channels; ++channel) {
|
||||
outputBuffer[outputOffset++] = (buffer[sourceOffset + ((channel > 0) ? (channel) : 0)] * firstWeight) + (buffer[sourceOffset + (channels + channel)] * secondWeight);
|
||||
}
|
||||
weight += ratioWeight;
|
||||
sourceOffset = Math.floor(weight) * channels;
|
||||
}
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
this.lastOutput[channel] = buffer[sourceOffset++];
|
||||
}
|
||||
return this.bufferSlice(outputOffset);
|
||||
};
|
||||
}
|
||||
|
||||
multiTap() {
|
||||
this.resampler = (buffer) => {
|
||||
let bufferLength = buffer.length,
|
||||
outLength,
|
||||
output_variable_list,
|
||||
channels = this.channels,
|
||||
ratioWeight,
|
||||
weight,
|
||||
channel,
|
||||
actualPosition,
|
||||
amountToNext,
|
||||
alreadyProcessedTail,
|
||||
outputBuffer,
|
||||
outputOffset,
|
||||
currentPosition;
|
||||
|
||||
if ((bufferLength % channels) !== 0) {
|
||||
throw (new Error("Buffer was of incorrect sample length."));
|
||||
}
|
||||
if (bufferLength <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
outLength = this.outputBufferSize;
|
||||
output_variable_list = [];
|
||||
ratioWeight = this.ratioWeight;
|
||||
weight = 0;
|
||||
actualPosition = 0;
|
||||
amountToNext = 0;
|
||||
alreadyProcessedTail = !this.tailExists;
|
||||
this.tailExists = false;
|
||||
outputBuffer = this.outputBuffer;
|
||||
outputOffset = 0;
|
||||
currentPosition = 0;
|
||||
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
output_variable_list[channel] = 0;
|
||||
}
|
||||
|
||||
do {
|
||||
if (alreadyProcessedTail) {
|
||||
weight = ratioWeight;
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
output_variable_list[channel] = 0;
|
||||
}
|
||||
} else {
|
||||
weight = this.lastWeight;
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
output_variable_list[channel] = this.lastOutput[channel];
|
||||
}
|
||||
alreadyProcessedTail = true;
|
||||
}
|
||||
while (weight > 0 && actualPosition < bufferLength) {
|
||||
amountToNext = 1 + actualPosition - currentPosition;
|
||||
if (weight >= amountToNext) {
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
output_variable_list[channel] += buffer[actualPosition++] * amountToNext;
|
||||
}
|
||||
currentPosition = actualPosition;
|
||||
weight -= amountToNext;
|
||||
} else {
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
output_variable_list[channel] += buffer[actualPosition + ((channel > 0) ? channel : 0)] * weight;
|
||||
}
|
||||
currentPosition += weight;
|
||||
weight = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (weight === 0) {
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
outputBuffer[outputOffset++] = output_variable_list[channel] / ratioWeight;
|
||||
}
|
||||
} else {
|
||||
this.lastWeight = weight;
|
||||
for (channel = 0; channel < channels; ++channel) {
|
||||
this.lastOutput[channel] = output_variable_list[channel];
|
||||
}
|
||||
this.tailExists = true;
|
||||
break;
|
||||
}
|
||||
} while (actualPosition < bufferLength && outputOffset < outLength);
|
||||
return this.bufferSlice(outputOffset);
|
||||
};
|
||||
}
|
||||
|
||||
resample(buffer) {
|
||||
if (this.fromSampleRate == this.toSampleRate) {
|
||||
this.ratioWeight = 1;
|
||||
} else {
|
||||
if (this.fromSampleRate < this.toSampleRate) {
|
||||
this.lastWeight = 1;
|
||||
} else {
|
||||
this.tailExists = false;
|
||||
this.lastWeight = 0;
|
||||
}
|
||||
this.initializeBuffers();
|
||||
this.ratioWeight = this.fromSampleRate / this.toSampleRate;
|
||||
}
|
||||
return this.resampler(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("recorder.worklet", RecorderProcessor)
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"app_name":"extera_next","version":"2.0.1","package_name":"extera_next"}
|
||||
|
|
@ -10,15 +10,15 @@ FluffyChat is provided as AppImage too. To Download, visit fluffychat.im.
|
|||
flutter build linux
|
||||
|
||||
# copy binaries to appimage dir
|
||||
cp -r build/linux/{x64,arm64}/release/bundle appimage/FluffyChat.AppDir
|
||||
cp -r build/linux/{x64,arm64}/release/bundle appimage/Extera.AppDir
|
||||
cd appimage
|
||||
|
||||
# prepare AppImage files
|
||||
cp FluffyChat.desktop FluffyChat.AppDir/
|
||||
mkdir -p FluffyChat.AppDir/usr/share/icons
|
||||
cp ../assets/logo.svg FluffyChat.AppDir/fluffychat.svg
|
||||
cp AppRun FluffyChat.AppDir
|
||||
cp FluffyChat.desktop Extera.AppDir/
|
||||
mkdir -p Extera.AppDir/usr/share/icons
|
||||
cp ../assets/logo.svg Extera.AppDir/fluffychat.svg
|
||||
cp AppRun Extera.AppDir
|
||||
|
||||
# build the AppImage
|
||||
appimagetool FluffyChat.AppDir
|
||||
appimagetool Extera.AppDir
|
||||
```
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class MessageContent extends StatelessWidget {
|
|||
switch (event.type) {
|
||||
case PollEvents.PollStart:
|
||||
Logs().v("Got poll event ${event.type}");
|
||||
return PollWidget(event, color: textColor, linkColor: linkColor, fontSize: fontSize);
|
||||
return PollWidget(event, color: textColor, linkColor: linkColor, fontSize: fontSize, timeline: timeline);
|
||||
case EventTypes.Message:
|
||||
case EventTypes.Encrypted:
|
||||
case EventTypes.Sticker:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:extera_next/utils/poll_events.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PollWidget extends StatefulWidget {
|
||||
|
|
@ -8,12 +7,14 @@ class PollWidget extends StatefulWidget {
|
|||
final Color linkColor;
|
||||
final double fontSize;
|
||||
final Event event;
|
||||
final Timeline timeline;
|
||||
|
||||
const PollWidget(
|
||||
this.event, {
|
||||
required this.color,
|
||||
required this.linkColor,
|
||||
required this.fontSize,
|
||||
required this.timeline,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -22,23 +23,375 @@ class PollWidget extends StatefulWidget {
|
|||
}
|
||||
|
||||
class PollWidgetState extends State<PollWidget> {
|
||||
List<String> selectedAnswers = [];
|
||||
List<String> originalVote = []; // Store the original vote to detect changes
|
||||
Map<String, int>? pollResults;
|
||||
bool hasVoted = false;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPollData();
|
||||
}
|
||||
|
||||
void _loadPollData() {
|
||||
final event = widget.event;
|
||||
final content =
|
||||
event.content[PollEvents.PollStart] as Map<String, dynamic>;
|
||||
|
||||
// Check if user has already voted
|
||||
_checkExistingVote();
|
||||
|
||||
// For disclosed polls, load initial results
|
||||
final kind = content['kind'] as String?;
|
||||
if (kind == 'org.matrix.msc3381.disclosed') {
|
||||
_calculateResults();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkExistingVote() {
|
||||
final room = widget.event.room;
|
||||
final currentUserId = room.client.userID;
|
||||
|
||||
|
||||
|
||||
// Find existing poll response events from current user
|
||||
final responses = widget.timeline!.events.where((e) {
|
||||
return e.type == 'org.matrix.msc3381.poll.response' &&
|
||||
e.senderId == currentUserId &&
|
||||
e.relationshipEventId == widget.event.eventId;
|
||||
}).toList();
|
||||
|
||||
if (responses.isNotEmpty) {
|
||||
// Use the latest response
|
||||
final latestResponse = responses.last;
|
||||
final responseContent = latestResponse
|
||||
.content['org.matrix.msc3381.poll.response'] as Map<String, dynamic>;
|
||||
if (responseContent != null) {
|
||||
final List<dynamic> answers = responseContent['answers'];
|
||||
setState(() {
|
||||
selectedAnswers = answers.cast<String>();
|
||||
originalVote = List.from(answers.cast<String>()); // Store original vote
|
||||
hasVoted = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _calculateResults() {
|
||||
final room = widget.event.room;
|
||||
final pollEventId = widget.event.eventId;
|
||||
final results = <String, int>{};
|
||||
|
||||
|
||||
|
||||
// Get all poll response events for this poll
|
||||
final responses = widget.timeline!.events.where((e) {
|
||||
return e.type == 'org.matrix.msc3381.poll.response' &&
|
||||
e.relationshipEventId == pollEventId;
|
||||
}).toList();
|
||||
|
||||
// Count votes for each answer
|
||||
for (final response in responses) {
|
||||
final responseContent = response
|
||||
.content['org.matrix.msc3381.poll.response'] as Map<String, dynamic>;
|
||||
if (responseContent != null) {
|
||||
final List<dynamic> answers = responseContent['answers'];
|
||||
for (final answer in answers.cast<String>()) {
|
||||
results[answer] = (results[answer] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
pollResults = results;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _vote(List<String> answers) async {
|
||||
if (isLoading) return;
|
||||
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final room = widget.event.room;
|
||||
|
||||
// Send poll response event
|
||||
await room.sendEvent(
|
||||
{
|
||||
'm.relates_to': {
|
||||
'rel_type': 'm.reference',
|
||||
'event_id': widget.event.eventId,
|
||||
},
|
||||
'org.matrix.msc3381.poll.response': {
|
||||
'answers': answers,
|
||||
},
|
||||
},
|
||||
type: 'org.matrix.msc3381.poll.response'
|
||||
);
|
||||
|
||||
setState(() {
|
||||
selectedAnswers = answers;
|
||||
originalVote = List.from(answers); // Update original vote after voting
|
||||
hasVoted = true;
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
// Recalculate results for disclosed polls
|
||||
final content =
|
||||
widget.event.content[PollEvents.PollStart] as Map<String, dynamic?>;
|
||||
final kind = content['kind'] as String?;
|
||||
if (kind == 'org.matrix.msc3381.disclosed') {
|
||||
_calculateResults();
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
// Show error message
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to vote: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onAnswerSelected(String answerId, bool selected) {
|
||||
final content =
|
||||
widget.event.content[PollEvents.PollStart] as Map<String, dynamic?>;
|
||||
final maxSelections = content['max_selections'] as int? ?? 1;
|
||||
final List<dynamic> answers = content['answers'];
|
||||
|
||||
setState(() {
|
||||
if (maxSelections == 1) {
|
||||
// Single selection - replace current selection
|
||||
selectedAnswers = selected ? [answerId] : [];
|
||||
} else {
|
||||
// Multiple selection
|
||||
if (selected) {
|
||||
if (selectedAnswers.length < maxSelections) {
|
||||
selectedAnswers.add(answerId);
|
||||
}
|
||||
} else {
|
||||
selectedAnswers.remove(answerId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _isPollEnded() {
|
||||
final room = widget.event.room;
|
||||
if (widget.timeline == null) return false;
|
||||
// Check if there's an end event for this poll
|
||||
final endEvents = widget.timeline!.events.where((e) {
|
||||
return e.type == 'org.matrix.msc3381.poll.end' &&
|
||||
e.senderId == widget.event.senderId &&
|
||||
e.relationshipEventId == widget.event.eventId;
|
||||
});
|
||||
return endEvents.isNotEmpty;
|
||||
}
|
||||
|
||||
bool _shouldShowResults() {
|
||||
final content =
|
||||
widget.event.content[PollEvents.PollStart] as Map<String, dynamic?>;
|
||||
final kind = content['kind'] as String?;
|
||||
final isDisclosed = kind == 'org.matrix.msc3381.disclosed';
|
||||
final isEnded = _isPollEnded();
|
||||
|
||||
return isDisclosed || isEnded || hasVoted;
|
||||
}
|
||||
|
||||
double _getAnswerPercentage(String answerId) {
|
||||
if (pollResults == null || pollResults!.isEmpty) return 0.0;
|
||||
|
||||
final totalVotes = pollResults!.values.reduce((a, b) => a + b);
|
||||
if (totalVotes == 0) return 0.0;
|
||||
|
||||
return pollResults![answerId]?.toDouble() ?? 0 / totalVotes.toDouble();
|
||||
}
|
||||
|
||||
// Check if the current selection is different from the original vote
|
||||
bool _hasSelectionChanged() {
|
||||
if (selectedAnswers.length != originalVote.length) return true;
|
||||
|
||||
// Sort both lists to compare regardless of order
|
||||
final sortedSelected = List.from(selectedAnswers)..sort();
|
||||
final sortedOriginal = List.from(originalVote)..sort();
|
||||
|
||||
for (int i = 0; i < sortedSelected.length; i++) {
|
||||
if (sortedSelected[i] != sortedOriginal[i]) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final event = widget.event;
|
||||
final content = event.content[PollEvents.PollStart] as Map<String, dynamic?>;
|
||||
return Padding(
|
||||
padding: EdgeInsetsGeometry.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(content?['question']['m.text'] as String, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(4, 4, 4, 2),
|
||||
child: Column(
|
||||
children: [
|
||||
final content =
|
||||
event.content[PollEvents.PollStart] as Map<String, dynamic?>;
|
||||
final question = content?['question']?['m.text'] as String? ?? 'Poll';
|
||||
final List<dynamic> answers = content?['answers'] ?? [];
|
||||
final maxSelections = content?['max_selections'] as int? ?? 1;
|
||||
final kind = content?['kind'] as String?;
|
||||
|
||||
],
|
||||
final shouldShowResults = _shouldShowResults();
|
||||
final isEnded = _isPollEnded();
|
||||
final canVote = !isEnded && !isLoading;
|
||||
final hasChanged = _hasSelectionChanged();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Question
|
||||
Text(
|
||||
question,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: widget.fontSize,
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Answers
|
||||
...answers.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final answer = entry.value as Map<String, dynamic>;
|
||||
final answerId = answer['id'] as String;
|
||||
final answerText = answer['m.text'] as String? ??
|
||||
answer['org.matrix.msc1767.text'] as String? ??
|
||||
'Answer ${index + 1}';
|
||||
final isSelected = selectedAnswers.contains(answerId);
|
||||
final percentage = _getAnswerPercentage(answerId);
|
||||
final voteCount = pollResults?[answerId] ?? 0;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Answer row
|
||||
Row(
|
||||
children: [
|
||||
// Selection indicator
|
||||
if (canVote) ...[
|
||||
if (maxSelections == 1)
|
||||
Radio<bool>(
|
||||
value: true,
|
||||
groupValue: isSelected,
|
||||
onChanged: (_) =>
|
||||
_onAnswerSelected(answerId, !isSelected),
|
||||
)
|
||||
else
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) =>
|
||||
_onAnswerSelected(answerId, !isSelected),
|
||||
),
|
||||
] else if (isSelected)
|
||||
Icon(Icons.check, color: Colors.green, size: 20),
|
||||
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: canVote
|
||||
? () => _onAnswerSelected(answerId, !isSelected)
|
||||
: null,
|
||||
child: Text(
|
||||
answerText,
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize - 1,
|
||||
color: widget.color.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Vote count and percentage
|
||||
if (shouldShowResults && pollResults != null)
|
||||
Text(
|
||||
'${(percentage * 100).toStringAsFixed(1)}% ($voteCount)',
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize - 2,
|
||||
color: widget.color.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Progress bar
|
||||
if (shouldShowResults && pollResults != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, left: 40),
|
||||
child: LinearProgressIndicator(
|
||||
value: percentage,
|
||||
backgroundColor: widget.color.withOpacity(0.2),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
isSelected
|
||||
? Colors.blue
|
||||
: widget.color.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Vote button and info
|
||||
Row(
|
||||
children: [
|
||||
if (canVote && selectedAnswers.isNotEmpty)
|
||||
// Show "Change Vote" button only when selection has changed
|
||||
if (!hasVoted || (hasVoted && hasChanged))
|
||||
ElevatedButton(
|
||||
onPressed: isLoading ? null : () => _vote(selectedAnswers),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(hasVoted ? 'Change Vote' : 'Vote'),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Poll info
|
||||
Text(
|
||||
'${maxSelections == 1 ? 'Single' : 'Multiple'} choice • '
|
||||
'${kind?.contains('undisclosed') == true ? 'Anonymous' : 'Public'} • '
|
||||
'${isEnded ? 'Ended' : 'Active'}',
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize - 2,
|
||||
color: widget.color.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (selectedAnswers.isNotEmpty && maxSelections > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'${selectedAnswers.length} of $maxSelections selected',
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize - 2,
|
||||
color: widget.color.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:extera_next/config/app_config.dart';
|
||||
import 'package:extera_next/utils/clean_exif.dart';
|
||||
import 'package:extera_next/widgets/matrix.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
|
@ -12,7 +13,6 @@ import 'package:mime/mime.dart';
|
|||
|
||||
import 'package:extera_next/utils/localized_exception_extension.dart';
|
||||
import 'package:extera_next/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'package:extera_next/utils/other_party_can_receive.dart';
|
||||
import 'package:extera_next/utils/platform_infos.dart';
|
||||
import 'package:extera_next/utils/size_string.dart';
|
||||
import 'package:extera_next/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
|
|
|
|||