├── .gitignore ├── modes ├── r.png ├── c_cpp.png ├── css.png ├── html.png ├── java.png ├── json.png ├── php.png ├── ruby.png ├── rust.png ├── text.png ├── golang.png ├── pascal.png ├── python.png ├── javascript.png ├── markdown.png └── typescript.png ├── assets ├── icon.png └── app_icon.png ├── README.md ├── Discovery.md ├── Spacefile ├── deta ├── deta.go ├── https.go ├── updater.go ├── utils.go ├── query.go ├── drive.go └── base.go ├── main.go ├── LICENSE ├── go.mod ├── static ├── app.html ├── view.html └── editor.html ├── styles ├── app.css └── style.css ├── api.go ├── scripts ├── view.js ├── app.js └── editor.js └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .space -------------------------------------------------------------------------------- /modes/r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/r.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/assets/icon.png -------------------------------------------------------------------------------- /modes/c_cpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/c_cpp.png -------------------------------------------------------------------------------- /modes/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/css.png -------------------------------------------------------------------------------- /modes/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/html.png -------------------------------------------------------------------------------- /modes/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/java.png -------------------------------------------------------------------------------- /modes/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/json.png -------------------------------------------------------------------------------- /modes/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/php.png -------------------------------------------------------------------------------- /modes/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/ruby.png -------------------------------------------------------------------------------- /modes/rust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/rust.png -------------------------------------------------------------------------------- /modes/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/text.png -------------------------------------------------------------------------------- /modes/golang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/golang.png -------------------------------------------------------------------------------- /modes/pascal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/pascal.png -------------------------------------------------------------------------------- /modes/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/python.png -------------------------------------------------------------------------------- /assets/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/assets/app_icon.png -------------------------------------------------------------------------------- /modes/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/javascript.png -------------------------------------------------------------------------------- /modes/markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/markdown.png -------------------------------------------------------------------------------- /modes/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/codebin/HEAD/modes/typescript.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codebin 2 | A code editing & sharing utility on [Deta Space](https://deta.space/discovery/@gyrooo/codebin) 3 | 4 | # Features 5 | - Syntax hilighting 6 | - Multiple cool themes 7 | - Loose hints as you type 8 | - Auto save, file navigation and deletion 9 | - Drag and drop support for multiple files 10 | -------------------------------------------------------------------------------- /Discovery.md: -------------------------------------------------------------------------------- 1 | --- 2 | app_name: "Codebin" 3 | title: "Codebin" 4 | tagline: "Personal code editor on the cloud" 5 | git: "https://github.com/jnsougata/codebin" 6 | theme_color: "#0068fb" 7 | --- 8 | 9 | Write codes on cloud, access anywhere. Control the file access & share with others. 10 | 11 | # Features 12 | - Syntax highlighting 13 | - Multiple cool themes & auto save 14 | - Auto complete & minimal suggestions 15 | - Drag and drop support for multiple files 16 | - Individual file visibility control -------------------------------------------------------------------------------- /Spacefile: -------------------------------------------------------------------------------- 1 | v: 0 2 | icon: ./assets/app_icon.png 3 | micros: 4 | - name: backend 5 | src: . 6 | engine: custom 7 | primary: true 8 | commands: 9 | - go get 10 | - go build . 11 | include: 12 | - codebin 13 | - assets/ 14 | - styles/ 15 | - scripts/ 16 | - modes/ 17 | - static/ 18 | run: ./codebin 19 | dev: go run . 20 | public_routes: 21 | - "/api/public/bin/*" 22 | - "/shared/*" 23 | - "/modes/*" 24 | - "/assets/*" 25 | - "/styles/*" 26 | - "/scripts/*" -------------------------------------------------------------------------------- /deta/deta.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type deta struct { 8 | service *service 9 | } 10 | 11 | func (d *deta) Base(name string) *base { 12 | return &base{Name: name, service: d.service} 13 | } 14 | 15 | func (d *deta) Drive(name string) *drive { 16 | return &drive{Name: name, service: d.service} 17 | } 18 | 19 | func New(key string) *deta { 20 | fragments := strings.Split(key, "_") 21 | if len(fragments) != 2 { 22 | panic("invalid project key is given") 23 | } 24 | service := service{key: key, projectId: fragments[0]} 25 | return &deta{service: &service} 26 | } 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func main() { 8 | app := gin.Default() 9 | app.GET("/", func(c *gin.Context) { 10 | c.File("static/app.html") 11 | }) 12 | app.GET("/editor/:id", func(c *gin.Context) { 13 | c.File("static/editor.html") 14 | }) 15 | app.GET("/shared/:id", func(c *gin.Context) { 16 | c.File("static/view.html") 17 | }) 18 | app.Static("/assets", "assets") 19 | app.Static("/styles", "styles") 20 | app.Static("/scripts", "scripts") 21 | app.Static("/modes", "modes") 22 | api := app.Group("/api") 23 | api.Any("/bins", bins) 24 | api.Any("/bin/:id", bin) 25 | api.GET("/public/bin/:id", public) 26 | app.Run(":8080") 27 | } 28 | -------------------------------------------------------------------------------- /deta/https.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type service struct { 10 | key string 11 | projectId string 12 | } 13 | 14 | type HttpRequest struct { 15 | Body io.Reader 16 | Method string 17 | Key string 18 | Path string 19 | } 20 | 21 | func (r *HttpRequest) Do() (*http.Response, error) { 22 | req, err := http.NewRequest(r.Method, r.Path, r.Body) 23 | req.Header.Set("Content-Type", "application/json") 24 | req.Header.Set("X-API-Key", r.Key) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return http.DefaultClient.Do(req) 29 | } 30 | 31 | type DriveRequest struct { 32 | Body []byte 33 | Method string 34 | Key string 35 | Path string 36 | } 37 | 38 | func (r *DriveRequest) Do() (*http.Response, error) { 39 | req, err := http.NewRequest(r.Method, r.Path, bytes.NewReader(r.Body)) 40 | req.Header.Set("Content-Type", "application/octet-stream") 41 | req.Header.Set("X-Api-Key", r.Key) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return http.DefaultClient.Do(req) 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sougata Jana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deta/updater.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | type Updater struct { 4 | Key string 5 | updates map[string]interface{} 6 | } 7 | 8 | func NewUpdater(key string) *Updater { 9 | if key == "" { 10 | panic("key cannot be empty") 11 | } 12 | return &Updater{ 13 | Key: key, 14 | updates: map[string]interface{}{ 15 | "delete": []string{}, 16 | "set": map[string]interface{}{}, 17 | "append": map[string]interface{}{}, 18 | "prepend": map[string]interface{}{}, 19 | "increment": map[string]interface{}{}, 20 | }, 21 | } 22 | } 23 | 24 | func (u *Updater) Set(field string, value interface{}) { 25 | u.updates["set"].(map[string]interface{})[field] = value 26 | } 27 | 28 | func (u *Updater) Delete(fields ...string) { 29 | u.updates["delete"] = append(u.updates["delete"].([]string), fields...) 30 | } 31 | 32 | func (u *Updater) Increment(field string, value interface{}) { 33 | u.updates["increment"].(map[string]interface{})[field] = value 34 | } 35 | 36 | func (u *Updater) Append(field string, value []interface{}) { 37 | u.updates["append"].(map[string]interface{})[field] = value 38 | } 39 | 40 | func (u *Updater) Prepend(field string, value []interface{}) { 41 | u.updates["prepend"].(map[string]interface{})[field] = value 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module codebin 2 | 3 | go 1.19 4 | 5 | require github.com/gin-gonic/gin v1.9.1 6 | 7 | require ( 8 | github.com/bytedance/sonic v1.10.1 // indirect 9 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 10 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 11 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 12 | github.com/gin-contrib/sse v0.1.0 // indirect 13 | github.com/go-playground/locales v0.14.1 // indirect 14 | github.com/go-playground/universal-translator v0.18.1 // indirect 15 | github.com/go-playground/validator/v10 v10.15.4 // indirect 16 | github.com/goccy/go-json v0.10.2 // indirect 17 | github.com/json-iterator/go v1.1.12 // indirect 18 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 19 | github.com/leodido/go-urn v1.2.4 // indirect 20 | github.com/mattn/go-isatty v0.0.19 // indirect 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 22 | github.com/modern-go/reflect2 v1.0.2 // indirect 23 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 24 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 25 | github.com/ugorji/go/codec v1.2.11 // indirect 26 | golang.org/x/arch v0.5.0 // indirect 27 | golang.org/x/crypto v0.13.0 // indirect 28 | golang.org/x/net v0.15.0 // indirect 29 | golang.org/x/sys v0.12.0 // indirect 30 | golang.org/x/text v0.13.0 // indirect 31 | google.golang.org/protobuf v1.31.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /deta/utils.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | type Response struct { 13 | StatusCode int 14 | Bytes []byte 15 | Error error 16 | } 17 | 18 | func (r *Response) JSON() map[string]interface{} { 19 | var data map[string]interface{} 20 | json.Unmarshal(r.Bytes, &data) 21 | return data 22 | } 23 | 24 | func (r *Response) ArrayJSON() []map[string]interface{} { 25 | var data []map[string]interface{} 26 | _ = json.Unmarshal(r.Bytes, &data) 27 | return data 28 | } 29 | 30 | func mapToReader(data interface{}) io.Reader { 31 | body, _ := json.Marshal(data) 32 | return bytes.NewReader(body) 33 | } 34 | 35 | func buildErrFromStatus(status, ok int) error { 36 | if status == ok { 37 | return nil 38 | } 39 | switch status { 40 | case 400: 41 | return errors.New("bad request") 42 | case 401: 43 | return errors.New("unauthorized") 44 | case 403: 45 | return errors.New("forbidden") 46 | case 404: 47 | return errors.New("not found") 48 | case 409: 49 | return errors.New("conflict") 50 | default: 51 | return fmt.Errorf("unknown error with status code %d", status) 52 | } 53 | } 54 | 55 | func newResponse(resp *http.Response, err error, ok int) *Response { 56 | if err != nil { 57 | return &Response{Error: err} 58 | } 59 | err = buildErrFromStatus(resp.StatusCode, ok) 60 | if err != nil { 61 | return &Response{Error: err} 62 | } 63 | ba, _ := io.ReadAll(resp.Body) 64 | return &Response{StatusCode: resp.StatusCode, Bytes: ba, Error: nil} 65 | } 66 | -------------------------------------------------------------------------------- /static/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |30 | codebin 31 |
32 |31 | codebin 32 |
33 |${innerText}
` 111 | toast.style.backgroundColor = color 112 | toast.style.display = "flex" 113 | setTimeout(() => { 114 | toast.style.display = "none" 115 | }, 3000) 116 | } 117 | -------------------------------------------------------------------------------- /deta/drive.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const driveHost = "https://drive.deta.sh/v1" 9 | const maxChunkSize = 1024 * 1024 * 10 10 | 11 | type drive struct { 12 | Name string 13 | service *service 14 | } 15 | 16 | // Put uploads the file with the given name. 17 | // If the file already exists, it is overwritten. 18 | func (d *drive) Put(name string, content []byte) *Response { 19 | if len(content) <= maxChunkSize { 20 | req := DriveRequest{ 21 | Body: content, 22 | Method: "POST", 23 | Path: fmt.Sprintf("%s/%s/%s/files?name=%s", driveHost, d.service.projectId, d.Name, name), 24 | Key: d.service.key, 25 | } 26 | resp, err := req.Do() 27 | return newResponse(resp, err, 201) 28 | } 29 | chunks := len(content) / maxChunkSize 30 | if len(content)%maxChunkSize != 0 { 31 | chunks++ 32 | } 33 | var parts [][]byte 34 | for i := 0; i < chunks; i++ { 35 | start := i * maxChunkSize 36 | end := start + maxChunkSize 37 | if end > len(content) { 38 | end = len(content) 39 | } 40 | parts = append(parts, content[start:end]) 41 | } 42 | initReq := DriveRequest{ 43 | Method: "POST", 44 | Path: fmt.Sprintf("%s/%s/%s/uploads?name=%s", driveHost, d.service.projectId, d.Name, name), 45 | Key: d.service.key, 46 | } 47 | initResp, err := initReq.Do() 48 | if err != nil { 49 | panic(err) 50 | } 51 | var resp struct { 52 | Name string `json:"name"` 53 | UploadId string `json:"upload_id"` 54 | ProjectId string `json:"project_id"` 55 | DriveName string `json:"drive_name"` 56 | } 57 | err = json.NewDecoder(initResp.Body).Decode(&resp) 58 | if err != nil { 59 | panic(err) 60 | } 61 | codes := make(chan int, len(parts)) 62 | for i, part := range parts { 63 | go func(i int, part []byte) { 64 | req := DriveRequest{ 65 | Body: part, 66 | Method: "POST", 67 | Path: fmt.Sprintf( 68 | "%s/%s/%s/uploads/%s/parts?name=%s&part=%d", 69 | driveHost, d.service.projectId, d.Name, resp.UploadId, resp.Name, i+1), 70 | Key: d.service.key, 71 | } 72 | r, _ := req.Do() 73 | codes <- r.StatusCode 74 | }(i, part) 75 | } 76 | for i := 0; i < len(parts); i++ { 77 | <-codes 78 | } 79 | for i := 0; i < len(parts); i++ { 80 | code := <-codes 81 | if code != 200 { 82 | return newResponse(nil, fmt.Errorf("error uploading part %d", i+1), 200) 83 | } 84 | } 85 | end := DriveRequest{ 86 | Method: "PATCH", 87 | Path: fmt.Sprintf( 88 | "%s/%s/%s/uploads/%s?name=%s", 89 | driveHost, d.service.projectId, d.Name, resp.UploadId, resp.Name), 90 | Key: d.service.key, 91 | } 92 | final, err := end.Do() 93 | return newResponse(final, err, 200) 94 | } 95 | 96 | // Get returns the file as ReadCloser with the given name. 97 | func (d *drive) Get(name string) *Response { 98 | req := DriveRequest{ 99 | Method: "GET", 100 | Path: fmt.Sprintf("%s/%s/%s/files/download?name=%s", driveHost, d.service.projectId, d.Name, name), 101 | Key: d.service.key, 102 | } 103 | resp, err := req.Do() 104 | return newResponse(resp, err, 200) 105 | } 106 | 107 | // Delete deletes the files with the given names. 108 | func (d *drive) Delete(names ...string) *Response { 109 | req := HttpRequest{ 110 | Method: "DELETE", 111 | Path: fmt.Sprintf("%s/%s/%s/files", driveHost, d.service.projectId, d.Name), 112 | Key: d.service.key, 113 | Body: mapToReader(map[string][]string{"names": names}), 114 | } 115 | resp, err := req.Do() 116 | return newResponse(resp, err, 200) 117 | } 118 | 119 | // Files returns all the files in the drive with the given prefix. 120 | // If prefix is empty, all files are returned. 121 | // limit <- the number of files to return, defaults to 1000. 122 | // last <- last filename of the previous request to get the next set of files. 123 | // Use limit 0 and last "" to obtain the default behaviour of the drive. 124 | func (d *drive) Files(prefix string, limit int, last string) *Response { 125 | if limit < 0 || limit > 1000 { 126 | limit = 1000 127 | } 128 | path := fmt.Sprintf("%s/%s/%s/files?limit=%d", driveHost, d.service.projectId, d.Name, limit) 129 | if prefix != "" { 130 | path += fmt.Sprintf("&prefix=%s", prefix) 131 | } 132 | if last != "" { 133 | path += fmt.Sprintf("&last=%s", last) 134 | } 135 | req := HttpRequest{ 136 | Method: "GET", 137 | Path: path, 138 | Key: d.service.key, 139 | } 140 | resp, err := req.Do() 141 | return newResponse(resp, err, 200) 142 | } 143 | -------------------------------------------------------------------------------- /deta/base.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | const baseHost = "https://database.deta.sh/v1" 11 | 12 | type Record struct { 13 | Key string 14 | Value interface{} 15 | ExpireIn int64 // in seconds 16 | ExpireOn int64 // unix timestamp 17 | } 18 | 19 | func (r *Record) marshal() interface{} { 20 | data := map[string]interface{}{} 21 | if r.Key != "" { 22 | data["key"] = r.Key 23 | } 24 | if r.ExpireIn != 0 { 25 | data["__expires"] = r.ExpireIn + time.Now().Unix() 26 | } else if r.ExpireOn != 0 { 27 | data["__expires"] = r.ExpireOn 28 | } 29 | if r.Value != nil && reflect.TypeOf(r.Value).Kind() == reflect.Map { 30 | for k, v := range r.Value.(map[string]interface{}) { 31 | data[k] = v 32 | } 33 | } else { 34 | data["value"] = r.Value 35 | } 36 | return data 37 | } 38 | 39 | type base struct { 40 | Name string 41 | service *service 42 | } 43 | 44 | func (b *base) Put(records ...Record) *Response { 45 | if len(records) > 25 { 46 | records = records[:25] 47 | } 48 | var items []interface{} 49 | for _, item := range records { 50 | items = append(items, item.marshal()) 51 | } 52 | req := HttpRequest{ 53 | Body: mapToReader(map[string]interface{}{"items": items}), 54 | Method: "PUT", 55 | Path: fmt.Sprintf("%s/%s/%s/items", baseHost, b.service.projectId, b.Name), 56 | Key: b.service.key, 57 | } 58 | resp, err := req.Do() 59 | return newResponse(resp, err, 207) 60 | } 61 | 62 | func (b *base) Get(key string) *Response { 63 | req := HttpRequest{ 64 | Body: nil, 65 | Method: "GET", 66 | Path: fmt.Sprintf("%s/%s/%s/items/%s", baseHost, b.service.projectId, b.Name, key), 67 | Key: b.service.key, 68 | } 69 | resp, err := req.Do() 70 | return newResponse(resp, err, 200) 71 | } 72 | 73 | func (b *base) Delete(key string) *Response { 74 | req := HttpRequest{ 75 | Body: nil, 76 | Method: "DELETE", 77 | Path: fmt.Sprintf("%s/%s/%s/items/%s", baseHost, b.service.projectId, b.Name, key), 78 | Key: b.service.key, 79 | } 80 | resp, err := req.Do() 81 | return newResponse(resp, err, 200) 82 | } 83 | 84 | func (b *base) Insert(record Record) *Response { 85 | req := HttpRequest{ 86 | Body: mapToReader(map[string]interface{}{"item": record.marshal()}), 87 | Method: "POST", 88 | Path: fmt.Sprintf("%s/%s/%s/items", baseHost, b.service.projectId, b.Name), 89 | Key: b.service.key, 90 | } 91 | resp, err := req.Do() 92 | return newResponse(resp, err, 201) 93 | } 94 | 95 | func (b *base) Update(updater *Updater) *Response { 96 | req := HttpRequest{ 97 | Body: mapToReader(updater.updates), 98 | Method: "PATCH", 99 | Path: fmt.Sprintf("%s/%s/%s/items/%s", baseHost, b.service.projectId, b.Name, updater.Key), 100 | Key: b.service.key, 101 | } 102 | resp, err := req.Do() 103 | return newResponse(resp, err, 200) 104 | } 105 | 106 | func (b *base) Fetch(query *Query) *Response { 107 | body := map[string]interface{}{"query": query.Value} 108 | if query.Limit <= 0 || query.Limit > 1000 { 109 | body["limit"] = 1000 110 | } else { 111 | body["limit"] = query.Limit 112 | } 113 | if query.Last != "" { 114 | body["last"] = query.Last 115 | } 116 | req := HttpRequest{ 117 | Body: mapToReader(body), 118 | Method: "POST", 119 | Path: fmt.Sprintf("%s/%s/%s/query", baseHost, b.service.projectId, b.Name), 120 | Key: b.service.key, 121 | } 122 | resp, err := req.Do() 123 | return newResponse(resp, err, 200) 124 | } 125 | 126 | func (b *base) FetchUntilEnd(query *Query) *Response { 127 | var container []map[string]interface{} 128 | resp := b.Fetch(query) 129 | if resp.Error != nil { 130 | return resp 131 | } 132 | var fetchData struct { 133 | Paging struct { 134 | Size float64 `json:"size"` 135 | Last string `json:"last"` 136 | } `json:"paging"` 137 | Items []map[string]interface{} `json:"items"` 138 | } 139 | _ = json.Unmarshal(resp.Bytes, &fetchData) 140 | container = append(container, fetchData.Items...) 141 | for { 142 | if fetchData.Paging.Last == "" { 143 | break 144 | } 145 | query.Last = fetchData.Paging.Last 146 | resp := b.Fetch(query) 147 | if resp.Error != nil { 148 | return resp 149 | } 150 | _ = json.Unmarshal(resp.Bytes, &fetchData) 151 | container = append(container, fetchData.Items...) 152 | if fetchData.Paging.Last == query.Last { 153 | break 154 | } 155 | } 156 | nb, _ := json.Marshal(container) 157 | return &Response{ 158 | Bytes: nb, 159 | StatusCode: 200, 160 | Error: nil, 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | const bins = document.querySelector('#bins'); 2 | const add = document.querySelector('#add'); 3 | 4 | 5 | function newBin(data) { 6 | let bin = document.createElement("li") 7 | let info = document.createElement("div") 8 | info.classList.add("info") 9 | let name = document.createElement("h4") 10 | name.innerText = data.name 11 | name.addEventListener("click", (e) => { 12 | e.stopPropagation() 13 | name.contentEditable = true 14 | name.focus() 15 | }) 16 | name.spellcheck = false 17 | let nameInputTimer = null 18 | name.addEventListener("input", (ev) => { 19 | if (nameInputTimer) { 20 | clearTimeout(nameInputTimer) 21 | } 22 | nameInputTimer = setTimeout(() => { 23 | if (name.innerText.length == 0) { 24 | name.innerText = "Untitled" 25 | } 26 | fetch(`/api/bins`, { 27 | method: "PATCH", 28 | body: JSON.stringify({ id: data.id, name: name.innerText }), 29 | }) 30 | }, 1000) 31 | }) 32 | let description = document.createElement("p") 33 | description.innerText = data.description 34 | description.addEventListener("click", (e) => { 35 | e.stopPropagation() 36 | description.contentEditable = true 37 | description.focus() 38 | }) 39 | description.spellcheck = false 40 | let descriptionInputTimer = null 41 | description.addEventListener("input", (ev) => { 42 | if (descriptionInputTimer) { 43 | clearTimeout(descriptionInputTimer) 44 | } 45 | descriptionInputTimer = setTimeout(() => { 46 | if (description.innerText.length == 0) { 47 | description.innerText = "No description" 48 | } 49 | if (description.innerText.length > 100) { 50 | description.innerText = description.innerText.slice(0, 100) 51 | } 52 | fetch(`/api/bins`, { 53 | method: "PATCH", 54 | body: JSON.stringify({ id: data.id, description: description.innerText }), 55 | }) 56 | }, 1000) 57 | }) 58 | info.appendChild(name) 59 | info.appendChild(description) 60 | let options = document.createElement("div") 61 | options.classList.add("options") 62 | let deleteButton = document.createElement("button") 63 | deleteButton.title = "Delete" 64 | deleteButton.innerHTML = `delete` 65 | deleteButton.addEventListener("click", () => { 66 | fetch(`/api/bins`, { 67 | method: "DELETE", 68 | body: JSON.stringify({ id: data.id }), 69 | }) 70 | .then(() => { bin.remove() }) 71 | }) 72 | let shareButton = document.createElement("button") 73 | shareButton.style.cursor = "copy" 74 | shareButton.title = "Copy Share Link" 75 | shareButton.innerHTML = `share` 76 | shareButton.addEventListener("click", () => { 77 | navigator.clipboard.writeText(`${window.location.origin}/shared/${data.id}`) 78 | }) 79 | let openButton = document.createElement("button") 80 | openButton.title = "Open Editor" 81 | openButton.innerHTML = `expand_content` 82 | openButton.addEventListener("click", () => { 83 | window.location.href = `/editor/${data.id}` 84 | }) 85 | options.appendChild(deleteButton) 86 | options.appendChild(shareButton) 87 | options.appendChild(openButton) 88 | bin.appendChild(info) 89 | bin.appendChild(options) 90 | return bin 91 | } 92 | 93 | add.addEventListener('click', () => { 94 | let bin = { 95 | name: "Untitled", 96 | description: "No description", 97 | type: "parent", 98 | id: crypto.randomUUID(), 99 | }; 100 | fetch('/api/bins', { method: "PUT", body: JSON.stringify(bin) }) 101 | .then(response => response.json()) 102 | .then(data => { 103 | if (!data) return console.error("No data returned from server") 104 | bins.appendChild(newBin(bin)) 105 | }) 106 | }) 107 | 108 | 109 | window.addEventListener('DOMContentLoaded', () => { 110 | fetch('/api/bins') 111 | .then(response => response.json()) 112 | .then(data => { 113 | if (!data) return console.error("No data returned from server") 114 | data.forEach(bin => { 115 | bins.appendChild(newBin(bin)) 116 | }) 117 | }) 118 | }); -------------------------------------------------------------------------------- /styles/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | --primary-color: #1e2227; 9 | --primary-blue: #1c4ce4; 10 | } 11 | 12 | ::-webkit-scrollbar { 13 | width: 8px; 14 | height: 8px; 15 | } 16 | 17 | ::-webkit-scrollbar-track { 18 | background: var(--primary-color); 19 | } 20 | 21 | 22 | ::-webkit-scrollbar-thumb { 23 | background: #ffffff2a; 24 | } 25 | 26 | ::-webkit-scrollbar-thumb:hover { 27 | background: rgba(24, 30, 207, 0.396); 28 | } 29 | 30 | body { 31 | background-color: var(--primary-color); 32 | font-family: 'Poppins', sans-serif; 33 | } 34 | 35 | .container { 36 | width: 100vw; 37 | height: 100vh; 38 | height: 100dvh; 39 | background-color: var(--primary-color); 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | flex-direction: column; 44 | overflow: hidden; 45 | padding: 10px; 46 | } 47 | 48 | header { 49 | width: 100%; 50 | height: 50px; 51 | display: flex; 52 | align-items: center; 53 | justify-content: space-between; 54 | color: white; 55 | border-radius: 5px 5px 0 0; 56 | background-color: var(--primary-blue); 57 | } 58 | 59 | header > p { 60 | margin: 10px; 61 | padding: 3px 6px; 62 | border-radius: 5px; 63 | cursor: pointer; 64 | font-size: larger; 65 | cursor: pointer; 66 | } 67 | 68 | header > p > a { 69 | text-decoration: none; 70 | color: white; 71 | } 72 | 73 | header > .box { 74 | width: auto; 75 | margin: 5px; 76 | display: flex; 77 | align-items: center; 78 | justify-content: flex-end; 79 | } 80 | 81 | header > .box > button { 82 | border: none; 83 | margin: 5px; 84 | cursor: pointer; 85 | background-color: transparent; 86 | } 87 | 88 | header > .box > button > i { 89 | color: white; 90 | border-radius: 5px; 91 | font-size: 20px; 92 | padding: 5px; 93 | background-color: #ffffff38; 94 | } 95 | 96 | header > .box > button > i:hover { 97 | background-color: rgba(255, 255, 255, 0.219); 98 | } 99 | 100 | .editor { 101 | width: 100%; 102 | height: 100%; 103 | display: flex; 104 | align-items: center; 105 | justify-content: center; 106 | } 107 | .editor > .renderer { 108 | width: 100%; 109 | height: 100%; 110 | } 111 | 112 | .editor > .sidebar { 113 | width: 250px; 114 | height: 100%; 115 | display: flex; 116 | align-items: center; 117 | flex-direction: column; 118 | justify-content: flex-start; 119 | overflow-y: auto; 120 | overflow-x: hidden; 121 | background-color: #23272e; 122 | } 123 | 124 | .editor > .sidebar > .item { 125 | width: 100%; 126 | height: 35px; 127 | display: flex; 128 | color: white; 129 | justify-content: flex-start; 130 | align-items: center; 131 | padding: 5px; 132 | cursor: pointer; 133 | margin-bottom: 5px; 134 | } 135 | 136 | .editor > .sidebar > .item > img { 137 | width: 15px; 138 | height: 15px; 139 | margin: 0 5px; 140 | } 141 | .editor > .sidebar > .item > p { 142 | width: 150px; 143 | white-space: nowrap; 144 | overflow: hidden; 145 | text-overflow: ellipsis; 146 | font-size: 12px; 147 | } 148 | 149 | .editor > .sidebar > .item > i { 150 | font-size: 18px; 151 | padding: 5px; 152 | color: rgba(255, 255, 255, 0.342); 153 | } 154 | 155 | footer { 156 | height: 40px; 157 | width: 100%; 158 | display: flex; 159 | align-items: center; 160 | justify-content: space-between; 161 | background-color: var(--primary-blue); 162 | color: white; 163 | border-radius: 0 0 5px 5px; 164 | overflow-x: auto; 165 | } 166 | 167 | footer .box { 168 | height: 100%; 169 | display: flex; 170 | align-items: center; 171 | } 172 | 173 | footer .box .chip { 174 | font-size: 12px; 175 | color: white; 176 | border-radius: 5px; 177 | padding: 5px 10px; 178 | cursor: pointer; 179 | white-space: nowrap; 180 | margin: 0 5px; 181 | background-color: rgba(255, 255, 255, 0.158); 182 | display: flex; 183 | align-items: center; 184 | justify-content: center; 185 | } 186 | 187 | footer .box .chip span { 188 | font-size: 15px; 189 | } 190 | 191 | .ace_mobile-menu { 192 | display: none; 193 | } 194 | 195 | .toast { 196 | top: 0; 197 | right: 0; 198 | margin-top: 60px; 199 | margin-right: 10px; 200 | margin-left: auto; 201 | display: none; 202 | align-items: center; 203 | justify-content: center; 204 | position: fixed; 205 | width: 250px; 206 | max-width: 400px; 207 | height: 35px; 208 | overflow: hidden; 209 | background-color: #c32c59; 210 | animation: popup 0.5s; 211 | z-index: 1000 !important; 212 | } 213 | 214 | .toast > p { 215 | padding: 5px; 216 | white-space: nowrap; 217 | overflow: hidden; 218 | text-overflow: ellipsis; 219 | font-size: 16px; 220 | color: whitesmoke; 221 | transition: 0.5s; 222 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 3 | github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= 4 | github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 5 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 8 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 9 | github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= 10 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 15 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 16 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 17 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 18 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 19 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 20 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 21 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs= 27 | github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 28 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 29 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 30 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 31 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 32 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 34 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 35 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 36 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 37 | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= 38 | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 39 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 40 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 41 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 42 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 43 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 44 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 48 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 49 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 50 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 55 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 56 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 57 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 58 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 60 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 61 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 62 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 64 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 65 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 66 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 67 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 68 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 69 | golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= 70 | golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 71 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 72 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 73 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 74 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 75 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 78 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 80 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 84 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 85 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 90 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 91 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 92 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 93 | -------------------------------------------------------------------------------- /scripts/editor.js: -------------------------------------------------------------------------------- 1 | var editor = ace.edit("main"); 2 | var modelist = ace.require("ace/ext/modelist"); 3 | let editorStatusElem = document.querySelector("#editor-status") 4 | let editorLinkElem = document.querySelector("#editor-link") 5 | let editorModeElem = document.querySelector("#editor-mode") 6 | let editorThemeElem = document.querySelector("#editor-theme") 7 | let sidebar = document.querySelector(".sidebar") 8 | let toast = document.querySelector(".toast") 9 | let defaultId = null; 10 | let globalBinId = ""; 11 | let globalContextFile = null; 12 | const maxFileSize = 400 * 1024 13 | const primaryBlue = "#1c4ce4" 14 | const toastGreen = "#27ab5a" 15 | const toastRed = "#c32c59" 16 | const menu = document.querySelector("#menu") 17 | const visibilty = document.querySelector("#visibility") 18 | 19 | 20 | let sidebarHidden = false 21 | menu.addEventListener("click", () => { 22 | if (sidebarHidden) { 23 | sidebar.style.display = "flex" 24 | sidebarHidden = false 25 | } else { 26 | sidebar.style.display = "none" 27 | sidebarHidden = true 28 | } 29 | }) 30 | 31 | editor.setOptions({ 32 | fontSize: "15pt", 33 | copyWithEmptySelection: true, 34 | enableLiveAutocompletion: true, 35 | showPrintMargin: false, 36 | }); 37 | editor.setTheme("ace/theme/one_dark"); 38 | 39 | 40 | window.addEventListener("DOMContentLoaded", () => { 41 | globalBinId = window.location.pathname.split("/")[2] 42 | editorThemeElem.innerHTML = `One Dark`; 43 | editorLinkElem.innerHTML = `${window.location.origin}/shared/${globalBinId}`; 44 | fetch(`/api/bin/${globalBinId}`) 45 | .then(response => response.json()) 46 | .then(data => { 47 | if (!data) return console.error("No data returned from server") 48 | data.forEach(file => { 49 | let sidebarItem = newFile(file) 50 | sidebar.appendChild(sidebarItem) 51 | }) 52 | sidebar.firstElementChild.click() 53 | }) 54 | }); 55 | 56 | function modeToLabel(mode) { 57 | return String(mode).split("/")[2] 58 | } 59 | 60 | function updateLangMode(mode) { 61 | editorModeElem.innerHTML = `${modeToLabel(mode).toUpperCase()}`; 62 | } 63 | 64 | function generateRandomId() { 65 | return crypto.getRandomValues(new Uint32Array(1))[0].toString(16) 66 | } 67 | 68 | function modeToIcon(mode) { 69 | return `/modes/${mode.toLowerCase()}.png` 70 | } 71 | 72 | visibilty.addEventListener("click", () => { 73 | if (globalContextFile.access && globalContextFile.access !== "public") { 74 | globalContextFile.access = "public"; 75 | visibilty.innerHTML = "lock_open"; 76 | } else { 77 | globalContextFile.access = "private"; 78 | visibilty.innerHTML = "lock"; 79 | } 80 | saveButton.click(); 81 | }) 82 | 83 | let nameInputTimer = null; 84 | let previouslyClickedItem = null; 85 | function newFile(file) { 86 | let sidebarItem = document.createElement("div") 87 | sidebarItem.className = "item" 88 | sidebarItem.id = `item-${file.id}`; 89 | sidebarItem.addEventListener("click", () => { 90 | if (previouslyClickedItem) { 91 | previouslyClickedItem.style.backgroundColor = `transparent`; 92 | previouslyClickedItem.style.border = `none`; 93 | } 94 | if (file.access && file.access !== "public") { 95 | visibilty.innerHTML = "lock"; 96 | } else { 97 | visibilty.innerHTML = "lock_open"; 98 | } 99 | previouslyClickedItem = sidebarItem 100 | previouslyClickedItem.style.backgroundColor = `rgba(255, 255, 255, 0.075)`; 101 | previouslyClickedItem.style.border = `1px solid rgba(255, 255, 255, 0.062)`; 102 | globalContextFile = file 103 | editor.session.setMode(file.mode) 104 | updateLangMode(file.mode) 105 | editor.setValue(file.value) 106 | saveButton.click() 107 | }) 108 | let icon = document.createElement("img") 109 | icon.src = modeToIcon(modeToLabel(file.mode)) 110 | let name = document.createElement("p") 111 | name.innerHTML = file.name 112 | name.addEventListener("click", (e) => { 113 | name.contentEditable = true 114 | }) 115 | name.spellcheck = false 116 | name.addEventListener("input", (ev) => { 117 | if (nameInputTimer) { 118 | clearTimeout(nameInputTimer) 119 | } 120 | nameInputTimer = setTimeout(() => { 121 | editorStatusElem.style.display = "none" 122 | let mode = modelist.getModeForPath(ev.target.innerHTML).mode; 123 | editor.session.setMode(mode); 124 | updateLangMode(mode) 125 | file.name = ev.target.innerHTML 126 | file.mode = mode 127 | icon.src = modeToIcon(modeToLabel(mode)) 128 | saveButton.click() 129 | editorStatusElem.style.display = "block" 130 | }, 1000) 131 | }) 132 | let share = document.createElement("i") 133 | share.className = "material-symbols-outlined" 134 | share.innerHTML = "link" 135 | share.addEventListener("click", (e) => { 136 | e.stopPropagation() 137 | navigator.clipboard.writeText(`${window.location.origin}/shared/${file.id}`) 138 | showToast("File Link copied to clipboard", toastGreen) 139 | }) 140 | sidebarItem.appendChild(icon) 141 | sidebarItem.appendChild(name) 142 | sidebarItem.appendChild(share) 143 | return sidebarItem 144 | } 145 | 146 | let themeCounter = 0 147 | editorThemeElem.addEventListener("click", function() { 148 | var themes = ace.require("ace/ext/themelist").themes 149 | themes.reverse() 150 | if (themeCounter == themes.length - 1) { 151 | themeCounter = 0 152 | } else { 153 | themeCounter++ 154 | } 155 | const theme = themes[themeCounter] 156 | editor.setTheme(`ace/theme/${theme.name}`) 157 | editorThemeElem.innerHTML = `${theme.caption}` 158 | }) 159 | 160 | let saveButton = document.getElementById("save") 161 | saveButton.addEventListener("click", () => { 162 | editorStatusElem.style.display = "none" 163 | let bodyString = JSON.stringify(globalContextFile) 164 | let encoder = new TextEncoder(); 165 | if (encoder.encode(bodyString).length < maxFileSize) { 166 | fetch(`/api/bin/${globalContextFile.id}`, {method: "PUT", body: bodyString}) 167 | .then((response) => { 168 | if (response.status == 207) { 169 | editorStatusElem.style.display = "block" 170 | } 171 | }) 172 | } else { 173 | showToast("File size exceeded 400KB", toastRed) 174 | } 175 | }) 176 | 177 | let newButton = document.getElementById("new") 178 | newButton.addEventListener("click", () => { 179 | let file = { 180 | id: generateRandomId(), 181 | mode: 'ace/mode/text', 182 | name: "untitled", 183 | value: "", 184 | parent: globalBinId 185 | } 186 | let sidebarItem = newFile(file) 187 | sidebar.appendChild(sidebarItem) 188 | sidebarItem.click() 189 | }) 190 | 191 | function dropHandler(ev) { 192 | ev.preventDefault(); 193 | if (ev.dataTransfer.items) { 194 | [...ev.dataTransfer.items].forEach((item, _) => { 195 | if (item.kind === 'file') { 196 | const file = item.getAsFile(); 197 | var reader = new FileReader(); 198 | reader.onload = function(e) { 199 | let sidebarItem = newFile({ 200 | id: generateRandomId(), 201 | mode: modelist.getModeForPath(file.name).mode, 202 | name: file.name, 203 | value: e.target.result, 204 | parent: globalBinId 205 | }) 206 | sidebar.appendChild(sidebarItem) 207 | sidebarItem.click() 208 | } 209 | reader.readAsText(file); 210 | } 211 | }) 212 | } 213 | } 214 | 215 | let editorWindow = document.querySelector(".editor") 216 | editorWindow.ondrop = (e) => { 217 | dropHandler(e) 218 | } 219 | editorWindow.ondragover = (e) => { 220 | e.preventDefault() 221 | } 222 | 223 | // bottom bar link callback 224 | editorLinkElem.addEventListener("click", () => { 225 | saveButton.click() 226 | navigator.clipboard.writeText(editorLinkElem.innerText) 227 | showToast("Link copied to clipboard", toastGreen) 228 | }) 229 | 230 | // listen for edit events 231 | let autosaveTimer = null; 232 | let editorTextInput = document.getElementsByClassName("ace_text-input")[0] 233 | editorTextInput.addEventListener("keydown", (e) => { 234 | if (autosaveTimer) { 235 | clearTimeout(autosaveTimer); 236 | } 237 | autosaveTimer = setTimeout(function() { 238 | globalContextFile.value = editor.getValue() 239 | saveButton.click() 240 | }, 1000); 241 | }) 242 | 243 | // check keydown event 244 | document.addEventListener("keydown", function(e) { 245 | if (e.key == "Delete") { 246 | trashButton.click() 247 | } 248 | }); 249 | 250 | // handle delete button 251 | let trashButton = document.getElementById("trash") 252 | trashButton.addEventListener("click", () => { 253 | let sidebarItem = document.getElementById(`item-${globalContextFile.id}`) 254 | if (sidebarItem) { 255 | fetch(`/api/bin/${globalContextFile.id}`, {method: "DELETE"}) 256 | .then(() => { sidebarItem.remove() }) 257 | } 258 | }) 259 | 260 | // handle file upload 261 | let filesElement = document.getElementById("files") 262 | filesElement.addEventListener("change", () => { 263 | let file = filesElement.files[0] 264 | let mode = modelist.getModeForPath(file.name).mode; 265 | editor.session.setMode(mode); 266 | updateLangMode(mode) 267 | var reader = new FileReader(); 268 | reader.onload = (e) => { 269 | let sidebarItem = newFile({ 270 | id: generateRandomId(), 271 | mode: mode, 272 | name: file.name, 273 | value: e.target.result, 274 | parent: globalBinId 275 | }) 276 | sidebar.appendChild(sidebarItem) 277 | sidebarItem.click() 278 | } 279 | reader.readAsText(file); 280 | }) 281 | 282 | //handle upload button 283 | let uploadButton = document.getElementById("upload") 284 | uploadButton.addEventListener("click", () => { 285 | filesElement.click() 286 | }) 287 | 288 | function showToast(innerText, color="#1c4ce4") { 289 | toast.innerHTML = `${innerText}
` 290 | toast.style.backgroundColor = color 291 | toast.style.display = "flex" 292 | setTimeout(() => { 293 | toast.style.display = "none" 294 | }, 3000) 295 | } 296 | --------------------------------------------------------------------------------