mirror of https://github.com/kern/filepizza
WIP
parent
d8febe77be
commit
8ad9855e47
File diff suppressed because one or more lines are too long
@ -1,51 +0,0 @@
|
|||||||
// TODO: Rename this.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
// Taken from StackOverflow
|
|
||||||
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
|
|
||||||
function formatSize(bytes) {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
const k = 1000;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
let i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Arrow extends React.Component {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let classes = classnames('arrow', {
|
|
||||||
'arrow-up': this.props.dir === 'up',
|
|
||||||
'arrow-down': this.props.dir === 'down',
|
|
||||||
'arrow-animated': this.props.animated
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className={classes}>
|
|
||||||
|
|
||||||
<div className="arrow-border" />
|
|
||||||
<div className="arrow-image">{this.props.dir === 'up' ? '^' : 'v'}</div>
|
|
||||||
|
|
||||||
{this.props.name === null ? null
|
|
||||||
: <div className="arrow-name">{this.props.name}</div>}
|
|
||||||
{this.props.size === null ? null
|
|
||||||
: <div className="arrow-size">{formatSize(this.props.size)}</div>}
|
|
||||||
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Arrow.propTypes = {
|
|
||||||
dir: React.PropTypes.oneOf(['up', 'down']).isRequired,
|
|
||||||
name: React.PropTypes.string,
|
|
||||||
size: React.PropTypes.number,
|
|
||||||
animated: React.PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
Arrow.defaultProps = {
|
|
||||||
name: null,
|
|
||||||
size: null,
|
|
||||||
animated: false
|
|
||||||
};
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class DownloadButton extends React.Component {
|
||||||
|
|
||||||
|
onClick(e) {
|
||||||
|
this.props.onClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <button
|
||||||
|
className="download-button"
|
||||||
|
onClick={this.onClick.bind(this)}>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadButton.propTypes = {
|
||||||
|
onClick: React.PropTypes.func.isRequired
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
function formatProgress(dec) {
|
||||||
|
return (dec * 100).toPrecision(3) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ProgressBar extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const failed = this.props.value < 0;
|
||||||
|
const classes = classnames('progress-bar', {
|
||||||
|
'progress-bar-failed': failed
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatted = formatProgress(this.props.value)
|
||||||
|
|
||||||
|
return <div className={classes}>
|
||||||
|
{failed
|
||||||
|
? <div className="progress-bar-text">Failed</div>
|
||||||
|
: <div
|
||||||
|
className="progress-bar-inner"
|
||||||
|
style={{width: formatted}}>
|
||||||
|
<div className="progress-bar-text">
|
||||||
|
{formatted}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressBar.propTypes = {
|
||||||
|
value: React.PropTypes.number.isRequired
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
// Taken from StackOverflow
|
||||||
|
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const k = 1000
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Spinner extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const classes = classnames('spinner', {
|
||||||
|
'spinner-animated': this.props.animated
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className={classes}>
|
||||||
|
|
||||||
|
<div className="spinner-border" />
|
||||||
|
|
||||||
|
<div className="spinner-content">
|
||||||
|
|
||||||
|
<img
|
||||||
|
alt={this.props.name || this.props.dir}
|
||||||
|
src={`/images/${this.props.dir}.png`}
|
||||||
|
className="spinner-image" />
|
||||||
|
|
||||||
|
{this.props.name === null ? null
|
||||||
|
: <div className="spinner-name">{this.props.name}</div>}
|
||||||
|
{this.props.size === null ? null
|
||||||
|
: <div className="spinner-size">{formatSize(this.props.size)}</div>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spinner.propTypes = {
|
||||||
|
dir: React.PropTypes.oneOf(['up', 'down']).isRequired,
|
||||||
|
name: React.PropTypes.string,
|
||||||
|
size: React.PropTypes.number,
|
||||||
|
animated: React.PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
Spinner.defaultProps = {
|
||||||
|
name: null,
|
||||||
|
size: null,
|
||||||
|
animated: false
|
||||||
|
}
|
||||||
@ -1,20 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
|
|
||||||
export default class Tempalink extends React.Component {
|
export default class Tempalink extends React.Component {
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
this.refs.input.getDOMNode().setSelectionRange(0, 9999);
|
this.refs.input.getDOMNode().setSelectionRange(0, 9999)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var url = window.location.origin + '/d/' + this.props.token;
|
var url = window.location.origin + '/d/' + this.props.token
|
||||||
return <input
|
return <input
|
||||||
className="tempalink"
|
className="tempalink"
|
||||||
onClick={this.onClick.bind(this)}
|
onClick={this.onClick.bind(this)}
|
||||||
readOnly
|
readOnly
|
||||||
ref="input"
|
ref="input"
|
||||||
type="text"
|
type="text"
|
||||||
value={url} />;
|
value={url} />
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import ReactRouter from 'react-router';
|
import ReactRouter from 'react-router'
|
||||||
import routes from './routes';
|
import routes from './routes'
|
||||||
import alt from './alt';
|
import alt from './alt'
|
||||||
|
|
||||||
let bootstrap = document.documentElement.getAttribute('data-bootstrap');
|
let bootstrap = document.documentElement.getAttribute('data-bootstrap')
|
||||||
alt.bootstrap(bootstrap);
|
alt.bootstrap(bootstrap)
|
||||||
|
|
||||||
ReactRouter.run(routes, ReactRouter.HistoryLocation, function (Handler) {
|
ReactRouter.run(routes, ReactRouter.HistoryLocation, function (Handler) {
|
||||||
React.render(<Handler data={bootstrap} />, document);
|
React.render(<Handler data={bootstrap} />, document)
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import uuid from 'node-uuid';
|
import uuid from 'node-uuid'
|
||||||
|
|
||||||
let id = uuid.v4();
|
const id = uuid.v4()
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
var peer = { id: id };
|
var peer = { id: id }
|
||||||
} else {
|
} else {
|
||||||
let Peer = require('peerjs');
|
let Peer = require('peerjs')
|
||||||
var peer = new Peer(id, {
|
var peer = new Peer(id, {
|
||||||
host: window.location.hostname,
|
host: window.location.hostname,
|
||||||
port: window.location.port,
|
port: window.location.port,
|
||||||
path: '/peer'
|
path: '/peer'
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default peer;
|
export default peer
|
||||||
|
|||||||
@ -1,68 +1,63 @@
|
|||||||
import DownloadActions from '../actions/DownloadActions';
|
import DownloadActions from '../actions/DownloadActions'
|
||||||
import DownloadFile from '../DownloadFile';
|
import DownloadFile from '../DownloadFile'
|
||||||
import peer from '../peer';
|
import peer from '../peer'
|
||||||
import alt from '../alt';
|
import alt from '../alt'
|
||||||
import socket from '../socket';
|
import socket from '../socket'
|
||||||
|
|
||||||
export default alt.createStore(class DownloadStore {
|
export default alt.createStore(class DownloadStore {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.bindActions(DownloadActions);
|
this.bindActions(DownloadActions)
|
||||||
|
|
||||||
this.status = 'offline';
|
this.status = 'ready'
|
||||||
this.token = null;
|
this.token = null
|
||||||
this.file = null;
|
this.file = null
|
||||||
this.progress = 0;
|
this.progress = 0
|
||||||
|
|
||||||
this.on('bootstrap', () => {
|
this.on('bootstrap', () => {
|
||||||
if (this.file && !(this.file instanceof DownloadFile)) {
|
if (this.file && !(this.file instanceof DownloadFile)) {
|
||||||
this.file = new DownloadFile(this.file.name,
|
this.file = new DownloadFile(this.file.name,
|
||||||
this.file.size,
|
this.file.size,
|
||||||
this.file.type);
|
this.file.type)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onRequestDownload() {
|
onRequestDownload() {
|
||||||
if (this.status !== 'ready') return;
|
if (this.status !== 'ready') return
|
||||||
this.status = 'requesting';
|
this.status = 'requesting'
|
||||||
|
|
||||||
socket.emit('download', {
|
socket.emit('download', {
|
||||||
peerID: peer.id,
|
peerID: peer.id,
|
||||||
token: this.token
|
token: this.token
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeginDownload(conn) {
|
onBeginDownload(conn) {
|
||||||
if (this.status !== 'requesting') return;
|
if (this.status !== 'requesting') return
|
||||||
this.status = 'downloading';
|
this.status = 'downloading'
|
||||||
|
|
||||||
let chunkSize = conn.metadata.chunkSize;
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
conn.on('data', (data) => {
|
conn.on('data', (chunk) => {
|
||||||
if (this.status !== 'downloading') return;
|
if (this.status !== 'downloading') return
|
||||||
|
|
||||||
console.log(data.byteLength);
|
this.file.addChunk(chunk)
|
||||||
this.file.addPacket(data);
|
|
||||||
i++;
|
|
||||||
|
|
||||||
if (this.file.isComplete()) {
|
if (this.file.isComplete()) {
|
||||||
this.setState({ status: 'done', progress: 1 });
|
this.setState({ status: 'done', progress: 1 })
|
||||||
this.file.download();
|
this.file.download()
|
||||||
conn.close();
|
conn.close()
|
||||||
} else {
|
} else {
|
||||||
this.setState({ progress: this.file.getProgress() });
|
this.setState({ progress: this.file.getProgress() })
|
||||||
if (i % chunkSize === 0) conn.send('more');
|
conn.send('more')
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
})
|
||||||
|
|
||||||
conn.on('close', () => {
|
conn.on('close', () => {
|
||||||
if (this.status !== 'downloading') return;
|
if (this.status !== 'downloading') return
|
||||||
this.setState({ status: 'cancelled', progress: 0 });
|
this.setState({ status: 'cancelled', progress: 0 })
|
||||||
this.file.clearPackets();
|
this.file.clearChunks()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 'DownloadStore')
|
}, 'DownloadStore')
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
body
|
||||||
|
color red
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@ -1,142 +1,214 @@
|
|||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
.upload-page {
|
html {
|
||||||
margin: 100px auto 0;
|
height: 100%;
|
||||||
width: 300px;
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: table;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone {
|
.drop-zone {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
width: 100%;
|
||||||
bottom: 0;
|
height: 100%;
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
background: rgba(0, 0, 0, 0);
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone-focus {
|
.drop-zone-focus {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
z-index: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone-focus:after {
|
.drop-zone-focus:after {
|
||||||
color: white;
|
color: white;
|
||||||
content: 'DROP TO UPLOAD';
|
content: 'DROP TO UPLOAD';
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
font: 24px/40px sans-serif;
|
font: 24px/40px "Quicksand", sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0 1px #333;
|
text-shadow: 0 1px #333;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.spinner {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
margin-bottom: 40px;
|
||||||
|
|
||||||
.arrow .arrow-border {
|
display: flex;
|
||||||
position: absolute;
|
align-items: center;
|
||||||
width: 100%;
|
justify-content: center;
|
||||||
height: 100%;
|
}
|
||||||
border: 20px dotted #CCC;
|
|
||||||
border-radius: 100%;
|
.spinner .spinner-border {
|
||||||
|
position: absolute;
|
||||||
-webkit-transition: -webkit-transform 1s;
|
top: 0;
|
||||||
-moz-transition: -moz-transform 1s;
|
left: 0;
|
||||||
transition: transform 1s;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
border: 20px dotted #40C0CB;
|
||||||
.arrow.arrow-animated .arrow-border {
|
border-radius: 100%;
|
||||||
-webkit-animation: rotate 5s infinite linear;
|
|
||||||
-moz-animation: rotate 5s infinite linear;
|
-webkit-transition: -webkit-transform 1s;
|
||||||
animation: rotate 5s infinite linear;
|
-moz-transition: -moz-transform 1s;
|
||||||
}
|
transition: transform 1s;
|
||||||
|
}
|
||||||
.arrow .arrow-image {
|
|
||||||
position: absolute;
|
.spinner.spinner-animated .spinner-border {
|
||||||
top: 60px;
|
-webkit-animation: rotate 5s infinite linear;
|
||||||
left: 80px;
|
-moz-animation: rotate 5s infinite linear;
|
||||||
right: 80px;
|
animation: rotate 5s infinite linear;
|
||||||
bottom: 100px;
|
}
|
||||||
background: red;
|
|
||||||
}
|
.spinner .spinner-image {
|
||||||
|
display: block;
|
||||||
.arrow .arrow-name {
|
width: 150px;
|
||||||
bottom: 60px;
|
}
|
||||||
font: bold 18px/20px sans-serif;
|
|
||||||
left: 60px;
|
.spinner .spinner-name {
|
||||||
overflow: hidden;
|
font: bold 18px/20px "Quicksand", sans-serif;
|
||||||
position: absolute;
|
text-align: center;
|
||||||
right: 60px;
|
color: #333;
|
||||||
text-align: center;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: #333;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow .arrow-size {
|
.spinner .spinner-size {
|
||||||
bottom: 40px;
|
font: italic 12px/20px "Quicksand", sans-serif;
|
||||||
font: italic 12px/20px sans-serif;
|
text-align: center;
|
||||||
left: 60px;
|
color: #777;
|
||||||
position: absolute;
|
overflow: hidden;
|
||||||
right: 60px;
|
text-overflow: ellipsis;
|
||||||
text-align: center;
|
white-space: nowrap;
|
||||||
color: #777;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tempalink {
|
.tempalink {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid #EEE;
|
box-shadow: inset 0 1px 1px #AEE239, 0 1px 1px #EEE;
|
||||||
color: #333;
|
background: #8FBE00;
|
||||||
font: 18px/1 sans-serif;
|
color: white;
|
||||||
height: 40px;
|
border: 0;
|
||||||
margin: 60px 0 0;
|
margin: 20px 0;
|
||||||
padding: 10px;
|
font: 14px/1 monospace;
|
||||||
text-align: center;
|
height: 60px;
|
||||||
width: 100%;
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
color: #8FBE00;
|
||||||
font: bold 32px/40px sans-serif;
|
font: bold 32px/40px "Quicksand", sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 60px 0 10px;
|
margin: 0 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: #777;
|
color: #777;
|
||||||
font: 14px/20px sans-serif;
|
font: 14px/20px "Quicksand", sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 10px 0;
|
margin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data {
|
.data {
|
||||||
color: #777;
|
color: #777;
|
||||||
font: 14px/20px sans-serif;
|
font: 14px/20px "Quicksand", sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data > .datum {
|
.data > .datum {
|
||||||
float: left;
|
float: left;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button {
|
||||||
|
display: block;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 1px 1px #AEE239, 0 1px 1px #EEE;
|
||||||
|
padding: 20px;
|
||||||
|
background: #8FBE00;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
width: 100%;
|
||||||
|
font: bold 18px/20px "Quicksand", sans-serif;
|
||||||
|
transition: 0.25s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:hover, .download-button:focus {
|
||||||
|
box-shadow: inset 0 1px 1px #8FBE00;
|
||||||
|
background: #AEE239;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:active {
|
||||||
|
background: #8FBE00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #F9F2E7;
|
||||||
|
box-shadow: 0 1px 1px #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar .progress-bar-inner {
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
background: #8FBE00;
|
||||||
|
box-shadow: inset 0 1px 1px #AEE239;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar .progress-bar-text {
|
||||||
|
float: right;
|
||||||
|
font: 14px/60px "Quicksand", sans-serif;
|
||||||
|
color: white;
|
||||||
|
margin-right: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar.progress-bar-failed {
|
||||||
|
background: #C40D08;
|
||||||
|
box-shadow: inset 0 1px 1px #E23430, 0 1px 1px #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar.progress-bar-failed .progress-bar-text {
|
||||||
|
float: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes rotate {
|
@-webkit-keyframes rotate {
|
||||||
from { -webkit-transform: rotate(0deg); }
|
from { -webkit-transform: rotate(0deg); }
|
||||||
to { -webkit-transform: rotate(360deg); }
|
to { -webkit-transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@-moz-keyframes rotate {
|
@-moz-keyframes rotate {
|
||||||
from { -moz-transform: rotate(0deg); }
|
from { -moz-transform: rotate(0deg); }
|
||||||
to { -moz-transform: rotate(360deg); }
|
to { -moz-transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue