feat: add drag and drop for media uploads (#809)

* feat: add drag and drop for media uploads

fixes #65

* tweak colors
This commit is contained in:
Nolan Lawson 2018-12-15 02:06:12 -08:00 committed by GitHub
parent fd1310c2c1
commit 7ddfe3830a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 130 additions and 20 deletions

10
package-lock.json generated
View File

@ -236,6 +236,11 @@
"@xtuc/long": "4.2.1" "@xtuc/long": "4.2.1"
} }
}, },
"@webcomponents/custom-elements": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.2.1.tgz",
"integrity": "sha512-flmTp4rVbBkcUIF3eBO3LNoAaYvleTdhPZKzdzr6iztWLLrxCctcK+7MAQeC3/SPjc3JDdC3jYLMRF4R6C3f9g=="
},
"@xtuc/ieee754": { "@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -4528,6 +4533,11 @@
"remedial": ">= 1.0.7" "remedial": ">= 1.0.7"
} }
}, },
"file-drop-element": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/file-drop-element/-/file-drop-element-0.0.9.tgz",
"integrity": "sha512-LfcczsUadIDGh9uyVLF3fjHr1bT/E7NzyD/9m+/ANJTiys8k0IhLkb7ZWdFWNZPlvF8sdTuvnqZWYDPlmqj+rw=="
},
"file-entry-cache": { "file-entry-cache": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",

View File

@ -42,6 +42,7 @@
}, },
"dependencies": { "dependencies": {
"@gamestdio/websocket": "^0.2.8", "@gamestdio/websocket": "^0.2.8",
"@webcomponents/custom-elements": "^1.2.1",
"browserslist": "^4.3.5", "browserslist": "^4.3.5",
"cheerio": "^1.0.0-rc.2", "cheerio": "^1.0.0-rc.2",
"child-process-promise": "^2.2.1", "child-process-promise": "^2.2.1",
@ -56,6 +57,7 @@
"events-light": "^1.0.5", "events-light": "^1.0.5",
"express": "^4.16.4", "express": "^4.16.4",
"file-api": "^0.10.4", "file-api": "^0.10.4",
"file-drop-element": "0.0.9",
"font-awesome-svg-png": "^1.2.2", "font-awesome-svg-png": "^1.2.2",
"form-data": "^2.3.3", "form-data": "^2.3.3",
"glob": "^7.1.3", "glob": "^7.1.3",

View File

@ -93,4 +93,6 @@
--compose-autosuggest-outline: #{lighten($focus-outline, 5%)}; --compose-autosuggest-outline: #{lighten($focus-outline, 5%)};
--compose-button-halo: #{rgba(255, 255, 255, 0.1)}; --compose-button-halo: #{rgba(255, 255, 255, 0.1)};
--file-drop-mask: #{rgba(255, 255, 255, 0.8)};
} }

View File

@ -29,4 +29,6 @@
--compose-autosuggest-item-hover: #{lighten($main-bg-color, 10%)}; --compose-autosuggest-item-hover: #{lighten($main-bg-color, 10%)};
--compose-autosuggest-item-active: #{lighten($main-bg-color, 15%)}; --compose-autosuggest-item-active: #{lighten($main-bg-color, 15%)};
--compose-autosuggest-outline: #{lighten($border-color, 20%)}; --compose-autosuggest-outline: #{lighten($border-color, 20%)};
--file-drop-mask: #{rgba(30, 30, 30, 0.8)};
} }

View File

