ZeroBlog.coffee 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. class ZeroBlog extends ZeroFrame
  2. init: ->
  3. @data = null
  4. @site_info = null
  5. @server_info = null
  6. @event_page_load = $.Deferred()
  7. @event_site_info = $.Deferred()
  8. # Editable items on own site
  9. $.when(@event_page_load, @event_site_info).done =>
  10. if @site_info.settings.own or @data.demo
  11. @addInlineEditors()
  12. @checkPublishbar()
  13. $(".publishbar").on "click", @publish
  14. $(".posts .button.new").css("display", "inline-block")
  15. $(".editbar .icon-help").on "click", =>
  16. $(".editbar .markdown-help").css("display", "block")
  17. $(".editbar .markdown-help").toggleClassLater("visible", 10)
  18. $(".editbar .icon-help").toggleClass("active")
  19. return false
  20. $.when(@event_site_info).done =>
  21. @log "event site info"
  22. # Set avatar
  23. imagedata = new Identicon(@site_info.address, 70).toString();
  24. $("body").append("<style>.avatar { background-image: url(data:image/png;base64,#{imagedata}) }</style>")
  25. @log "inited!"
  26. loadData: (query="new") ->
  27. # Get blog parameters
  28. if query == "old" # Old type query for pre 0.3.0
  29. query = "SELECT key, value FROM json LEFT JOIN keyvalue USING (json_id) WHERE path = 'data.json'"
  30. else
  31. query = "SELECT key, value FROM json LEFT JOIN keyvalue USING (json_id) WHERE directory = '' AND file_name = 'data.json'"
  32. @cmd "dbQuery", [query], (res) =>
  33. @data = {}
  34. if res
  35. for row in res
  36. @data[row.key] = row.value
  37. $(".left h1 a").html(@data.title).data("content", @data.title)
  38. $(".left h2").html(Text.toMarked(@data.description)).data("content", @data.description)
  39. $(".left .links").html(Text.toMarked(@data.links)).data("content", @data.links)
  40. routeUrl: (url) ->
  41. @log "Routing url:", url
  42. if match = url.match /Post:([0-9]+)/
  43. $("body").addClass("page-post")
  44. @pagePost(parseInt(match[1]))
  45. else
  46. $("body").addClass("page-main")
  47. @pageMain()
  48. # - Pages -
  49. pagePost: (post_id) ->
  50. s = (+ new Date)
  51. @cmd "dbQuery", ["SELECT * FROM post WHERE post_id = #{post_id} LIMIT 1"], (res) =>
  52. if res.length
  53. @applyPostdata($(".post-full"), res[0], true)
  54. Comments.pagePost(post_id)
  55. else
  56. $(".post-full").html("<h1>Not found</h1>")
  57. @pageLoaded()
  58. pageMain: ->
  59. @cmd "dbQuery", ["SELECT post.*, COUNT(comment_id) AS comments FROM post LEFT JOIN comment USING (post_id) GROUP BY post_id ORDER BY date_published"], (res) =>
  60. s = (+ new Date)
  61. for post in res
  62. elem = $("#post_#{post.post_id}")
  63. if elem.length == 0 # Not exits yet
  64. elem = $(".post.template").clone().removeClass("template").attr("id", "post_#{post.post_id}")
  65. elem.prependTo(".posts")
  66. @applyPostdata(elem, post)
  67. @pageLoaded()
  68. @log "Posts loaded in", ((+ new Date)-s),"ms"
  69. $(".posts .new").on "click", => # Create new blog post
  70. @cmd "fileGet", ["data/data.json"], (res) =>
  71. data = JSON.parse(res)
  72. # Add to data
  73. data.post.unshift
  74. post_id: data.next_post_id
  75. title: "New blog post"
  76. date_published: (+ new Date)/1000
  77. body: "Blog post body"
  78. data.next_post_id += 1
  79. # Create html elements
  80. elem = $(".post.template").clone().removeClass("template")
  81. @applyPostdata(elem, data.post[0])
  82. elem.hide()
  83. elem.prependTo(".posts").slideDown()
  84. @addInlineEditors(elem)
  85. @writeData(data)
  86. return false
  87. # - EOF Pages -
  88. # All page content loaded
  89. pageLoaded: =>
  90. $("body").addClass("loaded") # Back/forward button keep position support
  91. $('pre code').each (i, block) -> # Higlight code blocks
  92. hljs.highlightBlock(block)
  93. @event_page_load.resolve()
  94. @cmd "innerLoaded", true
  95. addInlineEditors: (parent) ->
  96. @logStart "Adding inline editors"
  97. elems = $("[data-editable]:visible", parent)
  98. for elem in elems
  99. elem = $(elem)
  100. if not elem.data("editor") and not elem.hasClass("editor")
  101. editor = new InlineEditor(elem, @getContent, @saveContent, @getObject)
  102. elem.data("editor", editor)
  103. @logEnd "Adding inline editors"
  104. # Check if publishing is necessary
  105. checkPublishbar: ->
  106. if not @site_modified or @site_modified > @site_info.content.modified
  107. $(".publishbar").addClass("visible")
  108. else
  109. $(".publishbar").removeClass("visible")
  110. # Sign and Publish site
  111. publish: =>
  112. @cmd "wrapperPrompt", ["Enter your private key:", "password"], (privatekey) => # Prompt the private key
  113. $(".publishbar .button").addClass("loading")
  114. @cmd "sitePublish", [privatekey], (res) =>
  115. $(".publishbar .button").removeClass("loading")
  116. @log "Publish result:", res
  117. return false # Ignore link default event
  118. # Apply from data to post html element
  119. applyPostdata: (elem, post, full=false) ->
  120. title_hash = post.title.replace(/[#?& ]/g, "+").replace(/[+]+/g, "+")
  121. elem.data("object", "Post:"+post.post_id)
  122. $(".title .editable", elem).html(post.title).attr("href", "?Post:#{post.post_id}:#{title_hash}").data("content", post.title)
  123. date_published = Time.since(post.date_published)
  124. # Published date
  125. if post.body.match /^---/m # Has more over fold
  126. date_published += " &middot; #{Time.readtime(post.body)}" # If has break add readtime
  127. $(".more", elem).css("display", "inline-block").attr("href", "?Post:#{post.post_id}:#{title_hash}")
  128. $(".details .published", elem).html(date_published).data("content", post.date_published)
  129. # Comments num
  130. if post.comments > 0
  131. $(".details .comments-num", elem).css("display", "inline").attr("href", "?Post:#{post.post_id}:#{title_hash}#Comments")
  132. $(".details .comments-num .num", elem).text("#{post.comments} comments")
  133. else
  134. $(".details .comments-num", elem).css("display", "none")
  135. if full
  136. body = post.body
  137. else # On main page only show post until the first --- hr separator
  138. body = post.body.replace(/^([\s\S]*?)\n---\n[\s\S]*$/, "$1")
  139. $(".body", elem).html(Text.toMarked(body)).data("content", post.body)
  140. # Wrapper websocket connection ready
  141. onOpenWebsocket: (e) =>
  142. @loadData()
  143. @routeUrl(window.location.search.substring(1))
  144. @cmd "siteInfo", {}, @setSiteinfo
  145. @cmd "serverInfo", {}, (ret) => # Get server info
  146. @server_info = ret
  147. if @server_info.rev < 160
  148. @loadData("old")
  149. # Returns the elem parent object
  150. getObject: (elem) =>
  151. return elem.parents("[data-object]")
  152. # Get content from data.json
  153. getContent: (elem, raw=false) =>
  154. [type, id] = @getObject(elem).data("object").split(":")
  155. id = parseInt(id)
  156. content = elem.data("content")
  157. if elem.data("editable-mode") == "timestamp" # Convert to time
  158. content = Time.date(content, "full")
  159. if elem.data("editable-mode") == "simple" or raw # No markdown
  160. return content
  161. else
  162. return Text.toMarked(content)
  163. # Save content to data.json
  164. saveContent: (elem, content, cb=false) =>
  165. if elem.data("deletable") and content == null then return @deleteObject(elem) # Its a delete request
  166. elem.data("content", content)
  167. [type, id] = @getObject(elem).data("object").split(":")
  168. id = parseInt(id)
  169. @cmd "fileGet", ["data/data.json"], (res) =>
  170. data = JSON.parse(res)
  171. if type == "Post"
  172. post = (post for post in data.post when post.post_id == id)[0]
  173. if elem.data("editable-mode") == "timestamp" # Time parse to timestamp
  174. content = Time.timestamp(content)
  175. post[elem.data("editable")] = content
  176. else if type == "Site"
  177. data[elem.data("editable")] = content
  178. @writeData data, (res) =>
  179. if cb
  180. if res == true # OK
  181. if elem.data("editable-mode") == "simple" # No markdown
  182. cb(content)
  183. else if elem.data("editable-mode") == "timestamp" # Format timestamp
  184. cb(Time.since(content))
  185. else
  186. cb(Text.toMarked(content))
  187. else # Error
  188. cb(false)
  189. deleteObject: (elem) ->
  190. [type, id] = elem.data("object").split(":")
  191. id = parseInt(id)
  192. @cmd "fileGet", ["data/data.json"], (res) =>
  193. data = JSON.parse(res)
  194. if type == "Post"
  195. post = (post for post in data.post when post.post_id == id)[0]
  196. if not post then return false # No post found for this id
  197. data.post.splice(data.post.indexOf(post), 1) # Remove from data
  198. @writeData data, (res) =>
  199. if res == true then window.open("?Home", "_top") # Go to home
  200. writeData: (data, cb=null) ->
  201. if not data
  202. return @log "Data missing"
  203. @data["modified"] = data.modified = Time.timestamp()
  204. json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t'))) # Encode to json, encode utf8
  205. @cmd "fileWrite", ["data/data.json", btoa(json_raw)], (res) => # Convert to to base64 and send
  206. if res == "ok"
  207. if cb then cb(true)
  208. else
  209. @cmd "wrapperNotification", ["error", "File write error: #{res}"]
  210. if cb then cb(false)
  211. @checkPublishbar()
  212. # Updating title in content.json
  213. $.get "content.json", ((content) =>
  214. content = content.replace /"title": ".*?"/, "\"title\": \"#{data.title}\"" # Load as raw html to prevent js bignumber problems
  215. @cmd "fileWrite", ["content.json", btoa(content)], (res) =>
  216. if res != "ok"
  217. @cmd "wrapperNotification", ["error", "Content.json write error: #{res}"]
  218. ), "html"
  219. writePublish: (inner_path, data, cb) ->
  220. @cmd "fileWrite", [inner_path, data], (res) =>
  221. if res != "ok" # fileWrite failed
  222. @cmd "wrapperNotification", ["error", "File write error: #{res}"]
  223. cb(false)
  224. return false
  225. @cmd "sitePublish", {"inner_path": inner_path}, (res) =>
  226. if res == "ok"
  227. cb(true)
  228. else
  229. cb(res)
  230. # Parse incoming requests
  231. onRequest: (cmd, message) ->
  232. if cmd == "setSiteInfo" # Site updated
  233. @actionSetSiteInfo(message)
  234. else
  235. @log "Unknown command", message
  236. # Siteinfo changed
  237. actionSetSiteInfo: (message) =>
  238. @setSiteinfo(message.params)
  239. @checkPublishbar()
  240. setSiteinfo: (site_info) =>
  241. @site_info = site_info
  242. @event_site_info.resolve(site_info)
  243. if $("body").hasClass("page-post") then Comments.checkCert() # Update if username changed
  244. # User commented
  245. if site_info.event?[0] == "file_done" and site_info.event[1].match /.*users.*data.json$/
  246. if $("body").hasClass("page-post")
  247. Comments.loadComments() # Post page, reload comments
  248. if $("body").hasClass("page-main")
  249. RateLimit 500, =>
  250. @pageMain()
  251. else if site_info.event?[0] == "file_done" and site_info.event[1] == "data/data.json"
  252. @loadData()
  253. @pageMain()
  254. else
  255. window.Page = new ZeroBlog()