├── README.md ├── .gitignore ├── js ├── lib │ ├── Property.coffee │ ├── Dollar.coffee │ ├── Prototypes.coffee │ ├── Class.coffee │ ├── Promise.coffee │ ├── RateLimitCb.coffee │ ├── anime.min.js │ └── maquette.js ├── clone.js ├── utils │ ├── ItemList.coffee │ ├── Time.coffee │ ├── Autosize.coffee │ ├── Editable.coffee │ ├── Menu.coffee │ ├── ZeroFrame.coffee │ ├── Text.coffee │ └── Animation.coffee ├── Bg.coffee ├── Uploader.coffee ├── ZeroUp.coffee ├── List.coffee ├── Selector.coffee └── File.coffee ├── css ├── Editable.css ├── Animation.css ├── Menu.css ├── Button.css ├── icons.css └── ZeroUp.css ├── data └── users │ └── content.json ├── dbschema.json ├── index.html ├── content.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # ZeroUp 2 | Demo site for big file support 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hidden files 2 | .* 3 | !/.gitignore 4 | !/.travis.yml 5 | 6 | 7 | # Database 8 | *.db 9 | -------------------------------------------------------------------------------- /js/lib/Property.coffee: -------------------------------------------------------------------------------- 1 | Function::property = (prop, desc) -> 2 | Object.defineProperty @prototype, prop, desc 3 | -------------------------------------------------------------------------------- /js/lib/Dollar.coffee: -------------------------------------------------------------------------------- 1 | window.$ = (selector) -> 2 | if selector.startsWith("#") 3 | return document.getElementById(selector.replace("#", "")) 4 | -------------------------------------------------------------------------------- /js/clone.js: -------------------------------------------------------------------------------- 1 | function clone(obj) { 2 | if (null == obj || "object" != typeof obj) return obj; 3 | var copy = obj.constructor(); 4 | for (var attr in obj) { 5 | if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); 6 | } 7 | return copy; 8 | } -------------------------------------------------------------------------------- /js/lib/Prototypes.coffee: -------------------------------------------------------------------------------- 1 | String::startsWith = (s) -> @[...s.length] is s 2 | String::endsWith = (s) -> s is '' or @[-s.length..] is s 3 | String::repeat = (count) -> new Array( count + 1 ).join(@) 4 | 5 | window.isEmpty = (obj) -> 6 | for key of obj 7 | return false 8 | return true 9 | -------------------------------------------------------------------------------- /js/lib/Class.coffee: -------------------------------------------------------------------------------- 1 | class Class 2 | trace: true 3 | 4 | log: (args...) -> 5 | return unless @trace 6 | return if typeof console is 'undefined' 7 | args.unshift("[#{@.constructor.name}]") 8 | console.log(args...) 9 | @ 10 | 11 | logStart: (name, args...) -> 12 | return unless @trace 13 | @logtimers or= {} 14 | @logtimers[name] = +(new Date) 15 | @log "#{name}", args..., "(started)" if args.length > 0 16 | @ 17 | 18 | logEnd: (name, args...) -> 19 | ms = +(new Date)-@logtimers[name] 20 | @log "#{name}", args..., "(Done in #{ms}ms)" 21 | @ 22 | 23 | window.Class = Class -------------------------------------------------------------------------------- /js/utils/ItemList.coffee: -------------------------------------------------------------------------------- 1 | class ItemList 2 | constructor: (@item_class, @key) -> 3 | @items = [] 4 | @items_bykey = {} 5 | 6 | sync: (rows, item_class, key) -> 7 | @items.splice(0, @items.length) # Empty items 8 | for row in rows 9 | current_obj = @items_bykey[row[@key]] 10 | if current_obj 11 | current_obj.setRow(row) 12 | @items.push current_obj 13 | else 14 | item = new @item_class(row, @) 15 | @items_bykey[row[@key]] = item 16 | @items.push item 17 | 18 | deleteItem: (item) -> 19 | index = @items.indexOf(item) 20 | if index > -1 21 | @items.splice(index, 1) 22 | else 23 | console.log "Can't delete item", item 24 | delete @items_bykey[item.row[@key]] 25 | 26 | window.ItemList = ItemList -------------------------------------------------------------------------------- /css/Editable.css: -------------------------------------------------------------------------------- 1 | .editable .icon-edit { margin-left: -24px; padding: 7px; border-radius: 30px; margin-top: -5px; position: absolute; opacity: 0; transition: all 0.3s } 2 | .editable:hover .icon-edit { opacity: 0.7 } 3 | .editable .icon-edit:hover { opacity: 1; transition: none } 4 | .editable .editablebuttons { text-align: right; padding-bottom: 10px } 5 | .editable .empty { opacity: 0.6; } 6 | .editable.editing.overlay { 7 | padding: 10px; margin-left: -10px; background-color: rgba(255,255,255,0.9); 8 | box-shadow: 0px 0px 20px #EEE; margin-bottom: -62px; z-index: 999; position: relative; 9 | } 10 | .editable .editablebuttons .cancel { color: #6f6f6f; font-size 14px } 11 | .editable .icon-edit:hover + .body { background-color: #FFEB3B; outline: 4px solid #FFEB3B; box-shadow: 0px 0px 15px #929292 } 12 | -------------------------------------------------------------------------------- /data/users/content.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1uPLoaDwKzP6MCGoVzw48r4pxawRBdmQc", 3 | "files": {}, 4 | "ignore": ".*", 5 | "inner_path": "data/users/content.json", 6 | "modified": 1507230654, 7 | "signs": { 8 | "1uPLoaDwKzP6MCGoVzw48r4pxawRBdmQc": "HEju2E4GBXhztno4loOBEnRwOnicV5P4rDKj0eLAoYecPXvs7rpUKjJ8hgUIZec6JJczNZmeAnsm5dfcPd8VN68=" 9 | }, 10 | "user_contents": { 11 | "cert_signers": { 12 | "zeroid.bit": ["1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz"] 13 | }, 14 | "permission_rules": { 15 | ".*": { 16 | "files_allowed": "data.json", 17 | "max_size": 20000, 18 | "max_size_optional": 1000000000 19 | }, 20 | "bitid/.*@zeroid.bit": {"max_size": 40000}, 21 | "bitmsg/.*@zeroid.bit": {"max_size": 15000} 22 | }, 23 | "permissions": { 24 | "bad@zeroid.bit": false, 25 | "nofish@zeroid.bit": { 26 | "max_size": 100000, 27 | "max_size_optional": 10000000000 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /dbschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "ZeroUp", 3 | "db_file": "data/users/zeroup.db", 4 | "version": 2, 5 | "maps": { 6 | ".+/content.json": { 7 | "to_json_table": [ "cert_auth_type", "cert_user_id" ], 8 | "file_name": "data.json" 9 | }, 10 | ".+/data.json": { 11 | "to_table": [ 12 | {"node": "file", "table": "file", "key_col": "file_name"} 13 | ], 14 | "to_keyvalue": ["next_comment_id", "next_topic_id"] 15 | } 16 | }, 17 | "tables": { 18 | "json": { 19 | "cols": [ 20 | ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], 21 | ["directory", "TEXT"], 22 | ["file_name", "TEXT"], 23 | ["cert_auth_type", "TEXT"], 24 | ["cert_user_id", "TEXT"] 25 | ], 26 | "indexes": ["CREATE UNIQUE INDEX path ON json(directory, file_name)"], 27 | "schema_changed": 5 28 | }, 29 | "file": { 30 | "cols": [ 31 | ["file_name", "TEXT"], 32 | ["title", "TEXT"], 33 | ["size", "INT"], 34 | ["date_added", "DATETIME"], 35 | ["json_id", "INTEGER REFERENCES json (json_id)"] 36 | ], 37 | "indexes": ["CREATE UNIQUE INDEX file_key ON file(date_added, json_id)"], 38 | "schema_changed": 1 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /js/utils/Time.coffee: -------------------------------------------------------------------------------- 1 | class Time 2 | since: (timestamp) -> 3 | now = +(new Date)/1000 4 | if timestamp > 1000000000000 # In ms 5 | timestamp = timestamp/1000 6 | secs = now - timestamp 7 | if secs < 60 8 | back = "Just now" 9 | else if secs < 60*60 10 | back = "#{Math.round(secs/60)} minutes ago" 11 | else if secs < 60*60*24 12 | back = "#{Math.round(secs/60/60)} hours ago" 13 | else if secs < 60*60*24*3 14 | back = "#{Math.round(secs/60/60/24)} days ago" 15 | else 16 | back = "on "+@date(timestamp) 17 | back = back.replace(/^1 ([a-z]+)s/, "1 $1") # 1 days ago fix 18 | return back 19 | 20 | 21 | date: (timestamp, format="short") -> 22 | if timestamp > 1000000000000 # In ms 23 | timestamp = timestamp/1000 24 | parts = (new Date(timestamp*1000)).toString().split(" ") 25 | if format == "short" 26 | display = parts.slice(1, 4) 27 | else 28 | display = parts.slice(1, 5) 29 | return display.join(" ").replace(/( [0-9]{4})/, ",$1") 30 | 31 | 32 | timestamp: (date="") -> 33 | if date == "now" or date == "" 34 | return parseInt(+(new Date)/1000) 35 | else 36 | return parseInt(Date.parse(date)/1000) 37 | 38 | 39 | window.Time = new Time -------------------------------------------------------------------------------- /css/Animation.css: -------------------------------------------------------------------------------- 1 | .animate { transition: all 0.3s ease-out !important; } 2 | .animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } 3 | .animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } 4 | .animate-inback { transition: all 0.6s cubic-bezier(0.6, -0.28, 0.735, 0.045) !important; } 5 | .animate-in { transition: all 0.6s cubic-bezier(0.6, 0.04, 0.98, 0.335) !important; } 6 | .animate-out { transition: all 0.6s ease-out !important; } 7 | 8 | @keyframes flash-in { 9 | 0% { transform: scale(1.5); opacity: 0 } 10 | 80% { transform: scale(1); opacity: 1 } 11 | 100% { transform: scale(1); opacity: 0 } 12 | } 13 | 14 | @keyframes flash-in-big { 15 | 0% { transform: scale(1.2); opacity: 0 } 16 | 80% { transform: scale(1); opacity: 1 } 17 | 100% { transform: scale(1); opacity: 0 } 18 | } 19 | 20 | @keyframes flash-out { 21 | 0% { transform: scale(1); opacity: 1 } 22 | 100% { transform: scale(1.5); opacity: 0 } 23 | } 24 | @keyframes flash-out-big { 25 | 0% { transform: scale(1); opacity: 1 } 26 | 100% { transform: scale(1.2); opacity: 0 } 27 | } 28 | 29 | @keyframes bounce { 30 | 0% { transform: translateY(0); opacity: 1 } 31 | 100% { transform: translateY(-3px); opacity: 0.7 } 32 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ZeroUp! 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
Drop here to upload
17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /css/Menu.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | background-color: white; padding: 10px 0px; position: absolute; top: 0px; max-height: 0px; overflow: hidden; transform: translate(-100%, -30px); pointer-events: none; 3 | box-shadow: 0px 2px 8px rgba(0,0,0,0.1); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; z-index: 99; 4 | display: inline-block; z-index: 999; margin-top: 50px; 5 | } 6 | .menu-right { left: 100% } 7 | .menu.visible { opacity: 1; max-height: 350px; transform: translate(-100%, 0px); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; pointer-events: all } 8 | 9 | .menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; padding-left: 32px; white-space: nowrap; font-size: 16px } 10 | .menu-item-separator { margin-top: 3px; margin-bottom: 3px; border-top: 1px solid #eee } 11 | 12 | .menu-item:hover { background-color: #F6F6F6; transition: none; color: inherit; cursor: pointer; color: black; text-decoration: none } 13 | .menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; transition: none } 14 | .menu-item.selected:before { 15 | content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1); line-height: 15px; 16 | font-weight: bold; position: absolute; margin-left: -14px; font-size: 12px; margin-top: 2px; 17 | } -------------------------------------------------------------------------------- /content.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1uPLoaDwKzP6MCGoVzw48r4pxawRBdmQc", 3 | "address_index": 89091714, 4 | "background-color": "#FFF", 5 | "clone_root": "template-new", 6 | "cloned_from": "1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D", 7 | "description": "", 8 | "files": { 9 | "css/all.css": { 10 | "sha512": "dfc5bf0f5bfb30c56a0a32adc400b42ccd90a9a795ed122f0310330846c7a174", 11 | "size": 170152 12 | }, 13 | "dbschema.json": { 14 | "sha512": "596519604f8d34d750da86ada7ab618499b9257e2db30af546725d8784f70e3d", 15 | "size": 978 16 | }, 17 | "index.html": { 18 | "sha512": "86db9821b7bb669135416cefc255f57ea9f703e3c4061c184a4bb05751e8ef5d", 19 | "size": 1124 20 | }, 21 | "js/all.js": { 22 | "sha512": "d5eb71fd4717f2622a7081cad90783b4e9db91c08a49fcdf66037d933c56ff7e", 23 | "size": 125731 24 | } 25 | }, 26 | "ignore": "((js|css)/(?!all.(js|css))|data/.*)", 27 | "includes": { 28 | "data/users/content.json": { 29 | "signers": [], 30 | "signers_required": 1 31 | } 32 | }, 33 | "inner_path": "content.json", 34 | "modified": 1507240116, 35 | "postmessage_nonce_security": true, 36 | "signers_sign": "G92h5e3FmtEMZgSdewI3esFkw8ny/m9KuSQ0IwFgVMcafUaepy2HF0oiCYmYd2CVmZHGF45WAxBz6zxJEU1AngE=", 37 | "signs": { 38 | "1uPLoaDwKzP6MCGoVzw48r4pxawRBdmQc": "HKVyKiAdLbBa/3cwQ/aT50hHjIKI6dUKEkuEOZlymY+uU3Y1tEcsEzRMB39rEsvm/1CjXh+e02DWXRpVGm4fOaA=" 39 | }, 40 | "signs_required": 1, 41 | "title": "ZeroUp", 42 | "translate": ["js/all.js"], 43 | "zeronet_version": "0.6.0" 44 | } -------------------------------------------------------------------------------- /js/utils/Autosize.coffee: -------------------------------------------------------------------------------- 1 | class Autosize extends Class 2 | constructor: (@attrs={}) -> 3 | @node = null 4 | 5 | @attrs.classes ?= {} 6 | @attrs.classes.loading = false 7 | @attrs.oninput = @handleInput 8 | @attrs.onkeydown = @handleKeydown 9 | @attrs.afterCreate = @storeNode 10 | @attrs.rows = 1 11 | @attrs.disabled = false 12 | 13 | @property 'loading', 14 | get: -> @attrs.classes.loading 15 | set: (loading) -> 16 | @attrs.classes.loading = loading 17 | @node.value = @attrs.value 18 | @autoHeight() 19 | Page.projector.scheduleRender() 20 | 21 | storeNode: (node) => 22 | @node = node 23 | if @attrs.focused 24 | node.focus() 25 | setTimeout => 26 | @autoHeight() 27 | 28 | setValue: (value=null) => 29 | @attrs.value = value 30 | if @node 31 | @node.value = value 32 | @autoHeight() 33 | Page.projector.scheduleRender() 34 | 35 | autoHeight: => 36 | height_before = @node.style.height 37 | if height_before 38 | @node.style.height = "0px" 39 | h = @node.offsetHeight 40 | scrollh = @node.scrollHeight 41 | @node.style.height = height_before 42 | if scrollh > h 43 | anime({targets: @node, height: scrollh, scrollTop: 0}) 44 | else 45 | @node.style.height = height_before 46 | 47 | handleInput: (e=null) => 48 | @attrs.value = e.target.value 49 | RateLimit 300, @autoHeight 50 | 51 | handleKeydown: (e=null) => 52 | if e.which == 13 and not e.shiftKey and @attrs.onsubmit and @attrs.value.trim() 53 | @attrs.onsubmit() 54 | setTimeout ( => 55 | @autoHeight() 56 | ), 100 57 | return false 58 | 59 | render: (body=null) => 60 | if body and @attrs.value == undefined 61 | @setValue(body) 62 | if @loading 63 | attrs = clone(@attrs) 64 | #attrs.value = "Submitting..." 65 | attrs.disabled = true 66 | h("textarea.autosize", attrs) 67 | else 68 | h("textarea.autosize", @attrs) 69 | 70 | window.Autosize = Autosize -------------------------------------------------------------------------------- /js/utils/Editable.coffee: -------------------------------------------------------------------------------- 1 | class Editable extends Class 2 | constructor: (@type, @handleSave, @handleDelete) -> 3 | @node = null 4 | @editing = false 5 | @render_function = null 6 | @empty_text = "Click here to edit this field" 7 | 8 | storeNode: (node) => 9 | @node = node 10 | 11 | handleEditClick: (e) => 12 | @editing = true 13 | @field_edit = new Autosize({focused: 1, style: "height: 0px"}) 14 | return false 15 | 16 | handleCancelClick: => 17 | @editing = false 18 | return false 19 | 20 | handleDeleteClick: => 21 | Page.cmd "wrapperConfirm", ["Are you sure?", "Delete"], => 22 | @field_edit.loading = true 23 | @handleDelete (res) => 24 | @field_edit.loading = false 25 | return false 26 | 27 | handleSaveClick: => 28 | @field_edit.loading = true 29 | @handleSave @field_edit.attrs.value, (res) => 30 | @field_edit.loading = false 31 | if res 32 | @editing = false 33 | return false 34 | 35 | render: (body) => 36 | if @editing 37 | return h("div.editable.editing", {exitAnimation: Animation.slideUp}, 38 | @field_edit.render(body), 39 | h("div.editablebuttons", 40 | h("a.link.cancel", {href: "#Cancel", onclick: @handleCancelClick, tabindex: "-1"}, "Cancel"), 41 | if @handleDelete 42 | h("a.button.button-submit.button-small.button-outline", {href: "#Delete", onclick: @handleDeleteClick, tabindex: "-1"}, "Delete") 43 | h("a.button.button-submit.button-small", {href: "#Save", onclick: @handleSaveClick}, "Save") 44 | ) 45 | ) 46 | else 47 | return h("div.editable", {enterAnimation: Animation.slideDown}, 48 | h("a.icon.icon-edit", {key: @node, href: "#Edit", onclick: @handleEditClick}), 49 | if not body 50 | h(@type, h("span.empty", {onclick: @handleEditClick}, @empty_text)) 51 | else if @render_function 52 | h(@type, {innerHTML: @render_function(body)}) 53 | else 54 | h(@type, body) 55 | ) 56 | 57 | window.Editable = Editable -------------------------------------------------------------------------------- /js/lib/Promise.coffee: -------------------------------------------------------------------------------- 1 | class Promise 2 | @join: (tasks...) -> 3 | num_uncompleted = tasks.length 4 | args = new Array(num_uncompleted) 5 | promise = new Promise() 6 | 7 | for task, task_id in tasks 8 | ((task_id) -> 9 | task.then(() -> 10 | args[task_id] = Array.prototype.slice.call(arguments) 11 | num_uncompleted-- 12 | if num_uncompleted == 0 13 | for callback in promise.callbacks 14 | callback.apply(promise, args) 15 | ) 16 | )(task_id) 17 | 18 | return promise 19 | 20 | constructor: -> 21 | @resolved = false 22 | @end_promise = null 23 | @result = null 24 | @callbacks = [] 25 | 26 | resolve: -> 27 | if @resolved 28 | return false 29 | @resolved = true 30 | @data = arguments 31 | if not arguments.length 32 | @data = [true] 33 | @result = @data[0] 34 | for callback in @callbacks 35 | back = callback.apply callback, @data 36 | if @end_promise and back and back.then 37 | back.then (back_res) => 38 | @end_promise.resolve(back_res) 39 | 40 | fail: -> 41 | @resolve(false) 42 | 43 | then: (callback) -> 44 | if @resolved == true 45 | return callback.apply callback, @data 46 | 47 | @callbacks.push callback 48 | 49 | @end_promise = new Promise() 50 | return @end_promise 51 | 52 | window.Promise = Promise 53 | 54 | ### 55 | s = Date.now() 56 | log = (text) -> 57 | console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") 58 | 59 | log "Started" 60 | 61 | cmd = (query) -> 62 | p = new Promise() 63 | setTimeout ( -> 64 | p.resolve query+" Result" 65 | ), 100 66 | return p 67 | 68 | 69 | back = cmd("SELECT * FROM message").then (res) -> 70 | log res 71 | p = new Promise() 72 | setTimeout ( -> 73 | p.resolve("DONE parsing SELECT") 74 | ), 100 75 | return p 76 | .then (res) -> 77 | log "Back of messages", res 78 | return cmd("SELECT * FROM users") 79 | .then (res) -> 80 | log "End result", res 81 | 82 | log "Query started", back 83 | 84 | 85 | q1 = cmd("SELECT * FROM anything") 86 | q2 = cmd("SELECT * FROM something") 87 | 88 | Promise.join(q1, q2).then (res1, res2) -> 89 | log res1, res2 90 | ### -------------------------------------------------------------------------------- /js/utils/Menu.coffee: -------------------------------------------------------------------------------- 1 | class Menu 2 | constructor: -> 3 | @visible = false 4 | @items = [] 5 | @node = null 6 | 7 | show: => 8 | window.visible_menu?.hide() 9 | @visible = true 10 | window.visible_menu = @ 11 | 12 | hide: => 13 | @visible = false 14 | 15 | toggle: => 16 | if @visible 17 | @hide() 18 | else 19 | @show() 20 | Page.projector.scheduleRender() 21 | 22 | 23 | addItem: (title, cb, selected=false) -> 24 | @items.push([title, cb, selected]) 25 | 26 | 27 | storeNode: (node) => 28 | @node = node 29 | # Animate visible 30 | if @visible 31 | node.className = node.className.replace("visible", "") 32 | setTimeout (-> 33 | node.className += " visible" 34 | ), 10 35 | 36 | handleClick: (e) => 37 | keep_menu = false 38 | for item in @items 39 | [title, cb, selected] = item 40 | if title == e.target.textContent 41 | keep_menu = cb(item) 42 | if keep_menu != true 43 | @hide() 44 | return false 45 | 46 | renderItem: (item) => 47 | [title, cb, selected] = item 48 | if typeof(selected) == "function" 49 | selected = selected() 50 | if title == "---" 51 | h("div.menu-item-separator") 52 | else 53 | if typeof(cb) == "string" # Url 54 | href = cb 55 | onclick = true 56 | else # Callback 57 | href = "#"+title 58 | onclick = @handleClick 59 | h("a.menu-item", {href: href, onclick: onclick, key: title, classes: {"selected": selected}}, [title]) 60 | 61 | render: (class_name="") => 62 | if @visible or @node 63 | h("div.menu#{class_name}", {classes: {"visible": @visible}, afterCreate: @storeNode}, @items.map(@renderItem)) 64 | 65 | window.Menu = Menu 66 | 67 | # Hide menu on outside click 68 | document.body.addEventListener "mouseup", (e) -> 69 | if not window.visible_menu or not window.visible_menu.node 70 | return false 71 | if e.target != window.visible_menu.node.parentNode and e.target.parentNode != window.visible_menu.node and e.target.parentNode != window.visible_menu.node.parentNode and e.target.parentNode != window.visible_menu.node and e.target.parentNode.parentNode != window.visible_menu.node.parentNode 72 | window.visible_menu.hide() 73 | Page.projector.scheduleRender() -------------------------------------------------------------------------------- /css/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | margin-top: 4px; border: 1px solid hsla(236,100%,79%,1); color: #5d68ff; border-radius: 33px; display: inline-block; text-decoration: none; 3 | font-size: 19px; font-weight: lighter; text-align: center; transition: all 0.3s; padding: 8px 30px; background-position: -200px center; 4 | } 5 | .button:hover { background-color: #5d68ff; color: #F6F7F8; text-decoration: none; border-color: #5d68ff; transition: none } 6 | .button:hover .icon { background-color: #FFF; transition: none } 7 | .button:focus { transition: all 0.3s } 8 | .button:active { transform: translateY(1px); transition: all 0.3s, transform none; box-shadow: inset 0px 5px 7px -3px rgba(212, 212, 212, 0.41); outline: none; transition: none } 9 | 10 | .button.loading { 11 | color: rgba(0,0,0,0) !important; background: url(../img/loading.gif) no-repeat center center !important; border-color: rgba(0,0,0,0) !important; 12 | transition: all 0.5s ease-out; pointer-events: none; transition-delay: 0.5s 13 | } 14 | 15 | /* Follow */ 16 | .button-follow { width: 32px; line-height: 32px; padding: 0px; border: 1px solid #aaa; color: #999; padding-left: 1px; padding-bottom: 1px; } 17 | .button-follow:hover { background-color: rgba(255,255,255,0.3) !important; border-color: #2ecc71 !important; color: #2ecc71 } 18 | .button-follow-big { padding-left: 25px; float: none; border: 1px solid #2ecc71; color: #2ecc71; min-width: 100px; } 19 | .button-follow-big .icon-follow { margin-right: 10px; display: inline-block; transition: transform 0.3s ease-in-out } 20 | .button-follow-big:hover { border-color: #2ecc71 !important; color: #2ecc71; background-color: white; text-decoration: underline; } 21 | 22 | /* Submit */ 23 | .button-submit { 24 | padding: 12px 30px; border-radius: 3px; margin-top: 11px; background-color: #5d68ff; /*box-shadow: 0px 1px 4px rgba(93, 104, 255, 0.41);*/ 25 | border: none; border-bottom: 2px solid #4952c7; font-weight: bold; color: #ffffff; font-size: 12px; text-transform: uppercase; margin-left: 10px; 26 | } 27 | .button-submit:hover { color: white; background-color: #6d78ff } 28 | 29 | .button-small { padding: 7px 20px; margin-left: 10px } 30 | .button-outline { background-color: white; border: 1px solid #EEE; border-bottom: 2px solid #EEE; color: #AAA; } 31 | .button-outline:hover { background-color: white; border: 1px solid #CCC; border-bottom: 2px solid #CCC; color: #777 } 32 | -------------------------------------------------------------------------------- /js/lib/RateLimitCb.coffee: -------------------------------------------------------------------------------- 1 | last_time = {} 2 | calling = {} 3 | call_after_interval = {} 4 | 5 | # Rate limit function call and don't allow to run in parallel (until callback is called) 6 | window.RateLimitCb = (interval, fn, args=[]) -> 7 | cb = -> # Callback when function finished 8 | left = interval - (Date.now() - last_time[fn]) # Time life until next call 9 | # console.log "CB, left", left, "Calling:", calling[fn] 10 | if left <= 0 # No time left from rate limit interval 11 | delete last_time[fn] 12 | if calling[fn] # Function called within interval 13 | RateLimitCb(interval, fn, calling[fn]) 14 | delete calling[fn] 15 | else # Time left from rate limit interval 16 | setTimeout (-> 17 | delete last_time[fn] 18 | if calling[fn] # Function called within interval 19 | RateLimitCb(interval, fn, calling[fn]) 20 | delete calling[fn] 21 | ), left 22 | if last_time[fn] # Function called within interval 23 | calling[fn] = args # Schedule call and update arguments 24 | else # Not called within interval, call instantly 25 | last_time[fn] = Date.now() 26 | fn.apply(this, [cb, args...]) 27 | 28 | window.RateLimit = (interval, fn) -> 29 | if not calling[fn] 30 | call_after_interval[fn] = false 31 | fn() # First call is not delayed 32 | calling[fn] = setTimeout (-> 33 | if call_after_interval[fn] 34 | fn() 35 | delete calling[fn] 36 | delete call_after_interval[fn] 37 | ), interval 38 | else # Called within iterval, delay the call 39 | call_after_interval[fn] = true 40 | 41 | ### 42 | window.s = Date.now() 43 | window.load = (done, num) -> 44 | console.log "Loading #{num}...", Date.now()-window.s 45 | setTimeout (-> done()), 1000 46 | 47 | RateLimit 500, window.load, [0] # Called instantly 48 | RateLimit 500, window.load, [1] 49 | setTimeout (-> RateLimit 500, window.load, [300]), 300 50 | setTimeout (-> RateLimit 500, window.load, [600]), 600 # Called after 1000ms 51 | setTimeout (-> RateLimit 500, window.load, [1000]), 1000 52 | setTimeout (-> RateLimit 500, window.load, [1200]), 1200 # Called after 2000ms 53 | setTimeout (-> RateLimit 500, window.load, [3000]), 3000 # Called after 3000ms 54 | ### -------------------------------------------------------------------------------- /js/Bg.coffee: -------------------------------------------------------------------------------- 1 | class Bg extends Class 2 | constructor: (@bg_elem) -> 3 | @item_types = ["video", "gamepad", "ipod", "image", "file"] 4 | 5 | window.onresize = @handleResize 6 | @handleResize() 7 | 8 | @randomizePosition() 9 | setTimeout ( => 10 | @randomizeAnimation() 11 | ), 10 12 | @log "inited" 13 | 14 | handleResize: => 15 | @width = window.innerWidth 16 | @height = window.innerHeight 17 | 18 | 19 | randomizePosition: -> 20 | for item in @bg_elem.querySelectorAll(".bgitem") 21 | top = (Math.random() * @height * 0.8) 22 | left = (Math.random() * @width * 0.8) 23 | if Math.random() > 0.8 24 | [left, top] = @getRandomOutpos() 25 | 26 | rotate = 45 - (Math.random() * 90) 27 | scale = 0.5 + Math.min(0.5, Math.random()) 28 | 29 | item.style.transform = "TranslateX(#{left}px) TranslateY(#{top}px) rotateZ(#{rotate}deg) scale(#{scale})" 30 | 31 | getRandomOutpos: -> 32 | # Find new pos 33 | rand = Math.random() 34 | if rand < 0.25 # Out right 35 | left = @width + 100 36 | top = @height * Math.random() 37 | else if rand < 0.5 # Out bottom 38 | left = @width * Math.random() 39 | top = @height + 100 40 | else if rand < 0.75 # Out left 41 | left = -100 42 | top = @height * Math.random() 43 | else # Out top 44 | left = @width * Math.random() 45 | top = -100 46 | return [left, top] 47 | 48 | randomizeAnimation: -> 49 | return false 50 | for item in @bg_elem.querySelectorAll(".bgitem") 51 | item.style.visibility = "visible" 52 | interval = 30 + (Math.random() * 60) 53 | item.style.transition = "all #{interval}s linear" 54 | [left, top] = @getRandomOutpos() 55 | 56 | rotate = 360 - (Math.random() * 720) 57 | scale = 0.5 + Math.min(0.5, Math.random()) 58 | 59 | item.style.transform = "TranslateX(#{left}px) TranslateY(#{top}px) rotateZ(#{rotate}deg) scale(#{scale})" 60 | 61 | bg = @ 62 | item.addEventListener "transitionend", (e) -> 63 | if e.propertyName == "transform" 64 | bg.repositionItem(this) 65 | 66 | repositionItem: (item) => 67 | [left, top] = @getRandomOutpos() 68 | rotate = 360 - (Math.random() * 720) 69 | scale = 0.5 + Math.min(0.5, Math.random()) 70 | 71 | # @bg_elem.removeChild(item) # Avoid animation 72 | item.style.transform = "TranslateX(#{left}px) TranslateY(#{top}px) rotateZ(#{rotate}deg) scale(#{scale})" 73 | # @bg_elem.appendChild(item) # Re-enable animation 74 | 75 | # [target_left, target_top] = [500, 500] 76 | # target_rotate = 180 - (Math.random() * 360) 77 | # item.style.transform = "TranslateX(#{target_left}px) TranslateY(#{target_top}px) rotateZ(#{target_rotate}deg)" 78 | 79 | window.Bg = Bg 80 | -------------------------------------------------------------------------------- /js/utils/ZeroFrame.coffee: -------------------------------------------------------------------------------- 1 | class ZeroFrame extends Class 2 | constructor: (url) -> 3 | @queue = [] 4 | @url = url 5 | @waiting_cb = {} 6 | @history_state = {} 7 | @wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") 8 | @connect() 9 | @next_message_id = 1 10 | @init() 11 | @ready = false 12 | 13 | 14 | init: -> 15 | @ 16 | 17 | connect: -> 18 | @target = window.parent 19 | window.addEventListener("message", @onMessage, false) 20 | @send({"cmd": "innerReady"}) 21 | 22 | # Save scrollTop 23 | window.addEventListener "beforeunload", (e) => 24 | @log "Save scrollTop", window.pageYOffset 25 | @history_state["scrollTop"] = window.pageYOffset 26 | @cmd "wrapperReplaceState", [@history_state, null] 27 | 28 | # Restore scrollTop 29 | @cmd "wrapperGetState", [], (state) => 30 | @handleState(state) 31 | 32 | handleState: (state) -> 33 | @history_state = state if state? 34 | @log "Restore scrollTop", state, window.pageYOffset 35 | if window.pageYOffset == 0 and state 36 | window.scroll(window.pageXOffset, state.scrollTop) 37 | 38 | 39 | onMessage: (e) => 40 | message = e.data 41 | cmd = message.cmd 42 | if cmd == "response" 43 | if @waiting_cb[message.to]? 44 | @waiting_cb[message.to](message.result) 45 | else 46 | @log "Websocket callback not found:", message 47 | else if cmd == "wrapperReady" # Wrapper inited later 48 | @send({"cmd": "innerReady"}) 49 | else if cmd == "ping" 50 | @response message.id, "pong" 51 | else if cmd == "wrapperOpenedWebsocket" 52 | @onOpenWebsocket() 53 | @ready = true 54 | @processQueue() 55 | else if cmd == "wrapperClosedWebsocket" 56 | @onCloseWebsocket() 57 | else if cmd == "wrapperPopState" 58 | @handleState(message.params.state) 59 | @onRequest cmd, message.params 60 | else 61 | @onRequest cmd, message.params 62 | 63 | processQueue: -> 64 | for [cmd, params, cb] in @queue 65 | @cmd(cmd, params, cb) 66 | @queue = [] 67 | 68 | onRequest: (cmd, message) => 69 | @log "Unknown request", message 70 | 71 | 72 | response: (to, result) -> 73 | @send {"cmd": "response", "to": to, "result": result} 74 | 75 | 76 | cmd: (cmd, params={}, cb=null) -> 77 | if @ready 78 | @send {"cmd": cmd, "params": params}, cb 79 | else 80 | @queue.push([cmd, params, cb]) 81 | 82 | send: (message, cb=null) -> 83 | message.wrapper_nonce = @wrapper_nonce 84 | message.id = @next_message_id 85 | @next_message_id += 1 86 | @target.postMessage(message, "*") 87 | if cb 88 | @waiting_cb[message.id] = cb 89 | 90 | 91 | onOpenWebsocket: => 92 | @log "Websocket open" 93 | 94 | 95 | onCloseWebsocket: => 96 | @log "Websocket close" 97 | 98 | 99 | 100 | window.ZeroFrame = ZeroFrame -------------------------------------------------------------------------------- /js/Uploader.coffee: -------------------------------------------------------------------------------- 1 | class Uploader extends Class 2 | constructor: -> 3 | @ 4 | 5 | renderSpeed: => 6 | """ 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 0 17 | 20 18 | 40 19 | 60 20 | 80 21 | 100 22 | 120 23 | 24 | """ 25 | 26 | randomBase2: (len) => 27 | return (Math.random()).toString(2).slice(2,len) 28 | 29 | handleFinishUpload: => 30 | Page.state.page = "list" 31 | Page.projector.scheduleRender() 32 | setTimeout ( => 33 | Page.list.update() 34 | ), 1000 35 | return false 36 | 37 | render: => 38 | file_info = Page.selector.file_info 39 | # Efficient updating svg is not possible in maquette, so manupulated DOM directly 40 | dash_offset = Math.max(2390 - (486 * file_info.speed / 1024 / 1024 / 100), 1770) + Math.random() * 10 41 | if dash_offset != @last_dash_offset 42 | @last_dash_offset = dash_offset 43 | setTimeout (=> 44 | document.getElementById("speed_current")?.style.strokeDashoffset = dash_offset # 2390 - 1770 45 | ), 1 46 | 47 | 48 | h("div.Uploader", {classes: {hidden: Page.state.page != "uploader"}}, [ 49 | h("div.speed", {innerHTML: @renderSpeed()}) 50 | h("div.status", [ 51 | h("div.icon.icon-file-empty.file-fg", {style: "clip: rect(0px 100px #{114*file_info.percent}px 0px)"}, [ 52 | @randomBase2(13), h("br"), @randomBase2(13), h("br"), @randomBase2(13), h("br"), @randomBase2(40), @randomBase2(40), @randomBase2(40), @randomBase2(24) 53 | ]), 54 | h("div.icon.icon-file-empty.file-bg"), 55 | h("div.percent", {style: "transform: translateY(#{114*file_info.percent}px"}, [ 56 | Math.round(file_info.percent * 100), 57 | h("span.post", "% \u25B6") 58 | ]), 59 | h("div.name", file_info.name), 60 | h("div.size", Text.formatSize(file_info.size)), 61 | if file_info.status == "done" 62 | h("div.message.message-done", "File uploaded in #{((file_info.updated - file_info.started) / 1000).toFixed(1)}s @ #{Text.formatSize(file_info.speed)}/s!") 63 | else if file_info.speed 64 | h("div.message", "Hashing @ #{Text.formatSize(file_info.speed)}/s...") 65 | else 66 | h("div.message", "Opening file...") 67 | h("a.button-big.button-finish", {href: "?List", onclick: @handleFinishUpload, classes: {visible: file_info.status == "done"}}, "Finish upload \u00BB") 68 | ]) 69 | ]) 70 | 71 | window.Uploader = Uploader -------------------------------------------------------------------------------- /js/ZeroUp.coffee: -------------------------------------------------------------------------------- 1 | window.h = maquette.h 2 | 3 | class ZeroUp extends ZeroFrame 4 | init: -> 5 | @bg = new Bg($("#Bg")) 6 | @state = {} 7 | @state.page = "list" 8 | @on_site_info = new Promise() 9 | @on_loaded = new Promise() 10 | 11 | 12 | createProjector: -> 13 | @projector = maquette.createProjector() 14 | 15 | @list = new List() 16 | @selector = new Selector() 17 | @uploader = new Uploader() 18 | 19 | if base.href.indexOf("?") == -1 20 | @route("") 21 | else 22 | url = base.href.replace(/.*?\?/, "") 23 | @route(url) 24 | @history_state["url"] = url 25 | 26 | @projector.replace($("#List"), @list.render) 27 | @projector.replace($("#Uploader"), @uploader.render) 28 | 29 | setPage: (page_name) -> 30 | @state.page = page_name 31 | @projector.scheduleRender() 32 | 33 | setSiteInfo: (site_info) -> 34 | @site_info = site_info 35 | 36 | onOpenWebsocket: => 37 | @updateSiteInfo() 38 | @cmd "serverInfo", {}, (@server_info) => 39 | if @server_info.rev < 3090 40 | @cmd "wrapperNotification", ["error", "This site requires ZeroNet 0.6.0"] 41 | 42 | updateSiteInfo: => 43 | @cmd "siteInfo", {}, (site_info) => 44 | @address = site_info.address 45 | @setSiteInfo(site_info) 46 | @on_site_info.resolve() 47 | 48 | onRequest: (cmd, params) -> 49 | if cmd == "setSiteInfo" # Site updated 50 | @setSiteInfo(params) 51 | if params.event?[0] in ["file_done", "file_delete", "peernumber_updated"] 52 | RateLimit 1000, => 53 | @list.need_update = true 54 | Page.projector.scheduleRender() 55 | else if cmd == "wrapperPopState" # Site updated 56 | if params.state 57 | if not params.state.url 58 | params.state.url = params.href.replace /.*\?/, "" 59 | @on_loaded.resolved = false 60 | document.body.className = "" 61 | window.scroll(window.pageXOffset, params.state.scrollTop or 0) 62 | @route(params.state.url or "") 63 | else 64 | @log "Unknown command", cmd, params 65 | 66 | # Route site urls 67 | route: (query) -> 68 | @params = Text.queryParse(query) 69 | @log "Route", @params 70 | 71 | @content = @list 72 | if @params.url 73 | @list.type = @params.url 74 | @content.limit = 10 75 | @content.need_update = true 76 | 77 | @projector.scheduleRender() 78 | 79 | setUrl: (url, mode="push") -> 80 | url = url.replace(/.*?\?/, "") 81 | @log "setUrl", @history_state["url"], "->", url 82 | if @history_state["url"] == url 83 | @content.update() 84 | return false 85 | @history_state["url"] = url 86 | if mode == "replace" 87 | @cmd "wrapperReplaceState", [@history_state, "", url] 88 | else 89 | @cmd "wrapperPushState", [@history_state, "", url] 90 | @route url 91 | return false 92 | 93 | handleLinkClick: (e) => 94 | if e.which == 2 95 | # Middle click dont do anything 96 | return true 97 | else 98 | @log "save scrollTop", window.pageYOffset 99 | @history_state["scrollTop"] = window.pageYOffset 100 | @cmd "wrapperReplaceState", [@history_state, null] 101 | 102 | window.scroll(window.pageXOffset, 0) 103 | @history_state["scrollTop"] = 0 104 | 105 | @on_loaded.resolved = false 106 | document.body.className = "" 107 | 108 | @setUrl e.currentTarget.search 109 | return false 110 | 111 | 112 | # Add/remove/change parameter to current site url 113 | createUrl: (key, val) -> 114 | params = JSON.parse(JSON.stringify(@params)) # Clone 115 | if typeof key == "Object" 116 | vals = key 117 | for key, val of keys 118 | params[key] = val 119 | else 120 | params[key] = val 121 | return "?"+Text.queryEncode(params) 122 | 123 | returnFalse: -> 124 | return false 125 | 126 | 127 | window.Page = new ZeroUp() 128 | window.Page.createProjector() -------------------------------------------------------------------------------- /js/List.coffee: -------------------------------------------------------------------------------- 1 | class List extends Class 2 | constructor: -> 3 | @item_list = new ItemList(File, "id") 4 | @files = @item_list.items 5 | @need_update = true 6 | @loaded = false 7 | @type = "Popular" 8 | @limit = 10 9 | 10 | needFile: => 11 | @log args 12 | return false 13 | 14 | update: => 15 | @log "update" 16 | @loaded = false 17 | 18 | if @type == "Popular" 19 | order = "peer" 20 | else 21 | order = "date_added" 22 | 23 | if @search 24 | wheres = "WHERE file.title LIKE :search OR file.file_name LIKE :search" 25 | params = {search: "%#{@search}%"} 26 | else 27 | wheres = "" 28 | params = "" 29 | 30 | Page.cmd "dbQuery", ["SELECT * FROM file LEFT JOIN json USING (json_id) #{wheres} ORDER BY date_added DESC", params], (files_res) => 31 | orderby = "time_downloaded DESC, peer DESC" 32 | if @type == "My" 33 | orderby = "is_downloaded DESC" 34 | else if @type == "Latest" 35 | orderby = "is_downloaded DESC, time_added DESC" 36 | else if @type == "Seeding" 37 | orderby = "is_downloaded DESC, is_pinned DESC" 38 | 39 | Page.cmd "optionalFileList", {filter: "bigfile", limit: 2000, orderby: orderby}, (stat_res) => 40 | stats = {} 41 | for stat in stat_res 42 | stats[stat.inner_path] = stat 43 | 44 | for file in files_res 45 | file.id = file.directory + "_" + file.date_added 46 | file.inner_path = "data/users/#{file.directory}/#{file.file_name}" 47 | file.data_inner_path = "data/users/#{file.directory}/data.json" 48 | file.content_inner_path = "data/users/#{file.directory}/content.json" 49 | file.stats = stats[file.inner_path] 50 | file.stats ?= {} 51 | file.stats.peer ?= 0 52 | file.stats.peer_seed ?= 0 53 | file.stats.peer_leech ?= 0 54 | 55 | if order == "peer" 56 | files_res.sort (a,b) -> 57 | return Math.min(5, b.stats["peer_seed"]) + b.stats["peer"] - a.stats["peer"] - Math.min(5, a.stats["peer_seed"]) 58 | 59 | if @type == "Seeding" 60 | files_res = (file for file in files_res when file.stats.bytes_downloaded > 0 or file.stats.is_pinned == 1) 61 | 62 | if @type == "My" 63 | files_res = (file for file in files_res when file.directory == Page.site_info.auth_address) 64 | 65 | @item_list.sync(files_res) 66 | @loaded = true 67 | Page.projector.scheduleRender() 68 | 69 | handleMoreClick: => 70 | @limit += 20 71 | return false 72 | 73 | handleSearchClick: => 74 | @is_search_active = true 75 | document.querySelector(".input-search").focus() 76 | return false 77 | 78 | handleSearchInput: (e) => 79 | @search = e.currentTarget.value 80 | @update() 81 | return false 82 | 83 | handleSearchKeyup: (e) => 84 | if e.keyCode == 27 # Esc 85 | if not @search 86 | @is_search_active = false 87 | e.target.value = "" 88 | @search = "" 89 | @update() 90 | return false 91 | 92 | handleSearchBlur: (e) => 93 | if not @search 94 | @is_search_active = false 95 | 96 | 97 | render: => 98 | if @need_update 99 | @update() 100 | @need_update = false 101 | 102 | h("div.List", {ondragenter: document.body.ondragover, ondragover: document.body.ondragover, ondrop: Page.selector.handleFileDrop, classes: {hidden: Page.state.page != "list"}}, [ 103 | h("div.list-types", [ 104 | h("a.list-type.search", {href: "#Search", onclick: @handleSearchClick, classes: {active: @is_search_active}}, 105 | h("div.icon.icon-magnifier"), 106 | h("input.input-search", oninput: @handleSearchInput, onkeyup: @handleSearchKeyup, onblur: @handleSearchBlur) 107 | ), 108 | h("a.list-type", {href: "?Popular", onclick: Page.handleLinkClick, classes: {active: @type == "Popular"}}, "Popular"), 109 | h("a.list-type", {href: "?Latest", onclick: Page.handleLinkClick, classes: {active: @type == "Latest"}}, "Latest"), 110 | h("a.list-type", {href: "?Seeding", onclick: Page.handleLinkClick, classes: {active: @type == "Seeding"}}, "Seeding"), 111 | h("a.list-type", {href: "?My", onclick: Page.handleLinkClick, classes: {active: @type == "My"}}, "My uploads"), 112 | # h("input.filter", {placeholder: "Filter uploads..."}) 113 | ]), 114 | h("a.upload", {href: "#", onclick: Page.selector.handleBrowseClick}, [h("div.icon.icon-upload"), h("span.upload-title", "Upload new file")]), 115 | if @files.length then h("div.files", [ 116 | h("div.file.header", 117 | h("div.stats", [ 118 | h("div.stats-col.peers", "Peers"), 119 | h("div.stats-col.ratio", "Ratio"), 120 | h("div.stats-col.downloaded", "Uploaded") 121 | ]) 122 | ), 123 | @files[0..@limit].map (file) => 124 | file.render() 125 | ]) 126 | if @loaded and not @files.length 127 | if @type == "Seeding" 128 | h("h2", "Not seeded files yet :(") 129 | else 130 | h("h2", "No files submitted yet") 131 | if @files.length > @limit 132 | h("a.more.link", {href: "#", onclick: @handleMoreClick}, "Show more...") 133 | ]) 134 | 135 | window.List = List 136 | -------------------------------------------------------------------------------- /js/Selector.coffee: -------------------------------------------------------------------------------- 1 | class Selector extends Class 2 | constructor: -> 3 | @file_info = {} 4 | 5 | document.body.ondragover = (e) => 6 | if e.dataTransfer.items[0]?.kind == "file" 7 | document.body.classList.add("drag-over") 8 | @preventEvent(e) 9 | 10 | document.body.ondragleave = (e) => 11 | if not e.pageX 12 | document.body.classList.remove("drag-over") 13 | @preventEvent(e) 14 | 15 | checkContentJson: (cb) => 16 | inner_path = "data/users/" + Page.site_info.auth_address + "/content.json" 17 | Page.cmd "fileGet", [inner_path, false], (res) => 18 | if res 19 | res = JSON.parse(res) 20 | res ?= {} 21 | optional_pattern = "(?!data.json)" 22 | if res.optional == optional_pattern 23 | return cb() 24 | 25 | res.optional = optional_pattern 26 | Page.cmd "fileWrite", [inner_path, Text.fileEncode(res)], cb 27 | 28 | registerUpload: (title, file_name, file_size, date_added, cb) => 29 | inner_path = "data/users/" + Page.site_info.auth_address + "/data.json" 30 | Page.cmd "fileGet", [inner_path, false], (res) => 31 | if res 32 | res = JSON.parse(res) 33 | res ?= {} 34 | res.file ?= {} 35 | res.file[file_name] = { 36 | title: title, 37 | size: file_size, 38 | date_added: date_added 39 | } 40 | Page.cmd "fileWrite", [inner_path, Text.fileEncode(res)], cb 41 | 42 | handleUploadDone: (file) => 43 | Page.setUrl("?Latest") 44 | @log "Upload done", file 45 | 46 | uploadFile: (file) => 47 | if file.size > 200 * 1024 * 1024 48 | Page.cmd("wrapperNotification", ["info", "Maximum file size on this site during the testing period: 200MB"]) 49 | return false 50 | if file.size < 10 * 1024 * 1024 51 | Page.cmd("wrapperNotification", ["info", "Minimum file size: 10MB"]) 52 | return false 53 | if file.name.split(".").slice(-1)[0] not in ["mp4", "gz", "zip", "webm"] 54 | Page.cmd("wrapperNotification", ["info", "Only mp4, webm, tar.gz, zip files allowed on this site"]) 55 | debugger 56 | return false 57 | 58 | @file_info = {} 59 | @checkContentJson (res) => 60 | file_name = file.name 61 | 62 | # Add timestamp to filename if it has low amount of English characters 63 | if file_name.replace(/[^A-Za-z0-9]/g, "").length < 20 64 | file_name = Time.timestamp() + "-" + file_name 65 | 66 | Page.cmd "bigfileUploadInit", ["data/users/" + Page.site_info.auth_address + "/" + file_name, file.size], (init_res) => 67 | formdata = new FormData() 68 | formdata.append(file_name, file) 69 | 70 | req = new XMLHttpRequest() 71 | @req = req 72 | @file_info = {size: file.size, name: file_name, type: file.type, url: init_res.url} 73 | req.upload.addEventListener "loadstart", (progress) => 74 | @log "loadstart", arguments 75 | @file_info.started = progress.timeStamp 76 | Page.setPage("uploader") 77 | req.upload.addEventListener "loadend", => 78 | @log "loadend", arguments 79 | @file_info.status = "done" 80 | @registerUpload file.name.replace(/\.[^\.]+$/, ""), init_res.file_relative_path, file.size, Time.timestamp(), (res) => 81 | Page.cmd "siteSign", {inner_path: "data/users/" + Page.site_info.auth_address + "/content.json"}, (res) => 82 | Page.cmd "sitePublish", {inner_path: "data/users/" + Page.site_info.auth_address + "/content.json", "sign": false}, (res) => 83 | @handleUploadDone(file) 84 | 85 | req.upload.addEventListener "progress", (progress) => 86 | @file_info.speed = 1000 * progress.loaded / (progress.timeStamp - @file_info.started) 87 | 88 | @file_info.percent = progress.loaded / progress.total 89 | @file_info.loaded = progress.loaded 90 | @file_info.updated = progress.timeStamp 91 | Page.projector.scheduleRender() 92 | req.addEventListener "load", => 93 | @log "load", arguments 94 | req.addEventListener "error", => 95 | @log "error", arguments 96 | req.addEventListener "abort", => 97 | @log "abort", arguments 98 | 99 | req.withCredentials = true 100 | req.open("POST", init_res.url) 101 | req.send(formdata) 102 | 103 | handleFileDrop: (e) => 104 | @log "File drop", e 105 | document.body.classList.remove("drag-over") 106 | 107 | if not event.dataTransfer.files[0] 108 | return false 109 | @preventEvent(e) 110 | if Page.site_info.cert_user_id 111 | @uploadFile(event.dataTransfer.files[0]) 112 | else 113 | Page.cmd "certSelect", [["zeroid.bit"]], (res) => 114 | @uploadFile(event.dataTransfer.files[0]) 115 | 116 | handleBrowseClick: (e) => 117 | if Page.site_info.cert_user_id 118 | @handleUploadClick(e) 119 | else 120 | Page.cmd "certSelect", [["zeroid.bit"]], (res) => 121 | @handleUploadClick(e) 122 | 123 | handleUploadClick: (e) => 124 | input = document.createElement('input') 125 | document.body.appendChild(input) 126 | input.type = "file" 127 | input.style.visibility = "hidden" 128 | input.onchange = (e) => 129 | @uploadFile(input.files[0]) 130 | input.click() 131 | return false 132 | 133 | preventEvent: (e) => 134 | e.stopPropagation() 135 | e.preventDefault() 136 | 137 | render: => 138 | h("div#Selector.Selector", {classes: {hidden: Page.state.page != "selector"}}, 139 | h("div.browse", [ 140 | h("div.icon.icon-upload"), 141 | h("a.button", {href: "#Browse", onclick: @handleBrowseClick}, "Select file from computer") 142 | ]), 143 | h("div.dropzone", {ondragenter: @preventEvent, ondragover: @preventEvent, ondrop: @handleFileDrop}) 144 | ) 145 | 146 | window.Selector = Selector -------------------------------------------------------------------------------- /js/utils/Text.coffee: -------------------------------------------------------------------------------- 1 | class Text 2 | toColor: (text, saturation=30, lightness=50) -> 3 | hash = 0 4 | for i in [0..text.length-1] 5 | hash += text.charCodeAt(i)*i 6 | hash = hash % 1777 7 | return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)"; 8 | 9 | 10 | renderMarked: (text, options={}) => 11 | if not text 12 | return "" 13 | options["gfm"] = true 14 | options["breaks"] = true 15 | options["sanitize"] = true 16 | options["renderer"] = marked_renderer 17 | text = @fixReply(text) 18 | text = marked(text, options) 19 | text = text.replace(/(@[^\x00-\x1f^\x21-\x2f^\x3a-\x40^\x5b-\x60^\x7b-\x7f]{1,16}):/g, '$1:') # Highlight usernames 20 | return @fixHtmlLinks text 21 | 22 | renderLinks: (text) => 23 | text = text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') # Sanitize html tags 24 | text = text.replace /(https?:\/\/[^\s)]+)/g, (match) -> 25 | return "#{match}" # UnSanitize & -> & in links 26 | text = text.replace(/\n/g, '
') 27 | text = text.replace(/(@[^\x00-\x1f^\x21-\x2f^\x3a-\x40^\x5b-\x60^\x7b-\x7f]{1,16}):/g, '$1:') 28 | text = @fixHtmlLinks(text) 29 | 30 | return text 31 | 32 | emailLinks: (text) -> 33 | return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "$1@zeroid.bit") 34 | 35 | # Convert zeronet html links to relaitve 36 | fixHtmlLinks: (text) -> 37 | # Fix site links 38 | text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110\/(Me.ZeroNetwork.bit|1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH)\/\?/gi, 'href="?') 39 | if window.is_proxy 40 | text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/gi, 'href="http://zero') 41 | text = text.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") 42 | else 43 | text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="') 44 | # Add no-refresh linking to local links 45 | text = text.replace(/href="\?/g, 'onclick="return Page.handleLinkClick(window.event)" href="?') 46 | return text 47 | 48 | 49 | # Convert a single link to relative 50 | fixLink: (link) -> 51 | if window.is_proxy 52 | back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero') 53 | return back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") # Domain links 54 | else 55 | return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '') 56 | 57 | toUrl: (text) -> 58 | return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "") 59 | 60 | getSiteUrl: (address) -> 61 | if window.is_proxy 62 | if "." in address # Domain 63 | return "http://"+address+"/" 64 | else 65 | return "http://zero/"+address+"/" 66 | else 67 | return "/"+address+"/" 68 | 69 | 70 | fixReply: (text) -> 71 | return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2") 72 | 73 | toBitcoinAddress: (text) -> 74 | return text.replace(/[^A-Za-z0-9]/g, "") 75 | 76 | 77 | jsonEncode: (obj) -> 78 | return unescape(encodeURIComponent(JSON.stringify(obj))) 79 | 80 | jsonDecode: (obj) -> 81 | return JSON.parse(decodeURIComponent(escape(obj))) 82 | 83 | fileEncode: (obj) -> 84 | if typeof(obj) == "string" 85 | return btoa(unescape(encodeURIComponent(obj))) 86 | else 87 | return btoa(unescape(encodeURIComponent(JSON.stringify(obj, undefined, '\t')))) 88 | 89 | utf8Encode: (s) -> 90 | return unescape(encodeURIComponent(s)) 91 | 92 | utf8Decode: (s) -> 93 | return decodeURIComponent(escape(s)) 94 | 95 | 96 | distance: (s1, s2) -> 97 | s1 = s1.toLocaleLowerCase() 98 | s2 = s2.toLocaleLowerCase() 99 | next_find_i = 0 100 | next_find = s2[0] 101 | match = true 102 | extra_parts = {} 103 | for char in s1 104 | if char != next_find 105 | if extra_parts[next_find_i] 106 | extra_parts[next_find_i] += char 107 | else 108 | extra_parts[next_find_i] = char 109 | else 110 | next_find_i++ 111 | next_find = s2[next_find_i] 112 | 113 | if extra_parts[next_find_i] 114 | extra_parts[next_find_i] = "" # Extra chars on the end doesnt matter 115 | extra_parts = (val for key, val of extra_parts) 116 | if next_find_i >= s2.length 117 | return extra_parts.length + extra_parts.join("").length 118 | else 119 | return false 120 | 121 | 122 | queryParse: (query) -> 123 | params = {} 124 | parts = query.split('&') 125 | for part in parts 126 | [key, val] = part.split("=") 127 | if val 128 | params[decodeURIComponent(key)] = decodeURIComponent(val) 129 | else 130 | params["url"] = decodeURIComponent(key) 131 | params["urls"] = params["url"].split("/") 132 | return params 133 | 134 | queryEncode: (params) -> 135 | back = [] 136 | if params.url 137 | back.push(params.url) 138 | for key, val of params 139 | if not val or key == "url" 140 | continue 141 | back.push("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}") 142 | return back.join("&") 143 | 144 | highlight: (text, search) -> 145 | parts = text.split(RegExp(search, "i")) 146 | back = [] 147 | for part, i in parts 148 | back.push(part) 149 | if i < parts.length-1 150 | back.push(h("span.highlight", {key: i}, search)) 151 | return back 152 | 153 | sqlIn: (values) -> 154 | return "("+("'#{value}'" for value in values).join(',')+")" 155 | 156 | formatSize: (size) -> 157 | if not size 158 | return "0 KB" 159 | size_mb = size/1024/1024 160 | if size_mb >= 1000 161 | return (size_mb/1024).toFixed(1)+" GB" 162 | else if size_mb >= 100 163 | return size_mb.toFixed(0)+" MB" 164 | else if size/1024 >= 1000 165 | return size_mb.toFixed(2)+" MB" 166 | else 167 | return (size/1024).toFixed(2)+" KB" 168 | 169 | 170 | window.is_proxy = (document.location.host == "zero" or window.location.pathname == "/") 171 | window.Text = new Text() 172 | -------------------------------------------------------------------------------- /js/File.coffee: -------------------------------------------------------------------------------- 1 | class File 2 | constructor: (row, @item_list) -> 3 | @editable_title = null 4 | @status = "unknown" 5 | @menu = null 6 | @setRow(row) 7 | 8 | getRatioColor: (ratio) -> 9 | ratio_h = Math.min(ratio * 50, 145) 10 | ratio_s = Math.min(ratio * 100, 60) 11 | ratio_l = 80 - Math.min(ratio * 5, 30) 12 | 13 | return "hsl(#{ratio_h}, #{ratio_s}%, #{ratio_l}%)" 14 | 15 | setRow: (@row) -> 16 | @owned = Page.site_info.auth_address == @row.directory 17 | if @owned and not @editable_title 18 | @editable_title = new Editable("div.body", @handleTitleSave, @handleDelete) 19 | @editable_title.empty_text = " " 20 | 21 | if @row.stats.bytes_downloaded >= @row.size 22 | @status = "seeding" 23 | else if @row.stats.is_downloading or @row.stats.is_pinned 24 | @status = "downloading" 25 | else if 0 < @row.stats.bytes_downloaded < @row.size 26 | @status = "partial" 27 | else 28 | @status = "inactive" 29 | 30 | deleteFile: (cb) => 31 | Page.cmd "optionalFileDelete", @row.inner_path, => 32 | Page.cmd "optionalFileDelete", @row.inner_path + ".piecemap.msgpack", => 33 | cb?(true) 34 | 35 | deleteFromContentJson: (cb) => 36 | Page.cmd "fileGet", @row.content_inner_path, (res) => 37 | data = JSON.parse(res) 38 | delete data["files_optional"][@row.file_name] 39 | delete data["files_optional"][@row.file_name+ ".piecemap.msgpack"] 40 | Page.cmd "fileWrite", [@row.content_inner_path, Text.fileEncode(data)], (res) => 41 | cb?(res) 42 | 43 | deleteFromDataJson: (cb) => 44 | Page.cmd "fileGet", @row.data_inner_path, (res) => 45 | data = JSON.parse(res) 46 | delete data["file"][@row.file_name] 47 | delete data["file"][@row.file_name+ ".piecemap.msgpack"] 48 | Page.cmd "fileWrite", [@row.data_inner_path, Text.fileEncode(data)], (res) => 49 | cb?(res) 50 | 51 | handleDelete: (cb) => 52 | @deleteFile (res) => 53 | @deleteFromContentJson (res) => 54 | if not res == "ok" 55 | return cb(false) 56 | @deleteFromDataJson (res) => 57 | if res == "ok" 58 | Page.cmd "sitePublish", {"inner_path": @row.content_inner_path} 59 | Page.list.update() 60 | cb(true) 61 | 62 | handleTitleSave: (title, cb) => 63 | Page.cmd "fileGet", @row.data_inner_path, (res) => 64 | data = JSON.parse(res) 65 | data["file"][@row.file_name]["title"] = title 66 | @row.title = title 67 | Page.cmd "fileWrite", [@row.data_inner_path, Text.fileEncode(data)], (res) => 68 | if res == "ok" 69 | cb(true) 70 | Page.cmd "sitePublish", {"inner_path": @row.content_inner_path} 71 | else 72 | cb(false) 73 | 74 | handleSeedClick: => 75 | @status = "downloading" 76 | Page.cmd "fileNeed", @row.inner_path + "|all", (res) => 77 | console.log res 78 | Page.cmd "optionalFilePin", @row.inner_path 79 | return false 80 | 81 | handleOpenClick: => 82 | Page.cmd "serverShowdirectory", ["site", @row.inner_path] 83 | return false 84 | 85 | handleMenuClick: => 86 | if not @menu 87 | @menu = new Menu() 88 | @menu.items = [] 89 | @menu.items.push ["Delete file", @handleMenuDeleteClick] 90 | @menu.toggle() 91 | return false 92 | 93 | handleMenuDeleteClick: => 94 | @deleteFile() 95 | return false 96 | 97 | render: -> 98 | if @row.stats.bytes_downloaded 99 | ratio = @row.stats.uploaded / @row.stats.bytes_downloaded 100 | else 101 | ratio = 0 102 | 103 | ratio_color = @getRatioColor(ratio) 104 | 105 | if @status in ["downloading", "partial"] 106 | style = "box-shadow: inset #{@row.stats.downloaded_percent * 1.5}px 0px 0px #70fcd8" 107 | else 108 | style = "" 109 | 110 | ext = @row.file_name.toLowerCase().replace(/.*\./, "") 111 | if ext in ["mp4", "webm", "ogm"] 112 | type = "video" 113 | else 114 | type = "other" 115 | 116 | peer_num = Math.max((@row.stats.peer_seed + @row.stats.peer_leech) or 0, @row.stats.peer or 0) 117 | low_seeds = @row.stats.peer_seed <= peer_num * 0.1 and @row.stats.peer_leech >= peer_num * 0.2 118 | 119 | h("div.file.#{type}", {key: @row.id}, 120 | h("div.stats", [ 121 | h("div.stats-col.peers", {title: "Seeder: #{@row.stats.peer_seed}, Leecher: #{@row.stats.peer_leech}"}, [ 122 | h("span.value", peer_num), 123 | h("span.icon.icon-profile", {style: if low_seeds then "background: #f57676" else "background: #666"}) 124 | ]), 125 | h("div.stats-col.ratio", {title: "Hash id: #{@row.stats.hash_id}"}, h("span.value", {"style": "background-color: #{ratio_color}"}, if ratio >= 10 then ratio.toFixed(0) else ratio.toFixed(1))) 126 | h("div.stats-col.uploaded", "\u2BA5 #{Text.formatSize(@row.stats.uploaded)}") 127 | ]) 128 | if type == "video" 129 | h("a.open", {href: @row.inner_path}, "\u203A") 130 | else 131 | h("a.open", {href: @row.inner_path}, h("span.icon.icon-open-new")) 132 | 133 | h("div.left-info", [ 134 | if @editable_title?.editing 135 | @editable_title.render(@row.title) 136 | else 137 | h("a.title.link", {href: @row.inner_path, enterAnimation: Animation.slideDown}, @editable_title?.render(@row.title) or @row.title) 138 | 139 | h("div.details", [ 140 | if @status in ["inactive", "partial"] and not @row.stats.is_pinned 141 | h("a.add", {href: "#Add", title: "Download and seed", onclick: @handleSeedClick}, "+ seed") 142 | 143 | h("span.size", {classes: {downloading: @status == "downloading", partial: @status == "partial", seeding: @status == "seeding"}, style: style}, [ 144 | if @status == "seeding" 145 | h("span", "seeding: ") 146 | if @status == "downloading" or @status == "partial" then [ 147 | h("span.downloaded", Text.formatSize(@row.stats.bytes_downloaded)), 148 | " of " 149 | ], 150 | Text.formatSize(@row.size) 151 | ]), 152 | if @status != "inactive" 153 | [ 154 | h("a.menu-button", {href: "#Menu", onclick: Page.returnFalse, onmousedown: @handleMenuClick}, "\u22EE") 155 | if @menu then @menu.render(".menu-right") 156 | ] 157 | h("span.detail.added", {title: Time.date(@row.date_added, "long")}, Time.since(@row.date_added)), 158 | h("span.detail.uploader", [ 159 | "by ", 160 | h("span.username", 161 | {title: @row.cert_user_id + ": " + @row.directory}, 162 | @row.cert_user_id.split("@")[0] 163 | ) 164 | ]), 165 | if @status == "seeding" 166 | h("a.detail", h("a.link.filename", {href: "#Open+directory", title: "Open directory", onclick: @handleOpenClick}, @row.file_name)) 167 | else 168 | h("a.detail.filename", {title: @row.file_name}, @row.file_name) 169 | ]) 170 | ]) 171 | ) 172 | 173 | 174 | window.File = File -------------------------------------------------------------------------------- /css/icons.css: -------------------------------------------------------------------------------- 1 | .icon { background-repeat: no-repeat; display: inline-block; width: 32px; height: 32px; } 2 | 3 | .icon-profile { font-size: 8px; top: 0.1em; border-radius: 0.7em 0.7em 0 0; background: #888; width: 1.4em; height: 0.5em; position: relative; display: inline-block; margin-right: 4px; margin-left: 5px; } 4 | .icon-profile::before { position: absolute; content: ""; top: -1em; left: 0.31em; width: 0.8em; height: 0.85em; border-radius: 50%; background: inherit; } 5 | 6 | .icon-upload { 7 | width: 34px; height: 34px; 8 | background-image: url('data:image/svg+xml;utf8,') 9 | } 10 | 11 | .icon-video { 12 | width: 34px; height: 34px; 13 | background-image: url('data:image/svg+xml;utf8,') 14 | } 15 | 16 | .icon-gamepad { 17 | width: 34px; height: 34px; 18 | background-image: url('data:image/svg+xml;utf8,') 19 | } 20 | 21 | .icon-ipod { 22 | width: 34px; height: 34px; 23 | background-image: url('data:image/svg+xml;utf8,') 24 | } 25 | 26 | .icon-image { 27 | width: 34px; height: 34px; 28 | background-image: url('data:image/svg+xml;utf8,') 29 | } 30 | 31 | .icon-file { 32 | width: 34px; height: 34px; 33 | background-image: url('data:image/svg+xml;utf8,') 34 | } 35 | 36 | .icon-file-empty { 37 | width: 114px; height: 114px; 38 | background-image: url('data:image/svg+xml;utf8,') 39 | } 40 | 41 | .icon-open-new { 42 | width: 26px; height: 26px; 43 | background-image: url('data:image/svg+xml;utf8,') 44 | } 45 | 46 | .icon-edit { 47 | width: 16px; height: 16px; background-repeat: no-repeat; background-position: 6px center; 48 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAOVBMVEUAAAC9w8e9w8e9w8e9w8e/xMi9w8e9w8e+w8e9w8e9w8e9w8e9w8e9w8e9w8e+w8e/xMi9w8e9w8fvY4+KAAAAEnRSTlMASPv3WQbwOTCkt4/psX4YDMWr+RRCAAAAUUlEQVQY06XLORKAMAxDUTs7kA3d/7AYGju0UfffjIgoHkxm0vB5bZyxKHx9eX0FJw0Y4bcXKQ4/CTtS5yqp5GFFOjGpVGl00k1pNDIb3Nv9AHC7BOZC4ZjvAAAAAElFTkSuQmCC+d0ckOwyAMRVGHUOO0gUyd+P8f7WApz4Iki9wFmyOEATrXLZcFp5LrGogPOxKp6zfFf9fZ1/I/cY7YZSS3U6S3XFZJmGBwL+FuJX/F1K0wUUlZyZGlXgXESthTEs4B8fh7xoVUDPGYJnsfkCRarKAgz8cAKbpD6pqDPz3XB8K6HdUEeN9NAAAAAElFTkSuQmCC); 49 | } 50 | 51 | .icon-magnifier { 52 | position: absolute; display: inline-block; background: transparent; border-radius: 30px; z-index: 1; 53 | height: 9px; width: 9px; border: 1.5px solid currentColor; margin-top: 13px; pointer-events: none; margin-left: 7px; 54 | } 55 | .icon-magnifier:after { content: ""; height: 1.8px; width: 6px; background: currentColor; position: absolute; top: 10px; left: 7px; transform: rotate(45deg); } 56 | -------------------------------------------------------------------------------- /js/utils/Animation.coffee: -------------------------------------------------------------------------------- 1 | class Animation 2 | slideDown: (elem, props) -> 3 | h = elem.offsetHeight 4 | cstyle = window.getComputedStyle(elem) 5 | margin_top = cstyle.marginTop 6 | margin_bottom = cstyle.marginBottom 7 | padding_top = cstyle.paddingTop 8 | padding_bottom = cstyle.paddingBottom 9 | border_top_width = cstyle.borderTopWidth 10 | border_bottom_width = cstyle.borderBottomWidth 11 | transition = cstyle.transition 12 | 13 | if window.Animation.shouldScrollFix(elem, props) 14 | # Keep objects in the screen at same position 15 | top_after = document.body.scrollHeight 16 | next_elem = elem.nextSibling 17 | parent = elem.parentNode 18 | parent.removeChild(elem) 19 | top_before = document.body.scrollHeight 20 | console.log("Scrollcorrection down", (top_before - top_after)) 21 | window.scrollTo(window.scrollX, window.scrollY - (top_before - top_after)) 22 | if next_elem 23 | parent.insertBefore(elem, next_elem) 24 | else 25 | parent.appendChild(elem) 26 | return 27 | 28 | if props.animate_scrollfix and elem.getBoundingClientRect().top > 1600 29 | # console.log "Skip down", elem 30 | return 31 | 32 | elem.style.boxSizing = "border-box" 33 | elem.style.overflow = "hidden" 34 | if not props.animate_noscale 35 | elem.style.transform = "scale(0.6)" 36 | elem.style.opacity = "0" 37 | elem.style.height = "0px" 38 | elem.style.marginTop = "0px" 39 | elem.style.marginBottom = "0px" 40 | elem.style.paddingTop = "0px" 41 | elem.style.paddingBottom = "0px" 42 | elem.style.borderTopWidth = "0px" 43 | elem.style.borderBottomWidth = "0px" 44 | elem.style.transition = "none" 45 | 46 | setTimeout (-> 47 | elem.className += " animate-inout" 48 | elem.style.height = h+"px" 49 | elem.style.transform = "scale(1)" 50 | elem.style.opacity = "1" 51 | elem.style.marginTop = margin_top 52 | elem.style.marginBottom = margin_bottom 53 | elem.style.paddingTop = padding_top 54 | elem.style.paddingBottom = padding_bottom 55 | elem.style.borderTopWidth = border_top_width 56 | elem.style.borderBottomWidth = border_bottom_width 57 | ), 1 58 | 59 | 60 | elem.addEventListener "transitionend", -> 61 | elem.classList.remove("animate-inout") 62 | elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null 63 | elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null 64 | elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null 65 | elem.style.borderTopWidth = elem.style.borderBottomWidth = elem.style.overflow = null 66 | elem.removeEventListener "transitionend", arguments.callee, false 67 | 68 | shouldScrollFix: (elem, props) -> 69 | pos = elem.getBoundingClientRect() 70 | if props.animate_scrollfix and window.scrollY > 300 and pos.top < 0 and not document.querySelector(".noscrollfix:hover") 71 | return true 72 | else 73 | return false 74 | 75 | slideDownAnime: (elem, props) -> 76 | cstyle = window.getComputedStyle(elem) 77 | elem.style.overflowY = "hidden" 78 | anime({targets: elem, height: [0, elem.offsetHeight], easing: 'easeInOutExpo'}) 79 | 80 | slideUpAnime: (elem, remove_func, props) -> 81 | elem.style.overflowY = "hidden" 82 | anime({targets: elem, height: [elem.offsetHeight, 0], complete: remove_func, easing: 'easeInOutExpo'}) 83 | 84 | 85 | slideUp: (elem, remove_func, props) -> 86 | if window.Animation.shouldScrollFix(elem, props) and elem.nextSibling 87 | # Keep objects in the screen at same position 88 | top_after = document.body.scrollHeight 89 | next_elem = elem.nextSibling 90 | parent = elem.parentNode 91 | parent.removeChild(elem) 92 | top_before = document.body.scrollHeight 93 | console.log("Scrollcorrection down", (top_before - top_after)) 94 | window.scrollTo(window.scrollX, window.scrollY + (top_before - top_after)) 95 | if next_elem 96 | parent.insertBefore(elem, next_elem) 97 | else 98 | parent.appendChild(elem) 99 | remove_func() 100 | return 101 | 102 | if props.animate_scrollfix and elem.getBoundingClientRect().top > 1600 103 | remove_func() 104 | # console.log "Skip up", elem 105 | return 106 | 107 | elem.className += " animate-inout" 108 | elem.style.boxSizing = "border-box" 109 | elem.style.height = elem.offsetHeight+"px" 110 | elem.style.overflow = "hidden" 111 | elem.style.transform = "scale(1)" 112 | elem.style.opacity = "1" 113 | elem.style.pointerEvents = "none" 114 | 115 | setTimeout (-> 116 | cstyle = window.getComputedStyle(elem) 117 | elem.style.height = "0px" 118 | elem.style.marginTop = (0-parseInt(cstyle.borderTopWidth)-parseInt(cstyle.borderBottomWidth))+"px" 119 | elem.style.marginBottom = "0px" 120 | elem.style.paddingTop = "0px" 121 | elem.style.paddingBottom = "0px" 122 | elem.style.transform = "scale(0.8)" 123 | elem.style.opacity = "0" 124 | ), 1 125 | elem.addEventListener "transitionend", (e) -> 126 | if e.propertyName == "opacity" or e.elapsedTime >= 0.6 127 | elem.removeEventListener "transitionend", arguments.callee, false 128 | setTimeout ( -> 129 | remove_func() 130 | ), 2000 131 | 132 | 133 | showRight: (elem, props) -> 134 | elem.className += " animate" 135 | elem.style.opacity = 0 136 | elem.style.transform = "TranslateX(-20px) Scale(1.01)" 137 | setTimeout (-> 138 | elem.style.opacity = 1 139 | elem.style.transform = "TranslateX(0px) Scale(1)" 140 | ), 1 141 | elem.addEventListener "transitionend", -> 142 | elem.classList.remove("animate") 143 | elem.style.transform = elem.style.opacity = null 144 | elem.removeEventListener "transitionend", arguments.callee, false 145 | 146 | 147 | show: (elem, props) -> 148 | delay = arguments[arguments.length-2]?.delay*1000 or 1 149 | elem.className += " animate" 150 | elem.style.opacity = 0 151 | setTimeout (-> 152 | elem.style.opacity = 1 153 | ), delay 154 | elem.addEventListener "transitionend", -> 155 | elem.classList.remove("animate") 156 | elem.style.opacity = null 157 | elem.removeEventListener "transitionend", arguments.callee, false 158 | 159 | hide: (elem, remove_func, props) -> 160 | delay = arguments[arguments.length-2]?.delay*1000 or 1 161 | elem.className += " animate" 162 | setTimeout (-> 163 | elem.style.opacity = 0 164 | ), delay 165 | elem.addEventListener "transitionend", (e) -> 166 | if e.propertyName == "opacity" 167 | remove_func() 168 | elem.removeEventListener "transitionend", arguments.callee, false 169 | 170 | addVisibleClass: (elem, props) -> 171 | setTimeout -> 172 | elem.classList.add("visible") 173 | 174 | cloneAnimation: (elem, animation) -> 175 | window.requestAnimationFrame => 176 | if elem.style.pointerEvents == "none" # Fix if animation called on cloned element 177 | elem = elem.nextSibling 178 | elem.style.position = "relative" 179 | elem.style.zIndex = "2" 180 | clone = elem.cloneNode(true) 181 | cstyle = window.getComputedStyle(elem) 182 | clone.classList.remove("loading") 183 | clone.style.position = "absolute" 184 | clone.style.zIndex = "1" 185 | clone.style.pointerEvents = "none" 186 | clone.style.animation = "none" 187 | 188 | # Check the position difference between original and cloned object 189 | elem.parentNode.insertBefore(clone, elem) 190 | cloneleft = clone.offsetLeft 191 | 192 | clone.parentNode.removeChild(clone) # Remove from dom to avoid animation 193 | clone.style.marginLeft = parseInt(cstyle.marginLeft) + elem.offsetLeft - cloneleft + "px" 194 | elem.parentNode.insertBefore(clone, elem) 195 | 196 | clone.style.animation = "#{animation} 0.8s ease-in-out forwards" 197 | setTimeout ( -> clone.remove() ), 1000 198 | 199 | flashIn: (elem) -> 200 | if elem.offsetWidth > 100 201 | @cloneAnimation(elem, "flash-in-big") 202 | else 203 | @cloneAnimation(elem, "flash-in") 204 | 205 | flashOut: (elem) -> 206 | if elem.offsetWidth > 100 207 | @cloneAnimation(elem, "flash-out-big") 208 | else 209 | @cloneAnimation(elem, "flash-out") 210 | 211 | 212 | window.Animation = new Animation() -------------------------------------------------------------------------------- /js/lib/anime.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Anime v1.0.0 3 | * http://anime-js.com 4 | * JavaScript animation engine 5 | * Copyright (c) 2016 Julian Garnier 6 | * http://juliangarnier.com 7 | * Released under the MIT license 8 | */ 9 | (function(r,n){"function"===typeof define&&define.amd?define([],n):"object"===typeof module&&module.exports?module.exports=n():r.anime=n()})(this,function(){var r={duration:1E3,delay:0,loop:!1,autoplay:!0,direction:"normal",easing:"easeOutElastic",elasticity:400,round:!1,begin:void 0,update:void 0,complete:void 0},n="translateX translateY translateZ rotate rotateX rotateY rotateZ scale scaleX scaleY scaleZ skewX skewY".split(" "),e=function(){return{array:function(a){return Array.isArray(a)},object:function(a){return-1< 10 | Object.prototype.toString.call(a).indexOf("Object")},html:function(a){return a instanceof NodeList||a instanceof HTMLCollection},node:function(a){return a.nodeType},svg:function(a){return a instanceof SVGElement},number:function(a){return!isNaN(parseInt(a))},string:function(a){return"string"===typeof a},func:function(a){return"function"===typeof a},undef:function(a){return"undefined"===typeof a},"null":function(a){return"null"===typeof a},hex:function(a){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a)}, 11 | rgb:function(a){return/^rgb/.test(a)},rgba:function(a){return/^rgba/.test(a)},hsl:function(a){return/^hsl/.test(a)},color:function(a){return e.hex(a)||e.rgb(a)||e.rgba(a)||e.hsl(a)}}}(),z=function(){var a={},b={Sine:function(a){return 1-Math.cos(a*Math.PI/2)},Circ:function(a){return 1-Math.sqrt(1-a*a)},Elastic:function(a,b){if(0===a||1===a)return a;var f=1-Math.min(b,998)/1E3,h=a/1-1;return-(Math.pow(2,10*h)*Math.sin(2*(h-f/(2*Math.PI)*Math.asin(1))*Math.PI/f))},Back:function(a){return a*a*(3*a-2)}, 12 | Bounce:function(a){for(var b,f=4;a<((b=Math.pow(2,--f))-1)/11;);return 1/Math.pow(4,3-f)-7.5625*Math.pow((3*b-2)/22-a,2)}};["Quad","Cubic","Quart","Quint","Expo"].forEach(function(a,d){b[a]=function(a){return Math.pow(a,d+2)}});Object.keys(b).forEach(function(c){var d=b[c];a["easeIn"+c]=d;a["easeOut"+c]=function(a,b){return 1-d(1-a,b)};a["easeInOut"+c]=function(a,b){return.5>a?d(2*a,b)/2:1-d(-2*a+2,b)/2}});a.linear=function(a){return a};return a}(),u=function(a){return e.string(a)?a:a+""},A=function(a){return a.replace(/([a-z])([A-Z])/g, 13 | "$1-$2").toLowerCase()},B=function(a){if(e.color(a))return!1;try{return document.querySelectorAll(a)}catch(b){return!1}},v=function(a){return a.reduce(function(a,c){return a.concat(e.array(c)?v(c):c)},[])},p=function(a){if(e.array(a))return a;e.string(a)&&(a=B(a)||a);return e.html(a)?[].slice.call(a):[a]},C=function(a,b){return a.some(function(a){return a===b})},N=function(a,b){var c={};a.forEach(function(a){var f=JSON.stringify(b.map(function(b){return a[b]}));c[f]=c[f]||[];c[f].push(a)});return Object.keys(c).map(function(a){return c[a]})}, 14 | D=function(a){return a.filter(function(a,c,d){return d.indexOf(a)===c})},w=function(a){var b={},c;for(c in a)b[c]=a[c];return b},t=function(a,b){for(var c in b)a[c]=e.undef(a[c])?b[c]:a[c];return a},O=function(a){a=a.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(a,b,c,e){return b+b+c+c+e+e});var b=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(a);a=parseInt(b[1],16);var c=parseInt(b[2],16),b=parseInt(b[3],16);return"rgb("+a+","+c+","+b+")"},P=function(a){a=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(a); 15 | var b=parseInt(a[1])/360,c=parseInt(a[2])/100,d=parseInt(a[3])/100;a=function(a,b,c){0>c&&(c+=1);1c?b:c<2/3?a+(b-a)*(2/3-c)*6:a};if(0==c)c=d=b=d;else var f=.5>d?d*(1+c):d+c-d*c,h=2*d-f,c=a(h,f,b+1/3),d=a(h,f,b),b=a(h,f,b-1/3);return"rgb("+255*c+","+255*d+","+255*b+")"},k=function(a){return/([\+\-]?[0-9|auto\.]+)(%|px|pt|em|rem|in|cm|mm|ex|pc|vw|vh|deg)?/.exec(a)[2]},E=function(a,b,c){return k(b)?b:-1=a.delay&&(a.begin(b),a.begin=void 0);c.current>=b.duration?(a.loop?(c.start=+new Date,"alternate"===a.direction&&y(b,!0),e.number(a.loop)&& 25 | a.loop--,c.raf=requestAnimationFrame(c.tick)):(b.ended=!0,a.complete&&a.complete(b),b.pause()),c.last=0):c.raf=requestAnimationFrame(c.tick)}}};b.seek=function(a){L(b,a/100*b.duration)};b.pause=function(){b.running=!1;cancelAnimationFrame(c.raf);X(b);var a=m.indexOf(b);-1