@ -9,6 +9,9 @@ export async function doMediaUpload (realm, file) {
try { try {
let response = await uploadMedia(currentInstance, accessToken, file) let response = await uploadMedia(currentInstance, accessToken, file)
let composeMedia = store.getComposeData(realm, 'media') || [] let composeMedia = store.getComposeData(realm, 'media') || []
if (composeMedia.length === 4) {
throw new Error('Only 4 media max are allowed')
}
composeMedia.push({ composeMedia.push({
data: response, data: response,
file: { name: file.name }, file: { name: file.name },

View File

@ -1,20 +1,22 @@
{#if realm === 'home'} {#if realm === 'home'}
<h1 class="sr-only">Compose status</h1> <h1 class="sr-only">Compose status</h1>
{/if} {/if}
<div class="{computedClassName} {hideAndFadeIn}"> <ComposeFileDrop {realm} >
<ComposeAuthor /> <div class="{computedClassName} {hideAndFadeIn}">
{#if contentWarningShown} <ComposeAuthor />
<div class="compose-content-warning-wrapper" {#if contentWarningShown}
transition:slide="{duration: 333}"> <div class="compose-content-warning-wrapper"
<ComposeContentWarning {realm} {contentWarning} /> transition:slide="{duration: 333}">
</div> <ComposeContentWarning {realm} {contentWarning} />
{/if} </div>
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" /> {/if}
<ComposeLengthGauge {length} {overLimit} /> <ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
<ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} /> <ComposeLengthGauge {length} {overLimit} />
<ComposeLengthIndicator {length} {overLimit} /> <ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} />
<ComposeMedia {realm} {media} /> <ComposeLengthIndicator {length} {overLimit} />
</div> <ComposeMedia {realm} {media} />
</div>
</ComposeFileDrop>
<ComposeStickyButton {showSticky} <ComposeStickyButton {showSticky}
{overLimit} {overLimit}
{hideAndFadeIn} {hideAndFadeIn}
@ -22,6 +24,7 @@
{#if !hideBottomBorder} {#if !hideBottomBorder}
<div class="compose-box-border-bottom {hideAndFadeIn}"></div> <div class="compose-box-border-bottom {hideAndFadeIn}"></div>
{/if} {/if}
<style> <style>
.compose-box { .compose-box {
border-radius: 4px; border-radius: 4px;
@ -85,6 +88,7 @@
import ComposeStickyButton from './ComposeStickyButton.html' import ComposeStickyButton from './ComposeStickyButton.html'
import ComposeMedia from './ComposeMedia.html' import ComposeMedia from './ComposeMedia.html'
import ComposeContentWarning from './ComposeContentWarning.html' import ComposeContentWarning from './ComposeContentWarning.html'
import ComposeFileDrop from './ComposeFileDrop.html'
import { measureText } from '../../_utils/measureText' import { measureText } from '../../_utils/measureText'
import { POST_PRIVACY_OPTIONS } from '../../_static/statuses' import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
import { store } from '../../_store/store' import { store } from '../../_store/store'
@ -118,7 +122,8 @@
ComposeInput, ComposeInput,
ComposeStickyButton, ComposeStickyButton,
ComposeMedia, ComposeMedia,
ComposeContentWarning ComposeContentWarning,
ComposeFileDrop
}, },
data: () => ({ data: () => ({
size: void 0, size: void 0,

View File

@ -0,0 +1,71 @@
<file-drop class="file-drop" accept={mediaAccept} ref:fileDrop >
<div class="file-drop-info">
<div class="file-drop-info-text">
<span class="file-drop-info-text-valid">Drop to upload</span>
<span class="file-drop-info-text-invalid">Invalid file type</span>
</div>
</div>
<slot></slot>
</file-drop>
<style>
.file-drop {
display: block;
position: relative;
}
.file-drop-info {
display: none;
pointer-events: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 6000;
justify-content: center;
align-items: center;
}
.file-drop-info-text {
font-size: 1.2em;
color: var(--button-text);
padding: 10px 20px;
border: 2px dashed var(--button-text);
border-radius: 6px;
}
:global(.file-drop.drop-valid .file-drop-info, .file-drop.drop-invalid .file-drop-info) {
display: flex;
background: var(--file-drop-mask);
}
:global(.file-drop.drop-valid .file-drop-info-text-invalid, .file-drop.drop-invalid .file-drop-info-text-valid) {
display: none;
}
</style>
<script>
import { mediaAccept } from '../../_static/media'
import { doMediaUpload } from '../../_actions/media'
if (process.browser) {
require('file-drop-element')
}
export default {
oncreate () {
this.onFileDrop = this.onFileDrop.bind(this)
this.refs.fileDrop.addEventListener('filedrop', this.onFileDrop)
},
ondestroy () {
this.refs.fileDrop.removeEventListener('filedrop', this.onFileDRop)
},
data: () => ({
mediaAccept
}),
methods: {
onFileDrop (e) {
let { file } = e
let { realm } = this.get()
/* no await */ doMediaUpload(realm, file)
}
}
}
</script>

View File

@ -29,7 +29,7 @@
on:change="onFileChange(event)" on:change="onFileChange(event)"
style="display: none;" style="display: none;"
type="file" type="file"
accept=".jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,image/jpeg,image/png,image/gif,video/webm,video/mp4"> accept={mediaAccept} >
<ComposeAutosuggest {realm} {text} /> <ComposeAutosuggest {realm} {text} />
</div> </div>
<style> <style>
@ -50,6 +50,7 @@
import { doMediaUpload } from '../../_actions/media' import { doMediaUpload } from '../../_actions/media'
import { toggleContentWarningShown } from '../../_actions/contentWarnings' import { toggleContentWarningShown } from '../../_actions/contentWarnings'
import ComposeAutosuggest from './ComposeAutosuggest.html' import ComposeAutosuggest from './ComposeAutosuggest.html'
import { mediaAccept } from '../../_static/media'
export default { export default {
oncreate () { oncreate () {
@ -66,6 +67,9 @@
IconButton, IconButton,
ComposeAutosuggest ComposeAutosuggest
}, },
data: () => ({
mediaAccept
}),
store: () => store, store: () => store,
methods: { methods: {
async onEmojiClick () { async onEmojiClick () {
@ -92,4 +96,4 @@
} }
} }
} }
</script> </script>

View File

@ -3,3 +3,8 @@ export const DEFAULT_MEDIA_HEIGHT = 200
export const ONE_TRANSPARENT_PIXEL = export const ONE_TRANSPARENT_PIXEL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
export const mediaAccept = '.jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,.mov,image/jpeg,image/png,' +
'image/gif,video/webm,video/mp4,video/quicktime,' +
// some instances allow audio uploads
'audio/mpeg,audio/mp4,audio/vnd.wav,audio/wav,audio/x-wav,audio/x-wave,audio/ogg'

View File

@ -20,6 +20,10 @@ export const importIndexedDBGetAllShim = () => import(
/* webpackChunkName: 'indexeddb-getall-shim' */ 'indexeddb-getall-shim' /* webpackChunkName: 'indexeddb-getall-shim' */ 'indexeddb-getall-shim'
) )
export const importCustomElementsPolyfill = () => import(
/* webpackChunkName: '@webcomponents/custom-elements' */ '@webcomponents/custom-elements'
)
export const importPageLifecycle = () => import( export const importPageLifecycle = () => import(
/* webpackChunkName: 'page-lifecycle' */ 'page-lifecycle/dist/lifecycle.mjs' /* webpackChunkName: 'page-lifecycle' */ 'page-lifecycle/dist/lifecycle.mjs'
).then(getDefault) ).then(getDefault)

View File

@ -1,4 +1,5 @@
import { import {
importCustomElementsPolyfill,
importIndexedDBGetAllShim, importIndexedDBGetAllShim,
importIntersectionObserver, importIntersectionObserver,
importRequestIdleCallback, importRequestIdleCallback,
@ -10,6 +11,7 @@ export function loadPolyfills () {
typeof IntersectionObserver === 'undefined' && importIntersectionObserver(), typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(), typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
!Element.prototype.animate && importWebAnimationPolyfill(), !Element.prototype.animate && importWebAnimationPolyfill(),
!IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim() !IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
typeof customElements === 'undefined' && importCustomElementsPolyfill()
]) ])
} }

View File

@ -18,11 +18,11 @@
<!-- begin inline CSS --> <!-- begin inline CSS -->
<style> <style>
:root{--button-primary-bg: #6081e6;--button-primary-text: #fff;--button-primary-border: #132c76;--button-primary-bg-active: #456ce2;--button-primary-bg-hover: #6988e7;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--input-bg: #fff;--anchor-text: #4169e1;--main-bg: #fff;--body-bg: #e8edfb;--body-text-color: #333;--main-border: #dadada;--svg-fill: #4169e1;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #4169e1;--nav-border: #214cce;--nav-a-border: #4169e1;--nav-a-selected-border: #fff;--nav-a-selected-bg: #6d8ce8;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #839deb;--nav-a-bg-hover: #577ae4;--nav-a-border-hover: #4169e1;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #90a8ee;--action-button-fill-color-hover: #a2b6f0;--action-button-fill-color-active: #577ae4;--action-button-fill-color-pressed: #2351dc;--action-button-fill-color-pressed-hover: #3862e0;--action-button-fill-color-pressed-active: #1d44b8;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #4169e1;--settings-list-item-text-hover: #4169e1;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #c5d1f6;--very-deemphasized-link-color: rgba(65,105,225,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #d2dcf8;--main-theme-color: #4169e1;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #ced8f7;--compose-autosuggest-item-active: #b8c7f4;--compose-autosuggest-outline: #dbe3f9;--compose-button-halo: rgba(255,255,255,0.1)}:root{--scrollbar-face-color: #90a8ee;--scrollbar-track-color: #dbe3f9;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #99afef;--scrollbar-face-color-active: #839deb;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent} :root{--button-primary-bg: #6081e6;--button-primary-text: #fff;--button-primary-border: #132c76;--button-primary-bg-active: #456ce2;--button-primary-bg-hover: #6988e7;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--input-bg: #fff;--anchor-text: #4169e1;--main-bg: #fff;--body-bg: #e8edfb;--body-text-color: #333;--main-border: #dadada;--svg-fill: #4169e1;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #4169e1;--nav-border: #214cce;--nav-a-border: #4169e1;--nav-a-selected-border: #fff;--nav-a-selected-bg: #6d8ce8;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #839deb;--nav-a-bg-hover: #577ae4;--nav-a-border-hover: #4169e1;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #90a8ee;--action-button-fill-color-hover: #a2b6f0;--action-button-fill-color-active: #577ae4;--action-button-fill-color-pressed: #2351dc;--action-button-fill-color-pressed-hover: #3862e0;--action-button-fill-color-pressed-active: #1d44b8;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #4169e1;--settings-list-item-text-hover: #4169e1;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #c5d1f6;--very-deemphasized-link-color: rgba(65,105,225,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #d2dcf8;--main-theme-color: #4169e1;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #ced8f7;--compose-autosuggest-item-active: #b8c7f4;--compose-autosuggest-outline: #dbe3f9;--compose-button-halo: rgba(255,255,255,0.1);--file-drop-mask: rgba(255,255,255,0.8)}:root{--scrollbar-face-color: #90a8ee;--scrollbar-track-color: #dbe3f9;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #99afef;--scrollbar-face-color-active: #839deb;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent}
@font-face{font-family:PinaforeRegular;src:local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Oxygen-Sans"),local("Ubuntu"),local("Cantarell"),local("Fira Sans"),local("Droid Sans"),local("Helvetica Neue")}@font-face{font-family:PinaforeEmoji;src:local("Apple Color Emoji"),local("Segoe UI Emoji"),local("Segoe UI Symbol"),local("Twemoji Mozilla"),local("Noto Color Emoji"),local("EmojiOne Color"),local("Android Emoji")}body{margin:0;font-family:system-ui, -apple-system, PinaforeRegular, sans-serif;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);-webkit-tap-highlight-color:transparent;overflow-x:hidden}.main-content{contain:content;padding-top:42px}@supports (-webkit-overflow-scrolling: touch){.main-content{overflow-x:hidden}}@media (max-width: 991px){.main-content{padding-top:52px}}@media (max-width: 767px){.main-content{padding-top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:15px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}input[type=search]{-webkit-appearance:none}input,textarea{background:inherit;color:inherit}textarea{font-family:system-ui, -apple-system, PinaforeRegular, sans-serif, PinaforeEmoji}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}.container:focus{outline:none}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 1.5s infinite linear}.ellipsis::after{content:"\2026"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.inline-custom-emoji{width:1.4em;height:1.4em;margin:-0.1em 0;object-fit:contain;vertical-align:middle}.inline-emoji{font-family:PinaforeEmoji, sans-serif} @font-face{font-family:PinaforeRegular;src:local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Oxygen-Sans"),local("Ubuntu"),local("Cantarell"),local("Fira Sans"),local("Droid Sans"),local("Helvetica Neue")}@font-face{font-family:PinaforeEmoji;src:local("Apple Color Emoji"),local("Segoe UI Emoji"),local("Segoe UI Symbol"),local("Twemoji Mozilla"),local("Noto Color Emoji"),local("EmojiOne Color"),local("Android Emoji")}body{margin:0;font-family:system-ui, -apple-system, PinaforeRegular, sans-serif;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);-webkit-tap-highlight-color:transparent;overflow-x:hidden}.main-content{contain:content;padding-top:42px}@supports (-webkit-overflow-scrolling: touch){.main-content{overflow-x:hidden}}@media (max-width: 991px){.main-content{padding-top:52px}}@media (max-width: 767px){.main-content{padding-top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:15px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}input[type=search]{-webkit-appearance:none}input,textarea{background:inherit;color:inherit}textarea{font-family:system-ui, -apple-system, PinaforeRegular, sans-serif, PinaforeEmoji}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}.container:focus{outline:none}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 1.5s infinite linear}.ellipsis::after{content:"\2026"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.inline-custom-emoji{width:1.4em;height:1.4em;margin:-0.1em 0;object-fit:contain;vertical-align:middle}.inline-emoji{font-family:PinaforeEmoji, sans-serif}
</style> </style>
<style media="only x" id="theOfflineStyle"> <style media="only x" id="theOfflineStyle">
:root{--button-primary-bg: #ababab;--button-primary-text: #fff;--button-primary-border: #4d4d4d;--button-primary-bg-active: #9c9c9c;--button-primary-bg-hover: #b0b0b0;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--input-bg: #fff;--anchor-text: #999;--main-bg: #fff;--body-bg: #fafafa;--body-text-color: #333;--main-border: #dadada;--svg-fill: #999;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #999;--nav-border: gray;--nav-a-border: #999;--nav-a-selected-border: #fff;--nav-a-selected-bg: #b3b3b3;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #bfbfbf;--nav-a-bg-hover: #a6a6a6;--nav-a-border-hover: #999;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #c7c7c7;--action-button-fill-color-hover: #d1d1d1;--action-button-fill-color-active: #a6a6a6;--action-button-fill-color-pressed: #878787;--action-button-fill-color-pressed-hover: #949494;--action-button-fill-color-pressed-active: #737373;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #999;--settings-list-item-text-hover: #999;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #bfbfbf;--very-deemphasized-link-color: rgba(153,153,153,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #ededed;--main-theme-color: #999;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #c4c4c4;--compose-autosuggest-item-active: #b8b8b8;--compose-autosuggest-outline: #ccc;--compose-button-halo: rgba(255,255,255,0.1)}:root{--scrollbar-face-color: #c7c7c7;--scrollbar-track-color: #f2f2f2;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #ccc;--scrollbar-face-color-active: #bfbfbf;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent} :root{--button-primary-bg: #ababab;--button-primary-text: #fff;--button-primary-border: #4d4d4d;--button-primary-bg-active: #9c9c9c;--button-primary-bg-hover: #b0b0b0;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--input-bg: #fff;--anchor-text: #999;--main-bg: #fff;--body-bg: #fafafa;--body-text-color: #333;--main-border: #dadada;--svg-fill: #999;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #999;--nav-border: gray;--nav-a-border: #999;--nav-a-selected-border: #fff;--nav-a-selected-bg: #b3b3b3;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #bfbfbf;--nav-a-bg-hover: #a6a6a6;--nav-a-border-hover: #999;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #c7c7c7;--action-button-fill-color-hover: #d1d1d1;--action-button-fill-color-active: #a6a6a6;--action-button-fill-color-pressed: #878787;--action-button-fill-color-pressed-hover: #949494;--action-button-fill-color-pressed-active: #737373;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #999;--settings-list-item-text-hover: #999;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #bfbfbf;--very-deemphasized-link-color: rgba(153,153,153,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #ededed;--main-theme-color: #999;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #c4c4c4;--compose-autosuggest-item-active: #b8b8b8;--compose-autosuggest-outline: #ccc;--compose-button-halo: rgba(255,255,255,0.1);--file-drop-mask: rgba(255,255,255,0.8)}:root{--scrollbar-face-color: #c7c7c7;--scrollbar-track-color: #f2f2f2;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #ccc;--scrollbar-face-color-active: #bfbfbf;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent}
</style> </style>
<style media="all" id="theScrollbarStyle"> <style media="all" id="theScrollbarStyle">
html{scrollbar-face-color:var(--scrollbar-face-color);scrollbar-track-color:var(--scrollbar-track-color);scrollbar-color:var(--scrollbar-face-color) var(--scrollbar-track-color)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-height)}::-webkit-scrollbar-thumb{background:var(--scrollbar-face-color);border-radius:var(--scrollbar-border-radius)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-face-color-hover)}::-webkit-scrollbar-thumb:active{background:var(--scrollbar-face-color-active)}::-webkit-scrollbar-track,::-webkit-scrollbar-track:hover,::-webkit-scrollbar-track:active{background:var(--scrollbar-track-color)}::-webkit-scrollbar-corner{background:var(--scrollbar-background-color)} html{scrollbar-face-color:var(--scrollbar-face-color);scrollbar-track-color:var(--scrollbar-track-color);scrollbar-color:var(--scrollbar-face-color) var(--scrollbar-track-color)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-height)}::-webkit-scrollbar-thumb{background:var(--scrollbar-face-color);border-radius:var(--scrollbar-border-radius)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-face-color-hover)}::-webkit-scrollbar-thumb:active{background:var(--scrollbar-face-color-active)}::-webkit-scrollbar-track,::-webkit-scrollbar-track:hover,::-webkit-scrollbar-track:active{background:var(--scrollbar-track-color)}::-webkit-scrollbar-corner{background:var(--scrollbar-background-color)}