Added file uploads
This commit is contained in:
parent
73c8adc86a
commit
5319492760
@ -24,7 +24,10 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Application struct {
|
Application struct {
|
||||||
StaticDir string
|
Directories struct {
|
||||||
|
Static string
|
||||||
|
Upload string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Session struct {
|
Session struct {
|
||||||
|
49
file.go
Normal file
49
file.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
ID int
|
||||||
|
UserID int `db:"user_id"`
|
||||||
|
Filename string
|
||||||
|
Size int64
|
||||||
|
MIME string
|
||||||
|
MD5 string
|
||||||
|
Uploaded time.Time
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session Session) AddFile(file *File) (err error) {// {{{
|
||||||
|
file.UserID = session.UserID
|
||||||
|
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
rows, err = db.Queryx(`
|
||||||
|
INSERT INTO file(user_id, filename, size, mime, md5)
|
||||||
|
VALUES($1, $2, $3, $4, $5)
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
file.UserID,
|
||||||
|
file.Filename,
|
||||||
|
file.Size,
|
||||||
|
file.MIME,
|
||||||
|
file.MD5,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rows.Next()
|
||||||
|
err = rows.Scan(&file.ID)
|
||||||
|
fmt.Printf("%#v\n", file)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
84
main.go
84
main.go
@ -2,9 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
// Standard
|
// Standard
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"path/filepath"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -17,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const VERSION = "v0.1.2";
|
const VERSION = "v0.1.2";
|
||||||
const LISTEN_HOST = "0.0.0.0";
|
const LISTEN_HOST = "0.0.0.0";
|
||||||
const DB_SCHEMA = 2
|
const DB_SCHEMA = 4
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flagPort int
|
flagPort int
|
||||||
@ -59,7 +63,7 @@ func main() {// {{{
|
|||||||
connectionManager = NewConnectionManager()
|
connectionManager = NewConnectionManager()
|
||||||
go connectionManager.BroadcastLoop()
|
go connectionManager.BroadcastLoop()
|
||||||
|
|
||||||
static = http.FileServer(http.Dir(config.Application.StaticDir))
|
static = http.FileServer(http.Dir(config.Application.Directories.Static))
|
||||||
http.HandleFunc("/css_updated", cssUpdateHandler)
|
http.HandleFunc("/css_updated", cssUpdateHandler)
|
||||||
http.HandleFunc("/session/create", sessionCreate)
|
http.HandleFunc("/session/create", sessionCreate)
|
||||||
http.HandleFunc("/session/retrieve", sessionRetrieve)
|
http.HandleFunc("/session/retrieve", sessionRetrieve)
|
||||||
@ -70,6 +74,7 @@ func main() {// {{{
|
|||||||
http.HandleFunc("/node/update", nodeUpdate)
|
http.HandleFunc("/node/update", nodeUpdate)
|
||||||
http.HandleFunc("/node/rename", nodeRename)
|
http.HandleFunc("/node/rename", nodeRename)
|
||||||
http.HandleFunc("/node/delete", nodeDelete)
|
http.HandleFunc("/node/delete", nodeDelete)
|
||||||
|
http.HandleFunc("/node/upload", nodeUpload)
|
||||||
http.HandleFunc("/ws", websocketHandler)
|
http.HandleFunc("/ws", websocketHandler)
|
||||||
http.HandleFunc("/", staticHandler)
|
http.HandleFunc("/", staticHandler)
|
||||||
|
|
||||||
@ -352,6 +357,79 @@ func nodeDelete(w http.ResponseWriter, r *http.Request) {// {{{
|
|||||||
"OK": true,
|
"OK": true,
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
|
func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
log.Println("/node/upload")
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse our multipart form, 10 << 20 specifies a maximum
|
||||||
|
// upload of 10 MB files.
|
||||||
|
r.ParseMultipartForm(100 << 20)
|
||||||
|
|
||||||
|
// FormFile returns the first file for the given key `myFile`
|
||||||
|
// it also returns the FileHeader so we can get the Filename,
|
||||||
|
// the Header and the size of the file
|
||||||
|
file, handler, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read metadata of file for database,
|
||||||
|
// and also file contents for MD5, which is used
|
||||||
|
// to store the file on disk.
|
||||||
|
fileBytes, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
md5sumBytes := md5.Sum(fileBytes)
|
||||||
|
md5sum := hex.EncodeToString(md5sumBytes[:])
|
||||||
|
|
||||||
|
nodeFile := File{
|
||||||
|
Filename: handler.Filename,
|
||||||
|
Size: handler.Size,
|
||||||
|
MIME: handler.Header.Get("Content-Type"),
|
||||||
|
MD5: md5sum,
|
||||||
|
}
|
||||||
|
if err = session.AddFile(&nodeFile); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(
|
||||||
|
config.Application.Directories.Upload,
|
||||||
|
md5sum[0:1],
|
||||||
|
md5sum[1:2],
|
||||||
|
md5sum[2:3],
|
||||||
|
)
|
||||||
|
if err = os.MkdirAll(path, 0755); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path = filepath.Join(
|
||||||
|
config.Application.Directories.Upload,
|
||||||
|
md5sum[0:1],
|
||||||
|
md5sum[1:2],
|
||||||
|
md5sum[2:3],
|
||||||
|
md5sum,
|
||||||
|
)
|
||||||
|
if err = os.WriteFile(path, fileBytes, 0644); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
|
||||||
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
||||||
// Append index.html if needed for further reading of the file
|
// Append index.html if needed for further reading of the file
|
||||||
@ -359,7 +437,7 @@ func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{
|
|||||||
if p[len(p)-1] == '/' {
|
if p[len(p)-1] == '/' {
|
||||||
p += "index.html"
|
p += "index.html"
|
||||||
}
|
}
|
||||||
p = config.Application.StaticDir + p
|
p = config.Application.Directories.Static + p
|
||||||
|
|
||||||
base := path.Base(p)
|
base := path.Base(p)
|
||||||
if tmpl, err = template.New(base).ParseFiles(p); err != nil { return }
|
if tmpl, err = template.New(base).ParseFiles(p); err != nil { return }
|
||||||
|
10
sql/0003.sql
Normal file
10
sql/0003.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE public.file (
|
||||||
|
id serial NOT NULL,
|
||||||
|
user_id int4 NOT NULL,
|
||||||
|
filename varchar(256) NOT NULL DEFAULT '<noname>',
|
||||||
|
"size" int4 NOT NULL DEFAULT 0,
|
||||||
|
mime varchar(256) NOT NULL DEFAULT '',
|
||||||
|
uploaded timestamp NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT file_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT file_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
);
|
1
sql/0004.sql
Normal file
1
sql/0004.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE file ADD COLUMN md5 CHAR(32) DEFAULT ''
|
@ -24,8 +24,7 @@ h1 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
#menu-blackout {
|
#blackout {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
@ -34,9 +33,6 @@ h1 {
|
|||||||
z-index: 1024;
|
z-index: 1024;
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
#menu-blackout.show {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
#menu {
|
#menu {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -59,7 +55,7 @@ h1 {
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
#menu .item.separator {
|
#menu .item.separator {
|
||||||
border-bottom: 3px solid #000;
|
border-bottom: 2px solid #000;
|
||||||
}
|
}
|
||||||
#menu .item:hover {
|
#menu .item:hover {
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
@ -67,6 +63,54 @@ h1 {
|
|||||||
#menu .item:last-child {
|
#menu .item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
#upload {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1025;
|
||||||
|
}
|
||||||
|
#upload input {
|
||||||
|
border: 1px solid #000;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
#upload .files {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
#upload .files .file {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
#upload .files .file:nth-child(1) {
|
||||||
|
padding-top: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
#upload .files .file.done {
|
||||||
|
color: #0a0;
|
||||||
|
}
|
||||||
|
#upload .files .progress {
|
||||||
|
justify-self: end;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
#upload .files .progress:nth-child(2) {
|
||||||
|
padding-top: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
#upload .files .progress.done {
|
||||||
|
color: #0a0;
|
||||||
|
}
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr min-content min-content;
|
grid-template-columns: 1fr min-content min-content;
|
||||||
|
@ -73,7 +73,7 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
responseError({comm, app}) {//{{{
|
responseError({comm, app, upload}) {//{{{
|
||||||
if(comm !== undefined) {
|
if(comm !== undefined) {
|
||||||
comm.text().then(body=>alert(body))
|
comm.text().then(body=>alert(body))
|
||||||
return
|
return
|
||||||
@ -87,6 +87,11 @@ class App extends Component {
|
|||||||
if(app !== undefined) {
|
if(app !== undefined) {
|
||||||
alert(JSON.stringify(app))
|
alert(JSON.stringify(app))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(upload !== undefined) {
|
||||||
|
alert(upload)
|
||||||
|
return
|
||||||
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
async request(url, params) {//{{{
|
async request(url, params) {//{{{
|
||||||
return new Promise((resolve, reject)=>{
|
return new Promise((resolve, reject)=>{
|
||||||
|
@ -10,6 +10,7 @@ export class NodeUI extends Component {
|
|||||||
this.tree = signal(null)
|
this.tree = signal(null)
|
||||||
this.node = signal(null)
|
this.node = signal(null)
|
||||||
this.nodeContent = createRef()
|
this.nodeContent = createRef()
|
||||||
|
this.upload = signal(false)
|
||||||
window.addEventListener('popstate', evt=>{
|
window.addEventListener('popstate', evt=>{
|
||||||
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
||||||
this.goToNode(evt.state.nodeID, true)
|
this.goToNode(evt.state.nodeID, true)
|
||||||
@ -49,14 +50,17 @@ export class NodeUI extends Component {
|
|||||||
if(this.props.app.nodeModified.value)
|
if(this.props.app.nodeModified.value)
|
||||||
modified = 'modified';
|
modified = 'modified';
|
||||||
|
|
||||||
return html`
|
let upload = '';
|
||||||
<div id="menu-blackout" class="${this.menu.value ? 'show' : ''}" onclick=${()=>this.menu.value = false}></div>
|
if(this.upload.value)
|
||||||
<div id="menu" class="${this.menu.value ? 'show' : ''}">
|
upload = html`<${UploadUI} app=${this} />`
|
||||||
<div class="item" onclick=${()=>this.renameNode()}>Rename</div>
|
|
||||||
<div class="item separator" onclick=${()=>this.deleteNode()}>Delete</div>
|
|
||||||
<div class="item" onclick=${()=>this.logout()}>Log out</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
let menu = '';
|
||||||
|
if(this.menu.value)
|
||||||
|
upload = html`<${Menu} app=${this} />`
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${menu}
|
||||||
|
${upload}
|
||||||
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
||||||
<div class="name">Notes</div>
|
<div class="name">Notes</div>
|
||||||
<div class="add" onclick=${evt=>this.createNode(evt)}>+</div>
|
<div class="add" onclick=${evt=>this.createNode(evt)}>+</div>
|
||||||
@ -87,21 +91,19 @@ export class NodeUI extends Component {
|
|||||||
let handled = true
|
let handled = true
|
||||||
switch(evt.key.toUpperCase()) {
|
switch(evt.key.toUpperCase()) {
|
||||||
case 'S':
|
case 'S':
|
||||||
if(!evt.ctrlKey) {
|
if(evt.ctrlKey || (evt.shiftKey && evt.altKey))
|
||||||
handled = false
|
this.saveNode()
|
||||||
break
|
|
||||||
}
|
|
||||||
this.saveNode()
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'N':
|
case 'N':
|
||||||
if(!evt.ctrlKey && !evt.AltKey) {
|
if((evt.ctrlKey && evt.AltKey) || (evt.shiftKey && evt.altKey))
|
||||||
handled = false
|
this.createNode()
|
||||||
break
|
|
||||||
}
|
|
||||||
this.createNode()
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'U':
|
||||||
|
if((evt.ctrlKey && evt.altKey) || (evt.shiftKey && evt.altKey))
|
||||||
|
this.upload.value = true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
handled = false
|
handled = false
|
||||||
}
|
}
|
||||||
@ -135,7 +137,8 @@ export class NodeUI extends Component {
|
|||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
createNode(evt) {//{{{
|
createNode(evt) {//{{{
|
||||||
evt.stopPropagation()
|
if(evt)
|
||||||
|
evt.stopPropagation()
|
||||||
let name = prompt("Name")
|
let name = prompt("Name")
|
||||||
if(!name)
|
if(!name)
|
||||||
return
|
return
|
||||||
@ -257,4 +260,105 @@ class Node {
|
|||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Menu extends Component {
|
||||||
|
render({ app }) {//{{{
|
||||||
|
return html`
|
||||||
|
<div id="blackout" onclick=${()=>app.menu.value = false}></div>
|
||||||
|
<div id="menu" class="${app.menu.value ? 'show' : ''}">
|
||||||
|
<div class="item" onclick=${()=>app.renameNode()}>Rename</div>
|
||||||
|
<div class="item separator" onclick=${()=>app.deleteNode()}>Delete</div>
|
||||||
|
<div class="item separator" onclick=${()=>{ app.upload.value = true; app.menu.value = false }}>Upload</div>
|
||||||
|
<div class="item" onclick=${()=>app.logout()}>Log out</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
class UploadUI extends Component {
|
||||||
|
constructor() {//{{{
|
||||||
|
super()
|
||||||
|
this.file = createRef()
|
||||||
|
this.filelist = signal([])
|
||||||
|
this.fileRefs = []
|
||||||
|
this.progressRefs = []
|
||||||
|
}//}}}
|
||||||
|
render({ nodeui }) {//{{{
|
||||||
|
let filelist = this.filelist.value
|
||||||
|
let files = []
|
||||||
|
for(let i = 0; i < filelist.length; i++) {
|
||||||
|
files.push(html`<div key=file_${i} ref=${this.fileRefs[i]} class="file">${filelist.item(i).name}</div><div class="progress" ref=${this.progressRefs[i]}></div>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div id="blackout" onclick=${()=>nodeui.upload.value = false}></div>
|
||||||
|
<div id="upload">
|
||||||
|
<input type="file" ref=${this.file} onchange=${()=>this.upload()} multiple />
|
||||||
|
<div class="files">
|
||||||
|
${files}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
componentDidMount() {//{{{
|
||||||
|
this.file.current.focus()
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
upload() {//{{{
|
||||||
|
this.fileRefs = []
|
||||||
|
this.progressRefs = []
|
||||||
|
|
||||||
|
let input = this.file.current
|
||||||
|
this.filelist.value = input.files
|
||||||
|
for(let i = 0; i < input.files.length; i++) {
|
||||||
|
this.fileRefs.push(createRef())
|
||||||
|
this.progressRefs.push(createRef())
|
||||||
|
this.postFile(input.files[i], progress=>{
|
||||||
|
this.progressRefs[i].current.innerHTML = `${progress}%`
|
||||||
|
}, ()=>{
|
||||||
|
this.fileRefs[i].current.classList.add("done")
|
||||||
|
this.progressRefs[i].current.classList.add("done")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
postFile(file, progressCallback, doneCallback) {//{{{
|
||||||
|
var formdata = new FormData()
|
||||||
|
formdata.append('file', file)
|
||||||
|
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
|
||||||
|
request.addEventListener("error", ()=>{
|
||||||
|
window._app.current.responseError({ upload: "An unknown error occured" })
|
||||||
|
})
|
||||||
|
|
||||||
|
request.addEventListener("loadend", ()=>{
|
||||||
|
if(request.status != 200) {
|
||||||
|
window._app.current.responseError({ upload: request.statusText })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = JSON.parse(request.response)
|
||||||
|
if(!response.OK) {
|
||||||
|
window._app.current.responseError({ upload: response.Error })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doneCallback()
|
||||||
|
})
|
||||||
|
|
||||||
|
request.upload.addEventListener('progress', evt=>{
|
||||||
|
var fileSize = file.size
|
||||||
|
|
||||||
|
if(evt.loaded <= fileSize)
|
||||||
|
progressCallback(Math.round(evt.loaded / fileSize * 100))
|
||||||
|
if(evt.loaded == evt.total)
|
||||||
|
progressCallback(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
request.open('post', '/node/upload')
|
||||||
|
request.setRequestHeader("X-Session-Id", window._app.current.session.UUID)
|
||||||
|
//request.timeout = 45000
|
||||||
|
request.send(formdata)
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
@ -30,8 +30,7 @@ h1 {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu-blackout {
|
#blackout {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
@ -39,10 +38,6 @@ h1 {
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
z-index: 1024;
|
z-index: 1024;
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
&.show {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu {
|
#menu {
|
||||||
@ -68,7 +63,7 @@ h1 {
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&.separator {
|
&.separator {
|
||||||
border-bottom: 3px solid #000;
|
border-bottom: 2px solid #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -81,6 +76,62 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#upload {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1025;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid #000;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
padding-top: 12px;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
&:nth-child(1) {
|
||||||
|
padding-top: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
color: #0a0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
justify-self: end;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
&:nth-child(2) {
|
||||||
|
padding-top: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
color: #0a0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr min-content min-content;
|
grid-template-columns: 1fr min-content min-content;
|
||||||
|
Loading…
Reference in New Issue
Block a user