feat: view polls

chore: remove unused import in send_file_dialog
This commit is contained in:
OfficialDakari 2025-10-11 14:17:42 +05:00
parent 06d12c9e86
commit a904793528
53 changed files with 6092 additions and 21 deletions

View File

@ -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

View File

@ -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"]}

View File

@ -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"}]}]

View File

@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1 @@
Not Found

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -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}

View File

@ -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}

View File

@ -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;
});

View File

@ -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)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"app_name":"extera_next","version":"2.0.1","package_name":"extera_next"}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
```

View File

@ -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:

View File

@ -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,37 +7,391 @@ 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,
});
@override
State<StatefulWidget> createState() => PollWidgetState();
}
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?>;
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: EdgeInsetsGeometry.all(16),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(content?['question']['m.text'] as String, style: TextStyle(fontWeight: FontWeight.bold)),
Padding(
padding: EdgeInsets.fromLTRB(4, 4, 4, 2),
child: Column(
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),
),
),
),
)
],
),
);

View File

@ -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';
@ -79,7 +79,7 @@ class SendFileDialogState extends State<SendFileDialog> {
if (length > maxUploadSize) {
throw FileTooBigMatrixException(length, maxUploadSize);
}
// Else we just create a MatrixFile
file = MatrixFile(
bytes: Uint8List.fromList(ExifCleaner.removeExifData(await xfile.readAsBytes())),

2377
pubspec.lock Normal file

File diff suppressed because it is too large Load Diff