Graphical overhaul.
Remove all legacy pixeldrain pages, handlers and assets. Remove old theming engine. Make logo responsive to the accent colour. Lots of other stuff.
74
go.mod
@@ -6,6 +6,8 @@ replace (
|
|||||||
fornaxian.tech/nova_server => ../nova_server
|
fornaxian.tech/nova_server => ../nova_server
|
||||||
fornaxian.tech/pixeldrain_api_client => ../pixeldrain_api_client
|
fornaxian.tech/pixeldrain_api_client => ../pixeldrain_api_client
|
||||||
fornaxian.tech/util => ../util
|
fornaxian.tech/util => ../util
|
||||||
|
// nova_server's fork. Its replaces don't apply here, and we import its api
|
||||||
|
github.com/scylladb/gocqlx/v3 => ../gocqlx
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -14,17 +16,85 @@ require (
|
|||||||
fornaxian.tech/pixeldrain_api_client v0.0.0-20260701091325-dc9629676a79
|
fornaxian.tech/pixeldrain_api_client v0.0.0-20260701091325-dc9629676a79
|
||||||
fornaxian.tech/util v0.0.0-20260123171803-02d15be63d16
|
fornaxian.tech/util v0.0.0-20260123171803-02d15be63d16
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
codeberg.org/go-pdf/fpdf v0.12.0 // indirect
|
||||||
|
github.com/Fornaxian/unifilter v0.0.0-20200428082710-29e3cf43485c // indirect
|
||||||
|
github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/biter777/countries v1.7.5 // indirect
|
||||||
|
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||||
|
github.com/bodgit/sevenzip v1.6.5 // indirect
|
||||||
|
github.com/bodgit/windows v1.0.1 // indirect
|
||||||
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
|
github.com/emersion/go-imap/v2 v2.0.0-beta.8 // indirect
|
||||||
|
github.com/emersion/go-message v0.18.2 // indirect
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
|
github.com/fatih/pool v3.0.0+incompatible // indirect
|
||||||
|
github.com/fclairamb/ftpserverlib v0.32.1 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||||
|
github.com/go-chi/httprate v0.16.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/inbucket/html2text v1.0.0 // indirect
|
||||||
|
github.com/j-muller/go-torrent-parser v0.0.0-20211014072822-db02b4099054 // indirect
|
||||||
|
github.com/jhillyerd/enmime/v2 v2.4.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.19.1 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||||
|
github.com/klauspost/reedsolomon v1.14.1 // indirect
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
|
github.com/libp2p/go-reuseport v0.4.0 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.27 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
|
github.com/olekukonko/errors v1.3.0 // indirect
|
||||||
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pkg/sftp v1.13.11 // indirect
|
||||||
|
github.com/pquerna/otp v1.5.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/scylladb/go-reflectx v1.0.1 // indirect
|
||||||
|
github.com/scylladb/gocqlx/v3 v3.0.4 // indirect
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
|
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
||||||
|
github.com/ulikunitz/xz v0.5.16 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/zeebo/bencode v1.0.0 // indirect
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect
|
||||||
|
golang.org/x/image v0.44.0 // indirect
|
||||||
|
golang.org/x/net v0.57.0 // indirect
|
||||||
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
golang.org/x/text v0.40.0 // indirect
|
||||||
|
lukechampine.com/frand v1.5.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
fornaxian.tech/nova_server v0.0.0-00010101000000-000000000000
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/apache/cassandra-gocql-driver/v2 v2.1.2 // indirect
|
github.com/apache/cassandra-gocql-driver/v2 v2.1.2 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.54.0 // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
|||||||
183
go.sum
@@ -1,40 +1,223 @@
|
|||||||
|
codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU=
|
||||||
|
codeberg.org/go-pdf/fpdf v0.12.0/go.mod h1:WJNJ2bvCj81rZBdhOf7lKOGoSl+OKMXcIcXqDcP8r5Y=
|
||||||
fornaxian.tech/config v0.0.0-20211108212237-6133aed90586 h1:/4a0Iq3cYeyTWcPHsN9pVAGJH3UTq1GapK253lHfXnE=
|
fornaxian.tech/config v0.0.0-20211108212237-6133aed90586 h1:/4a0Iq3cYeyTWcPHsN9pVAGJH3UTq1GapK253lHfXnE=
|
||||||
fornaxian.tech/config v0.0.0-20211108212237-6133aed90586/go.mod h1:ULIXF4J1DbBw4EsIPRNQDf6J3hl4P/jlihjy6UCm9FM=
|
fornaxian.tech/config v0.0.0-20211108212237-6133aed90586/go.mod h1:ULIXF4J1DbBw4EsIPRNQDf6J3hl4P/jlihjy6UCm9FM=
|
||||||
fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640 h1:UPDxJwLRCfh/cv80UMSanzmZ0jIcfS1mcd0Y06HYuLw=
|
fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640 h1:UPDxJwLRCfh/cv80UMSanzmZ0jIcfS1mcd0Y06HYuLw=
|
||||||
fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640/go.mod h1:sN82qMToeHhP2u3ehvrcE8y1IudRZJAZO9yG5OBYblo=
|
fornaxian.tech/log v0.0.0-20211102185326-552e9b1f8640/go.mod h1:sN82qMToeHhP2u3ehvrcE8y1IudRZJAZO9yG5OBYblo=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/Fornaxian/unifilter v0.0.0-20200428082710-29e3cf43485c h1:ule8gPLT5sJCcIqUpa0MJqOSNHJ/WRpOM0bsFktNHXU=
|
||||||
|
github.com/Fornaxian/unifilter v0.0.0-20200428082710-29e3cf43485c/go.mod h1:ofV5syadd2nI4gOc/rP1yPnXkARgm+E1D/U38mbUj44=
|
||||||
|
github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f h1:z8MkSJCUyTmW5YQlxsMLBlwA7GmjxC7L4ooicxqnhz8=
|
||||||
|
github.com/akamensky/base58 v0.0.0-20210829145138-ce8bf8802e8f/go.mod h1:UdUwYgAXBiL+kLfcqxoQJYkHA/vl937/PbFhZM34aZs=
|
||||||
|
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||||
|
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/apache/cassandra-gocql-driver/v2 v2.1.2 h1:lu/p0Db2av18enHJvWJQoChLssI0P+AR06STq4VdvCc=
|
github.com/apache/cassandra-gocql-driver/v2 v2.1.2 h1:lu/p0Db2av18enHJvWJQoChLssI0P+AR06STq4VdvCc=
|
||||||
github.com/apache/cassandra-gocql-driver/v2 v2.1.2/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
|
github.com/apache/cassandra-gocql-driver/v2 v2.1.2/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
|
||||||
|
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
|
||||||
|
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||||
|
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||||
|
github.com/bodgit/sevenzip v1.6.5 h1:7H7BxgmeX0j6UX42lH+KXQ92WgMQJ49DoocFdfHbCng=
|
||||||
|
github.com/bodgit/sevenzip v1.6.5/go.mod h1:GhuB6Lq1xCpP1sps+horjZ8lgiKPJcy2zUX3prla9wc=
|
||||||
|
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||||
|
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||||
|
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||||
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
github.com/emersion/go-imap/v2 v2.0.0-beta.8 h1:5IXZK1E33DyeP526320J3RS7eFlCYGFgtbrfapqDPug=
|
||||||
|
github.com/emersion/go-imap/v2 v2.0.0-beta.8/go.mod h1:dhoFe2Q0PwLrMD7oZw8ODuaD0vLYPe5uj2wcOMnvh48=
|
||||||
|
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||||
|
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
|
github.com/fatih/pool v3.0.0+incompatible h1:3xXzI/t5o6aEU/R+xe7ed44CTw41lV3oB0gB5pNXS5U=
|
||||||
|
github.com/fatih/pool v3.0.0+incompatible/go.mod h1:v+kkrv3f2oJ1P9NHaKArMYdTVtNCwfR0DlXwnhA2L4k=
|
||||||
|
github.com/fclairamb/ftpserverlib v0.32.1 h1:JoBxoiu2sifjVbIAfkCQn7Oi3mFbX/PHRO+4PGOROj4=
|
||||||
|
github.com/fclairamb/ftpserverlib v0.32.1/go.mod h1:RJZgAyW8uSb/BtDwD2Wf2wUGUxLaQdmB6VXU7XGp8Oc=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||||
|
github.com/go-chi/httprate v0.16.0 h1:8V5DH9j6pSK6UQoBsTpvMyFxycqaKEIToyPKzHJjUa8=
|
||||||
|
github.com/go-chi/httprate v0.16.0/go.mod h1:A8lo+qRhk+s9LiuP5saS7XCGDXRXMcrueq0NfIuCa/I=
|
||||||
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||||
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
|
||||||
|
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
|
||||||
|
github.com/j-muller/go-torrent-parser v0.0.0-20211014072822-db02b4099054 h1:74rPb7GmzJVJkSQ1Tpd7Y8iCnNveIjnpgyos4Yu4Gzs=
|
||||||
|
github.com/j-muller/go-torrent-parser v0.0.0-20211014072822-db02b4099054/go.mod h1:mMeMpHRplLTgo3ttLVB8R43jtdEKcDpLVw2CvcwTv28=
|
||||||
|
github.com/jhillyerd/enmime/v2 v2.4.1 h1:VkBX8GJJ/wbQofWsKP3egRqgXcwmxlY94YUmXTj08kE=
|
||||||
|
github.com/jhillyerd/enmime/v2 v2.4.1/go.mod h1:TLpvqImPiumRecsJK5TYseRw2bPg3g0EtWc+SfU7cMs=
|
||||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||||
|
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
|
github.com/klauspost/reedsolomon v1.14.1 h1:swE9kzyWXD/wVG+l5Pe8bWnQ0giIY7D1GjCBKk3kG2U=
|
||||||
|
github.com/klauspost/reedsolomon v1.14.1/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
|
||||||
|
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
|
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||||
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||||
|
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||||
|
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
|
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||||
|
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||||
|
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ=
|
||||||
|
github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc=
|
||||||
|
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||||
|
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||||
|
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
|
||||||
|
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
|
||||||
|
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA=
|
||||||
|
github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||||
|
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
|
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM=
|
||||||
|
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||||
|
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=
|
||||||
|
lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q=
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg role="img" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<title>GitHub</title>
|
|
||||||
<path fill="#ffffff" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 866 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 216.4144 232.00976">
|
|
||||||
<path fill="#ffffff" d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915"/>
|
|
||||||
<path fill="#000000" d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 100 100">
|
|
||||||
<g fill="#ffffff" fill-rule="evenodd">
|
|
||||||
<path d="M64.1102,0.1004 C44.259,0.1004 28.1086,16.2486 28.1086,36.0986 C28.1086,55.8884 44.259,71.989 64.1102,71.989 C83.9,71.989 100,55.8884 100,36.0986 C100,16.2486 83.9,0.1004 64.1102,0.1004"/>
|
|
||||||
<polygon points=".012 95.988 17.59 95.988 17.59 .1 .012 .1"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 409 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
|
|
||||||
<path fill="#ffffff" d="M22,12.14C22,10.92 21,9.96 19.81,9.96C19.22,9.96 18.68,10.19 18.29,10.57C16.79,9.5 14.72,8.79 12.43,8.7L13.43,4L16.7,4.71C16.73,5.53 17.41,6.19 18.25,6.19C19.11,6.19 19.81,5.5 19.81,4.63C19.81,3.77 19.11,3.08 18.25,3.08C17.65,3.08 17.11,3.43 16.86,3.95L13.22,3.18C13.11,3.16 13,3.18 12.93,3.24C12.84,3.29 12.79,3.38 12.77,3.5L11.66,8.72C9.33,8.79 7.23,9.5 5.71,10.58C5.32,10.21 4.78,10 4.19,10C2.97,10 2,10.96 2,12.16C2,13.06 2.54,13.81 3.29,14.15C3.25,14.37 3.24,14.58 3.24,14.81C3.24,18.18 7.16,20.93 12,20.93C16.84,20.93 20.76,18.2 20.76,14.81C20.76,14.6 20.75,14.37 20.71,14.15C21.46,13.81 22,13.04 22,12.14M7,13.7C7,12.84 7.68,12.14 8.54,12.14C9.4,12.14 10.1,12.84 10.1,13.7A1.56,1.56 0 0,1 8.54,15.26C7.68,15.28 7,14.56 7,13.7M15.71,17.84C14.63,18.92 12.59,19 12,19C11.39,19 9.35,18.9 8.29,17.84C8.13,17.68 8.13,17.43 8.29,17.27C8.45,17.11 8.7,17.11 8.86,17.27C9.54,17.95 11,18.18 12,18.18C13,18.18 14.47,17.95 15.14,17.27C15.3,17.11 15.55,17.11 15.71,17.27C15.85,17.43 15.85,17.68 15.71,17.84M15.42,15.28C14.56,15.28 13.86,14.58 13.86,13.72A1.56,1.56 0 0,1 15.42,12.16C16.28,12.16 17,12.86 17,13.72C17,14.56 16.28,15.28 15.42,15.28Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24">
|
|
||||||
<path fill="#ffffff" d="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.7,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 777 B |
@@ -1,61 +0,0 @@
|
|||||||
# API documentation
|
|
||||||
|
|
||||||
Methods for using Nova programmatically.
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
All methods which create, modify or delete a resource require an API key. API
|
|
||||||
keys can be obtained from your user account's [API keys page](/user/api_keys).
|
|
||||||
|
|
||||||
To use the API key you need to enter it in the password field of [HTTP Basic
|
|
||||||
Access
|
|
||||||
Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). The
|
|
||||||
username field does not matter, it can be empty or anything else.
|
|
||||||
|
|
||||||
Example usage in JavaScript:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const resp = await fetch(
|
|
||||||
"https://nova.storage/api/user/files",
|
|
||||||
headers: {
|
|
||||||
"Authorization": "Basic "+btoa(":"+api_key),
|
|
||||||
// The btoa function encodes the key to Base64
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if(resp.status >= 400) {
|
|
||||||
throw new Error(await resp.json())
|
|
||||||
}
|
|
||||||
result = await resp.json()
|
|
||||||
```
|
|
||||||
|
|
||||||
Some JSON responses include fields which end in "_href" (some people don't know
|
|
||||||
this, but "href" stands for "Hypertext Reference", the more you know). These
|
|
||||||
point to different places in the API, which you can retrieve with a GET request.
|
|
||||||
The path is to be appended to the API URL, so "/file/someid/thumbnail" becomes
|
|
||||||
"{{apiUrl}}/file/someid/thumbnail".
|
|
||||||
|
|
||||||
The base URL for the API is "{{apiUrl}}", all paths below are relative to that
|
|
||||||
URL.
|
|
||||||
|
|
||||||
## curl example
|
|
||||||
|
|
||||||
To upload files to Nova you will need an API key. Get an API key from the
|
|
||||||
[API keys page](/user/api_keys) and enter it in the command. Replace the example
|
|
||||||
API key here with your own:
|
|
||||||
|
|
||||||
`curl -T "file_name.txt" -u :5f45f184-64bb-4eaa-be19-4a5f0459db49
|
|
||||||
https://nova.storage/api/file/`
|
|
||||||
|
|
||||||
## Form value order
|
|
||||||
|
|
||||||
I recommend you put files at the end of every file upload form. By doing this
|
|
||||||
the Nova server can respond to malformed requests before the file upload
|
|
||||||
finishes and this may save you a lot of time and bandwidth when uploading large
|
|
||||||
files. Make sure your HTTP client has support for premature responses,
|
|
||||||
Nova uses them a lot. If the server responds before your request is
|
|
||||||
finished it will always indicate an error and you may abort the connection.
|
|
||||||
|
|
||||||
|
|
||||||
{{template "api_file.md"}}
|
|
||||||
{{template "api_list.md"}}
|
|
||||||
{{template "api_user.md"}}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
## File Methods
|
|
||||||
|
|
||||||
<details class="api_doc_details request_post">
|
|
||||||
<summary><span class="method">POST</span>/file</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Upload a file. I recommend that you use the PUT API instead of the POST API.
|
|
||||||
It's easier to use and the multipart encoding of the POST API can cause
|
|
||||||
performance issues in certain environments.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Type | Required | Maximum Size | Default | Description
|
|
||||||
------|----------------|----------|------------------------------|---------------------|----------------------------
|
|
||||||
name | string | false | 255 characters | multipart file name | Name of the file to upload
|
|
||||||
file | multipart file | true | Depends on user subscription | none | File to upload
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"id": "abc123" // ID of the newly uploaded file
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 422: Unprocessable Entity
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "no_file",
|
|
||||||
"message": "The file does not exist or is empty."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 500: Internal Server Error
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "internal",
|
|
||||||
"message": "An internal server error occurred."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload Too Large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_too_large",
|
|
||||||
"message": "The file you tried to upload is too large"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 500: Internal Server Error
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "writing",
|
|
||||||
"message": "Something went wrong while writing the file to disk, the server may be out of storage space."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload Too Large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "name_too_long",
|
|
||||||
"message": "File Name is too long, Max 255 characters allowed."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_put">
|
|
||||||
<summary><span class="method">PUT</span>/file/{name}</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Upload a file.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Type | Required | Location | Maximum Size | Default | Description
|
|
||||||
------|--------|----------|--------------|------------------------------|---------|----------------------------
|
|
||||||
name | string | true | URL | 255 characters | none | Name of the file to upload
|
|
||||||
file | file | true | request body | Depends on user subscription | none | File to upload
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 201: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"id": "abc123" // ID of the newly uploaded file
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 422: Unprocessable Entity
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "no_file",
|
|
||||||
"message": "The file does not exist or is empty."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 500: Internal Server Error
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "internal",
|
|
||||||
"message": "An internal server error occurred."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload Too Large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_too_large",
|
|
||||||
"message": "The file you tried to upload is too large"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 500: Internal Server Error
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "writing",
|
|
||||||
"message": "Something went wrong while writing the file to disk, the server may be out of storage space."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload Too Large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "name_too_long",
|
|
||||||
"message": "File Name is too long, Max 255 characters allowed."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/file/{id}</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns the full file associated with the ID. Supports byte range requests.
|
|
||||||
|
|
||||||
When '?download' is added to the URL the server will send an attachment header
|
|
||||||
instead of inline rendering, which causes the browser to show a 'Save File'
|
|
||||||
dialog.
|
|
||||||
|
|
||||||
Warning: If a file is using too much bandwidth it can be rate limited. The rate
|
|
||||||
limit will be enabled if a file has three times more downloads than views. The
|
|
||||||
owner of a file can always download it. When a file is rate limited the user
|
|
||||||
will need to fill out a captcha in order to continue downloading the file. The
|
|
||||||
captcha will only appear on the file viewer page (nova.storage/u/{id}). Rate
|
|
||||||
limiting has been added to prevent the spread of viruses and to stop hotlinking.
|
|
||||||
Hotlinking is only allowed when files are uploaded using a Pro account.
|
|
||||||
|
|
||||||
Nova also includes a virus scanner. If a virus has been detected in a file
|
|
||||||
the user will also have to fill in a captcha to download it.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
---------|----------|----------|------------------------------------------
|
|
||||||
id | true | URL | ID of the file to request
|
|
||||||
download | false | URL | Sends attachment header instead of inline
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
```
|
|
||||||
HTTP 200: OK
|
|
||||||
|
|
||||||
Requested file data
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "not_found",
|
|
||||||
"message": "The entity you requested could not be found"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 403: Forbidden
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_rate_limited_captcha_required",
|
|
||||||
"message": "This file is using too much bandwidth. For anonymous downloads a captcha is required now. The captcha entry is available on the download page"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 403: Forbidden
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "virus_detected_captcha_required",
|
|
||||||
"message": "This file has been marked as malware by our scanning systems. To avoid infecting other systems through automated downloads we require you to enter a captcha. The captcha entry is available on the download page"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/file/{id}/info</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns information about one or more files. You can also put a comma separated
|
|
||||||
list of file IDs in the URL and it will return an array of file info, instead of
|
|
||||||
a single object. There's a limit of 1000 files per request.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
------|----------|----------|---------------
|
|
||||||
id | true | URL | ID of the file
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"id": "1234abcd",
|
|
||||||
"name": "screenshot.png",
|
|
||||||
// Size of the file in bytes
|
|
||||||
"size": 5694837,
|
|
||||||
// Number of unique file views, views are counted once per IP address
|
|
||||||
"views" 1234,
|
|
||||||
// Total bandwidth usage of the file
|
|
||||||
"bandwidth_used": 1234567890,
|
|
||||||
// Premium bandwidth usage, from users with a Pro subscription or bandwidth sharing
|
|
||||||
"bandwidth_used_paid": 1234567890,
|
|
||||||
// Unique downloads per IP address
|
|
||||||
"downloads": 1234,
|
|
||||||
"date_upload": 2020-02-04T18:34:05.706801Z,
|
|
||||||
"date_last_view": 2020-02-04T18:34:05.706801Z,
|
|
||||||
"mime_type" "image/png",
|
|
||||||
// Link to a thumbnail of this file
|
|
||||||
"thumbnail_href": "/file/1234abcd/thumbnail"
|
|
||||||
// SHA256 sum of the file, encoded in hexadecimal
|
|
||||||
"hash_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
||||||
// If the current logged in user can edit the file
|
|
||||||
"can_edit": true,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_not_found"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/file/{id}/thumbnail?width=x&height=x</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns a PNG thumbnail image representing the file. The thumbnail image will be
|
|
||||||
128x128 px by default. You can specify the width and height with parameters in
|
|
||||||
the URL. The width and height parameters need to be a multiple of 16. So the
|
|
||||||
allowed values are 16, 32, 48, 64, 80, 96, 112 and 128. If a thumbnail cannot be
|
|
||||||
generated for the file you will be redirected to a mime type image of 128x128
|
|
||||||
px.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
-------|----------|----------|--------------------------------------
|
|
||||||
id | true | URL | ID of the file to get a thumbnail for
|
|
||||||
width | false | URL | Width of the thumbnail image
|
|
||||||
height | false | URL | Height of the thumbnail image
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
A PNG image if a thumbnail can be generated. If a thumbnail cannot be generated
|
|
||||||
you will get a 301 redirect to an image representing the type of the file.
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_delete">
|
|
||||||
<summary><span class="method">DELETE</span>/file/{id}</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Deletes a file. Only works when the users owns the file.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
------|----------|----------|-------------------------
|
|
||||||
id | true | URL | ID of the file to delete
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"value": "file_deleted",
|
|
||||||
"message": "The file has been deleted."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_not_found",
|
|
||||||
"message": "File ID was not found in the database."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 401: Unauthorized
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "unauthorized",
|
|
||||||
"message": "You are not logged in."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 403: Forbidden
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "forbidden",
|
|
||||||
"message": "This is not your file."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
## File Methods
|
|
||||||
|
|
||||||
<details class="api_doc_details request_post">
|
|
||||||
<summary><span class="method">POST</span>/file</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Upload a file.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Type | Required | Maximum Size | Default | Description
|
|
||||||
----------|----------------|----------|------------------------------|---------------------|-----------------------------------
|
|
||||||
name | string | false | 255 characters | multipart file name | Name of the file to upload
|
|
||||||
anonymous | boolean | false | N/A | false | File is not linked to user if true
|
|
||||||
file | multipart file | true | Depends on user subscription | none | File to upload
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"id": "abc123" // ID of the newly uploaded file
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 422: Unprocessable Entity
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "no_file",
|
|
||||||
"message": "The file does not exist or is empty."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 500: Internal Server Error
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "internal",
|
|
||||||
"message": "An internal server error occurred."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload Too Large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_too_large",
|
|
||||||
"message": "The file you tried to upload is too large"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 500: Internal Server Error
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "writing",
|
|
||||||
"message": "Something went wrong while writing the file to disk, the server may be out of storage space."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload Too Large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "name_too_long",
|
|
||||||
"message": "File Name is too long, Max 255 characters allowed."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/file/{id}</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns the full file associated with the ID. Supports byte range requests.
|
|
||||||
|
|
||||||
When '?download' is added to the URL the server will send an attachment header
|
|
||||||
instead of inline rendering, which causes the browser to show a 'Save File'
|
|
||||||
dialog.
|
|
||||||
|
|
||||||
Warning: If a file is using too much bandwidth it can be rate limited. The rate
|
|
||||||
limit will be enabled if a file has ten times more downloads than views. The
|
|
||||||
owner of a file can always download it. When a file is rate limited the user
|
|
||||||
will need to fill out a captcha in order to continue downloading the file. The
|
|
||||||
captcha will only appear on the file viewer page (nova.storage/u/{id}). Rate
|
|
||||||
limiting has been added to prevent the spread of viruses and to stop direct
|
|
||||||
linking.
|
|
||||||
|
|
||||||
Nova also includes a virus scanner. If a virus has been detected in a file
|
|
||||||
the user will also have to fill in a captcha to download it.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
---------|----------|----------|------------------------------------------
|
|
||||||
id | true | URL | ID of the file to request
|
|
||||||
download | false | URL | Sends attachment header instead of inline
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
```
|
|
||||||
HTTP 200: OK
|
|
||||||
|
|
||||||
Requested file data
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "not_found",
|
|
||||||
"message": "The entity you requested could not be found"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 403: Forbidden
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_rate_limited_captcha_required",
|
|
||||||
"message": "This file is using too much bandwidth. For anonymous downloads a captcha is required now. The captcha entry is available on the download page"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 403: Forbidden
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "virus_detected_captcha_required",
|
|
||||||
"message": "This file has been marked as malware by our scanning systems. To avoid infecting other systems through automated downloads we require you to enter a captcha. The captcha entry is available on the download page"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/file/{id}/info</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns information about one or more files. You can also put a comma separated
|
|
||||||
list of file IDs in the URL and it will return an array of file info, instead of
|
|
||||||
a single object.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
------|----------|----------|---------------
|
|
||||||
id | true | URL | ID of the file
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"id": "1234abcd",
|
|
||||||
"name": "screenshot.png",
|
|
||||||
"date_upload": 2020-02-04T18:34:05.706801Z,
|
|
||||||
"date_last_view": 2020-02-04T18:34:05.706801Z,
|
|
||||||
"size": 5694837, // Bytes
|
|
||||||
"views" 1234, // Amount of unique file views
|
|
||||||
"bandwidth_used": 1234567890, // Bytes
|
|
||||||
"mime_type" "image/png",
|
|
||||||
"thumbnail_href": "/file/1234abcd/thumbnail" // Link to a thumbnail of this file
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_not_found"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/file/{id}/thumbnail?width=x&height=x</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns a PNG thumbnail image representing the file. The thumbnail image will be
|
|
||||||
128x128 px by default. You can specify the width and height with parameters in
|
|
||||||
the URL. The width and height parameters need to be a multiple of 16. So the
|
|
||||||
allowed values are 16, 32, 48, 64, 80, 96, 112 and 128. If a thumbnail cannot be
|
|
||||||
generated for the file you will be redirected to a mime type image of 128x128
|
|
||||||
px.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
-------|----------|----------|--------------------------------------
|
|
||||||
id | true | URL | ID of the file to get a thumbnail for
|
|
||||||
width | false | URL | Width of the thumbnail image
|
|
||||||
height | false | URL | Height of the thumbnail image
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
A PNG image if a thumbnail can be generated. If a thumbnail cannot be generated
|
|
||||||
you will get a 301 redirect to an image representing the type of the file.
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_delete">
|
|
||||||
<summary><span class="method">DELETE</span>/file/{id}</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Deletes a file. Only works when the users owns the file.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
------|----------|----------|-------------------------
|
|
||||||
id | true | URL | ID of the file to delete
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"value": "file_deleted",
|
|
||||||
"message": "The file has been deleted."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "file_not_found",
|
|
||||||
"message": "File ID was not found in the database."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 401: Unauthorized
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "unauthorized",
|
|
||||||
"message": "You are not logged in."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 403: Forbidden
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "forbidden",
|
|
||||||
"message": "This is not your file."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
## Filesystem Methods
|
|
||||||
|
|
||||||
<details class="api_doc_details request_post">
|
|
||||||
<summary><span class="method">POST</span>/filesystem/{path}</summary>
|
|
||||||
<div>
|
|
||||||
<h3>Description</h3>
|
|
||||||
<p>
|
|
||||||
Creates a new directory or uploads a file to an existing directory.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Parameters</h3>
|
|
||||||
<p>
|
|
||||||
The form parameters <b>must</b> be sent in the order displayed below
|
|
||||||
for the realtime error checking to work. If 'name' comes after
|
|
||||||
'file' it will be ignored.
|
|
||||||
</p>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Param</td>
|
|
||||||
<td>Location</td>
|
|
||||||
<td>Description</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>type</td>
|
|
||||||
<td>Form Values</td>
|
|
||||||
<td>The type of node to create, can either be 'directory', or 'file'</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>name</td>
|
|
||||||
<td>Form Values</td>
|
|
||||||
<td>
|
|
||||||
Name of the directory to create, or of file to create. Not
|
|
||||||
required if 'type' is 'file'
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>file</td>
|
|
||||||
<td>Form Values</td>
|
|
||||||
<td>
|
|
||||||
Multipart file to upload to the directory. Will be ignored
|
|
||||||
if 'type' is 'directory'
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3>Returns</h3>
|
|
||||||
<pre>HTTP 200: OK
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"id": "abc123" // ID of the newly uploaded file
|
|
||||||
}</pre>
|
|
||||||
todo
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/filesystem/{path}</summary>
|
|
||||||
<div>
|
|
||||||
<h3>Description</h3>
|
|
||||||
<p>
|
|
||||||
Returns information about the requested path.
|
|
||||||
</p>
|
|
||||||
<h3>Parameters</h3>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Param</td>
|
|
||||||
<td>Required</td>
|
|
||||||
<td>Location</td>
|
|
||||||
<td>Description</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>path</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>URL</td>
|
|
||||||
<td>Path to the directory or file to request</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>download</td>
|
|
||||||
<td>false</td>
|
|
||||||
<td>URL</td>
|
|
||||||
<td>
|
|
||||||
If the URL paramater '?download' is passed the requested
|
|
||||||
file will be downloaded (if it is a file)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<h3>Returns</h3>
|
|
||||||
<h4>When the requested entity is a directory:</h4>
|
|
||||||
<pre>HTTP 200: OK
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"name": "some dir",
|
|
||||||
"path": "/some dir",
|
|
||||||
"type": "directory",
|
|
||||||
"child_directories": [
|
|
||||||
{
|
|
||||||
"name": "some other directory",
|
|
||||||
"type": "directory",
|
|
||||||
"path": "/some dir/some other directory"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"child_files": [
|
|
||||||
{
|
|
||||||
"name": "11. Lenny Kravitz - Fly away.ogg",
|
|
||||||
"type": "file",
|
|
||||||
"path": "/some dir/11. Lenny Kravitz - Fly away.ogg"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}</pre>
|
|
||||||
<h4>When the requested entity is a file:</h4>
|
|
||||||
<pre>HTTP 200: OK
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"name": "11. Lenny Kravitz - Fly away.ogg",
|
|
||||||
"path": "/some dir/11. Lenny Kravitz - Fly away.ogg",
|
|
||||||
"type": "file",
|
|
||||||
"file_info": {
|
|
||||||
"success": true,
|
|
||||||
"id": "Jf_u5TI9",
|
|
||||||
"name": "11. Lenny Kravitz - Fly away.ogg",
|
|
||||||
"date_upload": "2018-07-04T22:24:48Z",
|
|
||||||
"date_last_view": "2018-07-04T22:24:48Z",
|
|
||||||
"size": 9757269,
|
|
||||||
"views": 0,
|
|
||||||
"mime_type": "application/ogg",
|
|
||||||
"thumbnail_href": "/file/Jf_u5TI9/thumbnail"
|
|
||||||
}
|
|
||||||
}</pre>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_delete">
|
|
||||||
<summary><span class="method">DELETE</span>/filesystem/{path}</summary>
|
|
||||||
<div>
|
|
||||||
<h3>Description</h3>
|
|
||||||
<p>
|
|
||||||
Deletes a filesystem node.
|
|
||||||
</p>
|
|
||||||
<h3>Parameters</h3>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Param</td>
|
|
||||||
<td>Required</td>
|
|
||||||
<td>Location</td>
|
|
||||||
<td>Description</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>path</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>URL</td>
|
|
||||||
<td>Path of the entity to delete</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<h3>Returns</h3>
|
|
||||||
<pre>HTTP 200: OK
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}</pre>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
## List Methods
|
|
||||||
|
|
||||||
<details class="api_doc_details request_post">
|
|
||||||
<summary><span class="method">POST</span>/list</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Creates a list of files that can be viewed together on the file viewer page.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
POST body should be a JSON object, example below. A list can contain at most
|
|
||||||
10000 files. If you try to add more the request will fail.
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"title": "My beautiful photos", // Defaults to "Nova List"
|
|
||||||
"anonymous": false / true, // If true this list will not be linked to your user account. Defaults to "false"
|
|
||||||
"files": [ // Ordered array of files to add to the list
|
|
||||||
{
|
|
||||||
"id": "abc123",
|
|
||||||
"description": "First photo of the week, such a beautiful valley"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "123abc",
|
|
||||||
"description": "The week went by so quickly, here's a photo from the plane back"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"id": "yay137" // ID of the newly created list
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 422: Unprocessable Entity
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "list_file_not_found",
|
|
||||||
"message": "File Oh42No was not found in the database.",
|
|
||||||
"extra": {
|
|
||||||
"file_not_found": "0h42No" // The file you tried to add with this ID does not exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload too large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "too_many_files",
|
|
||||||
"message": "This list contains too many files, max 10000 allowed."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 422: Unprocessable Entity
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "json_parse_failed",
|
|
||||||
"message": "The JSON object in the request body could not be read."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload too large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "title_too_long",
|
|
||||||
"message": "The title of this list is too long, max 300 characters allowed."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 413: Payload too large
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "description_too_long",
|
|
||||||
"message": "The description of one of the files in the list is too long, max 3000 characters allowed."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 422: Unprocessable Entity
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "cannot_create_empty_list",
|
|
||||||
"message": "You cannot make a list with no files."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/list/{id}</summary>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Returns information about a file list and the files in it.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
|
|
||||||
Param | Required | Location | Description
|
|
||||||
------|----------|----------|---------------
|
|
||||||
id | true | URL | ID of the list
|
|
||||||
|
|
||||||
### Returns
|
|
||||||
|
|
||||||
The API will return some basic information about every file. Every file also has
|
|
||||||
a "detail_href" field which contains a URL to the info API of the file. Follow
|
|
||||||
that link to get more information about the file like size, checksum, mime type,
|
|
||||||
etc. The address is relative to the API URL and should be appended to the end.
|
|
||||||
|
|
||||||
HTTP 200: OK
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"id": "L8bhwx",
|
|
||||||
"title": "Rust in Peace",
|
|
||||||
"date_created": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"files": [
|
|
||||||
// These structures are the same as the file info response, except for the detail_href and description fields
|
|
||||||
{
|
|
||||||
"detail_href": "/file/_SqVWi/info",
|
|
||||||
"description": "",
|
|
||||||
"success": true,
|
|
||||||
"id": "_SqVWi",
|
|
||||||
"name": "01 Holy Wars... The Punishment Due.mp3",
|
|
||||||
"size": 123456,
|
|
||||||
"date_created": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"date_last_view": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"mime_type": "audio/mp3",
|
|
||||||
"views": 1,
|
|
||||||
"bandwidth_used": 1234567890,
|
|
||||||
"thumbnail_href": "/file/_SqVWi/thumbnail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"detail_href": "/file/RKwgZb/info",
|
|
||||||
"description": "",
|
|
||||||
"success": true,
|
|
||||||
"id": "RKwgZb",
|
|
||||||
"name": "02 Hangar 18.mp3",
|
|
||||||
"size": 123456,
|
|
||||||
"date_created": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"date_last_view": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"mime_type": "audio/mp3",
|
|
||||||
"views": 2,
|
|
||||||
"bandwidth_used": 1234567890,
|
|
||||||
"thumbnail_href": "/file/RKwgZb/thumbnail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"detail_href": "/file/DRaL_e/info",
|
|
||||||
"description": "",
|
|
||||||
"success": true,
|
|
||||||
"id": "DRaL_e",
|
|
||||||
"name": "03 Take No Prisoners.mp3",
|
|
||||||
"size": 123456,
|
|
||||||
"date_created": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"date_last_view": 2020-02-04T18:34:13.466276Z,
|
|
||||||
"mime_type": "audio/mp3",
|
|
||||||
"views": 3,
|
|
||||||
"bandwidth_used": 1234567890,
|
|
||||||
"thumbnail_href": "/file/DRaL_e/thumbnail"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 404: Not Found
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"value": "list_not_found",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
## User Methods
|
|
||||||
|
|
||||||
These methods all require authentication.
|
|
||||||
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/user/files</summary>
|
|
||||||
<div>
|
|
||||||
Documentation pending. Click here to see the response for your account:
|
|
||||||
[/user/files](/api/user/files).
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details class="api_doc_details request_get">
|
|
||||||
<summary><span class="method">GET</span>/user/lists</summary>
|
|
||||||
<div>
|
|
||||||
Documentation pending. Click here to see the response for your account:
|
|
||||||
[/user/lists](/api/user/lists).
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 175 KiB |
29
res/static/img/nova.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="23.47 13.09 31.04 31.04">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="nbar" x1="27.2" y1="44.04" x2="27.2" y2="16.51" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:#45ed52"/>
|
||||||
|
<stop offset=".5" style="stop-color:#11a574"/>
|
||||||
|
<stop offset="1" style="stop-color:#000"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="nbar2" x1="50.8" y1="13.18" x2="50.8" y2="40.72" href="#nbar"/>
|
||||||
|
<linearGradient id="ndiagonal" x1="24.86" y1="28.61" x2="53.14" y2="28.61" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:#24b880"/>
|
||||||
|
<stop offset="1" style="stop-color:#39ef3d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="nedge" x1="27.2" y1="16.42" x2="27.2" y2="44.13" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:currentColor;stop-opacity:0"/>
|
||||||
|
<stop offset="1" style="stop-color:currentColor"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="nedge2" x1="50.8" y1="40.81" x2="50.8" y2="13.09" href="#nedge"/>
|
||||||
|
<linearGradient id="nedge3" x1="24.77" y1="28.61" x2="53.23" y2="28.61" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:currentColor;stop-opacity:0"/>
|
||||||
|
<stop offset=".54" style="stop-color:currentColor"/>
|
||||||
|
<stop offset="1" style="stop-color:currentColor;stop-opacity:0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g stroke-width=".18" stroke-miterlimit="10">
|
||||||
|
<path fill="url(#nbar)" stroke="url(#nedge)" d="M27.96,16.75c-2.08,0-2.93,1.83-3.11,3v21.81c0,1.37,1.05,2.48,2.34,2.48s2.34-1.11,2.34-2.48v-24.76c-.58-.63-.72-.05-1.57-.05Z"/>
|
||||||
|
<path fill="url(#nbar2)" stroke="url(#nedge2)" d="M50.03,40.48c2.08,0,2.93-1.83,3.11-3v-.54s0-21.28,0-21.28c0-1.37-1.05-2.48-2.34-2.48s-2.34,1.11-2.34,2.48v20.98s0,2.54,0,2.54v1.24c.58.63.72.05,1.57.05Z"/>
|
||||||
|
<path fill="url(#ndiagonal)" stroke="url(#nedge3)" d="M50.03,40.49c-.71,0-1.33-.32-1.86-.77-.18-.15-.33-.33-.45-.53l-1-1.66-12.34-21.84c-.8-1.43-2.19-2.5-3.83-2.5-.34,0-.68.05-1.01.13-2.49.63-4.68,3.44-4.68,5.96h0v.29c.18-1.1,1.03-2.83,3.11-2.83.71,0,1.33.32,1.86.77.18.15.33.33.45.53l1,1.66,12.34,21.84c.8,1.43,2.19,2.5,3.83,2.5.34,0,.68-.05,1.01-.13,2.49-.63,4.68-3.44,4.68-5.96h0v-.29c-.18,1.1-1.03,2.83-3.11,2.83Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -3,8 +3,95 @@
|
|||||||
Author : Fornax
|
Author : Fornax
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* The default theme, a dark and a light variant which follow the browser's
|
||||||
|
preference. Branding colours are applied on top of these by the app, see
|
||||||
|
Branding.ts */
|
||||||
:root {
|
:root {
|
||||||
--highlight_border: inset 0px 0px 5px 1px var(--highlight_color), 0px 0px 1px 0px var(--highlight_color);
|
--color_scheme: dark;
|
||||||
|
--background_color: #15161c;
|
||||||
|
--background: var(--background_color);
|
||||||
|
--background_text_color: #e6e8ef;
|
||||||
|
--body_color: #1c1d25;
|
||||||
|
--body_background: rgb(28 29 37 / 90%);
|
||||||
|
--shaded_background: rgb(28 29 37 / 90%);
|
||||||
|
--body_text_color: #e6e8ef;
|
||||||
|
--card_color: #252733;
|
||||||
|
--separator: #2e3040;
|
||||||
|
--shadow_color: #0c0d12;
|
||||||
|
--input_background: #2c2e3b;
|
||||||
|
--input_hover_background: #363949;
|
||||||
|
--input_text: #eceef4;
|
||||||
|
--input_disabled_text: #737890;
|
||||||
|
--accent_color: #49dfa0;
|
||||||
|
--accent_shade: #243d32;
|
||||||
|
--accent_background: #49dfa0;
|
||||||
|
--accent_text_color: #05130d;
|
||||||
|
--link_color: #70ebb8;
|
||||||
|
--scrollbar_foreground_color: #33364a;
|
||||||
|
--scrollbar_hover_color: #43475f;
|
||||||
|
|
||||||
|
/* Accents for success, errors, warnings and notices. They're the highlight
|
||||||
|
colour with a different hue, see Branding.ts. The tints are backgrounds
|
||||||
|
for messages */
|
||||||
|
--green_color: #49df7b;
|
||||||
|
--red_color: #df5d49;
|
||||||
|
--yellow_color: #dfba49;
|
||||||
|
--blue_color: #4987df;
|
||||||
|
--green_tint: rgb(73 223 123 / 12%);
|
||||||
|
--red_tint: rgb(223 93 73 / 12%);
|
||||||
|
--yellow_tint: rgb(223 186 73 / 12%);
|
||||||
|
--blue_tint: rgb(73 135 223 / 12%);
|
||||||
|
|
||||||
|
/* The logo's mint colourway, see NovaLogo.svelte */
|
||||||
|
--logo_color_1: #45ed52;
|
||||||
|
--logo_color_2: #11a574;
|
||||||
|
--logo_color_3: #24b880;
|
||||||
|
--logo_color_4: #39ef3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--color_scheme: light;
|
||||||
|
--background_color: #e9ebf0;
|
||||||
|
--background_text_color: #1b1d25;
|
||||||
|
--body_color: #f5f6f9;
|
||||||
|
--body_background: rgb(245 246 249 / 90%);
|
||||||
|
--shaded_background: rgb(245 246 249 / 90%);
|
||||||
|
--body_text_color: #1b1d25;
|
||||||
|
--card_color: #ffffff;
|
||||||
|
--separator: #d3d7e0;
|
||||||
|
--shadow_color: #b9bdc9;
|
||||||
|
--input_background: #e2e5ec;
|
||||||
|
--input_hover_background: #d7dbe4;
|
||||||
|
--input_text: #1b1d25;
|
||||||
|
--input_disabled_text: #80859a;
|
||||||
|
--accent_color: #09714b;
|
||||||
|
--accent_shade: #e2eeea;
|
||||||
|
--accent_background: #09714b;
|
||||||
|
--accent_text_color: #ffffff;
|
||||||
|
--link_color: #09714b;
|
||||||
|
--scrollbar_foreground_color: #c6cad5;
|
||||||
|
--scrollbar_hover_color: #b0b5c3;
|
||||||
|
|
||||||
|
/* Accents derived from the accent colour are too light on white,
|
||||||
|
these are the same ones branded light themes get */
|
||||||
|
--green_color: #15793a;
|
||||||
|
--red_color: #ba2c37;
|
||||||
|
--yellow_color: #995c00;
|
||||||
|
--blue_color: #2164ca;
|
||||||
|
--green_tint: rgb(21 121 58 / 12%);
|
||||||
|
--red_tint: rgb(186 44 55 / 12%);
|
||||||
|
--yellow_tint: rgb(153 92 0 / 12%);
|
||||||
|
--blue_tint: rgb(33 100 202 / 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Branding can set a different scheme on the body, for a shared directory. The
|
||||||
|
browser draws checkboxes and radio buttons in the accent colour */
|
||||||
|
:root,
|
||||||
|
body {
|
||||||
|
color-scheme: var(--color_scheme);
|
||||||
|
accent-color: var(--accent_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -14,7 +101,7 @@
|
|||||||
font-display: block;
|
font-display: block;
|
||||||
src: local('Material Icons'),
|
src: local('Material Icons'),
|
||||||
local('MaterialIcons-Regular'),
|
local('MaterialIcons-Regular'),
|
||||||
url(/res/misc/MaterialIcons.woff2) format('truetype');
|
url(/res/misc/MaterialIcons.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -117,21 +204,6 @@ pre>code {
|
|||||||
|
|
||||||
/* Page layout elements */
|
/* Page layout elements */
|
||||||
|
|
||||||
.page_navigation {
|
|
||||||
position: fixed;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
background: var(--navigation_background);
|
|
||||||
width: 300px;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
float: left;
|
|
||||||
padding: 20px 0 10px 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
text-align: left;
|
|
||||||
transition: left 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page_content {
|
.page_content {
|
||||||
background: var(--body_background);
|
background: var(--body_background);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -140,10 +212,6 @@ pre>code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.page_navigation {
|
|
||||||
left: -300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>h1 {
|
header>h1 {
|
||||||
/* We want the header text to appear below the menu button, so the top
|
/* We want the header text to appear below the menu button, so the top
|
||||||
margin needs to be fairly large when the screen is small */
|
margin needs to be fairly large when the screen is small */
|
||||||
@@ -173,29 +241,6 @@ section {
|
|||||||
|
|
||||||
/* Page contents */
|
/* Page contents */
|
||||||
|
|
||||||
.page_navigation a {
|
|
||||||
float: none;
|
|
||||||
display: block;
|
|
||||||
color: var(--background_text_color);
|
|
||||||
text-align: center;
|
|
||||||
padding: 4px;
|
|
||||||
margin: 0.2em 15px 0.2em 15px;
|
|
||||||
font-family: 'Open Sans Light', sans-serif;
|
|
||||||
font-weight: lighter;
|
|
||||||
font-size: 1.5em;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
transition: background 0.2s;
|
|
||||||
border-radius: 24px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page_navigation a:hover {
|
|
||||||
background: var(--input_background);
|
|
||||||
color: var(--input_text);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight_shaded,
|
.highlight_shaded,
|
||||||
.highlight_border,
|
.highlight_border,
|
||||||
.highlight_green,
|
.highlight_green,
|
||||||
@@ -218,23 +263,23 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlight_green {
|
.highlight_green {
|
||||||
background-color: rgba(0, 255, 0, 0.1);
|
background-color: var(--green_tint);
|
||||||
border-color: #00D000;
|
border-color: var(--green_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight_blue {
|
.highlight_blue {
|
||||||
background-color: rgba(0, 0, 255, 0.1);
|
background-color: var(--blue_tint);
|
||||||
border-color: #3636FF;
|
border-color: var(--blue_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight_yellow {
|
.highlight_yellow {
|
||||||
background-color: rgba(255, 255, 0, 0.1);
|
background-color: var(--yellow_tint);
|
||||||
border-color: #A0A000;
|
border-color: var(--yellow_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight_red {
|
.highlight_red {
|
||||||
background-color: rgba(255, 0, 0, 0.1);
|
background-color: var(--red_tint);
|
||||||
border-color: #B00000;
|
border-color: var(--red_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight_green,
|
.highlight_green,
|
||||||
@@ -386,53 +431,6 @@ tr>th {
|
|||||||
padding: 0.2em 0.5em;
|
padding: 0.2em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API documentation markup */
|
|
||||||
|
|
||||||
.api_doc_details {
|
|
||||||
border-top: 1px solid;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
margin: 15px 0 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details>summary {
|
|
||||||
padding: 2px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details>summary>.method {
|
|
||||||
display: inline-block;
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details>div {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details.request_get {
|
|
||||||
border-color: #3636ff;
|
|
||||||
background-color: rgba(32, 32, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details.request_post {
|
|
||||||
border-color: #00d000;
|
|
||||||
background-color: rgba(0, 255, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details.request_delete {
|
|
||||||
border-color: #B00000;
|
|
||||||
background-color: rgba(255, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details.request_put {
|
|
||||||
border-color: #B06000;
|
|
||||||
background-color: rgba(255, 128, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api_doc_details.request_patch {
|
|
||||||
border-color: #6000B0;
|
|
||||||
background-color: rgba(128, 0, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BUTTONS */
|
/* BUTTONS */
|
||||||
button,
|
button,
|
||||||
.button,
|
.button,
|
||||||
@@ -486,7 +484,7 @@ input[type="color"]:focus {
|
|||||||
color: var(--input_text);
|
color: var(--input_text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--input_hover_background);
|
background: var(--input_hover_background);
|
||||||
box-shadow: 0px 0px 0px 1px var(--highlight_color) !important;
|
box-shadow: 0px 0px 0px 1px var(--accent_color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active,
|
button:active,
|
||||||
@@ -499,13 +497,25 @@ input[type="color"]:active {
|
|||||||
padding: 4px 0px 0px 4px;
|
padding: 4px 0px 0px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Selected buttons get the accent colour on a shade of it. The outline shows
|
||||||
|
hover and keyboard focus, so it would be confusing to use it for this too */
|
||||||
.button_highlight {
|
.button_highlight {
|
||||||
box-shadow: 0px 0px 0px 1px var(--highlight_color) !important;
|
color: var(--accent_color) !important;
|
||||||
|
background: var(--accent_shade) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.button_highlight {
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button_red {
|
.button_red {
|
||||||
background: var(--danger_color) !important;
|
background: var(--red_tint) !important;
|
||||||
color: var(--danger_text_color) !important;
|
color: var(--red_color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_red:hover,
|
||||||
|
.button_red:focus {
|
||||||
|
box-shadow: 0px 0px 0px 1px var(--red_color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
@@ -631,7 +641,7 @@ input[type="email"]:focus,
|
|||||||
input[type="number"]:focus,
|
input[type="number"]:focus,
|
||||||
input[type="date"]:focus,
|
input[type="date"]:focus,
|
||||||
input[type="datetime-local"]:focus {
|
input[type="datetime-local"]:focus {
|
||||||
box-shadow: inset 0px 0px 0px 1px var(--highlight_color);
|
box-shadow: inset 0px 0px 0px 1px var(--accent_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea:disabled,
|
textarea:disabled,
|
||||||
@@ -657,8 +667,8 @@ input[type=file] {
|
|||||||
|
|
||||||
/* The selected option in multi select */
|
/* The selected option in multi select */
|
||||||
option:checked {
|
option:checked {
|
||||||
background-color: var(--highlight_background);
|
background-color: var(--accent_background);
|
||||||
color: var(--highlight_text_color);
|
color: var(--accent_text_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Webkit Scrollbars */
|
/* Webkit Scrollbars */
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
{{define "403"}}<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" "Forbidden"}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<header>
|
|
||||||
<h1>Forbidden</h1>
|
|
||||||
</header>
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
You do not have permission to access this resource.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{{define "404"}}<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" "Not Found"}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<header>
|
|
||||||
<h1>This page does not exist!</h1>
|
|
||||||
</header>
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
There's nothing to see here, so you'll have to
|
|
||||||
<a href='/'>head over to the home page</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Bye!
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{{define "451"}}<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" "451, Unavailable For Legal Reasons"}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<header>
|
|
||||||
<h1>451, Unavailable For Legal Reasons</h1>
|
|
||||||
</header>
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
Hello. This file has received an abuse report and has been taken
|
|
||||||
down. It cannot be shared anymore.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{{define "500"}}<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" "500, Internal Server Error"}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<header>
|
|
||||||
<h1>You broke Nova</h1>
|
|
||||||
</header>
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
Great job.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Just kidding, something went wrong on the Nova server and
|
|
||||||
it's my fault. This can happen when the website is overloaded,
|
|
||||||
there is a problem with the software or there is a hardware
|
|
||||||
problem. When there is a large scale outage you can usually find
|
|
||||||
more info on my twitter <a
|
|
||||||
href="https://twitter.com/Fornax96"></a>@Fornax96</a>. You can
|
|
||||||
try again in a few minutes (or hours), or go back to the <a
|
|
||||||
href='/'>home page</a> and start over.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{{define "markdown_wrapper"}}<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" .Title}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<header>
|
|
||||||
<h1>{{.Title}}</h1>
|
|
||||||
</header>
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
{{.Other}}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{{define "meta_tags"}}
|
|
||||||
<title>{{.}} ~ Nova</title>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
|
||||||
<meta name="theme-color" content="#220735" />
|
|
||||||
|
|
||||||
<link id="stylesheet_layout" rel="stylesheet" type="text/css" href="/res/style/layout.css?v{{cacheID}}"/>
|
|
||||||
<link id="stylesheet_theme" rel="stylesheet" type="text/css" href="/theme.css"/>
|
|
||||||
|
|
||||||
<link rel="icon" sizes="32x32" href="/res/img/nova_32.png" />
|
|
||||||
<link rel="icon" sizes="128x128" href="/res/img/nova_128.png" />
|
|
||||||
<link rel="icon" sizes="152x152" href="/res/img/nova_152.png" />
|
|
||||||
<link rel="icon" sizes="180x180" href="/res/img/nova_180.png" />
|
|
||||||
<link rel="icon" sizes="192x192" href="/res/img/nova_192.png" />
|
|
||||||
<link rel="icon" sizes="196x196" href="/res/img/nova_196.png" />
|
|
||||||
<link rel="icon" sizes="256x256" href="/res/img/nova_256.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/res/img/nova_152.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/res/img/nova_180.png" />
|
|
||||||
<link rel="shortcut icon" sizes="196x196" href="/res/img/nova_196.png" />
|
|
||||||
|
|
||||||
<meta name="description" content="Nova is a file transfer service, you
|
|
||||||
can upload any file and you will be given a shareable link right away.
|
|
||||||
Nova also supports previews for images, videos, audio, PDFs and much more." />
|
|
||||||
<meta name="keywords" content="file sharing, free file sharing, file transfer,
|
|
||||||
free file transfer, file hosting, free file hosting, hosting, file upload,
|
|
||||||
free file upload, uploading, send file, large file, free file sharing,
|
|
||||||
free file transfer, 20 GB free, free file sending, file storage, cloud storage,
|
|
||||||
free cloud storage, image hosting, image sharing, photo hosting,
|
|
||||||
photo transfer, photo sharing, video hosting, video sharing, video transfer,
|
|
||||||
document sharing, upload documents, send documents, huge file, large video,
|
|
||||||
large document, pdf, pdf sharing, pdf upload, pdf transfer, pdf reader,
|
|
||||||
free pdf upload, text file upload, text sharing, photo storage,
|
|
||||||
professional file transfer, cheap storage, cheap file storage, send large file,
|
|
||||||
send big file, send huge file, audio sharing, music sharing, audio upload,
|
|
||||||
music upload" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="{{.}} ~ Nova" />
|
|
||||||
<meta property="og:site_name" content="Nova" />
|
|
||||||
<meta property="og:description" content="Instant file and screenshot sharing." />
|
|
||||||
<meta property="og:url" content="http://nova.storage/" />
|
|
||||||
<meta property="og:image" content="/res/img/nova_256.png" />
|
|
||||||
<meta property="og:image:type" content="image/png" />
|
|
||||||
{{end}}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{{ define "opengraph" }}
|
|
||||||
|
|
||||||
{{ range $kv := .MetaPropRules }}
|
|
||||||
<meta property="{{ $kv.Key }}" content="{{ $kv.Value }}" />
|
|
||||||
{{ end }}
|
|
||||||
{{ range $kv := .MetaNameRules }}
|
|
||||||
<meta name="{{ $kv.Key }}" content="{{ $kv.Value }}" />
|
|
||||||
{{ end }}
|
|
||||||
{{ range $kv := .LinkRules }}
|
|
||||||
<link rel="{{ $kv.Key }}" href="{{ $kv.Value }}" />
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{{ define "status" }}<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" "Server status"}}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<script type="text/javascript" src="https://static.hetrix.io/iframeResizer/iframeResizer.min.js"></script>
|
|
||||||
<iframe
|
|
||||||
class="htframe"
|
|
||||||
src="https://wl.hetrixtools.com/r/3e1b7bf5ddf9858527cbf209d3d9ef60/"
|
|
||||||
width="100%"
|
|
||||||
scrolling="no"
|
|
||||||
style="border: none; display: block;"
|
|
||||||
sandbox="allow-scripts allow-same-origin allow-popups"
|
|
||||||
onload="iFrameResize([{log:false}],'.htframe')"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
{{define "widgets"}}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
{{template "meta_tags" "Cloud storage and data transfer services"}}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{template "page_top" .}}
|
|
||||||
<header>
|
|
||||||
<h1>Widget showcase</h1>
|
|
||||||
</header>
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
<h2>Size 2 header</h2>
|
|
||||||
<h3>Size 3 header</h3>
|
|
||||||
<h4>Size 4 header</h4>
|
|
||||||
<h5>Size 5 header</h5>
|
|
||||||
<h6>Size 6 header</h6>
|
|
||||||
|
|
||||||
<div class="highlight_shaded">Shaded highlight</div>
|
|
||||||
<br/>
|
|
||||||
Link <a href="#">A link to someplace</a>.
|
|
||||||
<hr/>
|
|
||||||
Buttons <button>Regular ol' button!</button>
|
|
||||||
Width indicator
|
|
||||||
<button class="button_highlight">Important button!</button>
|
|
||||||
Width indicator
|
|
||||||
<button class="button_red">Dangerous button!</button>
|
|
||||||
<hr/>
|
|
||||||
Textarea <textarea>Hello!</textarea>
|
|
||||||
<hr/>
|
|
||||||
Checkbox <input type="checkbox"/>
|
|
||||||
<hr/>
|
|
||||||
Radio
|
|
||||||
<input name="radioform" type="radio"/>
|
|
||||||
<input name="radioform" type="radio"/>
|
|
||||||
<hr/>
|
|
||||||
Text field <input type="text"/>
|
|
||||||
<hr/>
|
|
||||||
Password <input type="password"/>
|
|
||||||
<hr/>
|
|
||||||
Number <input type="number"/>
|
|
||||||
<hr/>
|
|
||||||
Select
|
|
||||||
<select name="select">
|
|
||||||
<option>cherry</option>
|
|
||||||
<option>orange</option>
|
|
||||||
<option>apple</option>
|
|
||||||
</select>
|
|
||||||
<hr/>
|
|
||||||
Select2 <select name="select2" size="3">
|
|
||||||
<option>cherry</option>
|
|
||||||
<option>orange</option>
|
|
||||||
<option>apple</option>
|
|
||||||
</select>
|
|
||||||
<hr/>
|
|
||||||
File <input type="file" name="file">
|
|
||||||
<hr/>
|
|
||||||
Color <input type="color" name="favcolor" value="#ff0000">
|
|
||||||
<hr/>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Legend</legend>
|
|
||||||
<p>
|
|
||||||
Fieldset
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
<iframe src="https://nova.storage/u/Nygt1on4?embed" style="border: none; width: 800px; max-width: 100%; height: 600px; max-height: 100%; border-radius: 16px;"></iframe>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{{template "page_bottom" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,35 +1,42 @@
|
|||||||
{{define "wrap"}}
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.Title}}</title>
|
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
|
|
||||||
<link id="stylesheet_layout" rel="stylesheet" type="text/css" href="/res/style/layout.css?v{{cacheID}}"/>
|
<link rel="stylesheet" href="/res/style/layout.css?v{{.CacheID}}"/>
|
||||||
<link id="stylesheet_theme" rel="stylesheet" type="text/css" href="/theme.css"/>
|
<link rel="preload" href="/res/misc/MaterialIcons.woff2" as="font" type="font/woff2" crossorigin/>
|
||||||
|
|
||||||
<link rel="icon" sizes="32x32" href="/res/img/nova_32.png" />
|
<link rel="icon" href="/favicon.ico" sizes="32x32"/>
|
||||||
<link rel="icon" sizes="64x64" href="/res/img/nova_64.png" />
|
<link rel="icon" href="/res/img/nova.svg" type="image/svg+xml"/>
|
||||||
<link rel="icon" sizes="128x128" href="/res/img/nova_128.png" />
|
<link rel="apple-touch-icon" href="/res/img/nova_180.png"/>
|
||||||
<link rel="icon" sizes="152x152" href="/res/img/nova_152.png" />
|
|
||||||
<link rel="icon" sizes="180x180" href="/res/img/nova_180.png" />
|
{{range .OGData.MetaPropRules}}
|
||||||
<link rel="icon" sizes="192x192" href="/res/img/nova_192.png" />
|
<meta property="{{.Key}}" content="{{.Value}}" />
|
||||||
<link rel="icon" sizes="196x196" href="/res/img/nova_196.png" />
|
{{end}}
|
||||||
<link rel="icon" sizes="256x256" href="/res/img/nova_256.png" />
|
{{range .OGData.MetaNameRules}}
|
||||||
<link rel="apple-touch-icon" sizes="128x128" href="/res/img/nova_128.png" />
|
<meta name="{{.Key}}" content="{{.Value}}" />
|
||||||
<link rel="apple-touch-icon" sizes="256x256" href="/res/img/nova_256.png" />
|
{{end}}
|
||||||
<link rel="shortcut icon" sizes="256x256" href="/res/img/nova_256.png" />
|
{{range .OGData.LinkRules}}
|
||||||
|
<link rel="{{.Key}}" href="{{.Value}}" />
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{ template "opengraph" .OGData }}
|
|
||||||
<script>
|
<script>
|
||||||
|
// The branding of the user's home directory, saved on the previous
|
||||||
|
// visit. Applying it before the page is drawn prevents a flash of
|
||||||
|
// the default theme while the app loads
|
||||||
|
try {
|
||||||
|
document.documentElement.style.cssText = localStorage.getItem("branding") ?? ""
|
||||||
|
} catch {}
|
||||||
|
|
||||||
window.api_endpoint = '{{.APIEndpoint}}';
|
window.api_endpoint = '{{.APIEndpoint}}';
|
||||||
window.server_hostname = "{{.Hostname}}";
|
window.server_hostname = "{{.Hostname}}";
|
||||||
|
{{with .User}}window.user = {{.}};{{end}}
|
||||||
|
{{with .Node}}window.initial_node = {{.}};{{end}}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script defer src='/res/svelte/wrap.js?v{{cacheID}}' type="module"></script>
|
<script src="/res/svelte/wrap.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export default api => {
|
|
||||||
api.cache(true);
|
|
||||||
return {
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
"@babel/env",
|
|
||||||
{
|
|
||||||
"targets": {
|
|
||||||
"firefox": "52",
|
|
||||||
"esmodules": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
2798
svelte/package-lock.json
generated
@@ -3,35 +3,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"rbuild": "rollup -c",
|
"build": "NODE_ENV=production vite build",
|
||||||
"rdev": "rollup -c -w",
|
"dev": "NODE_ENV=development vite build --watch"
|
||||||
"vbuild": "vite build",
|
|
||||||
"vdev": "NODE_ENV=development vite build --watch"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
|
||||||
"@babel/preset-env": "^7.26.0",
|
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
|
||||||
"@rollup/plugin-commonjs": "^26.0.3",
|
|
||||||
"@rollup/plugin-node-resolve": "^15.3.1",
|
"@rollup/plugin-node-resolve": "^15.3.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
|
||||||
"@types/jsmediatags": "^3.9.6",
|
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"rollup": "^4.24.4",
|
|
||||||
"rollup-plugin-livereload": "^2.0.5",
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
"rollup-plugin-svelte": "^7.2.2",
|
|
||||||
"svelte": "^5.0.0"
|
"svelte": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"behave-js": "^1.5.0",
|
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"country-data-list": "^1.4.0",
|
"country-data-list": "^1.4.0",
|
||||||
"pure-color": "^1.3.0",
|
"pure-color": "^1.3.0",
|
||||||
"rollup-plugin-includepaths": "^0.2.4",
|
|
||||||
"svelte-preprocess": "^6.0.3",
|
|
||||||
"tslib": "^2.8.1",
|
|
||||||
"vite": "^7.1.12"
|
"vite": "^7.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import svelte from 'rollup-plugin-svelte';
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import livereload from 'rollup-plugin-livereload';
|
|
||||||
import terser from '@rollup/plugin-terser';
|
|
||||||
import babel from '@rollup/plugin-babel'
|
|
||||||
import typescript from '@rollup/plugin-typescript'
|
|
||||||
import { sveltePreprocess } from 'svelte-preprocess';
|
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH;
|
|
||||||
|
|
||||||
const builddir = "../res/static/svelte"
|
|
||||||
export default [
|
|
||||||
"wrap",
|
|
||||||
].map((name, index) => ({
|
|
||||||
input: `src/${name}.js`,
|
|
||||||
output: {
|
|
||||||
sourcemap: true,
|
|
||||||
format: 'iife',
|
|
||||||
name: 'app',
|
|
||||||
file: `${builddir}/${name}.js`,
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
sveltePreprocess(),
|
|
||||||
|
|
||||||
svelte({
|
|
||||||
preprocess: sveltePreprocess(),
|
|
||||||
compilerOptions: {
|
|
||||||
// enable run-time checks when not in production
|
|
||||||
dev: !production,
|
|
||||||
},
|
|
||||||
emitCss: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
babel({
|
|
||||||
extensions: [".js", ".ts", ".svelte"],
|
|
||||||
babelHelpers: "bundled",
|
|
||||||
}),
|
|
||||||
|
|
||||||
// If you have external dependencies installed from npm, you'll most
|
|
||||||
// likely need these plugins. In some cases you'll need additional
|
|
||||||
// configuration - consult the documentation for details:
|
|
||||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
|
||||||
resolve({
|
|
||||||
browser: true,
|
|
||||||
exportConditions: ['svelte'],
|
|
||||||
modulePaths: [process.cwd() + "/src", process.cwd() + "/node_modules"],
|
|
||||||
extensions: [".svelte", ".mjs", ".js", ".json", ".mts", ".ts"],
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
typescript(),
|
|
||||||
|
|
||||||
// Watch the `public` directory and refresh the browser on changes when
|
|
||||||
// not in production
|
|
||||||
!production && livereload({
|
|
||||||
watch: `${builddir}/${name}.*`,
|
|
||||||
port: 5000 + index,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// If we're building for production (npm run build instead of npm run
|
|
||||||
// dev), minify
|
|
||||||
production && terser(),
|
|
||||||
],
|
|
||||||
watch: {
|
|
||||||
clearScreen: false
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -124,8 +124,8 @@ onMount(() => {
|
|||||||
data: [],
|
data: [],
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: color_by_name("highlight_color"),
|
borderColor: color_by_name("accent_color"),
|
||||||
backgroundColor: color_by_name("highlight_color"),
|
backgroundColor: color_by_name("accent_color"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
graphDownloads.data().datasets = [
|
graphDownloads.data().datasets = [
|
||||||
@@ -134,8 +134,8 @@ onMount(() => {
|
|||||||
data: [],
|
data: [],
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: color_by_name("highlight_color"),
|
borderColor: color_by_name("accent_color"),
|
||||||
backgroundColor: color_by_name("highlight_color"),
|
backgroundColor: color_by_name("accent_color"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Chart from "util/Chart.svelte";
|
import type Chart from "util/Chart.svelte";
|
||||||
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
|
import { formatDataVolume, formatDate, formatThousands } from "util/Formatting";
|
||||||
import Modal from "util/Modal.svelte";
|
import Modal from "util/Modal.svelte";
|
||||||
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI.svelte";
|
import { fs_path_url, fs_share_hotlink_url, fs_share_url, fs_timeseries, type FSNode } from "lib/FilesystemAPI.svelte";
|
||||||
import { color_by_name } from "util/Util";
|
import { color_by_name } from "util/Util";
|
||||||
import { tick } from "svelte";
|
|
||||||
import CopyButton from "layout/CopyButton.svelte";
|
import CopyButton from "layout/CopyButton.svelte";
|
||||||
import type { FSNavigator } from "./FSNavigator";
|
import type { FSNavigator } from "./FSNavigator";
|
||||||
|
|
||||||
@@ -43,12 +42,9 @@ let total_downloads = $state(0)
|
|||||||
let total_egress = $state(0)
|
let total_egress = $state(0)
|
||||||
|
|
||||||
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||||
if (chart === undefined) {
|
// The chart is not there until it's downloaded. The $effect above reads it
|
||||||
// Wait for the chart element to render, if it's not rendered already
|
// through this function, so it calls this function again when it's ready
|
||||||
await tick()
|
if (!visible || base.type !== "file" || !chart) {
|
||||||
}
|
|
||||||
|
|
||||||
if (!visible || base.type !== "file" || chart === undefined) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,15 +112,15 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
|||||||
label: "Downloads",
|
label: "Downloads",
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: color_by_name("highlight_color"),
|
borderColor: color_by_name("accent_color"),
|
||||||
backgroundColor: color_by_name("highlight_color"),
|
backgroundColor: color_by_name("accent_color"),
|
||||||
data: resp.downloads.amounts,
|
data: resp.downloads.amounts,
|
||||||
}, {
|
}, {
|
||||||
label: "Egress",
|
label: "Egress",
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: color_by_name("danger_color"),
|
borderColor: color_by_name("red_color"),
|
||||||
backgroundColor: color_by_name("danger_color"),
|
backgroundColor: color_by_name("red_color"),
|
||||||
yAxisID: "y1",
|
yAxisID: "y1",
|
||||||
data: resp.egress.amounts,
|
data: resp.egress.amounts,
|
||||||
},
|
},
|
||||||
@@ -244,7 +240,10 @@ let direct_share_url = $derived(fs_share_hotlink_url($nav.path))
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Chart bind:this={chart} />
|
<!-- Chart.js is big, so it's only downloaded when it's needed -->
|
||||||
|
{#await import("util/Chart.svelte") then c}
|
||||||
|
<c.default bind:this={chart} />
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { loading_finish, loading_start } from "lib/Loading";
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
import { fs_get_node, fs_encode_path, fs_split_path } from "lib/FilesystemAPI.svelte";
|
import { fs_get_node, fs_encode_path, fs_split_path, fs_path_from_json } from "lib/FilesystemAPI.svelte";
|
||||||
import type { FSNode, FSPath, FSPermissions, FSContext } from "lib/FilesystemAPI.svelte";
|
import type { FSNode, FSPath, FSPermissions, FSContext } from "lib/FilesystemAPI.svelte";
|
||||||
|
|
||||||
export class FSNavigator {
|
export class FSNavigator {
|
||||||
@@ -24,16 +24,6 @@ export class FSNavigator {
|
|||||||
this.history_enabled = history_enabled
|
this.history_enabled = history_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// The popstate event can be used to listen for navigation events. Register
|
|
||||||
// this event listener on the <svelte:window> in the parent element. When
|
|
||||||
// the user presses the back or forward buttons in the browser we'll catch
|
|
||||||
// the event and navigate to the proper directory
|
|
||||||
popstate = (e: PopStateEvent) => {
|
|
||||||
// Get the part of the URL after the fs root and navigate to it
|
|
||||||
const path = window.location.pathname.replace(/^\/d/, "")
|
|
||||||
this.navigate(decodeURI(path), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The FSNavigator acts as a svelte store. This allows for DOM reactivity.
|
// The FSNavigator acts as a svelte store. This allows for DOM reactivity.
|
||||||
// This works by implementing the store contract:
|
// This works by implementing the store contract:
|
||||||
// https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values
|
// https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values
|
||||||
@@ -70,7 +60,13 @@ export class FSNavigator {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loading_start()
|
loading_start()
|
||||||
const resp = await fs_get_node(path)
|
|
||||||
|
// The server embeds the node which the page was opened on. It's only
|
||||||
|
// valid for the first navigation, which is to that same page
|
||||||
|
const resp = window.initial_node !== undefined
|
||||||
|
? fs_path_from_json(window.initial_node)
|
||||||
|
: await fs_get_node(path)
|
||||||
|
window.initial_node = undefined
|
||||||
this.open_node(resp, push_history)
|
this.open_node(resp, push_history)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.value !== undefined && err.value === "path_not_found") {
|
if (err.value !== undefined && err.value === "path_not_found") {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import FilePreview from "./viewers/FilePreview.svelte";
|
|||||||
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
|
import FSUploadWidget from "./upload_widget/FSUploadWidget.svelte";
|
||||||
import { global_navigator } from "./FSNavigator"
|
import { global_navigator } from "./FSNavigator"
|
||||||
import { css_from_path } from "filesystem/edit_window/Branding";
|
import { css_from_path } from "filesystem/edit_window/Branding";
|
||||||
|
import { update_favicon } from "util/NovaLogo.svelte";
|
||||||
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
|
import AffiliatePrompt from "user_home/AffiliatePrompt.svelte";
|
||||||
import { current_page_store } from "wrap/RouterStore";
|
import { current_page_store } from "wrap/RouterStore";
|
||||||
import SearchBar from "./SearchBar.svelte";
|
import SearchBar from "./SearchBar.svelte";
|
||||||
@@ -46,11 +47,13 @@ onMount(() => {
|
|||||||
// styles take precedence since they're lower level, and we can clean it
|
// styles take precedence since they're lower level, and we can clean it
|
||||||
// up afterwards without overwriting global style
|
// up afterwards without overwriting global style
|
||||||
document.body.style = css_from_path(nav.path)
|
document.body.style = css_from_path(nav.path)
|
||||||
|
update_favicon()
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
page_sub()
|
page_sub()
|
||||||
nav_sub()
|
nav_sub()
|
||||||
document.body.style = ""
|
document.body.style = ""
|
||||||
|
update_favicon()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import NovaLogo from "util/NovaLogo.svelte";
|
|
||||||
import Button from "layout/Button.svelte";
|
|
||||||
import Euro from "util/Euro.svelte";
|
|
||||||
import { formatDataVolume } from "util/Formatting";
|
|
||||||
import { user } from "lib/UserStore";
|
|
||||||
import Dialog from "layout/Dialog.svelte";
|
|
||||||
|
|
||||||
let button: HTMLButtonElement = $state()
|
|
||||||
let dialog: Dialog = $state()
|
|
||||||
|
|
||||||
let {
|
|
||||||
no_login_label = "Nova",
|
|
||||||
hide_name = true,
|
|
||||||
hide_logo = false,
|
|
||||||
style = "",
|
|
||||||
embedded = false
|
|
||||||
}: {
|
|
||||||
no_login_label?: string;
|
|
||||||
// Hide the label if the screen is smaller than 800px
|
|
||||||
hide_name?: boolean;
|
|
||||||
hide_logo?: boolean;
|
|
||||||
style?: string;
|
|
||||||
embedded?: boolean;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
let target = $derived(embedded ? "_blank" : "_self")
|
|
||||||
|
|
||||||
const open = () => dialog.open(button.getBoundingClientRect())
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper">
|
|
||||||
<button bind:this={button} onclick={open} class="button round" title="Menu" style={style}>
|
|
||||||
{#if !hide_logo}
|
|
||||||
<NovaLogo style="height: 1.6em; width: 1.6em;"/>
|
|
||||||
{/if}
|
|
||||||
<span class="button_username" class:hide_name>
|
|
||||||
{#if $user !== null}
|
|
||||||
{$user.username === "" ? no_login_label : $user.username}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog bind:this={dialog}>
|
|
||||||
<div class="menu">
|
|
||||||
{#if $user !== null}
|
|
||||||
<Button link_href="/user" link_target={target} icon="dashboard" label="Dashboard" />
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<div class="stats_table">
|
|
||||||
<div>Subscription</div>
|
|
||||||
<div>{$user.subscription.name}</div>
|
|
||||||
|
|
||||||
{#if $user.subscription.type === "prepaid"}
|
|
||||||
<div>Credit</div>
|
|
||||||
<div><Euro amount={$user.balance_micro_eur}/></div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div>Storage used</div>
|
|
||||||
<div>{formatDataVolume($user.filesystem_storage_used, 3)}</div>
|
|
||||||
<div>Transfer used</div>
|
|
||||||
<div>{formatDataVolume($user.monthly_transfer_used, 3)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<Button link_href="/d/me" link_target={target} icon="folder" label="My Filesystem"/>
|
|
||||||
<Button link_href="/filesystem" link_target={target} icon="description" label="Filesystem Guide"/>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<Button link_href="/user/filemanager#files" link_target={target} icon="image" label="My Files"/>
|
|
||||||
<Button link_href="/user/filemanager#lists" link_target={target} icon="photo_library" label="My Albums"/>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<Button link_href="/user/settings" link_target={target} icon="settings" label="Account Settings"/>
|
|
||||||
<Button link_href="/user/subscription" link_target={target} icon="shopping_cart" label="Subscription"/>
|
|
||||||
<Button link_href="/user/subscription/transactions" link_target={target} icon="receipt" label="Transactions"/>
|
|
||||||
|
|
||||||
{#if $user.is_admin}
|
|
||||||
<div class="separator"></div>
|
|
||||||
<Button link_href="/admin" link_target={target} icon="admin_panel_settings" label="Admin Panel"/>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<Button link_href="/" link_target={target} icon="home" label="Home"/>
|
|
||||||
<Button link_href="/#pro" link_target={target} icon="star" label="Get Premium"/>
|
|
||||||
<Button link_href="/login" link_target={target} icon="person" label="Log in"/>
|
|
||||||
<Button link_href="/register" link_target={target} icon="person" label="Register"/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
flex: 0 0 content;
|
|
||||||
}
|
|
||||||
.button_username {
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
.menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 15em;
|
|
||||||
}
|
|
||||||
.separator {
|
|
||||||
height: 1px;
|
|
||||||
margin: 2px 0;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--separator);
|
|
||||||
}
|
|
||||||
.stats_table {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto;
|
|
||||||
gap: 0.2em 1em;
|
|
||||||
margin: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide username on small screen */
|
|
||||||
@media(max-width: 800px) {
|
|
||||||
.hide_name {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -287,12 +287,12 @@ const window_keydown = (e: KeyboardEvent) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-color);
|
color: var(--body_text_color);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
.node_selected {
|
.node_selected {
|
||||||
background: var(--highlight_background);
|
background: var(--accent_background);
|
||||||
color: var(--highlight_text_color);
|
color: var(--accent_text_color);
|
||||||
}
|
}
|
||||||
.node:hover:not(.node_selected) {
|
.node:hover:not(.node_selected) {
|
||||||
background: var(--input_background);
|
background: var(--input_background);
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ const add_password = (e: SubmitEvent) => {
|
|||||||
}
|
}
|
||||||
const generate_password = () => {
|
const generate_password = () => {
|
||||||
// 32 hexadecimal characters, which any S3 client accepts as secret key
|
// 32 hexadecimal characters, which any S3 client accepts as secret key
|
||||||
new_password = crypto.randomUUID().replaceAll("-", "")
|
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
||||||
|
new_password = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("")
|
||||||
}
|
}
|
||||||
// Passwords stay hidden until their reveal button is pressed
|
// Passwords stay hidden until their reveal button is pressed
|
||||||
let revealed: {[password: string]: boolean} = $state({})
|
let revealed: {[password: string]: boolean} = $state({})
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ type Style = {
|
|||||||
input_background?: string,
|
input_background?: string,
|
||||||
input_hover_background?: string,
|
input_hover_background?: string,
|
||||||
input_text?: string,
|
input_text?: string,
|
||||||
highlight_color?: string,
|
accent_color?: string,
|
||||||
highlight_background?: string,
|
accent_shade?: string,
|
||||||
highlight_text_color?: string,
|
accent_background?: string,
|
||||||
|
accent_text_color?: string,
|
||||||
link_color?: string,
|
link_color?: string,
|
||||||
danger_color?: string,
|
|
||||||
danger_text_color?: string,
|
|
||||||
background_color?: string,
|
background_color?: string,
|
||||||
background?: string,
|
background?: string,
|
||||||
background_text_color?: string,
|
background_text_color?: string,
|
||||||
@@ -32,8 +31,45 @@ type Style = {
|
|||||||
background_image_size?: string,
|
background_image_size?: string,
|
||||||
background_image_position?: string,
|
background_image_position?: string,
|
||||||
background_image_repeat?: string,
|
background_image_repeat?: string,
|
||||||
|
color_scheme?: string,
|
||||||
|
green_color?: string,
|
||||||
|
red_color?: string,
|
||||||
|
yellow_color?: string,
|
||||||
|
blue_color?: string,
|
||||||
|
green_tint?: string,
|
||||||
|
red_tint?: string,
|
||||||
|
yellow_tint?: string,
|
||||||
|
blue_tint?: string,
|
||||||
|
logo_color_1?: string,
|
||||||
|
logo_color_2?: string,
|
||||||
|
logo_color_3?: string,
|
||||||
|
logo_color_4?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The hues of the accents for success, errors, warnings and notices, and the
|
||||||
|
// accents for light themes. The dark defaults in layout.css are derived from
|
||||||
|
// the default accent colour, the light defaults are these light accents.
|
||||||
|
// Branding can also set each accent by itself
|
||||||
|
const accent_hues = { green: 140, red: 8, yellow: 45, blue: 215 }
|
||||||
|
const light_accents = { green: "#15793a", red: "#ba2c37", yellow: "#995c00", blue: "#2164ca" }
|
||||||
|
const default_accent = "#49dfa0"
|
||||||
|
|
||||||
|
// The colourways of the logo, as designed. The hue the colourway is closest to,
|
||||||
|
// the colours of the bars and the colours of the diagonal
|
||||||
|
const logo_colourways: [number, string, string, string, string][] = [
|
||||||
|
[1, "#f62e27", "#bb1d1c", "#bb1c1c", "#ee2c26"], // Red
|
||||||
|
[20, "#f78146", "#bb5420", "#ee7a40", "#c05823"], // Orange
|
||||||
|
[54, "#ffe500", "#8c8015", "#ead204", "#6d6310"], // Yellow
|
||||||
|
[71, "#dde741", "#74a511", "#8bb925", "#eaed38"], // Lime
|
||||||
|
[120, "#15c615", "#0c750c", "#0c730c", "#0c730c"], // Green
|
||||||
|
[141, "#45ed52", "#11a574", "#24b880", "#39ef3d"], // Mint
|
||||||
|
[190, "#48eef1", "#1378a8", "#258eba", "#38e8ec"], // Light blue
|
||||||
|
[230, "#4b82f5", "#1111a5", "#2354b5", "#393bee"], // Dark blue
|
||||||
|
[253, "#974af2", "#212eae", "#2929b8", "#8e39ee"], // Purple blue
|
||||||
|
[270, "#8447f0", "#7514a9", "#5924b8", "#b039ef"], // Purple
|
||||||
|
[330, "#f74bbb", "#ad174e", "#b72484", "#f13a77"], // Pink
|
||||||
|
]
|
||||||
|
|
||||||
// Generate a branding style from a file's properties map
|
// Generate a branding style from a file's properties map
|
||||||
export const branding_from_path = (path: Array<FSNode>) => {
|
export const branding_from_path = (path: Array<FSNode>) => {
|
||||||
const style = {}
|
const style = {}
|
||||||
@@ -57,7 +93,7 @@ export const css_from_path = (path: Array<FSNode>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gen_css = (style: Style) => {
|
const gen_css = (style: Style) => {
|
||||||
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
|
return Object.entries(with_accents(style)).map(([key, value]) => `--${key}:${value}`).join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
// add_styles adds the styles configured in the properties struct to the
|
// add_styles adds the styles configured in the properties struct to the
|
||||||
@@ -74,20 +110,33 @@ const add_styles = (style: Style, properties: FSNodeProperties) => {
|
|||||||
style.input_hover_background = properties.brand_input_color
|
style.input_hover_background = properties.brand_input_color
|
||||||
style.input_text = add_contrast(properties.brand_input_color, text_contrast)
|
style.input_text = add_contrast(properties.brand_input_color, text_contrast)
|
||||||
}
|
}
|
||||||
if (properties.brand_highlight_color) {
|
if (properties.brand_accent_color) {
|
||||||
style.highlight_color = properties.brand_highlight_color
|
style.accent_color = properties.brand_accent_color
|
||||||
style.highlight_background = properties.brand_highlight_color
|
style.accent_background = properties.brand_accent_color
|
||||||
style.highlight_text_color = add_contrast(properties.brand_highlight_color, text_contrast)
|
style.accent_text_color = add_contrast(properties.brand_accent_color, text_contrast)
|
||||||
|
|
||||||
// If we have a body colour to compare it to we use the highlight colour
|
// If we have a body colour to compare it to we use the accent colour
|
||||||
// to generate the link colour
|
// to generate the link colour
|
||||||
if (properties.brand_body_color) {
|
if (properties.brand_body_color) {
|
||||||
style.link_color = generate_link_color(properties.brand_highlight_color, properties.brand_body_color)
|
style.link_color = generate_link_color(properties.brand_accent_color, properties.brand_body_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The logo gets the colourway closest to the accent colour. Greys
|
||||||
|
// have no meaningful hue, they keep the default
|
||||||
|
const [hue, saturation] = rgb2hsl(parse(properties.brand_accent_color))
|
||||||
|
if (saturation > 15) {
|
||||||
|
const distance = (h: number) => Math.min(Math.abs(h - hue), 360 - Math.abs(h - hue))
|
||||||
|
const way = logo_colourways.reduce((a, b) => distance(b[0]) < distance(a[0]) ? b : a)
|
||||||
|
style.logo_color_1 = way[1]
|
||||||
|
style.logo_color_2 = way[2]
|
||||||
|
style.logo_color_3 = way[3]
|
||||||
|
style.logo_color_4 = way[4]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (properties.brand_danger_color) {
|
for (const name of Object.keys(accent_hues)) {
|
||||||
style.danger_color = properties.brand_danger_color
|
if (properties["brand_" + name + "_color"]) {
|
||||||
style.danger_text_color = add_contrast(properties.brand_danger_color, text_contrast)
|
style[name + "_color"] = properties["brand_" + name + "_color"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (properties.brand_background_color) {
|
if (properties.brand_background_color) {
|
||||||
style.background_color = properties.brand_background_color
|
style.background_color = properties.brand_background_color
|
||||||
@@ -102,6 +151,10 @@ const add_styles = (style: Style, properties: FSNodeProperties) => {
|
|||||||
style.shaded_background = set_alpha(properties.brand_body_color, body_alpha)
|
style.shaded_background = set_alpha(properties.brand_body_color, body_alpha)
|
||||||
style.separator = add_contrast(properties.brand_body_color, 15)
|
style.separator = add_contrast(properties.brand_body_color, 15)
|
||||||
style.shadow_color = darken(properties.brand_body_color, 0.8)
|
style.shadow_color = darken(properties.brand_body_color, 0.8)
|
||||||
|
|
||||||
|
// Makes the browser draw its own controls, like scrollbars and date
|
||||||
|
// pickers, in matching colours
|
||||||
|
style.color_scheme = rgb2hsl(parse(properties.brand_body_color))[2] < 50 ? "dark" : "light"
|
||||||
}
|
}
|
||||||
if (properties.brand_card_color) {
|
if (properties.brand_card_color) {
|
||||||
style.card_color = properties.brand_card_color
|
style.card_color = properties.brand_card_color
|
||||||
@@ -114,6 +167,45 @@ const add_styles = (style: Style, properties: FSNodeProperties) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// with_accents adds the accents for success, errors, warnings and notices, and
|
||||||
|
// their tints. Accents which the branding doesn't set get the saturation and
|
||||||
|
// lightness of the accent colour, so they match it. A light theme's
|
||||||
|
// accent colour is too light for text, those get fixed accents. A branded
|
||||||
|
// page always gets all accents, else a shared directory would inherit the
|
||||||
|
// accents of the visitor's own theme
|
||||||
|
const with_accents = (style: Style): Style => {
|
||||||
|
if (Object.keys(style).length === 0) {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greys have no hue to match, those use the default accent colour
|
||||||
|
let [, saturation, lightness] = rgb2hsl(parse(style.accent_color ?? default_accent))
|
||||||
|
if (saturation <= 15) {
|
||||||
|
[, saturation, lightness] = rgb2hsl(parse(default_accent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without its own background colours the branding is shown on the default
|
||||||
|
// theme, which follows the browser's preference
|
||||||
|
const light = (style.color_scheme ?? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark")) === "light"
|
||||||
|
|
||||||
|
const full = { ...style }
|
||||||
|
|
||||||
|
// The background of selected buttons. A subtle dark shade of the accent on
|
||||||
|
// dark themes and a light one on light themes, so the accent is readable on it
|
||||||
|
const [accent_hue, accent_saturation] = rgb2hsl(parse(style.accent_color ?? default_accent))
|
||||||
|
full.accent_shade = rgb2hex(hsl2rgb([accent_hue, Math.min(accent_saturation, 25), light ? 91 : 19]))
|
||||||
|
|
||||||
|
for (const [name, hue] of Object.entries(accent_hues)) {
|
||||||
|
let colour = style[name + "_color"]
|
||||||
|
if (!colour) {
|
||||||
|
colour = light ? light_accents[name] : rgb2hex(hsl2rgb([hue, saturation, lightness]))
|
||||||
|
}
|
||||||
|
full[name + "_color"] = colour
|
||||||
|
full[name + "_tint"] = set_alpha(colour, 0.12)
|
||||||
|
}
|
||||||
|
return full
|
||||||
|
}
|
||||||
|
|
||||||
const add_contrast = (color: string, amt: number) => {
|
const add_contrast = (color: string, amt: number) => {
|
||||||
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
||||||
// If the lightness is less than 40 it is considered a dark colour. This
|
// If the lightness is less than 40 it is considered a dark colour. This
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const handle_picker = async (nodes: FSNode[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlight_info = $state(false)
|
let accent_info = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -93,14 +93,14 @@ let highlight_info = $state(false)
|
|||||||
<fieldset class="grid" disabled={!enabled}>
|
<fieldset class="grid" disabled={!enabled}>
|
||||||
<legend>Custom colours</legend>
|
<legend>Custom colours</legend>
|
||||||
<div>
|
<div>
|
||||||
<div style="display: inline-block">Highlight</div>
|
<div style="display: inline-block">Accent</div>
|
||||||
<HelpButton bind:toggle={highlight_info}/>
|
<HelpButton bind:toggle={accent_info}/>
|
||||||
</div>
|
</div>
|
||||||
<input type="color" value={options.brand_highlight_color} onchange={e => options.brand_highlight_color = (e.target as HTMLInputElement).value}/>
|
<input type="color" value={options.brand_accent_color} onchange={e => options.brand_accent_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_highlight_color}/>
|
<input type="text" bind:value={options.brand_accent_color}/>
|
||||||
{#if highlight_info}
|
{#if accent_info}
|
||||||
<p class="span3">
|
<p class="span3">
|
||||||
The highlight colour is used for highlighting selected buttons and
|
The accent colour is used for highlighting selected buttons and
|
||||||
other elements. It's also used as the page's theme colour, this
|
other elements. It's also used as the page's theme colour, this
|
||||||
affects things like the embed colour in Discord and the colour of
|
affects things like the embed colour in Discord and the colour of
|
||||||
the address bar in some web browsers.
|
the address bar in some web browsers.
|
||||||
@@ -109,9 +109,6 @@ let highlight_info = $state(false)
|
|||||||
<div>Button and input</div>
|
<div>Button and input</div>
|
||||||
<input type="color" value={options.brand_input_color} onchange={e => options.brand_input_color = (e.target as HTMLInputElement).value}/>
|
<input type="color" value={options.brand_input_color} onchange={e => options.brand_input_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_input_color}/>
|
<input type="text" bind:value={options.brand_input_color}/>
|
||||||
<div>Delete button</div>
|
|
||||||
<input type="color" value={options.brand_danger_color} onchange={e => options.brand_danger_color = (e.target as HTMLInputElement).value}/>
|
|
||||||
<input type="text" bind:value={options.brand_danger_color}/>
|
|
||||||
<div>Background</div>
|
<div>Background</div>
|
||||||
<input type="color" value={options.brand_background_color} onchange={e => options.brand_background_color = (e.target as HTMLInputElement).value}/>
|
<input type="color" value={options.brand_background_color} onchange={e => options.brand_background_color = (e.target as HTMLInputElement).value}/>
|
||||||
<input type="text" bind:value={options.brand_background_color}/>
|
<input type="text" bind:value={options.brand_background_color}/>
|
||||||
@@ -123,6 +120,26 @@ let highlight_info = $state(false)
|
|||||||
<input type="text" bind:value={options.brand_card_color}/>
|
<input type="text" bind:value={options.brand_card_color}/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="grid" disabled={!enabled}>
|
||||||
|
<legend>Accent colours</legend>
|
||||||
|
<p class="span3">
|
||||||
|
Colours for success messages, errors, warnings and notices. Colours you
|
||||||
|
leave empty will be derived from the accent colour, so they match it.
|
||||||
|
</p>
|
||||||
|
<div>Success</div>
|
||||||
|
<input type="color" value={options.brand_green_color} onchange={e => options.brand_green_color = (e.target as HTMLInputElement).value}/>
|
||||||
|
<input type="text" bind:value={options.brand_green_color}/>
|
||||||
|
<div>Error</div>
|
||||||
|
<input type="color" value={options.brand_red_color} onchange={e => options.brand_red_color = (e.target as HTMLInputElement).value}/>
|
||||||
|
<input type="text" bind:value={options.brand_red_color}/>
|
||||||
|
<div>Warning</div>
|
||||||
|
<input type="color" value={options.brand_yellow_color} onchange={e => options.brand_yellow_color = (e.target as HTMLInputElement).value}/>
|
||||||
|
<input type="text" bind:value={options.brand_yellow_color}/>
|
||||||
|
<div>Notice</div>
|
||||||
|
<input type="color" value={options.brand_blue_color} onchange={e => options.brand_blue_color = (e.target as HTMLInputElement).value}/>
|
||||||
|
<input type="text" bind:value={options.brand_blue_color}/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="grid" disabled={!enabled}>
|
<fieldset class="grid" disabled={!enabled}>
|
||||||
<legend>Image customization</legend>
|
<legend>Image customization</legend>
|
||||||
<p class="span3">
|
<p class="span3">
|
||||||
@@ -174,6 +191,13 @@ let highlight_info = $state(false)
|
|||||||
<input type="text" value="A text field"/>
|
<input type="text" value="A text field"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="example_button_row" style="gap: 4px;">
|
||||||
|
<div class="highlight_green">Success</div>
|
||||||
|
<div class="highlight_red">Error</div>
|
||||||
|
<div class="highlight_yellow">Warning</div>
|
||||||
|
<div class="highlight_blue">Notice</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<div class="example example_card">
|
<div class="example example_card">
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ let current_theme = $state(-1)
|
|||||||
const set_theme = (index: number) => {
|
const set_theme = (index: number) => {
|
||||||
current_theme = index
|
current_theme = index
|
||||||
|
|
||||||
|
// Presets without accents derive them from the accent colour, the
|
||||||
|
// accents of an earlier preset shouldn't stick
|
||||||
|
properties.brand_green_color = ""
|
||||||
|
properties.brand_red_color = ""
|
||||||
|
properties.brand_yellow_color = ""
|
||||||
|
properties.brand_blue_color = ""
|
||||||
|
|
||||||
// Copy all the properties of the theme to the file, except for the theme
|
// Copy all the properties of the theme to the file, except for the theme
|
||||||
// name
|
// name
|
||||||
Object.keys(themes[index]).forEach(key => {
|
Object.keys(themes[index]).forEach(key => {
|
||||||
@@ -25,99 +32,91 @@ const themes = [
|
|||||||
{
|
{
|
||||||
name: "PD classic",
|
name: "PD classic",
|
||||||
brand_input_color: "#2d2d2d",
|
brand_input_color: "#2d2d2d",
|
||||||
brand_highlight_color: "#75b72d",
|
brand_accent_color: "#75b72d",
|
||||||
brand_danger_color: "#821b3f",
|
|
||||||
brand_background_color: "#141414",
|
brand_background_color: "#141414",
|
||||||
brand_body_color: "#1e1e1e",
|
brand_body_color: "#1e1e1e",
|
||||||
brand_card_color: "#282828",
|
brand_card_color: "#282828",
|
||||||
}, {
|
}, {
|
||||||
name: "Nord (dark)",
|
name: "Nord (dark)",
|
||||||
brand_input_color: "#4f596d",
|
brand_input_color: "#4f596d",
|
||||||
brand_highlight_color: "#a4be8c",
|
brand_accent_color: "#a4be8c",
|
||||||
brand_danger_color: "#bd5f69",
|
|
||||||
brand_background_color: "#2f3541",
|
brand_background_color: "#2f3541",
|
||||||
brand_body_color: "#3b4252",
|
brand_body_color: "#3b4252",
|
||||||
brand_card_color: "#434c5f",
|
brand_card_color: "#434c5f",
|
||||||
}, {
|
}, {
|
||||||
name: "Nord (light)",
|
name: "Nord (light)",
|
||||||
brand_input_color: "#cad2e1",
|
brand_input_color: "#cad2e1",
|
||||||
brand_highlight_color: "#a4be8c",
|
brand_accent_color: "#a4be8c",
|
||||||
brand_danger_color: "#bd5f69",
|
|
||||||
brand_background_color: "#d7dde8",
|
brand_background_color: "#d7dde8",
|
||||||
brand_body_color: "#ebeef3",
|
brand_body_color: "#ebeef3",
|
||||||
brand_card_color: "#e5e9f0",
|
brand_card_color: "#e5e9f0",
|
||||||
}, {
|
}, {
|
||||||
name: "Black",
|
name: "Black",
|
||||||
brand_input_color: "#202020",
|
brand_input_color: "#202020",
|
||||||
brand_highlight_color: "#57e389",
|
brand_accent_color: "#57e389",
|
||||||
brand_danger_color: "#ed333b",
|
|
||||||
brand_background_color: "#000000",
|
brand_background_color: "#000000",
|
||||||
brand_body_color: "#080808",
|
brand_body_color: "#080808",
|
||||||
brand_card_color: "#101010",
|
brand_card_color: "#101010",
|
||||||
}, {
|
}, {
|
||||||
name: "White",
|
name: "White",
|
||||||
brand_input_color: "#f4f4f4",
|
brand_input_color: "#f4f4f4",
|
||||||
brand_highlight_color: "#2ec27e",
|
brand_accent_color: "#2ec27e",
|
||||||
brand_danger_color: "#c01c28",
|
|
||||||
brand_background_color: "#f8f8f8",
|
brand_background_color: "#f8f8f8",
|
||||||
brand_body_color: "#ffffff",
|
brand_body_color: "#ffffff",
|
||||||
brand_card_color: "#f0f0f0",
|
brand_card_color: "#f0f0f0",
|
||||||
}, {
|
}, {
|
||||||
name: "Solarized (dark)",
|
name: "Solarized (dark)",
|
||||||
brand_input_color: "#084453",
|
brand_input_color: "#084453",
|
||||||
brand_highlight_color: "#849900",
|
brand_accent_color: "#849900",
|
||||||
brand_danger_color: "#db302d",
|
|
||||||
brand_background_color: "#002c38",
|
brand_background_color: "#002c38",
|
||||||
brand_body_color: "#063540",
|
brand_body_color: "#063540",
|
||||||
brand_card_color: "#073c49",
|
brand_card_color: "#073c49",
|
||||||
}, {
|
}, {
|
||||||
name: "Solarized (light)",
|
name: "Solarized (light)",
|
||||||
brand_input_color: "#e7dfc5",
|
brand_input_color: "#e7dfc5",
|
||||||
brand_highlight_color: "#849900",
|
brand_accent_color: "#849900",
|
||||||
brand_danger_color: "#db302d",
|
|
||||||
brand_background_color: "#ede7d3",
|
brand_background_color: "#ede7d3",
|
||||||
brand_body_color: "#fdf5e2",
|
brand_body_color: "#fdf5e2",
|
||||||
brand_card_color: "#fcf2d8",
|
brand_card_color: "#fcf2d8",
|
||||||
}, {
|
}, {
|
||||||
name: "Mocha",
|
name: "Mocha",
|
||||||
brand_input_color: "#313244",
|
brand_input_color: "#313244",
|
||||||
brand_highlight_color: "#a6e3a1",
|
brand_accent_color: "#a6e3a1",
|
||||||
brand_danger_color: "#f38ba8",
|
|
||||||
brand_background_color: "#1e1e2e",
|
brand_background_color: "#1e1e2e",
|
||||||
brand_body_color: "#181825",
|
brand_body_color: "#181825",
|
||||||
brand_card_color: "#313244",
|
brand_card_color: "#313244",
|
||||||
}, {
|
}, {
|
||||||
name: "Dracula",
|
name: "Dracula",
|
||||||
brand_input_color: "#424450",
|
brand_input_color: "#424450",
|
||||||
brand_highlight_color: "#50FA7B",
|
brand_accent_color: "#50FA7B",
|
||||||
brand_danger_color: "#FF5555",
|
brand_green_color: "#50FA7B",
|
||||||
|
brand_red_color: "#FF5555",
|
||||||
|
brand_yellow_color: "#F1FA8C",
|
||||||
|
brand_blue_color: "#8BE9FD",
|
||||||
brand_background_color: "#191A21",
|
brand_background_color: "#191A21",
|
||||||
brand_body_color: "#21222C",
|
brand_body_color: "#21222C",
|
||||||
brand_card_color: "#343746",
|
brand_card_color: "#343746",
|
||||||
}, {
|
}, {
|
||||||
name: "Nova",
|
name: "Deep purple",
|
||||||
brand_input_color: "#28033f",
|
brand_input_color: "#342848",
|
||||||
brand_highlight_color: "#57e389",
|
brand_accent_color: "#c49efa",
|
||||||
brand_danger_color: "#ed333b",
|
brand_background_color: "#160f1f",
|
||||||
brand_background_color: "#0d0015",
|
brand_body_color: "#1d1429",
|
||||||
brand_body_color: "#130020",
|
brand_card_color: "#281e38",
|
||||||
brand_card_color: "#1a002b",
|
|
||||||
}, {
|
}, {
|
||||||
name: "Green Phosphor",
|
name: "Green Phosphor",
|
||||||
brand_input_color: "#001e09",
|
brand_input_color: "#263129",
|
||||||
brand_highlight_color: "#57e389",
|
brand_accent_color: "#69d38c",
|
||||||
brand_danger_color: "#ed333b",
|
brand_background_color: "#0d120e",
|
||||||
brand_background_color: "#000502",
|
brand_body_color: "#141a16",
|
||||||
brand_body_color: "#000903",
|
brand_card_color: "#1d261f",
|
||||||
brand_card_color: "#001707",
|
|
||||||
}, {
|
}, {
|
||||||
name: "Amber",
|
name: "Amber",
|
||||||
brand_input_color: "#3c2b00",
|
brand_input_color: "#363026",
|
||||||
brand_highlight_color: "#f5c211",
|
brand_accent_color: "#e2b05a",
|
||||||
brand_danger_color: "#c01c28",
|
brand_background_color: "#16130e",
|
||||||
brand_background_color: "#171100",
|
brand_body_color: "#1f1b14",
|
||||||
brand_body_color: "#201700",
|
brand_card_color: "#2a251d",
|
||||||
brand_card_color: "#281d00",
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ let {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--body_text-color);
|
color: var(--body_text_color);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--body_background);
|
background: var(--body_background);
|
||||||
@@ -78,8 +78,8 @@ let {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.node.node_selected {
|
.node.node_selected {
|
||||||
background-color: var(--highlight_color) !important;
|
background-color: var(--accent_color) !important;
|
||||||
color: var(--highlight_text_color);
|
color: var(--accent_text_color);
|
||||||
}
|
}
|
||||||
.node>* {
|
.node>* {
|
||||||
flex: 0 0 content;
|
flex: 0 0 content;
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ const move_here = async () => {
|
|||||||
class="container"
|
class="container"
|
||||||
use:drop_target={{
|
use:drop_target={{
|
||||||
upload: (files) => upload_widget.upload_files(files),
|
upload: (files) => upload_widget.upload_files(files),
|
||||||
shadow: "var(--highlight_color) 0 0 10px 2px inset",
|
shadow: "var(--accent_color) 0 0 10px 2px inset",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="width_container bubble">
|
<div class="width_container bubble">
|
||||||
@@ -443,10 +443,7 @@ const move_here = async () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.toolbar_edit {
|
.toolbar_edit {
|
||||||
background-color: rgba(0, 255, 0, 0.05);
|
background-color: var(--green_tint);
|
||||||
}
|
|
||||||
.icon.button_highlight {
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large icon mode only supported on wide screens. Hide the button on small screen */
|
/* Large icon mode only supported on wide screens. Hide the button on small screen */
|
||||||
|
|||||||
@@ -237,7 +237,4 @@ onMount(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
.icon.button_highlight {
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ let {
|
|||||||
background: var(--input_hover_background);
|
background: var(--input_hover_background);
|
||||||
}
|
}
|
||||||
.file.selected {
|
.file.selected {
|
||||||
background: var(--highlight_background);
|
background: var(--accent_background);
|
||||||
color: var(--highlight_text_color);
|
color: var(--accent_text_color);
|
||||||
}
|
}
|
||||||
.node_icon {
|
.node_icon {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ td {
|
|||||||
}
|
}
|
||||||
.node {
|
.node {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--body_text-color);
|
color: var(--body_text_color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.node:not(:last-child) {
|
.node:not(:last-child) {
|
||||||
@@ -214,8 +214,8 @@ td {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.node.node_selected {
|
.node.node_selected {
|
||||||
background-color: var(--highlight_color) !important;
|
background-color: var(--accent_color) !important;
|
||||||
color: var(--highlight_text_color);
|
color: var(--accent_text_color);
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ onMount(() => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: var(--danger_color, #c0392b);
|
color: var(--red_color);
|
||||||
}
|
}
|
||||||
.trash_table {
|
.trash_table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ const cancel = () => {
|
|||||||
align-items: center; /* Stop stretching the button */
|
align-items: center; /* Stop stretching the button */
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
background: var(--danger_color);
|
background: var(--red_tint);
|
||||||
color: var(--highlight_text_color);
|
color: var(--red_color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -186,6 +186,6 @@ onMount(() => {
|
|||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
.playing {
|
.playing {
|
||||||
color: var(--highlight_color);
|
color: var(--accent_color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ const video_keydown = (e: KeyboardEvent) => {
|
|||||||
.compatibility_warning {
|
.compatibility_warning {
|
||||||
background-color: var(--shaded_background);
|
background-color: var(--shaded_background);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
border-bottom: 2px solid #6666FF;
|
border-bottom: 2px solid var(--blue_color);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
14
svelte/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { User } from "lib/NovaAPI"
|
||||||
|
import type { FSPath } from "lib/FilesystemAPI.svelte"
|
||||||
|
|
||||||
|
// Values which the server embeds in the page, see res/template/wrap.html
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
api_endpoint: string
|
||||||
|
server_hostname: string
|
||||||
|
|
||||||
|
// Only on filesystem pages. null means the user is not logged in
|
||||||
|
user?: User | null
|
||||||
|
initial_node?: FSPath
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -333,7 +333,7 @@ import { user } from "lib/UserStore";
|
|||||||
border: 1px solid var(--background_color);
|
border: 1px solid var(--background_color);
|
||||||
}
|
}
|
||||||
.pro_feat {
|
.pro_feat {
|
||||||
border: 1px solid var(--highlight_color);
|
border: 1px solid var(--accent_color);
|
||||||
}
|
}
|
||||||
.free_feat {
|
.free_feat {
|
||||||
border: 1px solid #ebcb8b;
|
border: 1px solid #ebcb8b;
|
||||||
@@ -345,9 +345,9 @@ import { user } from "lib/UserStore";
|
|||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
.span2.left {
|
.span2.left {
|
||||||
border-image: linear-gradient(to right, #ebcb8b 0%, var(--highlight_color) 100%) 1;
|
border-image: linear-gradient(to right, #ebcb8b 0%, var(--accent_color) 100%) 1;
|
||||||
}
|
}
|
||||||
.span2.right {
|
.span2.right {
|
||||||
border-image: linear-gradient(to right, var(--highlight_color) 0%, #ec2cfa 100%) 1;
|
border-image: linear-gradient(to right, var(--accent_color) 0%, #ec2cfa 100%) 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import Menu from "filesystem/Menu.svelte";
|
|
||||||
import Footer from "layout/Footer.svelte";
|
import Footer from "layout/Footer.svelte";
|
||||||
import { drop_target } from "lib/DropTarget";
|
import { drop_target } from "lib/DropTarget";
|
||||||
import GetStarted from "./GetStarted.svelte";
|
import GetStarted from "./GetStarted.svelte";
|
||||||
import Pricing from "./Pricing.svelte";
|
import Pricing from "./Pricing.svelte";
|
||||||
import SubscriptionPlans from "layout/checkout/SubscriptionPlans.svelte";
|
import SubscriptionPlans from "layout/checkout/SubscriptionPlans.svelte";
|
||||||
|
import NovaLogo from "util/NovaLogo.svelte";
|
||||||
|
|
||||||
let upload_widget
|
let upload_widget
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="logo_header">
|
<header class="logo_header">
|
||||||
<div class="header_image_container"></div>
|
<NovaLogo full style="display: block; height: 100px; max-width: 100%; margin: 50px auto;"/>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="page_content">
|
<div class="page_content">
|
||||||
@@ -60,17 +60,6 @@ let upload_widget
|
|||||||
</header>
|
</header>
|
||||||
<SubscriptionPlans/>
|
<SubscriptionPlans/>
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-image: url("/res/img/northernlights.webp");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svelte:head>
|
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
@@ -82,21 +71,8 @@ header {
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
header > h1 {
|
header > h1 {
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 1px 1px 5px #000000;
|
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header_image_container {
|
|
||||||
text-align: initial;
|
|
||||||
margin: 50px auto;
|
|
||||||
height: 100px;
|
|
||||||
width: 500px;
|
|
||||||
max-width: 100%;
|
|
||||||
background-image: url("/res/img/header.webp");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let { title = "" }: {
|
|
||||||
title?: string;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h1>{title}</h1>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
h1 {
|
|
||||||
text-shadow: 1px 1px 2px #000000;
|
|
||||||
line-break: anywhere;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -42,9 +42,9 @@ const click = (e: MouseEvent) => {
|
|||||||
class:flat
|
class:flat
|
||||||
>
|
>
|
||||||
{#if on}
|
{#if on}
|
||||||
<i class="icon green">{icon_on}</i>
|
<i class="icon on">{icon_on}</i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="icon red">{icon_off}</i>
|
<i class="icon off">{icon_off}</i>
|
||||||
{/if}
|
{/if}
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</button>
|
</button>
|
||||||
@@ -53,10 +53,10 @@ const click = (e: MouseEvent) => {
|
|||||||
.button {
|
.button {
|
||||||
flex: 0 0 content;
|
flex: 0 0 content;
|
||||||
}
|
}
|
||||||
.green {
|
.on {
|
||||||
color: var(--highlight_color);
|
color: var(--accent_color);
|
||||||
}
|
}
|
||||||
.red {
|
.off {
|
||||||
color: var(--danger_color);
|
color: var(--red_color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ const submit = async (e: SubmitEvent) => {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.date {
|
.date {
|
||||||
color: var(--highlight_color);
|
color: var(--accent_color);
|
||||||
}
|
}
|
||||||
/* layout.css collapses the table borders, which makes the browser ignore any
|
/* layout.css collapses the table borders, which makes the browser ignore any
|
||||||
padding on the table itself. The cells line the table up with the rest of
|
padding on the table itself. The cells line the table up with the rest of
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ get_misc_subscriptions().then(v => plans = v).catch(err => {
|
|||||||
background-color: var(--body_color);
|
background-color: var(--body_color);
|
||||||
}
|
}
|
||||||
.plan.highlighted {
|
.plan.highlighted {
|
||||||
border-color: var(--highlight_color);
|
border-color: var(--accent_color);
|
||||||
}
|
}
|
||||||
.plan > div {
|
.plan > div {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const drop_target = (
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (args.shadow === undefined) {
|
if (args.shadow === undefined) {
|
||||||
node.style.boxShadow = "var(--highlight_color) 0 0 2px 2px"
|
node.style.boxShadow = "var(--accent_color) 0 0 2px 2px"
|
||||||
} else {
|
} else {
|
||||||
node.style.boxShadow = args.shadow
|
node.style.boxShadow = args.shadow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ export class FSNode {
|
|||||||
export type FSNodeProperties = {
|
export type FSNodeProperties = {
|
||||||
branding_enabled?: string,
|
branding_enabled?: string,
|
||||||
brand_input_color?: string,
|
brand_input_color?: string,
|
||||||
brand_highlight_color?: string,
|
brand_accent_color?: string,
|
||||||
brand_danger_color?: string,
|
brand_green_color?: string,
|
||||||
|
brand_red_color?: string,
|
||||||
|
brand_yellow_color?: string,
|
||||||
|
brand_blue_color?: string,
|
||||||
brand_background_color?: string,
|
brand_background_color?: string,
|
||||||
brand_body_color?: string,
|
brand_body_color?: string,
|
||||||
brand_card_color?: string,
|
brand_card_color?: string,
|
||||||
@@ -168,10 +171,13 @@ export const fs_mkdirall = async (path: string, opts: NodeOptions) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fs_get_node = async (path: string) => {
|
export const fs_get_node = async (path: string) => {
|
||||||
const resp = await fs_check_response(
|
return fs_path_from_json(await fs_check_response(
|
||||||
await fetch(fs_path_url(path) + "?stat")
|
await fetch(fs_path_url(path) + "?stat")
|
||||||
) as FSPath
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns the nodes in a decoded API response into FSNode objects
|
||||||
|
export const fs_path_from_json = (resp: FSPath) => {
|
||||||
const fsp = new FSPath()
|
const fsp = new FSPath()
|
||||||
fsp.path = new Array(resp.path.length)
|
fsp.path = new Array(resp.path.length)
|
||||||
resp.path.forEach((node, index) => {
|
resp.path.forEach((node, index) => {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { current_page_store } from "wrap/RouterStore"
|
|||||||
export const highlight_current_page = (node: HTMLAnchorElement) => {
|
export const highlight_current_page = (node: HTMLAnchorElement) => {
|
||||||
const set_highlight = () => {
|
const set_highlight = () => {
|
||||||
const currentpath = window.location.pathname + "/"
|
const currentpath = window.location.pathname + "/"
|
||||||
const hrefpath = URL.parse(node.href).pathname + "/"
|
const hrefpath = node.pathname + "/"
|
||||||
if (
|
// Links to other sites, like the documentation, are never the current page
|
||||||
|
if (node.host === window.location.host && (
|
||||||
currentpath === hrefpath ||
|
currentpath === hrefpath ||
|
||||||
(hrefpath !== "" && hrefpath !== "/" && currentpath.startsWith(hrefpath))
|
(hrefpath !== "" && hrefpath !== "/" && currentpath.startsWith(hrefpath))
|
||||||
) {
|
)) {
|
||||||
node.classList.add("button_highlight")
|
node.classList.add("button_highlight")
|
||||||
} else {
|
} else {
|
||||||
node.classList.remove("button_highlight")
|
node.classList.remove("button_highlight")
|
||||||
|
|||||||
@@ -51,21 +51,8 @@ export type Subscription = {
|
|||||||
// Utility funcs
|
// Utility funcs
|
||||||
// =============
|
// =============
|
||||||
|
|
||||||
export const get_endpoint = () => {
|
export const get_endpoint = () => window.api_endpoint
|
||||||
if ((window as any).api_endpoint !== undefined) {
|
export const get_hostname = () => window.server_hostname
|
||||||
return (window as any).api_endpoint as string
|
|
||||||
}
|
|
||||||
console.warn("api_endpoint property is not defined on window")
|
|
||||||
return "/api"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const get_hostname = () => {
|
|
||||||
if ((window as any).server_hostname !== undefined) {
|
|
||||||
return (window as any).server_hostname as string
|
|
||||||
}
|
|
||||||
console.warn("server_hostname property is not defined on window")
|
|
||||||
return "undefined"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const check_response = async (resp: Response) => {
|
export const check_response = async (resp: Response) => {
|
||||||
let text = await resp.text()
|
let text = await resp.text()
|
||||||
@@ -111,8 +98,8 @@ export type UserSession = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If cached_user is undefined it means that the value is not initialized yet,
|
// If cached_user is undefined it means that the value is not initialized yet,
|
||||||
// null means the user is not logged in
|
// null means the user is not logged in. Some pages have it embedded by the server
|
||||||
let cached_user: User = undefined
|
let cached_user: User = window.user
|
||||||
|
|
||||||
// The request which is currently in flight. Callers which arrive while the
|
// The request which is currently in flight. Callers which arrive while the
|
||||||
// request is still running share this promise, so the user is only requested
|
// request is still running share this promise, so the user is only requested
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
const get_cookie = (cname: string) => {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for(let i = 0; i <ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = get_cookie("style")
|
|
||||||
let hue = +get_cookie("hue")
|
|
||||||
|
|
||||||
// Style selector
|
|
||||||
onMount(() => {
|
|
||||||
document.getElementsByName("style").forEach(function(elem) {
|
|
||||||
elem.addEventListener("change", e => {
|
|
||||||
style = (e.target as HTMLInputElement).id.substring(6)
|
|
||||||
|
|
||||||
var date = new Date();
|
|
||||||
date.setTime(date.getTime() + (10 * 365 * 24 * 60 * 60 * 1000));
|
|
||||||
document.cookie = "style="+style+"; expires=" + date.toUTCString() + "; path=/"
|
|
||||||
|
|
||||||
reload_sheet()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
document.getElementsByName("hue").forEach(function(elem) {
|
|
||||||
elem.addEventListener("change", e => {
|
|
||||||
hue = +(e.target as HTMLInputElement).id.substring(4)
|
|
||||||
if (hue === -1) {
|
|
||||||
document.cookie = "hue=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"
|
|
||||||
}
|
|
||||||
|
|
||||||
var date = new Date();
|
|
||||||
date.setTime(date.getTime() + (10 * 365 * 24 * 60 * 60 * 1000));
|
|
||||||
document.cookie = "hue="+hue+"; expires=" + date.toUTCString() + "; path=/"
|
|
||||||
|
|
||||||
reload_sheet()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
let stylesheet_2: HTMLLinkElement = $state()
|
|
||||||
const reload_sheet = () => {
|
|
||||||
let stylesheet1 = document.getElementById("stylesheet_theme") as HTMLLinkElement
|
|
||||||
|
|
||||||
// First load the sheet in the secondary tag, wait for it to load,
|
|
||||||
// and replace the original sheet when it has finished loading
|
|
||||||
stylesheet_2.href= "/theme.css?style="+style+"&hue="+hue
|
|
||||||
stylesheet_2.onload = e => {
|
|
||||||
stylesheet1.href= "/theme.css?style="+style+"&hue="+hue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<link bind:this={stylesheet_2} id="stylesheet_theme_2" rel="stylesheet" type="text/css" href="/theme.css"/>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<div id="page_content" class="page_content">
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
You can change how Nova looks! Your theme choice will
|
|
||||||
be saved in a cookie.
|
|
||||||
</p>
|
|
||||||
<h2>Theme</h2>
|
|
||||||
<input type="radio" id="style_nord" name="style"><label for="style_nord">Nord</label>
|
|
||||||
(Inspired by <a href="https://www.nordtheme.com/" target="_blank">Nord</a>)
|
|
||||||
<br/>
|
|
||||||
Dynamic theme, changes based on operating system settings. Here you can choose a specific variant:
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_nord_dark" name="style"><label for="style_nord_dark">Nord dark</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_nord_light" name="style"><label for="style_nord_light">Nord light</label>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_solarized" name="style"><label for="style_solarized">Solarized</label>
|
|
||||||
(Inspired by <a href="https://ethanschoonover.com/solarized/" target="_blank">Solarized</a>)
|
|
||||||
<br/>
|
|
||||||
Dynamic theme, changes based on operating system settings. Here you can choose a specific variant:
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_solarized_dark" name="style"><label for="style_solarized_dark">Solarized dark</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_solarized_light" name="style"><label for="style_solarized_light">Solarized light</label>
|
|
||||||
<br/>
|
|
||||||
<!-- <br/> -->
|
|
||||||
<!-- <input type="radio" id="style_adwaita" name="style"><label for="style_adwaita">Adwaita</label><br/> -->
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_purple_drain" name="style"><label for="style_purple_drain">Purple drain</label>
|
|
||||||
<br/>
|
|
||||||
Classic 2022 style, with purple gradients
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_classic" name="style"><label for="style_classic">Nova classic (gray)</label>
|
|
||||||
<br/>
|
|
||||||
Classic pre-2020 Nova style, dark gray
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
Other (experimental) themes
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_maroon" name="style"><label for="style_maroon">Maroon Style</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_hacker" name="style"><label for="style_hacker">Hacker Style</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_canta" name="style"><label for="style_canta">Canta Style</label>
|
|
||||||
(Inspired by <a href="https://github.com/vinceliuice/Canta-theme" target="_blank">Canta GTK</a>)
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_skeuos" name="style"><label for="style_skeuos">Skeuos Style</label>
|
|
||||||
(Inspired by <a href="https://www.gnome-look.org/p/1441725/" target="_blank">Skeuos GTK</a>)
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_sweet" name="style"><label for="style_sweet">Sweet</label>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_adwaita" name="style"><label for="style_adwaita">Adwaita (dynamic)</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_adwaita_dark" name="style"><label for="style_adwaita_dark">Adwaita dark</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_adwaita_light" name="style"><label for="style_adwaita_light">Adwaita light</label>
|
|
||||||
<br/><br/>
|
|
||||||
<input type="radio" id="style_nova98" name="style"><label for="style_nova98">Nova 98</label>
|
|
||||||
<br/>
|
|
||||||
<input type="radio" id="style_nova98_dark" name="style"><label for="style_nova98_dark">Nova 98 (dark)</label>
|
|
||||||
|
|
||||||
<h2>Hue</h2>
|
|
||||||
<p>
|
|
||||||
Many themes support custom hues. The hue does not change the
|
|
||||||
contrast of the theme, only the color itself.
|
|
||||||
</p>
|
|
||||||
<input type="radio" id="hue_-1" name="hue"><label for="hue_-1">Default</label><br/>
|
|
||||||
<input type="radio" id="hue_354" name="hue"><label for="hue_354">Red</label><br/>
|
|
||||||
<input type="radio" id="hue_14" name="hue"><label for="hue_14">Orange</label><br/>
|
|
||||||
<input type="radio" id="hue_40" name="hue"><label for="hue_40">Yellow</label><br/>
|
|
||||||
<input type="radio" id="hue_92" name="hue"><label for="hue_92">Green</label><br/>
|
|
||||||
<input type="radio" id="hue_311" name="hue"><label for="hue_311">Purple</label><br/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
@@ -3,6 +3,7 @@ import { preventDefault } from 'svelte/legacy';
|
|||||||
import { loading_finish, loading_start } from "lib/Loading";
|
import { loading_finish, loading_start } from "lib/Loading";
|
||||||
import { formatDate } from "util/Formatting";
|
import { formatDate } from "util/Formatting";
|
||||||
import CopyButton from 'layout/CopyButton.svelte';
|
import CopyButton from 'layout/CopyButton.svelte';
|
||||||
|
import NovaLogo from "util/NovaLogo.svelte";
|
||||||
|
|
||||||
let loaded = $state(false)
|
let loaded = $state(false)
|
||||||
let rows = $state([])
|
let rows = $state([])
|
||||||
@@ -147,7 +148,7 @@ const logout = async (key) => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="1">
|
<td colspan="1">
|
||||||
{#if row.app_name === "website login"}
|
{#if row.app_name === "website login"}
|
||||||
<img src="/res/img/nova_32.png" alt="Nova logo" class="app_icon"/>
|
<NovaLogo style="height: 1.6em; vertical-align: middle;"/>
|
||||||
Nova website
|
Nova website
|
||||||
{:else if row.app_name === "website keys page"}
|
{:else if row.app_name === "website keys page"}
|
||||||
<i class="icon">vpn_key</i>
|
<i class="icon">vpn_key</i>
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ let included = $derived($user === null ? 0 : $user.subscription.monthly_transfer
|
|||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
<p>
|
||||||
Upgrade your plan{#if billable} or enable
|
Upgrade your plan or enable
|
||||||
<a href="/user/sharing">usage based billing</a>{/if} to continue
|
<a href="/user/sharing">usage based billing</a> to continue
|
||||||
sharing files.
|
sharing files.
|
||||||
</p>
|
</p>
|
||||||
<a class="button button_highlight" href="/user/subscription/manage">
|
<a class="button button_highlight" href="/user/subscription/manage">
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ let billed = $derived(billable && $user.usage_based_billing)
|
|||||||
<p>
|
<p>
|
||||||
You are storing more than your plan allows, so you can't upload new
|
You are storing more than your plan allows, so you can't upload new
|
||||||
files. Your existing files stay available. Delete some files or
|
files. Your existing files stay available. Delete some files or
|
||||||
upgrade your plan to continue uploading{#if billable}, or enable
|
upgrade your plan to continue uploading, or enable
|
||||||
<a href="/user/sharing">usage based billing</a> to pay for the
|
<a href="/user/sharing">usage based billing</a> to pay for the
|
||||||
storage above the limit{/if}.
|
storage above the limit.
|
||||||
</p>
|
</p>
|
||||||
<a class="button button_highlight" href="/user/subscription/manage">
|
<a class="button button_highlight" href="/user/subscription/manage">
|
||||||
<i class="icon">bolt</i> Upgrade options
|
<i class="icon">bolt</i> Upgrade options
|
||||||
|
|||||||
@@ -84,6 +84,6 @@ onMount(async () => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.red {
|
.red {
|
||||||
background-color: rgba(255, 0, 0, 0.2);
|
background-color: var(--red_tint);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ onMount(() => {
|
|||||||
label: "Downloads",
|
label: "Downloads",
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: color_by_name("highlight_color"),
|
borderColor: color_by_name("accent_color"),
|
||||||
backgroundColor: color_by_name("highlight_color"),
|
backgroundColor: color_by_name("accent_color"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
graph_egress.data().datasets = [
|
graph_egress.data().datasets = [
|
||||||
@@ -96,8 +96,8 @@ onMount(() => {
|
|||||||
label: "Egress",
|
label: "Egress",
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: color_by_name("highlight_color"),
|
borderColor: color_by_name("accent_color"),
|
||||||
backgroundColor: color_by_name("highlight_color"),
|
backgroundColor: color_by_name("accent_color"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ let { symbol = "€", amount = 0, precision = 2 }: {
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
{symbol}
|
{symbol}
|
||||||
<span class:red={amount < 0} class:green={amount > 0}>
|
<span class:negative={amount < 0} class:positive={amount > 0}>
|
||||||
{(amount / 1000000).toLocaleString(undefined, {minimumFractionDigits: precision, maximumFractionDigits: precision})}
|
{(amount / 1000000).toLocaleString(undefined, {minimumFractionDigits: precision, maximumFractionDigits: precision})}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.red {
|
.negative {
|
||||||
color: var(--danger_color);
|
color: var(--red_color);
|
||||||
}
|
}
|
||||||
.green {
|
.positive {
|
||||||
color: var(--highlight_color);
|
color: var(--accent_color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,77 @@
|
|||||||
<script lang="ts">
|
<script lang="ts" module>
|
||||||
let { style = "" } = $props();
|
// The logo as designed, with the colours of one of its colourways. The bars
|
||||||
</script>
|
// fade to black, and the thin edges and the lettering use the text colour, so
|
||||||
<svg style={style} enable-background="new 0 0 283.46 283.46" version="1.1" viewBox="0 0 288 288" xmlns="http://www.w3.org/2000/svg">
|
// the logo works on dark and light themes
|
||||||
<path d="m142 2.23c-77 0-139 62.5-139 140 0 77 62.5 139 139 139 77 0 139-62.5 139-139 1e-3 -77-62.5-140-139-140zm0 258c-65.7 0-119-53.2-119-119s53.2-119 119-119c65.7 0 119 53.2 119 119 0 65.7-53.2 119-119 119zm0-219c-55.1 0-99.8 44.7-99.8 99.8 0 55.1 44.7 99.8 99.8 99.8s99.8-44.7 99.8-99.8c0-55.1-44.7-99.8-99.8-99.8zm49.3 36c8.69 0 15.7 7.04 15.7 15.7 0 8.69-7.04 15.7-15.7 15.7s-15.7-7.04-15.7-15.7c0-8.69 7.04-15.7 15.7-15.7zm-49.3-20c8.69 0 15.7 7.04 15.7 15.7 0 8.69-7.04 15.7-15.7 15.7s-15.7-7.04-15.7-15.7c0-8.69 7.04-15.7 15.7-15.7zm-48.7 20c8.69 0 15.7 7.04 15.7 15.7 0 8.69-7.04 15.7-15.7 15.7s-15.7-7.04-15.7-15.7c-1e-3 -8.69 7.04-15.7 15.7-15.7zm-35 63.8c0-8.69 7.04-15.7 15.7-15.7s15.7 7.04 15.7 15.7c0 8.69-7.04 15.7-15.7 15.7-8.69 0-15.7-7.04-15.7-15.7zm35 65.6c-8.69 0-15.7-7.04-15.7-15.7s7.04-15.7 15.7-15.7 15.7 7.04 15.7 15.7-7.04 15.7-15.7 15.7zm48.7 20.7c-8.69 0-15.7-7.04-15.7-15.7 0-8.69 7.04-15.7 15.7-15.7 8.69 0 15.7 7.04 15.7 15.7 1e-3 8.68-7.04 15.7-15.7 15.7zm2e-3 -47c-21.2 0-38.5-17.2-38.5-38.5 0-21.2 17.2-38.5 38.5-38.5 21.2 0 38.5 17.2 38.5 38.5 0 21.2-17.2 38.5-38.5 38.5zm49.3 26.3c-8.69 0-15.7-7.04-15.7-15.7s7.04-15.7 15.7-15.7 15.7 7.04 15.7 15.7-7.04 15.7-15.7 15.7zm18.6-49.9c-8.69 0-15.7-7.04-15.7-15.7 0-8.69 7.04-15.7 15.7-15.7s15.7 7.04 15.7 15.7c0 8.69-7.04 15.7-15.7 15.7z"/>
|
const markup = (id: string, colours: string[], full: boolean) => `
|
||||||
</svg>
|
<defs>
|
||||||
|
<linearGradient id="${id}bar" x1="27.2" y1="44.04" x2="27.2" y2="16.51" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:${colours[0]}"/>
|
||||||
|
<stop offset=".5" style="stop-color:${colours[1]}"/>
|
||||||
|
<stop offset="1" style="stop-color:#000"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${id}bar2" x1="50.8" y1="13.18" x2="50.8" y2="40.72" href="#${id}bar"/>
|
||||||
|
<linearGradient id="${id}diagonal" x1="24.86" y1="28.61" x2="53.14" y2="28.61" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:${colours[2]}"/>
|
||||||
|
<stop offset="1" style="stop-color:${colours[3]}"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${id}edge" x1="27.2" y1="16.42" x2="27.2" y2="44.13" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:currentColor;stop-opacity:0"/>
|
||||||
|
<stop offset="1" style="stop-color:currentColor"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="${id}edge2" x1="50.8" y1="40.81" x2="50.8" y2="13.09" href="#${id}edge"/>
|
||||||
|
<linearGradient id="${id}edge3" x1="24.77" y1="28.61" x2="53.23" y2="28.61" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:currentColor;stop-opacity:0"/>
|
||||||
|
<stop offset=".54" style="stop-color:currentColor"/>
|
||||||
|
<stop offset="1" style="stop-color:currentColor;stop-opacity:0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g stroke-width=".18" stroke-miterlimit="10">
|
||||||
|
<path fill="url(#${id}bar)" stroke="url(#${id}edge)" d="M27.96,16.75c-2.08,0-2.93,1.83-3.11,3v21.81c0,1.37,1.05,2.48,2.34,2.48s2.34-1.11,2.34-2.48v-24.76c-.58-.63-.72-.05-1.57-.05Z"/>
|
||||||
|
<path fill="url(#${id}bar2)" stroke="url(#${id}edge2)" d="M50.03,40.48c2.08,0,2.93-1.83,3.11-3v-.54s0-21.28,0-21.28c0-1.37-1.05-2.48-2.34-2.48s-2.34,1.11-2.34,2.48v20.98s0,2.54,0,2.54v1.24c.58.63.72.05,1.57.05Z"/>
|
||||||
|
<path fill="url(#${id}diagonal)" stroke="url(#${id}edge3)" d="M50.03,40.49c-.71,0-1.33-.32-1.86-.77-.18-.15-.33-.33-.45-.53l-1-1.66-12.34-21.84c-.8-1.43-2.19-2.5-3.83-2.5-.34,0-.68.05-1.01.13-2.49.63-4.68,3.44-4.68,5.96h0v.29c.18-1.1,1.03-2.83,3.11-2.83.71,0,1.33.32,1.86.77.18.15.33.33.45.53l1,1.66,12.34,21.84c.8,1.43,2.19,2.5,3.83,2.5.34,0,.68-.05,1.01-.13,2.49-.63,4.68-3.44,4.68-5.96h0v-.29c-.18,1.1-1.03,2.83-3.11,2.83Z"/>
|
||||||
|
</g>
|
||||||
|
${full ? lettering : ""}`
|
||||||
|
|
||||||
<style>
|
const lettering = `
|
||||||
svg {
|
<g fill="currentColor">
|
||||||
fill: currentColor;
|
<path d="M57.91,33.07c0-6.24,4.33-11.02,11.5-11.02s11.5,4.78,11.5,11.02-4.33,11.02-11.5,11.02-11.5-4.78-11.5-11.02ZM76.29,33.07c0-3.54-2.24-6.84-6.84-6.84s-6.87,3.3-6.87,6.84,2.24,6.84,6.87,6.84,6.84-3.3,6.84-6.84Z"/>
|
||||||
animation: rotation 20s infinite linear;
|
<path d="M81.81,22.48h4.71c.09,0,.17.06.2.14l5.52,14.84c.08.22.4.22.48,0l5.55-14.84c.03-.08.11-.14.2-.14h4.65c.15,0,.26.15.2.3l-7.31,18.47c-.58,1.46-1.99,2.42-3.56,2.42h0c-1.57,0-2.98-.96-3.56-2.42l-7.28-18.47c-.06-.14.05-.3.2-.3Z"/>
|
||||||
|
<path d="M116.86,24.92c-.67-1.49-2.15-2.44-3.78-2.44s-3.11.96-3.78,2.44l-8.31,18.46c-.06.13.04.28.18.28h4.55c.08,0,.16-.05.19-.12l6.62-16.01c.18-.42.78-.42.95,0l3.25,7.81h0l3.4,8.2c.03.08.1.12.19.12h4.67c.15,0,.24-.15.18-.28l-8.31-18.46Z"/>
|
||||||
|
<polygon points="112.36 35.47 113.08 33.53 113.8 35.47 115.74 36.18 113.8 36.9 113.08 38.84 112.36 36.9 110.43 36.18 112.36 35.47"/>
|
||||||
|
<path d="M31.22,55.07c0,1.29-.53,1.75-1.97,1.75h-4.28c.02-.4.05-.79.08-1.19h4.12c.64,0,.83-.15.83-.67s-.18-.67-.8-.67h-2.46c-1.45,0-1.92-.52-1.92-1.63v-.2c0-1.21.48-1.7,1.96-1.7h4.25c-.04.41-.1.8-.15,1.21h-4.1c-.58,0-.75.19-.75.66s.17.67.75.67h2.42c1.51,0,2.03.49,2.03,1.56v.21Z"/>
|
||||||
|
<path d="M42.35,56.82v-4.86h-2.21v-1.2h5.61v1.2h-2.21v4.86h-1.19Z"/>
|
||||||
|
<path d="M56.73,56.82c-1.47,0-1.89-.48-1.89-1.97v-2.13c0-1.49.42-1.97,1.89-1.97h3.2c1.44,0,1.89.48,1.89,1.97v2.13c0,1.49-.45,1.97-1.89,1.97h-3.2ZM60.63,52.73c0-.6-.19-.77-.71-.77h-3.19c-.53,0-.71.18-.71.77v2.13c0,.6.17.77.71.77h3.19c.55,0,.71-.17.71-.77v-2.13Z"/>
|
||||||
|
<path d="M76.43,56.82l-2.1-1.98h-1.91v1.98h-1.2v-6.06h4.95c1.29,0,1.74.5,1.74,1.88v.37c0,1.48-.56,1.82-2.05,1.82l2.25,2h-1.67ZM76.73,52.84c0-.59-.07-.87-.59-.87h-3.72v1.88h3.72c.42,0,.59-.19.59-.71v-.29Z"/>
|
||||||
|
<path d="M93.29,56.82l-.76-1.41h-3.66l-.76,1.41h-1.36l3.32-6.06h1.24l3.32,6.06h-1.35ZM90.71,51.95l-1.38,2.51h2.75l-1.37-2.51Z"/>
|
||||||
|
<path d="M108.94,56.82l-.04-.74c-.08.35-.48.74-1.16.74h-2.57c-1.5,0-1.88-.45-1.88-1.97v-2.13c0-1.45.39-1.97,1.88-1.97h3.08c.89,0,1.31.55,1.45.89l-.75.78c-.11-.24-.31-.47-.78-.47h-2.99c-.55,0-.71.17-.71.77v1.96c0,.84.15.95.71.95h2.76c.67,0,.89-.2.89-.57v-.78h-2.27v-.99h3.39v3.53h-1Z"/>
|
||||||
|
<path d="M119.33,56.82v-6.06h5.82v1.2h-4.64v1.34h4.51v.98h-4.51v1.36h4.64v1.19h-5.82Z"/>
|
||||||
|
</g>`
|
||||||
|
|
||||||
|
// The mark on its own is square, the full logo has the lettering next to and
|
||||||
|
// below it
|
||||||
|
const view_box = (full: boolean) => full ? "24.6 12.9 100.8 44.2" : "23.47 13.09 31.04 31.04"
|
||||||
|
|
||||||
|
// A favicon is drawn as a separate image, so it can't use the theme's CSS
|
||||||
|
// variables. This gives it the colours of the current theme instead
|
||||||
|
export const update_favicon = () => {
|
||||||
|
const style = getComputedStyle(document.body)
|
||||||
|
const colours = [1, 2, 3, 4].map(i => style.getPropertyValue("--logo_color_" + i))
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view_box(false)}">${markup("f", colours, false)}</svg>`
|
||||||
|
document.querySelector<HTMLLinkElement>("link[type='image/svg+xml']").href = "data:image/svg+xml," + encodeURIComponent(svg)
|
||||||
}
|
}
|
||||||
@keyframes rotation {
|
</script>
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
<script lang="ts">
|
||||||
}
|
let { full = false, style = "" }: {
|
||||||
to {
|
full?: boolean
|
||||||
transform: rotate(359deg);
|
style?: string
|
||||||
}
|
} = $props()
|
||||||
}
|
|
||||||
</style>
|
// Gradient IDs have to be unique in the page, and the logo can be on the page
|
||||||
|
// more than once
|
||||||
|
const id = $props.id()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg viewBox={view_box(full)} style={style} role="img" aria-label="Nova.storage">
|
||||||
|
{@html markup(id, ["var(--logo_color_1)", "var(--logo_color_2)", "var(--logo_color_3)", "var(--logo_color_4)"], full)}
|
||||||
|
</svg>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ $effect(() => {
|
|||||||
<div
|
<div
|
||||||
class="progress_bar_inner"
|
class="progress_bar_inner"
|
||||||
class:no_animation
|
class:no_animation
|
||||||
|
class:yellow={percent > 80}
|
||||||
class:red={percent > 90}
|
class:red={percent > 90}
|
||||||
style="width: {percent}%; transition-timing-function: {animation}; transition-duration: {speed}ms;">
|
style="width: {percent}%; transition-timing-function: {animation}; transition-duration: {speed}ms;">
|
||||||
</div>
|
</div>
|
||||||
@@ -53,14 +54,17 @@ $effect(() => {
|
|||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
.progress_bar_inner {
|
.progress_bar_inner {
|
||||||
background: var(--highlight_background);
|
background: var(--accent_background);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0;
|
width: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
}
|
}
|
||||||
|
.yellow {
|
||||||
|
background: var(--yellow_color);
|
||||||
|
}
|
||||||
.red {
|
.red {
|
||||||
background: var(--danger_color);
|
background: var(--red_color);
|
||||||
}
|
}
|
||||||
.no_animation {
|
.no_animation {
|
||||||
transition-property: none;
|
transition-property: none;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
color: var(--highlight_color);
|
color: var(--accent_color);
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -125,7 +125,4 @@ header, .submenu {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
.button_highlight {
|
|
||||||
background: var(--body_background);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ export function domain_url() {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Theme colours can be functions like light-dark(), which only a real colour
|
||||||
|
// property can resolve. The body can have a shared directory's branding
|
||||||
export const color_by_name = (name: string) => {
|
export const color_by_name = (name: string) => {
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue("--" + name);
|
const probe = document.createElement("span")
|
||||||
}
|
probe.style.color = "var(--" + name + ")"
|
||||||
export const color_by_name_no_prefix = (name: string) => {
|
document.body.append(probe)
|
||||||
return color_by_name(name).replace(/^#/, "");
|
const color = getComputedStyle(probe).color
|
||||||
|
probe.remove()
|
||||||
|
return color
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const drop = (e: DragEvent, drop_idx: number) => {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.highlight {
|
.highlight {
|
||||||
box-shadow: 0 0 0px 1px var(--highlight_color);
|
box-shadow: 0 0 0px 1px var(--accent_color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import MenuEntry from "./MenuEntry.svelte";
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { breadcrumbs_store } from "./BreadcrumbStore";
|
import { breadcrumbs_store } from "./BreadcrumbStore";
|
||||||
import ProgressBar from "util/ProgressBar.svelte";
|
import ProgressBar from "util/ProgressBar.svelte";
|
||||||
|
import NovaLogo, { update_favicon } from "util/NovaLogo.svelte";
|
||||||
|
|
||||||
// The menu swipe will be detected if it was less than this much pixels from the
|
// The menu swipe will be detected if it was less than this much pixels from the
|
||||||
// screen edge
|
// screen edge
|
||||||
@@ -38,13 +39,24 @@ onMount(async () => {
|
|||||||
menu_close()
|
menu_close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The page template already applied the branding saved on the previous
|
||||||
|
// visit, the favicon follows it
|
||||||
|
update_favicon()
|
||||||
|
|
||||||
await loading_run(async () => {
|
await loading_run(async () => {
|
||||||
const user = await get_user()
|
const user = await get_user()
|
||||||
if (user.username === undefined || user.username === "") {
|
if (!user?.username) {
|
||||||
|
// Forget the branding of a user who logged out
|
||||||
|
localStorage.removeItem("branding")
|
||||||
|
document.documentElement.style = ""
|
||||||
|
update_favicon()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const root = await fs_get_node("/me")
|
const root = await fs_get_node("/me")
|
||||||
document.documentElement.style = css_from_path(root.path)
|
const branding = css_from_path(root.path)
|
||||||
|
document.documentElement.style = branding
|
||||||
|
localStorage.setItem("branding", branding)
|
||||||
|
update_favicon()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -166,7 +178,7 @@ const set_offset = (off: number) => {
|
|||||||
|
|
||||||
<a class="button" style="flex: 1 1 auto;" href="/" use:highlight_current_page>
|
<a class="button" style="flex: 1 1 auto;" href="/" use:highlight_current_page>
|
||||||
<span style="flex: 1 1 auto;">Nova.storage</span>
|
<span style="flex: 1 1 auto;">Nova.storage</span>
|
||||||
<img src="/res/img/nova_64.png" style="width: 1.5em; height: 1.5em; flex: 0 0 auto;" alt="Nova.storage logo">
|
<NovaLogo style="width: 1.5em; height: 1.5em; flex: 0 0 auto;"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if $user !== null}
|
{#if $user !== null}
|
||||||
@@ -252,10 +264,6 @@ const set_offset = (off: number) => {
|
|||||||
<i class="icon">speed</i>
|
<i class="icon">speed</i>
|
||||||
<span>Speedtest</span>
|
<span>Speedtest</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="button" href="/appearance" use:highlight_current_page>
|
|
||||||
<i class="icon">palette</i>
|
|
||||||
<span>Themes</span>
|
|
||||||
</a>
|
|
||||||
<a class="button" href="https://docs.nova.storage" use:highlight_current_page>
|
<a class="button" href="https://docs.nova.storage" use:highlight_current_page>
|
||||||
<i class="icon">text_snippet</i>
|
<i class="icon">text_snippet</i>
|
||||||
<span>Documentation</span>
|
<span>Documentation</span>
|
||||||
@@ -299,7 +307,8 @@ const set_offset = (off: number) => {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
color: var(--body_text_color);
|
color: var(--body_text_color);
|
||||||
background-image: var(--background_image);
|
/* Without a branding image there's a faint glow in the accent colour */
|
||||||
|
background-image: var(--background_image, radial-gradient(ellipse 70% 60% at 15% 0%, color-mix(in srgb, var(--accent_color) 7%, transparent), transparent));
|
||||||
background-color: var(--background_color);
|
background-color: var(--background_color);
|
||||||
background-size: var(--background_image_size, initial);
|
background-size: var(--background_image_size, initial);
|
||||||
background-position: var(--background_image_position, initial);
|
background-position: var(--background_image_position, initial);
|
||||||
|
|||||||
@@ -1,58 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick, type Component } from "svelte";
|
||||||
import HomePage from "home_page/HomePage.svelte";
|
|
||||||
import LoginRouter from "login/Router.svelte";
|
|
||||||
import HomeRouter from "user_home/Router.svelte";
|
|
||||||
import AdminRouter from "admin_panel/Router.svelte";
|
|
||||||
import Filesystem from "filesystem/Filesystem.svelte";
|
|
||||||
import NotFound from "./NotFound.svelte";
|
import NotFound from "./NotFound.svelte";
|
||||||
import SpeedtestPage from "speedtest/SpeedtestPage.svelte";
|
|
||||||
import Appearance from "pages/Appearance.svelte";
|
|
||||||
import Footer from "layout/Footer.svelte";
|
import Footer from "layout/Footer.svelte";
|
||||||
import { current_page_store, type Tab } from "./RouterStore";
|
import { current_page_store, type Tab } from "./RouterStore";
|
||||||
import { get_user, type User } from "lib/NovaAPI";
|
import { get_user, type User } from "lib/NovaAPI";
|
||||||
import { breadcrumbs_store } from "./BreadcrumbStore";
|
import { breadcrumbs_store } from "./BreadcrumbStore";
|
||||||
|
import { loading_run } from "lib/Loading";
|
||||||
|
|
||||||
|
// Pages are downloaded when they are opened for the first time. Someone who
|
||||||
|
// opens a shared file doesn't have to download the dashboard and admin panel
|
||||||
let pages: Tab[] = [
|
let pages: Tab[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
title: "Home",
|
title: "Home",
|
||||||
component: HomePage,
|
component: () => import("home_page/HomePage.svelte"),
|
||||||
}, {
|
}, {
|
||||||
path: "/login",
|
path: "/login",
|
||||||
title: "Login",
|
title: "Login",
|
||||||
component: LoginRouter,
|
component: () => import("login/Router.svelte"),
|
||||||
}, {
|
}, {
|
||||||
path: "/register",
|
path: "/register",
|
||||||
title: "Register",
|
title: "Register",
|
||||||
component: LoginRouter,
|
component: () => import("login/Router.svelte"),
|
||||||
}, {
|
}, {
|
||||||
path: "/user",
|
path: "/user",
|
||||||
prefix: "/user/",
|
prefix: "/user/",
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
component: HomeRouter,
|
component: () => import("user_home/Router.svelte"),
|
||||||
login: true,
|
login: true,
|
||||||
}, {
|
}, {
|
||||||
path: "/d/me",
|
path: "/d/me",
|
||||||
prefix: "/d/",
|
prefix: "/d/",
|
||||||
title: "Filesystem",
|
title: "Filesystem",
|
||||||
component: Filesystem,
|
component: () => import("filesystem/Filesystem.svelte"),
|
||||||
footer: false,
|
footer: false,
|
||||||
custom_breadcrumbs: true,
|
custom_breadcrumbs: true,
|
||||||
}, {
|
}, {
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
prefix: "/admin/",
|
prefix: "/admin/",
|
||||||
title: "Admin Panel",
|
title: "Admin Panel",
|
||||||
component: AdminRouter,
|
component: () => import("admin_panel/Router.svelte"),
|
||||||
login: true,
|
login: true,
|
||||||
}, {
|
}, {
|
||||||
path: "/speedtest",
|
path: "/speedtest",
|
||||||
title: "Speedtest",
|
title: "Speedtest",
|
||||||
component: SpeedtestPage,
|
component: () => import("speedtest/SpeedtestPage.svelte"),
|
||||||
}, {
|
|
||||||
path: "/appearance",
|
|
||||||
title: "Appearance",
|
|
||||||
component: Appearance,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -61,6 +53,12 @@ onMount(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let current_page: Tab = $state(null)
|
let current_page: Tab = $state(null)
|
||||||
|
let Page: Component = $state(null)
|
||||||
|
|
||||||
|
// Every navigation gets a number. A navigation which is still loading when a
|
||||||
|
// newer one starts is abandoned, else it would replace the newer page when it
|
||||||
|
// finishes
|
||||||
|
let navigation_id = 0
|
||||||
|
|
||||||
// find_page resolves a URL path to the page which renders it. It returns null
|
// find_page resolves a URL path to the page which renders it. It returns null
|
||||||
// when no page matches, in which case the browser has to handle the navigation
|
// when no page matches, in which case the browser has to handle the navigation
|
||||||
@@ -80,7 +78,8 @@ const find_page = (pathname: string): Tab => {
|
|||||||
|
|
||||||
// href can be a full URL or a path, and may contain a query string and hash.
|
// href can be a full URL or a path, and may contain a query string and hash.
|
||||||
// Those are not used for routing, but they do need to survive the navigation
|
// Those are not used for routing, but they do need to survive the navigation
|
||||||
const load_page = async (href: string, history: boolean): Promise<boolean> => {
|
const load_page = async (href: string, history: boolean) => {
|
||||||
|
const id = ++navigation_id
|
||||||
const url = new URL(href, window.location.href)
|
const url = new URL(href, window.location.href)
|
||||||
console.debug(
|
console.debug(
|
||||||
"Page router: Navigating to page", url.pathname,
|
"Page router: Navigating to page", url.pathname,
|
||||||
@@ -93,11 +92,14 @@ const load_page = async (href: string, history: boolean): Promise<boolean> => {
|
|||||||
current_page = {
|
current_page = {
|
||||||
path: "",
|
path: "",
|
||||||
title: "Not Found",
|
title: "Not Found",
|
||||||
component: NotFound,
|
|
||||||
}
|
}
|
||||||
return false
|
Page = NotFound
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download the page while we check the login
|
||||||
|
const component = loading_run(page.component)
|
||||||
|
|
||||||
let user: User
|
let user: User
|
||||||
try {
|
try {
|
||||||
user = await get_user()
|
user = await get_user()
|
||||||
@@ -116,6 +118,22 @@ const load_page = async (href: string, history: boolean): Promise<boolean> => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let new_page: Component
|
||||||
|
try {
|
||||||
|
new_page = (await component).default
|
||||||
|
} catch (err) {
|
||||||
|
// An update deletes the code of the previous version from the server.
|
||||||
|
// Tabs which were opened before the update need a full page load
|
||||||
|
if (current_page === null) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
window.location.assign(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (id !== navigation_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
window.document.title = page.title+" / Nova"
|
window.document.title = page.title+" / Nova"
|
||||||
|
|
||||||
// The address bar has to be updated before the page is mounted. Pages read
|
// The address bar has to be updated before the page is mounted. Pages read
|
||||||
@@ -128,6 +146,7 @@ const load_page = async (href: string, history: boolean): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
current_page = page
|
current_page = page
|
||||||
|
Page = new_page
|
||||||
|
|
||||||
if (current_page.custom_breadcrumbs !== true) {
|
if (current_page.custom_breadcrumbs !== true) {
|
||||||
breadcrumbs_store.set(breadcrumbs)
|
breadcrumbs_store.set(breadcrumbs)
|
||||||
@@ -140,8 +159,6 @@ const load_page = async (href: string, history: boolean): Promise<boolean> => {
|
|||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
current_page_store.set(current_page)
|
current_page_store.set(current_page)
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const click = (e: MouseEvent) => {
|
const click = (e: MouseEvent) => {
|
||||||
@@ -164,24 +181,23 @@ const click = (e: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Other hosts, and protocols like mailto: and magnet:, have no host match
|
// Other hosts, and protocols like mailto: and magnet:, have no host match
|
||||||
const url = URL.parse(origin.href)
|
if (origin.host !== window.location.host) {
|
||||||
if (url === null || url.host !== window.location.host) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-page anchors are left to the browser, it knows how to scroll to them
|
// In-page anchors are left to the browser, it knows how to scroll to them
|
||||||
if (url.pathname === window.location.pathname && url.hash !== "") {
|
if (origin.pathname === window.location.pathname && origin.hash !== "") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paths without a page of their own are served by the backend, like the API
|
// Paths without a page of their own are served by the backend, like the API
|
||||||
// and file downloads. Loading them here would replace the page the user is
|
// and file downloads. Loading them here would replace the page the user is
|
||||||
// looking at with a Not Found error while the browser navigates away
|
// looking at with a Not Found error while the browser navigates away
|
||||||
if (find_page(url.pathname) === null) {
|
if (find_page(origin.pathname) === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Caught link click to", url.pathname)
|
console.log("Caught link click to", origin.pathname)
|
||||||
|
|
||||||
// The event has to be cancelled before the first await, after that the
|
// The event has to be cancelled before the first await, after that the
|
||||||
// browser has already committed to the navigation
|
// browser has already committed to the navigation
|
||||||
@@ -202,7 +218,7 @@ const popstate = (e: PopStateEvent) => {
|
|||||||
<svelte:window onpopstate={popstate}/>
|
<svelte:window onpopstate={popstate}/>
|
||||||
|
|
||||||
{#if current_page !== null}
|
{#if current_page !== null}
|
||||||
<current_page.component />
|
<Page />
|
||||||
|
|
||||||
{#if current_page.footer !== false}
|
{#if current_page.footer !== false}
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type Tab = {
|
|||||||
path: string,
|
path: string,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
title: string,
|
title: string,
|
||||||
component?: Component,
|
component?: () => Promise<{ default: Component }>,
|
||||||
footer?: boolean,
|
footer?: boolean,
|
||||||
login?: boolean,
|
login?: boolean,
|
||||||
custom_breadcrumbs?: boolean,
|
custom_breadcrumbs?: boolean,
|
||||||
|
|||||||
@@ -1,68 +1,47 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
import { sveltePreprocess } from 'svelte-preprocess';
|
|
||||||
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import livereload from 'rollup-plugin-livereload';
|
import livereload from 'rollup-plugin-livereload';
|
||||||
import terser from '@rollup/plugin-terser';
|
|
||||||
import babel from '@rollup/plugin-babel'
|
|
||||||
import typescript from '@rollup/plugin-typescript'
|
|
||||||
|
|
||||||
const production = process.env.NODE_ENV === "production"
|
const production = process.env.NODE_ENV === "production"
|
||||||
console.log("Production mode:", production)
|
console.log("Production mode:", production)
|
||||||
|
|
||||||
const builddir = "../res/static/svelte"
|
const builddir = "../res/static/svelte"
|
||||||
const name = "wrap"
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
mode: production ? "production" : "development",
|
mode: production ? "production" : "development",
|
||||||
|
// Chunks are loaded relative to the script which imports them
|
||||||
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
outDir: builddir,
|
outDir: builddir,
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
minify: production,
|
minify: production,
|
||||||
sourcemap: !production,
|
sourcemap: !production,
|
||||||
lib: {
|
// Compile newer syntax down to what browsers from September 2021 support
|
||||||
entry: "src/wrap.js",
|
target: ["chrome94", "edge94", "firefox91", "safari15"],
|
||||||
name: "nova_web",
|
rollupOptions: {
|
||||||
fileName: name,
|
input: "src/wrap.js",
|
||||||
}
|
// The entrypoint keeps its name so the HTML template can find it,
|
||||||
|
// the chunks it imports get a hash in their names
|
||||||
|
output: { entryFileNames: "wrap.js" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
svelte({
|
// Svelte can't strip TypeScript enums by itself
|
||||||
preprocess: sveltePreprocess(),
|
svelte({ preprocess: vitePreprocess({ script: true }), emitCss: false }),
|
||||||
compilerOptions: {},
|
|
||||||
emitCss: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
|
// Resolves imports relative to the src directory, like "lib/NovaAPI"
|
||||||
babel({
|
|
||||||
extensions: [".js", ".ts", ".svelte"],
|
|
||||||
babelHelpers: "bundled",
|
|
||||||
}),
|
|
||||||
|
|
||||||
// If you have external dependencies installed from npm, you'll most
|
|
||||||
// likely need these plugins. In some cases you'll need additional
|
|
||||||
// configuration - consult the documentation for details:
|
|
||||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
exportConditions: ['svelte'],
|
exportConditions: ['svelte'],
|
||||||
modulePaths: [process.cwd() + "/src", process.cwd() + "/node_modules"],
|
modulePaths: [process.cwd() + "/src", process.cwd() + "/node_modules"],
|
||||||
extensions: [".svelte", ".mjs", ".js", ".json", ".mts", ".ts"],
|
extensions: [".svelte", ".mjs", ".js", ".json", ".mts", ".ts"],
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
|
||||||
typescript(),
|
|
||||||
|
|
||||||
// Watch the `public` directory and refresh the browser on changes when
|
// Refresh the browser when the build changes
|
||||||
// not in production
|
|
||||||
!production && livereload({
|
!production && livereload({
|
||||||
watch: `${builddir}/${name}.*`,
|
watch: builddir,
|
||||||
port: 5000,
|
port: 5000,
|
||||||
}),
|
}),
|
||||||
|
],
|
||||||
// If we're building for production (npm run build instead of npm run
|
|
||||||
// dev), minify
|
|
||||||
production && terser(),
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|||||||
8
web.go
@@ -24,7 +24,6 @@ api_url_internal = "https://nova.storage/api"
|
|||||||
# prefix will be derived from there
|
# prefix will be derived from there
|
||||||
api_socket_path = ""
|
api_socket_path = ""
|
||||||
|
|
||||||
session_cookie_domain = ""
|
|
||||||
resource_dir = "res"
|
resource_dir = "res"
|
||||||
|
|
||||||
# Parse all the templates every time a request comes in
|
# Parse all the templates every time a request comes in
|
||||||
@@ -33,13 +32,10 @@ debug_mode = true
|
|||||||
# Create proxy listeners to forward all requests made to /api to
|
# Create proxy listeners to forward all requests made to /api to
|
||||||
# api_url_internal
|
# api_url_internal
|
||||||
proxy_api_requests = true
|
proxy_api_requests = true
|
||||||
|
|
||||||
# When this is true every request will return a maintainance HTML page
|
|
||||||
maintenance_mode = false
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// Init initializes the Nova Web UI controllers
|
// Init initializes the Nova Web UI controllers
|
||||||
func Init(r *httprouter.Router, prefix string, setLogLevel bool) {
|
func Init(r *httprouter.Router, setLogLevel bool) {
|
||||||
log.Colours = true
|
log.Colours = true
|
||||||
log.Info("Starting web UI server (PID %v)", os.Getpid())
|
log.Info("Starting web UI server (PID %v)", os.Getpid())
|
||||||
|
|
||||||
@@ -60,5 +56,5 @@ func Init(r *httprouter.Router, prefix string, setLogLevel bool) {
|
|||||||
log.SetLogLevel(log.LevelInfo)
|
log.SetLogLevel(log.LevelInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
webcontroller.New(r, prefix, conf)
|
webcontroller.New(r, conf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,63 @@
|
|||||||
package webcontroller
|
package webcontroller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"fornaxian.tech/log"
|
"fornaxian.tech/log"
|
||||||
|
"fornaxian.tech/nova_server/api"
|
||||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||||
"fornaxian.tech/util"
|
"fornaxian.tech/util"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
var err error
|
|
||||||
var td = wc.newTemplateData(r)
|
var td = wc.newTemplateData(r)
|
||||||
var path = strings.TrimPrefix(p.ByName("path"), "/")
|
var pdapi = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
||||||
|
|
||||||
// Prevent search engines from indexing this page for privacy reasons
|
// Prevent search engines from indexing this page for privacy reasons
|
||||||
w.Header().Set("X-Robots-Tag", "noindex, nofollow")
|
w.Header().Set("X-Robots-Tag", "noindex, nofollow")
|
||||||
|
|
||||||
if path == "" {
|
// The user and the node are embedded in the page so the JS doesn't have to
|
||||||
wc.templates.Run(w, r, "404", td)
|
// request them again. When a request fails the JS requests it again and
|
||||||
return
|
// shows the error
|
||||||
}
|
|
||||||
|
|
||||||
var pdapi = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
|
||||||
|
|
||||||
if apikey, err := wc.getAPIKey(r); err == nil {
|
if apikey, err := wc.getAPIKey(r); err == nil {
|
||||||
pdapi = pdapi.Login(apikey)
|
pdapi = pdapi.Login(apikey)
|
||||||
|
_ = pdapi.GetJSON("user", &td.User)
|
||||||
|
} else {
|
||||||
|
td.User = json.RawMessage("null")
|
||||||
}
|
}
|
||||||
|
|
||||||
node, err := pdapi.GetFilesystemPath(path)
|
var path = strings.TrimPrefix(p.ByName("path"), "/")
|
||||||
if err != nil {
|
var err = pdapi.GetJSON("filesystem/"+url.PathEscape(path)+"?stat", &td.Node)
|
||||||
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
|
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
|
||||||
// Set the proper response code for the error message
|
// Set the proper response code for the error message
|
||||||
switch apiErr.StatusCode {
|
switch apiErr.StatusCode {
|
||||||
case "not_found", "path_not_found":
|
case "not_found", "path_not_found":
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
case "forbidden", "permission_denied":
|
case "forbidden", "permission_denied":
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
case "unavailable_for_legal_reasons":
|
case "unavailable_for_legal_reasons":
|
||||||
w.WriteHeader(http.StatusUnavailableForLegalReasons)
|
w.WriteHeader(http.StatusUnavailableForLegalReasons)
|
||||||
case "authentication_required":
|
case "authentication_required":
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// Let the JS figure it out
|
|
||||||
if err = wc.templates.Run(w, r, "wrap", td); err != nil {
|
|
||||||
log.Error("Failed to run template: %s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Error("Failed to get path: %s", err)
|
|
||||||
wc.templates.Run(w, r, "500", td)
|
|
||||||
}
|
}
|
||||||
return
|
} else if err != nil {
|
||||||
|
log.Error("Failed to get path: %s", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
} else {
|
||||||
|
var node api.FilesystemPath
|
||||||
|
if err = json.Unmarshal(td.Node, &node); err != nil {
|
||||||
|
panic(fmt.Errorf("decode filesystem path: %w", err))
|
||||||
|
}
|
||||||
|
td.Title = fmt.Sprintf("%s ~ Nova", node.Path[node.BaseIndex].Name)
|
||||||
|
td.OGData = wc.metadataFromFilesystem(r, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
td.Title = fmt.Sprintf("%s ~ Nova", node.Path[node.BaseIndex].Name)
|
wc.renderPage(w, r, td)
|
||||||
td.Other = node
|
|
||||||
td.OGData = wc.metadataFromFilesystem(r, node)
|
|
||||||
err = wc.templates.Run(w, r, "wrap", td)
|
|
||||||
if err != nil && !util.IsNetError(err) {
|
|
||||||
log.Error("Error executing template filesystem: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
"fornaxian.tech/nova_server/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultThemeColour = "#220735"
|
const defaultThemeColour = "#49dfa0"
|
||||||
const defaultHost = "https://nova.storage"
|
const defaultHost = "https://nova.storage"
|
||||||
|
const siteDescription = "Cost-effective cloud storage and content delivery"
|
||||||
|
|
||||||
type ogData struct {
|
type ogData struct {
|
||||||
MetaPropRules []ogProp
|
MetaPropRules []ogProp
|
||||||
@@ -78,6 +79,22 @@ func generateOGData(name, filetype, pageurl, fileurl, thumbnailurl, themecolour
|
|||||||
return og
|
return og
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// siteOGData describes the site itself, for link previews of pages which are
|
||||||
|
// not a shared file
|
||||||
|
func siteOGData(r *http.Request) (og ogData) {
|
||||||
|
var addr = getRequestAddress(r)
|
||||||
|
og.addProp("og:type", "website")
|
||||||
|
og.addProp("og:site_name", "Nova.storage")
|
||||||
|
og.addProp("og:title", "Nova.storage")
|
||||||
|
og.addProp("og:description", siteDescription)
|
||||||
|
og.addProp("og:url", addr+r.URL.Path)
|
||||||
|
og.addProp("og:image", addr+"/res/img/nova_512.png")
|
||||||
|
og.addName("description", siteDescription)
|
||||||
|
og.addName("theme-color", defaultThemeColour)
|
||||||
|
og.addName("twitter:card", "summary")
|
||||||
|
return og
|
||||||
|
}
|
||||||
|
|
||||||
func getRequestAddress(r *http.Request) (addr string) {
|
func getRequestAddress(r *http.Request) (addr string) {
|
||||||
if r.Host == "" {
|
if r.Host == "" {
|
||||||
return defaultHost
|
return defaultHost
|
||||||
@@ -87,7 +104,7 @@ func getRequestAddress(r *http.Request) (addr string) {
|
|||||||
return "https://" + r.Host
|
return "https://" + r.Host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (wc *WebController) metadataFromFilesystem(r *http.Request, f pixelapi.FilesystemPath) (og ogData) {
|
func (wc *WebController) metadataFromFilesystem(r *http.Request, f api.FilesystemPath) (og ogData) {
|
||||||
var addr = getRequestAddress(r)
|
var addr = getRequestAddress(r)
|
||||||
var base = &f.Path[f.BaseIndex]
|
var base = &f.Path[f.BaseIndex]
|
||||||
|
|
||||||
@@ -102,8 +119,8 @@ func (wc *WebController) metadataFromFilesystem(r *http.Request, f pixelapi.File
|
|||||||
var colour = defaultThemeColour
|
var colour = defaultThemeColour
|
||||||
for _, node := range f.Path {
|
for _, node := range f.Path {
|
||||||
if node.Properties["branding_enabled"] == "true" &&
|
if node.Properties["branding_enabled"] == "true" &&
|
||||||
node.Properties["brand_highlight_color"] != "" {
|
node.Properties["brand_accent_color"] != "" {
|
||||||
colour = node.Properties["brand_highlight_color"]
|
colour = node.Properties["brand_accent_color"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,733 +0,0 @@
|
|||||||
package webcontroller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (wc *WebController) themeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
w.Header().Set("Content-Type", "text/css")
|
|
||||||
w.Write([]byte(userStyleFromRequest(r)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func userStyleFromRequest(r *http.Request) (s template.CSS) {
|
|
||||||
// Get the chosen style from the URL
|
|
||||||
var style = r.URL.Query().Get("style")
|
|
||||||
var hue = -1
|
|
||||||
|
|
||||||
if hueStr := r.URL.Query().Get("hue"); hueStr != "" {
|
|
||||||
hue, _ = strconv.Atoi(hueStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the URL style was empty use the cookie value
|
|
||||||
if style == "" {
|
|
||||||
if cookie, err := r.Cookie("style"); err == nil {
|
|
||||||
style = cookie.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hue == -1 {
|
|
||||||
if cookie, err := r.Cookie("hue"); err == nil {
|
|
||||||
hue, _ = strconv.Atoi(cookie.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userStyle(style, hue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func userStyle(style string, hue int) template.CSS {
|
|
||||||
var (
|
|
||||||
def styleSheet
|
|
||||||
light styleSheet
|
|
||||||
hasLight bool
|
|
||||||
)
|
|
||||||
|
|
||||||
switch style {
|
|
||||||
default:
|
|
||||||
fallthrough
|
|
||||||
case "nova":
|
|
||||||
def = novaStyle
|
|
||||||
case "adwaita":
|
|
||||||
def = adwaitaDarkStyle
|
|
||||||
light = adwaitaLightStyle
|
|
||||||
hasLight = true
|
|
||||||
case "nord":
|
|
||||||
def = nordDarkStyle
|
|
||||||
light = nordLightStyle
|
|
||||||
hasLight = true
|
|
||||||
case "nord_dark":
|
|
||||||
def = nordDarkStyle
|
|
||||||
case "nord_light", "snowstorm":
|
|
||||||
def = nordLightStyle
|
|
||||||
case "solarized":
|
|
||||||
def = solarizedDarkStyle
|
|
||||||
light = solarizedLightStyle
|
|
||||||
hasLight = true
|
|
||||||
case "solarized_dark":
|
|
||||||
def = solarizedDarkStyle
|
|
||||||
case "solarized_light":
|
|
||||||
def = solarizedLightStyle
|
|
||||||
case "classic":
|
|
||||||
def = classicStyle
|
|
||||||
hue = -1 // Does not support custom hues
|
|
||||||
case "purple_drain":
|
|
||||||
def = purpleDrainStyle
|
|
||||||
hue = -1 // Does not support custom hues
|
|
||||||
case "maroon":
|
|
||||||
def = maroonStyle
|
|
||||||
case "hacker":
|
|
||||||
def = hackerStyle
|
|
||||||
hue = -1 // Does not support custom hues
|
|
||||||
case "canta":
|
|
||||||
def = cantaNovaStyle
|
|
||||||
case "skeuos":
|
|
||||||
def = skeuosNovaStyle
|
|
||||||
case "sweet":
|
|
||||||
def = sweetNovaStyle
|
|
||||||
case "adwaita_dark":
|
|
||||||
def = adwaitaDarkStyle
|
|
||||||
case "adwaita_light":
|
|
||||||
def = adwaitaLightStyle
|
|
||||||
case "nova98":
|
|
||||||
def = nova98Style
|
|
||||||
case "nova98_dark":
|
|
||||||
def = nova98StyleDark
|
|
||||||
}
|
|
||||||
|
|
||||||
if hue >= 0 && hue <= 360 {
|
|
||||||
def = def.withHue(hue)
|
|
||||||
light = light.withHue(hue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasLight {
|
|
||||||
return template.CSS(def.withLight(light))
|
|
||||||
} else {
|
|
||||||
return template.CSS(def.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type styleSheet struct {
|
|
||||||
Link HSL // Based on Highlight if undefined
|
|
||||||
Input CSS
|
|
||||||
InputHover CSS
|
|
||||||
InputText CSS
|
|
||||||
InputDisabledText CSS
|
|
||||||
HighlightBackground CSS
|
|
||||||
Highlight HSL // Links, highlighted buttons, list navigation
|
|
||||||
HighlightText HSL // Text on buttons
|
|
||||||
Danger HSL
|
|
||||||
ScrollbarForeground CSS // Based on Input if undefined
|
|
||||||
ScrollbarHover CSS // Based on ScrollbarForeground if undefined
|
|
||||||
|
|
||||||
BackgroundColor HSL
|
|
||||||
Background CSS
|
|
||||||
BackgroundText HSL
|
|
||||||
BackgroundPattern CSS
|
|
||||||
Navigation CSS
|
|
||||||
BodyColor HSL
|
|
||||||
BodyBackground CSS
|
|
||||||
BodyText HSL
|
|
||||||
Separator HSL
|
|
||||||
CardColor HSL
|
|
||||||
CardText HSL
|
|
||||||
|
|
||||||
// Colors to use in graphs
|
|
||||||
Chart1 HSL
|
|
||||||
Chart2 HSL
|
|
||||||
Chart3 HSL
|
|
||||||
|
|
||||||
StyleOverrides string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s styleSheet) withDefaults() styleSheet {
|
|
||||||
// Set default colors
|
|
||||||
var noColor = HSL{0, 0, 0}
|
|
||||||
var defaultHSL = func(target *HSL, def HSL) {
|
|
||||||
if *target == noColor {
|
|
||||||
*target = def
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var defaultCSS = func(target *CSS, def CSS) {
|
|
||||||
if *target == nil {
|
|
||||||
*target = def
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defaultHSL(&s.Link, s.Highlight.Add(0, 0, -.05))
|
|
||||||
defaultCSS(&s.ScrollbarForeground, s.Input)
|
|
||||||
defaultCSS(&s.ScrollbarHover, s.InputHover)
|
|
||||||
defaultHSL(&s.Chart1, s.Highlight)
|
|
||||||
defaultHSL(&s.Chart2, s.Chart1.Add(120, 0, 0))
|
|
||||||
defaultHSL(&s.Chart3, s.Chart2.Add(120, 0, 0))
|
|
||||||
defaultCSS(&s.HighlightBackground, s.Highlight)
|
|
||||||
defaultCSS(&s.Background, s.BackgroundColor)
|
|
||||||
defaultCSS(&s.BackgroundPattern, s.BackgroundColor)
|
|
||||||
defaultCSS(&s.Navigation, RawCSS("none"))
|
|
||||||
defaultCSS(&s.BodyBackground, s.BodyColor)
|
|
||||||
defaultHSL(&s.BackgroundText, s.BodyText)
|
|
||||||
defaultHSL(&s.Separator, s.BodyColor.Add(0, 0, .06))
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s styleSheet) withHue(hue int) styleSheet {
|
|
||||||
s = s.withDefaults()
|
|
||||||
|
|
||||||
var setHue = func(c CSS) CSS {
|
|
||||||
if hsl, ok := c.(HSL); ok {
|
|
||||||
hsl.Hue = hue
|
|
||||||
return hsl
|
|
||||||
} else {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Input = setHue(s.Input)
|
|
||||||
s.InputHover = setHue(s.InputHover)
|
|
||||||
s.InputText = setHue(s.InputText)
|
|
||||||
s.InputDisabledText = setHue(s.InputDisabledText)
|
|
||||||
s.ScrollbarForeground = setHue(s.ScrollbarForeground)
|
|
||||||
s.ScrollbarHover = setHue(s.ScrollbarHover)
|
|
||||||
s.BackgroundColor.Hue = hue
|
|
||||||
s.Background = setHue(s.Background)
|
|
||||||
s.BackgroundText.Hue = hue
|
|
||||||
s.BackgroundPattern = setHue(s.BackgroundPattern)
|
|
||||||
s.Navigation = setHue(s.Navigation)
|
|
||||||
s.BodyColor.Hue = hue
|
|
||||||
s.BodyBackground = setHue(s.BodyBackground)
|
|
||||||
s.BodyText.Hue = hue
|
|
||||||
s.Separator.Hue = hue
|
|
||||||
s.CardColor.Hue = hue
|
|
||||||
s.CardText.Hue = hue
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s styleSheet) String() string {
|
|
||||||
s = s.withDefaults()
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`:root {
|
|
||||||
--link_color: %s;
|
|
||||||
--input_background: %s;
|
|
||||||
--input_hover_background: %s;
|
|
||||||
--input_text: %s;
|
|
||||||
--input_disabled_text: %s;
|
|
||||||
--highlight_background: %s;
|
|
||||||
--highlight_color: %s;
|
|
||||||
--highlight_text_color: %s;
|
|
||||||
--danger_color: %s;
|
|
||||||
--danger_text_color: %s;
|
|
||||||
--scrollbar_foreground_color: %s;
|
|
||||||
--scrollbar_hover_color: %s;
|
|
||||||
|
|
||||||
--background_color: %s;
|
|
||||||
--background: %s;
|
|
||||||
--background_text_color: %s;
|
|
||||||
--background_pattern_color: %s;
|
|
||||||
--navigation_background: %s;
|
|
||||||
--body_color: %s;
|
|
||||||
--body_background: %s;
|
|
||||||
--body_text_color: %s;
|
|
||||||
--separator: %s;
|
|
||||||
--shaded_background: %s;
|
|
||||||
--card_color: %s;
|
|
||||||
|
|
||||||
--chart_1_color: %s;
|
|
||||||
--chart_2_color: %s;
|
|
||||||
--chart_3_color: %s;
|
|
||||||
|
|
||||||
--shadow_color: %s;
|
|
||||||
}
|
|
||||||
|
|
||||||
%s
|
|
||||||
`,
|
|
||||||
s.Link.CSS(),
|
|
||||||
s.Input.CSS(),
|
|
||||||
s.InputHover.CSS(),
|
|
||||||
s.InputText.CSS(),
|
|
||||||
s.InputDisabledText.CSS(),
|
|
||||||
s.HighlightBackground.CSS(),
|
|
||||||
s.Highlight.CSS(),
|
|
||||||
s.HighlightText.CSS(),
|
|
||||||
s.Danger.CSS(),
|
|
||||||
s.HighlightText.CSS(),
|
|
||||||
s.ScrollbarForeground.CSS(),
|
|
||||||
s.ScrollbarHover.CSS(),
|
|
||||||
s.BackgroundColor.CSS(),
|
|
||||||
s.Background.CSS(),
|
|
||||||
s.BackgroundText.CSS(),
|
|
||||||
s.BackgroundPattern.CSS(),
|
|
||||||
s.Navigation.CSS(),
|
|
||||||
s.BodyColor.CSS(),
|
|
||||||
s.BodyBackground.CSS(),
|
|
||||||
s.BodyText.CSS(),
|
|
||||||
s.Separator.CSS(),
|
|
||||||
s.BodyColor.WithAlpha(0.9).CSS(), // shaded_background
|
|
||||||
s.CardColor.CSS(),
|
|
||||||
s.Chart1.CSS(),
|
|
||||||
s.Chart2.CSS(),
|
|
||||||
s.Chart3.CSS(),
|
|
||||||
s.BodyColor.Darken(0.8).CSS(),
|
|
||||||
s.StyleOverrides,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dark styleSheet) withLight(light styleSheet) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`%s
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
%s
|
|
||||||
}`,
|
|
||||||
dark.String(),
|
|
||||||
light.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Following are all the available styles
|
|
||||||
|
|
||||||
var purpleDrainStyle = styleSheet{
|
|
||||||
Input: RGBA{77, 19, 236, .3},
|
|
||||||
InputHover: RGBA{77, 19, 236, .4},
|
|
||||||
InputText: HSL{0, 0, .9},
|
|
||||||
InputDisabledText: HSL{266, .85, .4},
|
|
||||||
HighlightBackground: NewGradient(160, HSL{150, .84, .39}, HSL{85, .85, .35}),
|
|
||||||
Highlight: HSL{117, .63, .46},
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{357, .63, .46},
|
|
||||||
ScrollbarForeground: HSL{266, .85, .40},
|
|
||||||
ScrollbarHover: HSL{266, .85, .50},
|
|
||||||
|
|
||||||
BackgroundColor: HSL{270, .84, .12},
|
|
||||||
Background: NewGradient(120, HSL{255, .84, .16}, HSL{300, .85, .14}),
|
|
||||||
BackgroundPattern: RawCSS("none"),
|
|
||||||
Navigation: RawCSS("none"),
|
|
||||||
BodyColor: HSL{274, .9, .14},
|
|
||||||
BodyBackground: NewGradient(120, HSL{255, .84, .18}, HSL{300, .85, .16}),
|
|
||||||
BodyText: HSL{0, 0, .8},
|
|
||||||
CardColor: HSL{275, .8, .18},
|
|
||||||
}
|
|
||||||
|
|
||||||
var classicStyle = styleSheet{
|
|
||||||
Input: HSL{0, 0, .18},
|
|
||||||
InputHover: HSL{0, 0, .22},
|
|
||||||
InputText: HSL{0, 0, .9},
|
|
||||||
InputDisabledText: HSL{0, 0, .4},
|
|
||||||
Highlight: HSL{89, .60, .45},
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{339, .65, .31},
|
|
||||||
|
|
||||||
BackgroundColor: HSL{0, 0, .08},
|
|
||||||
BodyColor: HSL{0, 0, .12},
|
|
||||||
BodyText: HSL{0, 0, .8},
|
|
||||||
CardColor: HSL{0, 0, .16},
|
|
||||||
}
|
|
||||||
|
|
||||||
var novaStyle = styleSheet{
|
|
||||||
Input: HSL{94, 1, .1},
|
|
||||||
InputHover: HSL{94, 1, .15},
|
|
||||||
InputText: HSL{0, 0, .9},
|
|
||||||
InputDisabledText: HSL{0, 0, .4},
|
|
||||||
Highlight: HSL{94, .70, .45},
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{339, .65, .31},
|
|
||||||
|
|
||||||
BackgroundColor: HSL{94, 1, .02},
|
|
||||||
BodyColor: HSL{94, 1, .04},
|
|
||||||
BodyText: HSL{94, 1, .9},
|
|
||||||
CardColor: HSL{94, 1, .06},
|
|
||||||
}
|
|
||||||
|
|
||||||
var maroonStyle = styleSheet{
|
|
||||||
Input: HSL{0, .8, .20}, // hsl(0, 87%, 40%)
|
|
||||||
InputHover: HSL{0, .8, .25},
|
|
||||||
InputText: HSL{0, 0, 1},
|
|
||||||
InputDisabledText: HSL{0, 0, .5},
|
|
||||||
Highlight: HSL{137, 1, .37}, //hsl(137, 100%, 37%)
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{9, .96, .42}, //hsl(9, 96%, 42%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{0, .7, .05},
|
|
||||||
BodyColor: HSL{0, .8, .08}, // HSL{0, .8, .15},
|
|
||||||
BodyText: HSL{0, 0, .8},
|
|
||||||
CardColor: HSL{0, .9, .14},
|
|
||||||
}
|
|
||||||
|
|
||||||
var hackerStyle = styleSheet{
|
|
||||||
Input: HSL{0, 0, .1},
|
|
||||||
InputHover: HSL{0, 0, .15},
|
|
||||||
InputText: HSL{0, 0, 1},
|
|
||||||
InputDisabledText: HSL{0, 0, .5},
|
|
||||||
Highlight: HSL{120, .8, .5},
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{0, 1, .4},
|
|
||||||
|
|
||||||
BackgroundColor: HSL{0, 0, 0},
|
|
||||||
BodyColor: HSL{0, 0, .03},
|
|
||||||
BodyText: HSL{0, 0, .8},
|
|
||||||
CardColor: HSL{120, .4, .05},
|
|
||||||
}
|
|
||||||
|
|
||||||
var cantaNovaStyle = styleSheet{
|
|
||||||
Input: HSL{167, .06, .30}, // hsl(167, 6%, 30%)
|
|
||||||
InputHover: HSL{167, .06, .35}, // hsl(167, 6%, 30%)
|
|
||||||
InputText: HSL{0, 0, 1},
|
|
||||||
InputDisabledText: HSL{0, 0, .5},
|
|
||||||
Highlight: HSL{165, 1, .40}, // hsl(165, 100%, 40%)
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{40, 1, .5}, // hsl(40, 100%, 50%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{180, .04, .16},
|
|
||||||
BodyColor: HSL{168, .05, .21},
|
|
||||||
BodyText: HSL{0, 0, .8},
|
|
||||||
CardColor: HSL{170, .05, .26},
|
|
||||||
}
|
|
||||||
|
|
||||||
var skeuosNovaStyle = styleSheet{
|
|
||||||
Input: HSL{226, .15, .23}, //hsl(226, 15%, 23%)
|
|
||||||
InputHover: HSL{226, .15, .28},
|
|
||||||
InputText: HSL{60, .06, .93},
|
|
||||||
InputDisabledText: HSL{0, 0, .5},
|
|
||||||
Highlight: HSL{282, .65, .54}, // hsl(282, 65%, 54%)
|
|
||||||
HighlightText: HSL{0, 0, 1},
|
|
||||||
Danger: HSL{0, .79, .43}, // hsl(0, 79%, 43%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{232, .14, .11}, //hsl(232, 14%, 11%)
|
|
||||||
BodyColor: HSL{229, .14, .16}, // hsl(229, 14%, 16%)
|
|
||||||
BodyText: HSL{60, .06, .93}, // hsl(60, 6%, 93%)
|
|
||||||
CardColor: HSL{225, .14, .17}, // hsl(225, 14%, 17%)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
nord0 = HSL{220, .16, .22} // Dark
|
|
||||||
nord1 = HSL{222, .16, .28} // Dark
|
|
||||||
nord2 = HSL{220, .17, .32} // Dark
|
|
||||||
nord3 = HSL{220, .16, .36} // Light
|
|
||||||
nord4 = HSL{219, .28, .88} // Light
|
|
||||||
nord5 = HSL{218, .27, .92} // Light
|
|
||||||
nord6 = HSL{218, .27, .94} // Light
|
|
||||||
nord7 = HSL{179, .25, .65} // Teal
|
|
||||||
nord8 = HSL{193, .43, .67} // Light blue
|
|
||||||
nord11 = HSL{354, .42, .56} // Red
|
|
||||||
nord14 = HSL{92, .28, .65} // Green
|
|
||||||
)
|
|
||||||
|
|
||||||
var nordDarkStyle = styleSheet{
|
|
||||||
Input: nord3.Add(0, 0, 0.01),
|
|
||||||
InputHover: nord3.Add(0, 0, 0.03),
|
|
||||||
InputText: nord5,
|
|
||||||
InputDisabledText: nord0,
|
|
||||||
Highlight: nord14,
|
|
||||||
HighlightText: nord0,
|
|
||||||
Danger: nord11,
|
|
||||||
ScrollbarForeground: nord7,
|
|
||||||
ScrollbarHover: nord8,
|
|
||||||
|
|
||||||
BackgroundColor: nord0,
|
|
||||||
BodyColor: nord1,
|
|
||||||
BodyText: nord4,
|
|
||||||
CardColor: nord2,
|
|
||||||
}
|
|
||||||
|
|
||||||
var nordLightStyle = styleSheet{
|
|
||||||
Link: HSL{92, .40, .32},
|
|
||||||
Input: nord4.Add(0, 0, -0.04),
|
|
||||||
InputHover: nord4.Add(0, 0, -0.06),
|
|
||||||
InputText: nord0,
|
|
||||||
InputDisabledText: nord6,
|
|
||||||
Highlight: nord14,
|
|
||||||
HighlightText: nord1,
|
|
||||||
Danger: nord11,
|
|
||||||
ScrollbarForeground: nord7,
|
|
||||||
ScrollbarHover: nord8,
|
|
||||||
|
|
||||||
BackgroundColor: nord4,
|
|
||||||
BackgroundText: nord0,
|
|
||||||
BodyColor: nord6,
|
|
||||||
BodyText: nord2,
|
|
||||||
Separator: nord4,
|
|
||||||
BackgroundPattern: nord4,
|
|
||||||
CardColor: nord5,
|
|
||||||
}
|
|
||||||
|
|
||||||
var sweetNovaStyle = styleSheet{
|
|
||||||
Input: HSL{229, .25, .18}, // hsl(229, 25%, 14%)
|
|
||||||
InputHover: HSL{229, .25, .23}, // hsl(229, 25%, 14%)
|
|
||||||
InputText: HSL{223, .13, .79},
|
|
||||||
InputDisabledText: HSL{0, 0, .5},
|
|
||||||
Highlight: HSL{296, .88, .44},
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{356, 1, .64}, // hsl(356, 100%, 64%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{225, .25, .06}, // hsl(225, 25%, 6%)
|
|
||||||
BodyColor: HSL{228, .25, .12}, // hsl(228, 25%, 12%)
|
|
||||||
BodyText: HSL{223, .13, .79}, // hsl(223, 13%, 79%)
|
|
||||||
CardColor: HSL{229, .25, .14}, // hsl(229, 25%, 14%)
|
|
||||||
}
|
|
||||||
|
|
||||||
var adwaitaDarkStyle = styleSheet{
|
|
||||||
Input: RGBA{255, 255, 255, .06},
|
|
||||||
InputHover: RGBA{255, 255, 255, .11},
|
|
||||||
InputText: HSL{0, 0, 1},
|
|
||||||
InputDisabledText: HSL{0, 0, .5},
|
|
||||||
Highlight: HSL{152, .62, .39}, // hsl(152, 62%, 39%)
|
|
||||||
HighlightText: HSL{0, 0, 0},
|
|
||||||
Danger: HSL{9, 1, .69}, // hsl(9, 100%, 69%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{0, 0, .12},
|
|
||||||
BodyColor: HSL{0, 0, .14},
|
|
||||||
BodyText: HSL{0, 0, 1},
|
|
||||||
CardColor: HSL{0, 0, .21},
|
|
||||||
}
|
|
||||||
|
|
||||||
var adwaitaLightStyle = styleSheet{
|
|
||||||
Input: RGBA{0, 0, 0, .06},
|
|
||||||
InputHover: RGBA{0, 0, 0, .11},
|
|
||||||
InputText: HSL{0, 0, .2},
|
|
||||||
InputDisabledText: HSL{0, 0, .7},
|
|
||||||
Highlight: HSL{152, .62, .47}, // hsl(152, 62%, 47%)
|
|
||||||
HighlightText: HSL{0, 0, 1},
|
|
||||||
Danger: HSL{356, .75, .43}, // hsl(356, 75%, 43%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{0, 0, .92},
|
|
||||||
BodyColor: HSL{0, 0, .98},
|
|
||||||
BodyText: HSL{0, 0, .2},
|
|
||||||
Separator: HSL{0, 0, .86},
|
|
||||||
CardColor: HSL{0, 0, 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
var solarizedDarkStyle = styleSheet{
|
|
||||||
Input: HSL{192, .81, .18}, // hsl(194, 14%, 40%)
|
|
||||||
InputHover: HSL{192, .81, .23}, // hsl(196, 13%, 45%)
|
|
||||||
InputText: HSL{180, .07, .80}, // hsl(44, 87%, 94%)
|
|
||||||
InputDisabledText: HSL{194, .14, .30}, // hsl(194, 14%, 40%)
|
|
||||||
Highlight: HSL{68, 1, .30}, // hsl(68, 100%, 30%)
|
|
||||||
HighlightText: HSL{192, .81, .14}, // hsl(192, 100%, 11%)
|
|
||||||
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{192, 1, .11}, //hsl(192, 100%, 11%)
|
|
||||||
BodyColor: HSL{192, .81, .14}, // hsl(192, 81%, 14%)
|
|
||||||
BodyText: HSL{180, .07, .60}, // hsl(180, 7%, 60%)
|
|
||||||
CardColor: HSL{192, .81, .16},
|
|
||||||
}
|
|
||||||
|
|
||||||
var solarizedLightStyle = styleSheet{
|
|
||||||
Input: HSL{46, .42, .84},
|
|
||||||
InputHover: HSL{46, .42, .80},
|
|
||||||
InputText: HSL{194, .14, .20}, // hsl(192, 81%, 14%)
|
|
||||||
InputDisabledText: HSL{44, .87, .94},
|
|
||||||
Highlight: HSL{68, 1, .30}, // hsl(68, 100%, 30%)
|
|
||||||
HighlightText: HSL{44, .87, .94},
|
|
||||||
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{46, .42, .88}, // hsl(46, 42%, 88%)
|
|
||||||
BackgroundText: HSL{192, .81, .14},
|
|
||||||
BodyColor: HSL{44, .87, .94}, // hsl(44, 87%, 94%)
|
|
||||||
BodyText: HSL{194, .14, .40}, // hsl(194, 14%, 40%)
|
|
||||||
Separator: HSL{46, .42, .88},
|
|
||||||
CardColor: HSL{44, .87, .92},
|
|
||||||
}
|
|
||||||
|
|
||||||
const override98 = `
|
|
||||||
button, .button,
|
|
||||||
dialog,
|
|
||||||
.highlight_border,
|
|
||||||
.page_content,
|
|
||||||
footer, .footer_content,
|
|
||||||
.card,
|
|
||||||
.window,
|
|
||||||
.block,
|
|
||||||
.sharebar,
|
|
||||||
.directory_sorters, .directory_area,
|
|
||||||
.container > .directory,
|
|
||||||
.file_preview > .directory,
|
|
||||||
.directory > .node,
|
|
||||||
.file, .file_button,
|
|
||||||
.upload_task,
|
|
||||||
.add_button,
|
|
||||||
.expandable,
|
|
||||||
.bubble,
|
|
||||||
.upload_widget {
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
box-shadow: inset -1px -1px #0a0a0a,
|
|
||||||
inset 1px 1px #dfdfdf,
|
|
||||||
inset -2px -2px grey,
|
|
||||||
inset 2px 2px #fff !important;
|
|
||||||
}
|
|
||||||
.window, .page_content, .file_button, .expandable, pre {
|
|
||||||
padding: 3px !important;
|
|
||||||
}
|
|
||||||
.window > .header,
|
|
||||||
.headerbar,
|
|
||||||
.upload_widget > .header {
|
|
||||||
background: linear-gradient(90deg,navy,#1084d0) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.tab_bar > button, .tab_bar > .button {
|
|
||||||
border-top-left-radius: 3px !important;
|
|
||||||
border-top-right-radius: 3px !important;
|
|
||||||
box-shadow: inset -1px 0 #0a0a0a,inset 1px 1px #dfdfdf,inset -2px 0 grey,inset 2px 2px #fff !important;
|
|
||||||
}
|
|
||||||
.directory > tr {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
.button_toggle_navigation {
|
|
||||||
padding: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="email"],
|
|
||||||
input[type="number"],
|
|
||||||
input[type="password"],
|
|
||||||
input[type="text"],
|
|
||||||
input[type="date"],
|
|
||||||
select, textarea, pre {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
background-color: #fff !important;
|
|
||||||
box-shadow: inset -1px -1px #fff,
|
|
||||||
inset 1px 1px grey,
|
|
||||||
inset -2px -2px #dfdfdf,
|
|
||||||
inset 2px 2px #0a0a0a !important;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
background-color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr,
|
|
||||||
.toolbar > .separator,
|
|
||||||
.toolbar > .grid > .separator,
|
|
||||||
.highlight_shaded {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: inset -1px -1px #fff,
|
|
||||||
inset 1px 1px grey,
|
|
||||||
inset -2px -2px #dfdfdf,
|
|
||||||
inset 2px 2px #0a0a0a !important;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var nova98Style = styleSheet{
|
|
||||||
Input: RGB{190, 190, 190},
|
|
||||||
InputHover: RGB{220, 220, 220},
|
|
||||||
InputText: RGB{0, 0, 0},
|
|
||||||
InputDisabledText: RGB{128, 128, 128},
|
|
||||||
Highlight: HSL{240, 1, .5}, // hsl(68, 100%, 30%)
|
|
||||||
HighlightText: HSL{0, 1, 1},
|
|
||||||
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
|
|
||||||
|
|
||||||
BackgroundColor: HSL{180, 1, .25},
|
|
||||||
BackgroundText: HSL{0, 1, 1},
|
|
||||||
BodyColor: HSL{0, 0, .75},
|
|
||||||
BodyText: HSL{0, 0, 0},
|
|
||||||
Separator: HSL{0, 1, 1},
|
|
||||||
CardColor: HSL{0, 0, .75},
|
|
||||||
|
|
||||||
StyleOverrides: override98,
|
|
||||||
}
|
|
||||||
|
|
||||||
var nova98StyleDark = styleSheet{
|
|
||||||
Input: HSL{0, 0, .25},
|
|
||||||
InputHover: HSL{0, 0, .30},
|
|
||||||
InputText: RGB{255, 255, 255},
|
|
||||||
InputDisabledText: RGB{128, 128, 128},
|
|
||||||
Highlight: HSL{240, 1, .7},
|
|
||||||
HighlightText: HSL{0, 1, 1},
|
|
||||||
Danger: HSL{1, .71, .7},
|
|
||||||
Link: HSL{240, 1, .8},
|
|
||||||
|
|
||||||
BackgroundColor: HSL{180, 1, .08},
|
|
||||||
BackgroundText: HSL{0, 1, 1},
|
|
||||||
BodyColor: HSL{0, 0, .25},
|
|
||||||
BodyText: HSL{0, 1, 1},
|
|
||||||
Separator: HSL{0, 0, .33},
|
|
||||||
CardColor: HSL{0, 0, .25},
|
|
||||||
|
|
||||||
StyleOverrides: `
|
|
||||||
:root {
|
|
||||||
--border_98: inset -1px -1px #222,
|
|
||||||
inset 1px 1px #666,
|
|
||||||
inset -2px -2px #333,
|
|
||||||
inset 2px 2px #555;
|
|
||||||
--inset_98: inset -1px -1px #666,
|
|
||||||
inset 1px 1px #111,
|
|
||||||
inset -2px -2px #555,
|
|
||||||
inset 2px 2px #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, .button,
|
|
||||||
dialog,
|
|
||||||
fieldset,
|
|
||||||
.highlight_border,
|
|
||||||
.page_content,
|
|
||||||
footer, .footer_content,
|
|
||||||
.card,
|
|
||||||
.window,
|
|
||||||
.block,
|
|
||||||
.sharebar,
|
|
||||||
.directory_sorters, .directory_area,
|
|
||||||
.container > .directory,
|
|
||||||
.file_preview > .directory,
|
|
||||||
.directory > .node,
|
|
||||||
.file, .file_button,
|
|
||||||
.upload_task,
|
|
||||||
.add_button,
|
|
||||||
.expandable,
|
|
||||||
.bubble,
|
|
||||||
.upload_widget,
|
|
||||||
.highlight_shaded {
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
box-shadow: var(--border_98) !important;
|
|
||||||
}
|
|
||||||
legend {
|
|
||||||
background-color: var(--body_color);
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
.window, .page_content, .file_button, .expandable, pre {
|
|
||||||
padding: 3px !important;
|
|
||||||
}
|
|
||||||
.window > .header,
|
|
||||||
.headerbar,
|
|
||||||
.upload_widget > .header {
|
|
||||||
background: linear-gradient(90deg, #003d66, #005c99) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.tab_bar > button, .tab_bar > .button {
|
|
||||||
border-top-left-radius: 3px !important;
|
|
||||||
border-top-right-radius: 3px !important;
|
|
||||||
box-shadow: var(--border_98) !important;
|
|
||||||
}
|
|
||||||
.directory > tr {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
.button_toggle_navigation {
|
|
||||||
padding: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="email"],
|
|
||||||
input[type="number"],
|
|
||||||
input[type="password"],
|
|
||||||
input[type="text"],
|
|
||||||
input[type="date"],
|
|
||||||
select, textarea, pre {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: var(--inset_98) !important;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr,
|
|
||||||
.toolbar > .separator,
|
|
||||||
.toolbar > .grid > .separator {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: var(--inset_98) !important;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
package webcontroller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Color interface {
|
|
||||||
CSS
|
|
||||||
HSL() HSL
|
|
||||||
RGB() RGB
|
|
||||||
}
|
|
||||||
|
|
||||||
type CSS interface {
|
|
||||||
CSS() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Raw CSS
|
|
||||||
type RawCSS string
|
|
||||||
|
|
||||||
func (c RawCSS) CSS() string { return string(c) }
|
|
||||||
|
|
||||||
// HSL color
|
|
||||||
type HSL struct {
|
|
||||||
Hue int
|
|
||||||
Saturation float64
|
|
||||||
Lightness float64
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Color = HSL{} // Confirm interface compliance
|
|
||||||
|
|
||||||
func (hsl HSL) CSS() string { return hsl.RGB().CSS() }
|
|
||||||
func (hsl HSL) HSL() HSL { return hsl }
|
|
||||||
func (hsl HSL) RGB() RGB {
|
|
||||||
var r, g, b, q, p float64
|
|
||||||
var h, s, l = float64(hsl.Hue) / 360, hsl.Saturation, hsl.Lightness
|
|
||||||
|
|
||||||
if s == 0 {
|
|
||||||
r, g, b = l, l, l
|
|
||||||
} else {
|
|
||||||
var hue2rgb = func(p, q, t float64) float64 {
|
|
||||||
if t < 0 {
|
|
||||||
t++
|
|
||||||
}
|
|
||||||
if t > 1 {
|
|
||||||
t--
|
|
||||||
}
|
|
||||||
|
|
||||||
if t < 1.0/6.0 {
|
|
||||||
return p + (q-p)*6*t
|
|
||||||
} else if t < 1.0/2.0 {
|
|
||||||
return q
|
|
||||||
} else if t < 2.0/3.0 {
|
|
||||||
return p + (q-p)*(2.0/3.0-t)*6
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
if l < 0.5 {
|
|
||||||
q = l * (1 + s)
|
|
||||||
} else {
|
|
||||||
q = l + s - l*s
|
|
||||||
}
|
|
||||||
|
|
||||||
p = 2*l - q
|
|
||||||
r = hue2rgb(p, q, h+1.0/3.0)
|
|
||||||
g = hue2rgb(p, q, h)
|
|
||||||
b = hue2rgb(p, q, h-1.0/3.0)
|
|
||||||
}
|
|
||||||
return RGB{R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add returns a NEW HSL struct, it doesn't modify the current one
|
|
||||||
func (hsl HSL) Add(hue int, saturation float64, lightness float64) HSL {
|
|
||||||
var new = HSL{
|
|
||||||
hsl.Hue + hue,
|
|
||||||
hsl.Saturation + saturation,
|
|
||||||
hsl.Lightness + lightness,
|
|
||||||
}
|
|
||||||
// Hue bounds correction
|
|
||||||
if new.Hue < 0 {
|
|
||||||
new.Hue += 360
|
|
||||||
} else if new.Hue > 360 {
|
|
||||||
new.Hue -= 360
|
|
||||||
}
|
|
||||||
// Saturation bounds check
|
|
||||||
if new.Saturation < 0 {
|
|
||||||
new.Saturation = 0
|
|
||||||
} else if new.Saturation > 1 {
|
|
||||||
new.Saturation = 1
|
|
||||||
}
|
|
||||||
// Lightness bounds check
|
|
||||||
if new.Lightness < 0 {
|
|
||||||
new.Lightness = 0
|
|
||||||
} else if new.Lightness > 1 {
|
|
||||||
new.Lightness = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return new
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hsl HSL) Darken(percent float64) HSL {
|
|
||||||
hsl.Lightness = hsl.Lightness * percent
|
|
||||||
hsl.Saturation = hsl.Saturation * percent
|
|
||||||
return hsl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hsl HSL) WithAlpha(alpha float64) HSLA {
|
|
||||||
return HSLA{hsl.Hue, hsl.Saturation, hsl.Lightness, alpha}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HSLA struct {
|
|
||||||
Hue int
|
|
||||||
Saturation float64
|
|
||||||
Lightness float64
|
|
||||||
Alpha float64
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Color = HSLA{}
|
|
||||||
|
|
||||||
func (hsla HSLA) CSS() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"hsla(%d, %.2f%%, %.2f%%, %.2f)",
|
|
||||||
hsla.Hue, hsla.Saturation*100, hsla.Lightness*100, hsla.Alpha,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
func (hsla HSLA) HSL() HSL { return HSL{hsla.Hue, hsla.Saturation, hsla.Lightness} }
|
|
||||||
func (hsla HSLA) RGB() RGB { return hsla.HSL().RGB() }
|
|
||||||
|
|
||||||
type RGB struct {
|
|
||||||
R uint8
|
|
||||||
G uint8
|
|
||||||
B uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Color = RGB{}
|
|
||||||
|
|
||||||
func (rgb RGB) CSS() string { return fmt.Sprintf("#%02x%02x%02x", rgb.R, rgb.G, rgb.B) }
|
|
||||||
func (rgb RGB) HSL() HSL {
|
|
||||||
var r, g, b = float64(rgb.R), float64(rgb.G), float64(rgb.B)
|
|
||||||
var h, s, l float64
|
|
||||||
|
|
||||||
max := math.Max(math.Max(r, g), b)
|
|
||||||
min := math.Min(math.Min(r, g), b)
|
|
||||||
|
|
||||||
// Luminosity is the average of the max and min rgb color intensities.
|
|
||||||
l = (max + min) / 2
|
|
||||||
|
|
||||||
// saturation
|
|
||||||
delta := max - min
|
|
||||||
if delta == 0 {
|
|
||||||
// it's gray
|
|
||||||
return HSL{0, 0, l}
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's not gray
|
|
||||||
if l < 0.5 {
|
|
||||||
s = delta / (max + min)
|
|
||||||
} else {
|
|
||||||
s = delta / (2 - max - min)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hue
|
|
||||||
r2 := (((max - r) / 6) + (delta / 2)) / delta
|
|
||||||
g2 := (((max - g) / 6) + (delta / 2)) / delta
|
|
||||||
b2 := (((max - b) / 6) + (delta / 2)) / delta
|
|
||||||
switch {
|
|
||||||
case r == max:
|
|
||||||
h = b2 - g2
|
|
||||||
case g == max:
|
|
||||||
h = (1.0 / 3.0) + r2 - b2
|
|
||||||
case b == max:
|
|
||||||
h = (2.0 / 3.0) + g2 - r2
|
|
||||||
}
|
|
||||||
|
|
||||||
if h < 0 {
|
|
||||||
h += 1
|
|
||||||
} else if h > 1 {
|
|
||||||
h -= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return HSL{int(h), s, l}
|
|
||||||
}
|
|
||||||
func (rgb RGB) RGB() RGB { return rgb }
|
|
||||||
|
|
||||||
type RGBA struct {
|
|
||||||
R uint8
|
|
||||||
G uint8
|
|
||||||
B uint8
|
|
||||||
A float64
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Color = RGBA{}
|
|
||||||
|
|
||||||
func (rgba RGBA) CSS() string {
|
|
||||||
return fmt.Sprintf("rgba(%d, %d, %d, %f)", rgba.R, rgba.G, rgba.B, rgba.A)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rgba RGBA) HSL() HSL { return rgba.RGB().HSL() }
|
|
||||||
func (rgba RGBA) RGB() RGB { return RGB{rgba.R, rgba.G, rgba.B} }
|
|
||||||
|
|
||||||
type Gradient struct {
|
|
||||||
Angle int
|
|
||||||
Colors []Color
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ CSS = Gradient{}
|
|
||||||
|
|
||||||
func NewGradient(angle int, colors ...Color) Gradient {
|
|
||||||
return Gradient{angle, colors}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g Gradient) CSS() string {
|
|
||||||
var colors string
|
|
||||||
for i, color := range g.Colors {
|
|
||||||
if i != 0 {
|
|
||||||
colors += ", "
|
|
||||||
}
|
|
||||||
colors += color.CSS()
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("linear-gradient(%ddeg, %s)", g.Angle, colors)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package webcontroller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"fornaxian.tech/log"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New initializes a new WebController by registering all the request handlers
|
|
||||||
// and parsing all templates in the resource directory
|
|
||||||
func serveSK(r *httprouter.Router) (err error) {
|
|
||||||
// r.ServeFiles("/_app/*filepath", http.Dir(assetsPath+"/_app"))
|
|
||||||
// r.ServeFiles("/style/*filepath", http.Dir(assetsPath+"/style"))
|
|
||||||
|
|
||||||
readdir, err := os.ReadDir(assetsPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, entry := range readdir {
|
|
||||||
if entry.Type().IsRegular() {
|
|
||||||
serveFile(r, "/"+entry.Name(), assetsPath+"/"+entry.Name())
|
|
||||||
} else if entry.IsDir() {
|
|
||||||
r.ServeFiles("/"+entry.Name()+"/*filepath", http.Dir(assetsPath+"/"+entry.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Debug("Running fallback handler for %s", r.URL.Path)
|
|
||||||
http.ServeFile(w, r, assetsPath+"/fallback.html")
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsPath = "/home/wim/Workspace/svelte/fnx_sk/build"
|
|
||||||
|
|
||||||
func serveFile(r *httprouter.Router, path, file string) {
|
|
||||||
var handler = func(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
p httprouter.Params,
|
|
||||||
) {
|
|
||||||
http.ServeFile(w, r, assetsPath+"/"+file)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.GET(path, handler)
|
|
||||||
r.HEAD(path, handler)
|
|
||||||
r.OPTIONS(path, handler)
|
|
||||||
r.POST(path, handler)
|
|
||||||
r.PUT(path, handler)
|
|
||||||
r.PATCH(path, handler)
|
|
||||||
r.DELETE(path, handler)
|
|
||||||
}
|
|
||||||
@@ -1,314 +1,61 @@
|
|||||||
package webcontroller
|
package webcontroller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fornaxian.tech/log"
|
"fornaxian.tech/log"
|
||||||
"fornaxian.tech/util"
|
"fornaxian.tech/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateData is a struct that every template expects when being rendered. In
|
// TemplateData is the data for the page template, res/template/wrap.html
|
||||||
// the field Other you can pass your own template-specific variables.
|
|
||||||
type TemplateData struct {
|
type TemplateData struct {
|
||||||
tpm *TemplateManager
|
|
||||||
APIEndpoint template.URL
|
APIEndpoint template.URL
|
||||||
Hostname template.HTML
|
Hostname template.HTML
|
||||||
|
CacheID int64
|
||||||
|
|
||||||
// Only used on file viewer page
|
// Describe the site by default, or the file on a filesystem page
|
||||||
Title string
|
Title string
|
||||||
OGData ogData
|
OGData ogData
|
||||||
|
|
||||||
Other any
|
// Embedded in the page as window.user and window.initial_node, so the JS
|
||||||
URLQuery url.Values
|
// doesn't have to request them. Left empty when they were not requested
|
||||||
|
User json.RawMessage
|
||||||
|
Node json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebController) newTemplateData(r *http.Request) (t *TemplateData) {
|
|
||||||
t = &TemplateData{
|
|
||||||
tpm: wc.templates,
|
|
||||||
APIEndpoint: template.URL(wc.config.APIURLExternal),
|
|
||||||
Hostname: template.HTML(wc.hostname),
|
|
||||||
URLQuery: r.URL.Query(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateManager parses templates and provides utility functions to the
|
|
||||||
// templates' scripting language
|
|
||||||
type TemplateManager struct {
|
|
||||||
tpl *template.Template
|
|
||||||
|
|
||||||
// Config
|
|
||||||
resourceDir string
|
|
||||||
externalAPIEndpoint string
|
|
||||||
debugModeEnabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTemplateManager creates a new template manager
|
|
||||||
func NewTemplateManager(resourceDir, externalAPIEndpoint string, debugMode bool) *TemplateManager {
|
|
||||||
return &TemplateManager{
|
|
||||||
resourceDir: resourceDir,
|
|
||||||
externalAPIEndpoint: externalAPIEndpoint,
|
|
||||||
debugModeEnabled: debugMode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseTemplates parses the templates in the template directory which is
|
|
||||||
// defined in the config file.
|
|
||||||
// If silent is false it will print an info log message for every template found
|
|
||||||
func (tm *TemplateManager) ParseTemplates(silent bool) {
|
|
||||||
var err error
|
|
||||||
var templatePaths []string
|
|
||||||
tpl := template.New("")
|
|
||||||
|
|
||||||
// Import template functions
|
|
||||||
tpl.Funcs(template.FuncMap{
|
|
||||||
"cacheID": tm.cacheID,
|
|
||||||
"debugMode": tm.debugMode,
|
|
||||||
"apiUrl": tm.apiURL,
|
|
||||||
"pageNr": tm.pageNr,
|
|
||||||
"add": tm.add,
|
|
||||||
"sub": tm.sub,
|
|
||||||
"mul": tm.mul,
|
|
||||||
"div": tm.div,
|
|
||||||
"formatData": tm.formatData,
|
|
||||||
"formatDataBits": tm.formatDataBits,
|
|
||||||
"formatSC": tm.formatSC,
|
|
||||||
"noescape": tm.noEscape,
|
|
||||||
"noescapeJS": tm.noEscapeJS,
|
|
||||||
"slashes": tm.slashes,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Parse dynamic templates
|
|
||||||
if err = filepath.Walk(tm.resourceDir+"/template", func(path string, f os.FileInfo, err error) error {
|
|
||||||
if f == nil || f.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
templatePaths = append(templatePaths, path)
|
|
||||||
if !silent {
|
|
||||||
log.Debug("Template found: %s", path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
log.Error("Failed to parse templates: %s", err)
|
|
||||||
}
|
|
||||||
if _, err = tpl.ParseFiles(templatePaths...); err != nil {
|
|
||||||
log.Error("Template parsing failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse static resources
|
|
||||||
var file []byte
|
|
||||||
if err = filepath.Walk(tm.resourceDir+"/include", func(path string, f os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("walk err: %w", err)
|
|
||||||
}
|
|
||||||
if f == nil || f.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if file, err = os.ReadFile(path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(path, ".png") {
|
|
||||||
file = []byte("data:image/png;base64," + base64.StdEncoding.EncodeToString(file))
|
|
||||||
} else if strings.HasSuffix(path, ".gif") {
|
|
||||||
file = []byte("data:image/gif;base64," + base64.StdEncoding.EncodeToString(file))
|
|
||||||
} else if strings.HasSuffix(path, ".webp") {
|
|
||||||
file = []byte("data:image/webp;base64," + base64.StdEncoding.EncodeToString(file))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the resources in a template definition
|
|
||||||
if _, err = tpl.Parse(
|
|
||||||
`{{define "` + f.Name() + `"}}` + string(file) + `{{end}}`,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse '%s': %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !silent {
|
|
||||||
log.Debug("Template parsed: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
log.Error("Failed to parse templates: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tm.tpl = tpl
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run runs a template by name
|
|
||||||
func (tm *TemplateManager) Run(w io.Writer, r *http.Request, name string, data any) (err error) {
|
|
||||||
if tm.debugModeEnabled {
|
|
||||||
tm.ParseTemplates(true)
|
|
||||||
}
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tm.tpl.ExecuteTemplate(w, name, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template functions. These can be called from within the template to execute
|
|
||||||
// more specialized actions
|
|
||||||
|
|
||||||
// The cache ID is used to invalidate caches when the webserver is restarted.
|
// The cache ID is used to invalidate caches when the webserver is restarted.
|
||||||
// It's rounded to a full hour to prevent cached from being emptied too often if
|
// It's rounded to a full hour to prevent cached from being emptied too often if
|
||||||
// the server is regularly restarted
|
// the server is regularly restarted
|
||||||
var cacheID = time.Now().Unix() / 3600
|
var cacheID = time.Now().Unix() / 3600
|
||||||
|
|
||||||
func (tm *TemplateManager) cacheID() int64 {
|
func (wc *WebController) newTemplateData(r *http.Request) *TemplateData {
|
||||||
return cacheID
|
return &TemplateData{
|
||||||
}
|
APIEndpoint: template.URL(wc.config.APIURLExternal),
|
||||||
func (tm *TemplateManager) debugMode() bool {
|
Hostname: template.HTML(wc.hostname),
|
||||||
return tm.debugModeEnabled
|
CacheID: cacheID,
|
||||||
}
|
Title: "Nova.storage",
|
||||||
func (tm *TemplateManager) apiURL() string {
|
OGData: siteOGData(r),
|
||||||
return tm.externalAPIEndpoint
|
|
||||||
}
|
|
||||||
func (tm *TemplateManager) pageNr(s string) (nr int) {
|
|
||||||
// Atoi returns 0 on error, which is fine for page numbers
|
|
||||||
if nr, _ = strconv.Atoi(s); nr < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
return nr
|
|
||||||
}
|
|
||||||
func (tm *TemplateManager) add(a, b any) float64 { return toFloat(a) + toFloat(b) }
|
|
||||||
func (tm *TemplateManager) sub(a, b any) float64 { return toFloat(a) - toFloat(b) }
|
|
||||||
func (tm *TemplateManager) mul(a, b any) float64 { return toFloat(a) * toFloat(b) }
|
|
||||||
func (tm *TemplateManager) div(a, b any) float64 { return toFloat(a) / toFloat(b) }
|
|
||||||
|
|
||||||
func (tm *TemplateManager) formatData(i any) string {
|
|
||||||
return util.FormatData(detectInt(i))
|
|
||||||
}
|
|
||||||
func (tm *TemplateManager) formatDataBits(i any) string {
|
|
||||||
var size = detectInt(i) * 8
|
|
||||||
var sizef = float64(size)
|
|
||||||
|
|
||||||
var fmtSize = func(n float64, u string) string {
|
|
||||||
var f string
|
|
||||||
if n >= 100 {
|
|
||||||
f = "%.1f"
|
|
||||||
} else if n >= 10 {
|
|
||||||
f = "%.2f"
|
|
||||||
} else {
|
|
||||||
f = "%.3f"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(f+" "+u, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
if size >= 1e18 {
|
|
||||||
// An exabyte is the largest volume of data you can express in a signed
|
|
||||||
// 64-bit integer
|
|
||||||
return fmtSize(sizef/1e18, "Eb")
|
|
||||||
} else if sizef >= 1e15 {
|
|
||||||
return fmtSize(sizef/1e15, "Pb")
|
|
||||||
} else if sizef >= 1e12 {
|
|
||||||
return fmtSize(sizef/1e12, "Tb")
|
|
||||||
} else if sizef >= 1e9 {
|
|
||||||
return fmtSize(sizef/1e9, "Gb")
|
|
||||||
} else if sizef >= 1e6 {
|
|
||||||
return fmtSize(sizef/1e6, "Mb")
|
|
||||||
} else if sizef >= 1e3 {
|
|
||||||
return fmtSize(sizef/1e3, "kb")
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.0f b", sizef)
|
|
||||||
}
|
|
||||||
func (tm *TemplateManager) formatSC(amt float64) string {
|
|
||||||
var fmtSize = func(n float64, u string) string {
|
|
||||||
var f string
|
|
||||||
if n >= 100 {
|
|
||||||
f = "%.1f"
|
|
||||||
} else if n >= 10 {
|
|
||||||
f = "%.2f"
|
|
||||||
} else {
|
|
||||||
f = "%.3f"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(f+" "+u, n)
|
|
||||||
}
|
|
||||||
if amt >= 1 {
|
|
||||||
return fmtSize(amt, "SC")
|
|
||||||
} else if amt >= 1e-3 {
|
|
||||||
return fmtSize(amt/1e-3, "mS")
|
|
||||||
} else if amt >= 1e-6 {
|
|
||||||
return fmtSize(amt/1e-6, "μS")
|
|
||||||
} else if amt >= 1e-9 {
|
|
||||||
return fmtSize(amt/1e-9, "nS")
|
|
||||||
} else if amt >= 1e-12 {
|
|
||||||
return fmtSize(amt/1e-12, "pS")
|
|
||||||
}
|
|
||||||
return fmtSize(amt/1e-24, "H")
|
|
||||||
}
|
|
||||||
func (tm *TemplateManager) noEscape(t string) template.HTML { return template.HTML(t) }
|
|
||||||
func (tm *TemplateManager) noEscapeJS(t string) template.JS { return template.JS(t) }
|
|
||||||
func (tm *TemplateManager) slashes() template.HTML { return template.HTML("//") }
|
|
||||||
|
|
||||||
func detectInt(i any) int {
|
|
||||||
switch v := i.(type) {
|
|
||||||
case int:
|
|
||||||
return int(v)
|
|
||||||
case int8:
|
|
||||||
return int(v)
|
|
||||||
case int16:
|
|
||||||
return int(v)
|
|
||||||
case int32:
|
|
||||||
return int(v)
|
|
||||||
case int64:
|
|
||||||
return int(v)
|
|
||||||
case uint:
|
|
||||||
return int(v)
|
|
||||||
case uint8:
|
|
||||||
return int(v)
|
|
||||||
case uint16:
|
|
||||||
return int(v)
|
|
||||||
case uint32:
|
|
||||||
return int(v)
|
|
||||||
case uint64:
|
|
||||||
return int(v)
|
|
||||||
case float32:
|
|
||||||
return int(v)
|
|
||||||
case float64:
|
|
||||||
return int(v)
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("%v is not an int", i))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toFloat(i any) float64 {
|
func (wc *WebController) parseTemplate() *template.Template {
|
||||||
switch v := i.(type) {
|
return template.Must(template.ParseFiles(wc.config.ResourceDir + "/template/wrap.html"))
|
||||||
case int:
|
}
|
||||||
return float64(v)
|
|
||||||
case int8:
|
// renderPage writes the page which loads the Svelte app. In debug mode the
|
||||||
return float64(v)
|
// template is parsed on every request, so changes show up without a restart
|
||||||
case int16:
|
func (wc *WebController) renderPage(w http.ResponseWriter, r *http.Request, td *TemplateData) {
|
||||||
return float64(v)
|
var tpl = wc.tpl
|
||||||
case int32:
|
if wc.config.DebugMode {
|
||||||
return float64(v)
|
tpl = wc.parseTemplate()
|
||||||
case int64:
|
}
|
||||||
return float64(v)
|
if r.Method == "HEAD" {
|
||||||
case uint:
|
return
|
||||||
return float64(v)
|
}
|
||||||
case uint8:
|
if err := tpl.Execute(w, td); err != nil && !util.IsNetError(err) {
|
||||||
return float64(v)
|
log.Error("Error executing page template: %s", err)
|
||||||
case uint16:
|
}
|
||||||
return float64(v)
|
|
||||||
case uint32:
|
|
||||||
return float64(v)
|
|
||||||
case uint64:
|
|
||||||
return float64(v)
|
|
||||||
case float32:
|
|
||||||
return float64(v)
|
|
||||||
case float64:
|
|
||||||
return float64(v)
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("%v is not a number", i))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package webcontroller
|
package webcontroller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
@@ -10,37 +9,30 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"fornaxian.tech/log"
|
"fornaxian.tech/log"
|
||||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||||
"fornaxian.tech/util"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
blackfriday "github.com/russross/blackfriday/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
APIURLExternal string `toml:"api_url_external"`
|
APIURLExternal string `toml:"api_url_external"`
|
||||||
APIURLInternal string `toml:"api_url_internal"`
|
APIURLInternal string `toml:"api_url_internal"`
|
||||||
APISocketPath string `toml:"api_socket_path"`
|
APISocketPath string `toml:"api_socket_path"`
|
||||||
SessionCookieDomain string `toml:"session_cookie_domain"`
|
ResourceDir string `toml:"resource_dir"`
|
||||||
ResourceDir string `toml:"resource_dir"`
|
DebugMode bool `toml:"debug_mode"`
|
||||||
DebugMode bool `toml:"debug_mode"`
|
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
||||||
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
|
||||||
MaintenanceMode bool `toml:"maintenance_mode"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebController controls how requests are handled and makes sure they have
|
// WebController controls how requests are handled and makes sure they have
|
||||||
// proper context when running
|
// proper context when running
|
||||||
type WebController struct {
|
type WebController struct {
|
||||||
templates *TemplateManager
|
tpl *template.Template
|
||||||
config Config
|
config Config
|
||||||
|
|
||||||
// Server hostname, displayed in the footer of every web page
|
// Server hostname, displayed in the footer of every web page
|
||||||
hostname string
|
hostname string
|
||||||
|
|
||||||
httpClient *http.Client
|
|
||||||
|
|
||||||
// API client to use for all requests. If the user is authenticated you
|
// API client to use for all requests. If the user is authenticated you
|
||||||
// should call Login() on this object. Calling Login will create a copy and
|
// should call Login() on this object. Calling Login will create a copy and
|
||||||
// not alter the original PixelAPI, but it will use the same HTTP Transport
|
// not alter the original PixelAPI, but it will use the same HTTP Transport
|
||||||
@@ -48,49 +40,44 @@ type WebController struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New initializes a new WebController by registering all the request handlers
|
// New initializes a new WebController by registering all the request handlers
|
||||||
// and parsing all templates in the resource directory
|
// and parsing the page template
|
||||||
func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
func New(r *httprouter.Router, conf Config) (wc *WebController) {
|
||||||
var err error
|
hostname, err := os.Hostname()
|
||||||
wc = &WebController{
|
if err != nil {
|
||||||
config: conf,
|
panic(fmt.Errorf("could not get hostname: %s", err))
|
||||||
httpClient: &http.Client{Timeout: time.Minute * 10},
|
|
||||||
api: pixelapi.New(conf.APIURLInternal),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wc = &WebController{
|
||||||
|
config: conf,
|
||||||
|
hostname: hostname,
|
||||||
|
api: pixelapi.New(conf.APIURLInternal),
|
||||||
|
}
|
||||||
if conf.APISocketPath != "" {
|
if conf.APISocketPath != "" {
|
||||||
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
|
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
|
||||||
}
|
}
|
||||||
|
wc.tpl = wc.parseTemplate()
|
||||||
wc.templates = NewTemplateManager(conf.ResourceDir, conf.APIURLExternal, conf.DebugMode)
|
|
||||||
wc.templates.ParseTemplates(false)
|
|
||||||
|
|
||||||
if wc.hostname, err = os.Hostname(); err != nil {
|
|
||||||
panic(fmt.Errorf("could not get hostname: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve static files
|
// Serve static files
|
||||||
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
|
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
|
||||||
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
// Cache resources for a year
|
// Cache resources for a year. Except the Svelte entrypoint, it keeps its
|
||||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
// name when it changes. The chunks it imports have a hash in their name
|
||||||
|
if p.ByName("filepath") == "/svelte/wrap.js" {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||||
|
}
|
||||||
r.URL.Path = p.ByName("filepath")
|
r.URL.Path = p.ByName("filepath")
|
||||||
fs.ServeHTTP(w, r)
|
fs.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
r.HEAD(prefix+"/res/*filepath", resourceHandler)
|
r.HEAD("/res/*filepath", resourceHandler)
|
||||||
r.OPTIONS(prefix+"/res/*filepath", resourceHandler)
|
r.OPTIONS("/res/*filepath", resourceHandler)
|
||||||
r.GET(prefix+"/res/*filepath", resourceHandler)
|
r.GET("/res/*filepath", resourceHandler)
|
||||||
|
|
||||||
// Static assets
|
// Browsers and crawlers look for these in the root
|
||||||
r.GET(prefix+"/favicon.ico" /* */, wc.serveFile("/favicon.ico"))
|
var rootFile = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fs.ServeHTTP(w, r) }
|
||||||
r.GET(prefix+"/robots.txt" /* */, wc.serveFile("/robots.txt"))
|
r.GET("/favicon.ico", rootFile)
|
||||||
|
r.GET("/robots.txt", rootFile)
|
||||||
if conf.MaintenanceMode {
|
|
||||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(r))
|
|
||||||
})
|
|
||||||
return wc
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.ProxyAPIRequests {
|
if conf.ProxyAPIRequests {
|
||||||
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
|
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
|
||||||
@@ -100,50 +87,30 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
|||||||
|
|
||||||
log.Info("Starting API proxy to %s", remoteURL)
|
log.Info("Starting API proxy to %s", remoteURL)
|
||||||
var prox = httputil.NewSingleHostReverseProxy(remoteURL)
|
var prox = httputil.NewSingleHostReverseProxy(remoteURL)
|
||||||
prox.Transport = wc.httpClient.Transport
|
|
||||||
|
|
||||||
var proxyHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
var proxyHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
log.Info("Proxying request to %s", r.URL)
|
log.Info("Proxying request to %s", r.URL)
|
||||||
r.Host = remoteURL.Host
|
r.Host = remoteURL.Host
|
||||||
r.Header.Set("Origin", remoteURL.String())
|
r.Header.Set("Origin", remoteURL.String())
|
||||||
prox.ServeHTTP(w, r)
|
prox.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
for _, method := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"} {
|
||||||
r.Handle("OPTIONS", "/api/*p", proxyHandler)
|
r.Handle(method, "/api/*p", proxyHandler)
|
||||||
r.Handle("POST", "/api/*p", proxyHandler)
|
|
||||||
r.Handle("GET", "/api/*p", proxyHandler)
|
|
||||||
r.Handle("PUT", "/api/*p", proxyHandler)
|
|
||||||
r.Handle("PATCH", "/api/*p", proxyHandler)
|
|
||||||
r.Handle("DELETE", "/api/*p", proxyHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
wc.serveTemplate("wrap")(w, r, nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Request method shorthands. These help keep the array of handlers aligned
|
|
||||||
const PST, GET = "POST", "GET"
|
|
||||||
|
|
||||||
// Loop over the handlers and register all of them in the router
|
|
||||||
for _, h := range []struct {
|
|
||||||
method string // HTTP request method this API handler uses
|
|
||||||
path string // The URL path this API will be registered on
|
|
||||||
handler httprouter.Handle // The function to run when this API is called
|
|
||||||
}{
|
|
||||||
{GET, "api" /* */, wc.serveMarkdown("api.md")},
|
|
||||||
{GET, "d/*path" /* */, wc.serveDirectory},
|
|
||||||
{GET, "widgets" /* */, wc.serveTemplate("widgets")},
|
|
||||||
{GET, "status" /* */, wc.serveTemplate("status")},
|
|
||||||
{GET, "theme.css", wc.themeHandler},
|
|
||||||
} {
|
|
||||||
r.Handle(h.method, prefix+"/"+h.path, middleware(h.handler))
|
|
||||||
|
|
||||||
// Also support HEAD requests
|
|
||||||
if h.method == GET {
|
|
||||||
r.HEAD(prefix+"/"+h.path, middleware(h.handler))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every other path is a page of the app, which shows its own not found
|
||||||
|
// message for paths it doesn't know
|
||||||
|
var page = middleware(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
w.Header().Set("X-Frame-Options", "DENY")
|
||||||
|
wc.renderPage(w, r, wc.newTemplateData(r))
|
||||||
|
})
|
||||||
|
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { page(w, r, nil) })
|
||||||
|
|
||||||
|
// Filesystem pages embed the requested node, for link previews
|
||||||
|
var directory = middleware(wc.serveDirectory)
|
||||||
|
r.GET("/d/*path", directory)
|
||||||
|
r.HEAD("/d/*path", directory)
|
||||||
|
|
||||||
return wc
|
return wc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,96 +132,6 @@ func middleware(handle httprouter.Handle) httprouter.Handle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *WebController) serveTemplate(tpl string) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
||||||
// if opts.NoEmbed {
|
|
||||||
w.Header().Set("X-Frame-Options", "DENY")
|
|
||||||
// }
|
|
||||||
|
|
||||||
err := wc.templates.Run(w, r, tpl, wc.newTemplateData(r))
|
|
||||||
if err != nil && !util.IsNetError(err) {
|
|
||||||
log.Error("Error executing template '%s': %s", tpl, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc *WebController) serveMarkdown(tpl string) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
var err error
|
|
||||||
// if opts.NoEmbed {
|
|
||||||
w.Header().Set("X-Frame-Options", "DENY")
|
|
||||||
// }
|
|
||||||
|
|
||||||
var tpld = wc.newTemplateData(r)
|
|
||||||
|
|
||||||
// Execute the raw markdown template and save the result in a buffer
|
|
||||||
var tplBuf bytes.Buffer
|
|
||||||
err = wc.templates.Run(&tplBuf, r, tpl, tpld)
|
|
||||||
if err != nil && !util.IsNetError(err) {
|
|
||||||
log.Error("Error executing template '%s': %s", tpl, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the markdown document and save the resulting HTML in a buffer
|
|
||||||
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
|
||||||
Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
|
|
||||||
})
|
|
||||||
|
|
||||||
// We parse the markdown document, walk through the nodes. Extract the
|
|
||||||
// title of the document, and the rest of the nodes are rendered like
|
|
||||||
// normal
|
|
||||||
var mdBuf bytes.Buffer
|
|
||||||
|
|
||||||
blackfriday.New(
|
|
||||||
blackfriday.WithRenderer(renderer),
|
|
||||||
blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
|
|
||||||
).Parse(
|
|
||||||
tplBuf.Bytes(),
|
|
||||||
).Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
|
||||||
// Capture the title of the document so we can put it at the top of
|
|
||||||
// the template and in the metadata. When entering a h1 node the
|
|
||||||
// first child will be the title of the document. Save that value
|
|
||||||
if node.Type == blackfriday.Heading && node.HeadingData.Level == 1 {
|
|
||||||
tpld.Title = string(node.FirstChild.Literal)
|
|
||||||
return blackfriday.SkipChildren
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this text node contains solely the text "[TOC]" then we render
|
|
||||||
// the table of contents
|
|
||||||
if node.Type == blackfriday.Text && bytes.Equal(node.Literal, []byte("[TOC]")) {
|
|
||||||
// Find the document node and render its TOC
|
|
||||||
for parent := node.Parent; ; parent = parent.Parent {
|
|
||||||
if parent.Type == blackfriday.Document {
|
|
||||||
renderer.RenderHeader(&mdBuf, parent)
|
|
||||||
return blackfriday.SkipChildren
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderer.RenderNode(&mdBuf, node, entering)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Pass the buffer's parsed contents to the wrapper template
|
|
||||||
tpld.Other = template.HTML(mdBuf.Bytes())
|
|
||||||
|
|
||||||
// Execute the wrapper template
|
|
||||||
err = wc.templates.Run(w, r, "markdown_wrapper", tpld)
|
|
||||||
if err != nil && !util.IsNetError(err) {
|
|
||||||
log.Error("Error executing template '%s': %s", tpl, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc *WebController) serveFile(path string) httprouter.Handle {
|
|
||||||
return func(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
p httprouter.Params,
|
|
||||||
) {
|
|
||||||
http.ServeFile(w, r, wc.config.ResourceDir+"/static"+path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
|
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
|
||||||
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
|
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
|
||||||
if len(cookie.Value) == 36 {
|
if len(cookie.Value) == 36 {
|
||||||
|
|||||||