Browse Source

initial zeroblog release

HelloZeroNet 9 years ago
parent
commit
1f99be6e27

+ 6 - 0
README.md

@@ -1,2 +1,8 @@
 # ZeroBlog
 Demo for decentralized, self publishing blogging platform.
+
+## Screenshot
+
+![Screenshot](http://i.imgur.com/diTYHcm.png) 
+
+ZeroNet address: http://127.0.0.1:43110/1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8

+ 45 - 0
content.json

@@ -0,0 +1,45 @@
+{
+    "address": "1BLogC9LN4oPDcruNz3qo1ysa133E9AGg8", 
+    "background-color": "white", 
+    "description": "Blogging platform Demo", 
+    "files": {
+        "css/all.css": {
+            "sha1": "21e4c12bb5ab2a627eba5263fcb703ce16a37068", 
+            "sha512": "a558198f52601022e4d2955b6a8cc807dc32b5c35b838e6c26e59255faf4acc7", 
+            "size": 103665
+        }, 
+        "data.json": {
+            "sha1": "95ae20525b71991d299010c4dc7e0fdc6d60d2d4", 
+            "sha512": "3a6082e17aedb6265fb66785279aa305b54f1a210c42bb72a9680bdbcb90244d", 
+            "size": 4966
+        }, 
+        "data.json-default": {
+            "sha1": "c5e086beb6c4fb2243a0833e33fee134053a7ef0", 
+            "sha512": "53b13a1d181295af27bdeca328adfe7a45d4784044ce632bc647d5bbf7661831", 
+            "size": 367
+        }, 
+        "img/loading.gif": {
+            "sha1": "a669dbd8a577f6957656fc88ba4960b93fcd23d9", 
+            "sha512": "8a42b98962faea74618113166886be488c09dad10ca47fe97005edc5fb40cc00", 
+            "size": 723
+        }, 
+        "index.html": {
+            "sha1": "e0f67772ba195dee1fc6bb12995e89711eda8568", 
+            "sha512": "13788bd35787f601e9fc81d635cee5dd0fa183d4d54e26d53f15301d53fc763e", 
+            "size": 2779
+        }, 
+        "js/all.js": {
+            "sha1": "964293829a59809e0fcebfa8e048db54d31052ee", 
+            "sha512": "275479ad1688007d2ea23b710ce09ecd2bab9d393cc8dcc108cea864eea5d464", 
+            "size": 175292
+        }
+    }, 
+    "ignore": "(js|css)/(?!all.(js|css))", 
+    "modified": 1422150921.947, 
+    "sign": [
+        94853846199073492279879342423084724620790215497516934869657435399769106624650, 
+        102846947520069373619469728596000712163818394583545292081093360672824618217431
+    ], 
+    "title": "ZeroBlog", 
+    "zeronet_version": "0.1.6"
+}

+ 114 - 0
css/ZeroBlog.css

@@ -0,0 +1,114 @@
+/* Design based on medium */
+
+body { background-color: white; color: #333332; margin: 10px; padding: 0px; font-family: 'Roboto', sans-serif; height: 15000px; overflow: hidden }
+body.loaded { height: auto; overflow: auto }
+h1, h2, h3, h4 { font-family: 'Roboto', sans-serif; font-weight: normal; margin: 0px; padding: 0px }
+h1 { font-size: 32px; line-height: 1.2em; font-weight: bold; letter-spacing: -0.5px; margin-bottom: 5px }
+h2 { margin-top: 3em }
+h3 { font-size: 24px; margin-top: 2em }
+h1 + h2, h2 + h3 { margin-top: inherit }
+
+p { margin-top: 0.9em; margin-bottom: 0.9em }
+hr { margin: 20px 0px; border: none; border-bottom: 1px solid #eee; margin-left: auto; margin-right: auto; width: 120px; }
+small { font-size: 80%; color: #999; }
+
+a { border-bottom: 1px solid #3498db; text-decoration: none; color: black; font-weight: bold }
+a.nolink { border-bottom: none }
+a:hover { color: #3498db }
+
+.button { 
+	padding: 5px 10px; margin-left: 10px; background-color: #DDE0E0; border-bottom: 2px solid #999998; background-position: left center; 
+	border-radius: 2px; text-decoration: none; transition: all 0.5s ease-out; color: #333
+}
+.button:hover { background-color: #FFF400; border-color: white; border-bottom: 2px solid #4D4D4C; transition: none; color: inherit }
+.button:active { position: relative; top: 1px }
+
+/*.button-delete { background-color: #e74c3c; border-bottom-color: #A83F34; color: white }*/
+.button-outline { background-color: white; color: #DDD; border: 1px solid #eee }
+
+.button-delete:hover { background-color: #FF5442; border: 1px solid #FF5442; color: white } 
+.button-ok:hover { background-color: #27AE60; border: 1px solid #27AE60; color: white } 
+
+.button.loading { 
+	color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; 
+	transition: all 0.5s ease-out; pointer-events: none; border-bottom: 2px solid #666
+} 
+
+.cancel { margin-left: 10px; font-size: 80%; color: #999; }
+
+
+.template { display: none }
+
+.editable { outline: none }
+.editable-edit:hover { opacity: 1 }
+.editable-edit { 
+	opacity: 0; float: left; margin-top: 0px; margin-left: -40px; padding: 2px 20px; transition: all 0.3s; width: 0px;
+	color: rgba(100,100,100,0.5); text-decoration: none; font-size: 18px; font-weight: normal; border: none;
+}
+.editing { white-space: pre-wrap; z-index: 1; position: relative; outline: 10000px solid rgba(255,255,255,0.9) !important; }
+.editing p { margin: 0px; padding: 0px } /* IE FIX */
+
+
+/* -- Editbar -- */
+
+.bottombar { 
+	display: none; position: fixed; padding: 10px 20px; opacity: 0; background-color: rgba(255,255,255,0.9);
+	right: 30px; bottom: 0px; z-index: 999; transition: all 0.3s; transform: translateY(50px) 
+}
+.bottombar.visible { transform: translateY(0px); opacity: 1 }
+.publishbar { z-index: 990; }
+.publishbar.visible { display: inline-block; }
+
+
+/* -- Left -- */
+
+.left { float: left; position: absolute; width: 170px; padding-left: 60px; padding-right: 20px; margin-top: 60px; text-align: right }
+.right { float: left; padding-left: 60px; margin-left: 240px; max-width: 650px; padding-right: 60px; padding-top: 60px }
+
+.left .avatar { 
+	background-color: #F0F0F0; width: 60px; height: 60px; border-radius: 100%; margin-bottom: 10px; 
+	background-position: center center; background-size: 70%; display: inline-block;
+}
+.left h1 a { font-family: Tinos; display: inline-block }
+.left h2 { font-size: 15px; font-family: Tinos; line-height: 1.6em; color: #AAA; margin-top: 14px; letter-spacing: 0.2px }
+.left ul, .left li { padding: 0px; margin: 0px; list-style-type: none; line-height: 2em }
+.left hr { margin-left: 100px; margin-right: 0px; width: auto }
+.left .links { width: 230px; margin-left: -60px }
+.left .links.editing { text-align: left }
+
+/* -- Post -- */
+
+.posts .new { display: none; position: absolute; top: -50px; margin-left: 0px; left: 50%; transform: translateX(-50%) }
+
+.posts, .post-full { display: none; position: relative; }
+.page-main .posts { display: block }
+.page-post .post-full { display: block }
+
+
+.post { margin-bottom: 50px; padding-bottom: 50px; border-bottom: 1px solid #eee; min-width: 500px }
+.post .title a { text-decoration: none; color: inherit; display: inline-block; border-bottom: none; font-weight: inherit }
+.posts .title a:visited { color: #666969 }
+.post .details { color: #BBB; margin-top: 5px; margin-bottom: 20px }
+.post .body { font-size: 21.5px; line-height: 1.6; font-family: Tinos; margin-top: 20px }
+
+.post .body h1 { text-align: center; margin-top: 50px }
+.post .body h1:before { content: " "; border-top: 1px solid #EEE; width: 120px; display: block; margin-left: auto; margin-right: auto; margin-bottom: 50px; }
+
+.post .body li { margin-top: 0.5em; margin-bottom: 0.5em }
+.post .body hr:first-of-type { display: none }
+
+.post .body code { 
+	background-color: #f5f5f5; border: 1px solid #ccc; padding: 0px 5px; overflow: auto; border-radius: 2px; display: inline-block;
+	color: #444; font-weight: normal; font-size: 60%; vertical-align: text-bottom; border-bottom-width: 2px;
+}
+.post .body pre { table-layout: fixed; width: 100%; display: table; white-space: normal; }
+.post .body pre code { padding: 10px 20px; white-space: pre; max-width: 850px }
+
+/*.post .more { 
+	display: inline-block; border: 1px solid #eee; padding: 10px 25px; border-radius: 26px; font-size: 11px; color: #AAA; font-weight: normal; 
+	left: 50%; position: relative; transform: translateX(-50%);
+}*/
+
+.post .more { font-size: 19px; border-bottom: none; display: none }
+.post .more .readmore { border-bottom: 2px solid #eee }
+.post .more:hover .readmore { border-bottom: 2px solid #3498db }

File diff suppressed because it is too large
+ 129 - 0
css/all.css


File diff suppressed because it is too large
+ 5 - 0
css/fonts.css


+ 124 - 0
css/github.css

@@ -0,0 +1,124 @@
+/*
+
+github.com style (c) Vasily Polovnyov <vast@whiteants.net>
+
+*/
+
+.hljs {
+  display: block;
+  overflow-x: auto;
+  padding: 0.5em;
+  color: #333;
+  background: #f8f8f8;
+  -webkit-text-size-adjust: none;
+}
+
+.hljs-comment,
+.diff .hljs-header,
+.hljs-javadoc {
+  color: #998;
+  font-style: italic;
+}
+
+.hljs-keyword,
+.css .rule .hljs-keyword,
+.hljs-winutils,
+.nginx .hljs-title,
+.hljs-subst,
+.hljs-request,
+.hljs-status {
+  color: #333;
+  font-weight: bold;
+}
+
+.hljs-number,
+.hljs-hexcolor,
+.ruby .hljs-constant {
+  color: #008080;
+}
+
+.hljs-string,
+.hljs-tag .hljs-value,
+.hljs-phpdoc,
+.hljs-dartdoc,
+.tex .hljs-formula {
+  color: #d14;
+}
+
+.hljs-title,
+.hljs-id,
+.scss .hljs-preprocessor {
+  color: #900;
+  font-weight: bold;
+}
+
+.hljs-list .hljs-keyword,
+.hljs-subst {
+  font-weight: normal;
+}
+
+.hljs-class .hljs-title,
+.hljs-type,
+.vhdl .hljs-literal,
+.tex .hljs-command {
+  color: #458;
+  font-weight: bold;
+}
+
+.hljs-tag,
+.hljs-tag .hljs-title,
+.hljs-rules .hljs-property,
+.django .hljs-tag .hljs-keyword {
+  color: #000080;
+  font-weight: normal;
+}
+
+.hljs-attribute,
+.hljs-variable,
+.lisp .hljs-body {
+  color: #008080;
+}
+
+.hljs-regexp {
+  color: #009926;
+}
+
+.hljs-symbol,
+.ruby .hljs-symbol .hljs-string,
+.lisp .hljs-keyword,
+.clojure .hljs-keyword,
+.scheme .hljs-keyword,
+.tex .hljs-special,
+.hljs-prompt {
+  color: #990073;
+}
+
+.hljs-built_in {
+  color: #0086b3;
+}
+
+.hljs-preprocessor,
+.hljs-pragma,
+.hljs-pi,
+.hljs-doctype,
+.hljs-shebang,
+.hljs-cdata {
+  color: #999;
+  font-weight: bold;
+}
+
+.hljs-deletion {
+  background: #fdd;
+}
+
+.hljs-addition {
+  background: #dfd;
+}
+
+.diff .hljs-change {
+  background: #0086b3;
+}
+
+.hljs-chunk {
+  color: #aaa;
+}

File diff suppressed because it is too large
+ 27 - 0
data.json


+ 17 - 0
data.json-default

@@ -0,0 +1,17 @@
+{
+	"title": "MyZeroBlog",
+	"description": "Demo for decentralized, self publishing blogging platform.",
+	"links": "- [Source code](https://github.com/HelloZeroNet)",
+	"next_id": 2,
+	"demo": false,
+	"modified": 1422102473,
+	"posts": [
+		{
+			"id": 1,
+			"title": "Hello ZeroBlog!",
+			"posted": 1422065620.441,
+			"edited": false,
+			"body": "Your first post"
+		}
+	]
+}

BIN
img/loading.gif


+ 83 - 0
index.html

@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>ZeroBlog Demo</title>
+ <meta charset="utf-8">
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <link rel="stylesheet" href="css/all.css" />
+ <base href="/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/" target="_top" id="base">
+ <script>base.href = document.location.href.replace("/media", "").replace("index.html", "") // Make hashtags work</script>
+
+</head>
+<body>
+
+<!-- editbar -->
+<div class="editbar bottombar">
+ Editing: <span class="object">Post:21.body</span> <a href="#Save" class="button save">Save</a> <a href="#Delete" class="button button-delete button-outline delete">Delete</a> <a href="#Cancel" class="cancel">Cancel</a>
+</div>
+<!-- EOF editbar -->
+
+
+<!-- publishbar -->
+<div class="publishbar bottombar">
+ <small>Content changed</small> <a href="#Publish" class="button button-outline button-ok publish">Sign &amp; Publish new content</a>
+</div>
+<!-- EOF publishbar -->
+
+
+<!-- left -->
+<div class="left" data-object="Site">
+ <a href="?Home" class="nolink"><div class="avatar"> </div></a>
+ <h1><a href="?Home" class="nolink" data-editable="title" data-editable-mode="simple"></a></h1>
+ <h2 data-editable="description"></h2>
+ <hr>
+ <div class="links" data-editable="links">
+ </div>
+</div>
+<!-- EOF left -->
+
+
+<!-- right -->
+<div class="right">
+ <!-- posts -->
+ <div class="posts">
+  <a href="#New+Post" class="button button-outline new">Add new post</a>
+  
+  <!-- Template: post -->
+  <div class="post template" data-object="Post:23" data-deletable="True">
+   <h1 class="title"><a href="?Post:23:Title" data-editable="title" data-editable-mode="simple">Title</a></h1>
+   <div class="details" data-editable="posted" data-editable-mode="timestamp">21 hours ago &middot; 2 min read</div>
+   <div class="body">Body</div>
+   <a class="more" href="#"><span class='readmore'>Read more</span> →</a>
+  </div>
+  <!-- EOF Template: post -->
+ 
+ </div>
+ <!-- EOF posts -->
+
+ <!-- post-full -->
+ <div class="post post-full" data-object="Post:23" data-deletable="True">
+  <h1 class="title"><a href="?Post:23:Title" data-editable="title" data-editable-mode="simple">Title</a></h1>
+  <div class="details" data-editable="posted" data-editable-mode="timestamp">21 hours ago &middot; 2 min read</div>
+  <div class="body" data-editable="body"></div>
+ </div>
+ <!-- EOF post-full -->
+
+</div>
+<!-- EOF right -->
+
+
+<div style="clear: both"></div>
+
+<script>
+// Load from media from backward compatibility
+var script = document.createElement('script');
+window.media_root = location.pathname.replace(/(media\/.*)\/.*/, "$1")
+script.src = media_root+"/js/all.js";
+document.getElementsByTagName('head')[0].appendChild(script);
+</script>
+<!-- from 0.1.6 works: <script type="text/javascript" src="js/all.js" asyc></script> -->
+
+</body>
+</html>

+ 121 - 0
js/InlineEditor.coffee

@@ -0,0 +1,121 @@
+class InlineEditor
+	constructor: (@elem, @getContent, @saveContent, @getObject) ->
+		@edit_button = $("<a href='#Edit' class='editable-edit'>§</a>")
+		@edit_button.on "click", @startEdit
+		@elem.addClass("editable").before(@edit_button)
+		@elem.on "mouseenter", (e) =>
+			@edit_button.css("opacity", "1")
+		@elem.on "mouseenter contextmenu", (e) =>
+			# Keep in display
+			scrolltop = $(window).scrollTop()
+			top = @edit_button.offset().top-parseInt(@edit_button.css("margin-top"))
+			if scrolltop > top
+				@edit_button.css("margin-top", scrolltop-top+e.clientY-20)
+			else
+				@edit_button.css("margin-top", "")
+		@elem.on "mouseleave", =>
+			@edit_button.css("opacity", "")
+
+
+	startEdit: =>
+		@elem.attr("contenteditable", "true")
+		@content_before = @elem.html() # Save current to restore on cancel
+		@elem.html @markdownToEditable(@getContent(@elem, true)) # Convert to html
+		@elem.css("outline", "10000px solid rgba(255,255,255,0)").cssLater("transition", "outline 0.3s", 5).addClassLater("editing",10) # Animate other elements fadeout
+		if $(window).scrollTop() == 0 then @elem.focus()
+		@elem.on "paste", => # Fix for html formatted paste
+			setTimeout (=>
+				fixed = @markdownToEditable(@editableToMarkdown( @elem.html() ))
+				if fixed != @elem.html()
+					@elem.html(fixed)
+			), 0
+
+		$(".editable-edit").css("display", "none") # Hide all edit button until its not finished
+
+		$(".editbar").css("display", "inline-block").addClassLater("visible", 10) 
+		$(".publishbar").css("opacity", 0) # Hide publishbar
+		$(".editbar .object").text @getObject(@elem).data("object")+"."+@elem.data("editable")
+		$(".editbar .button").removeClass("loading")
+
+		$(".editbar .save").off("click").on "click", @saveEdit
+		$(".editbar .delete").off("click").on "click", @deletePost
+		$(".editbar .cancel").off("click").on "click", @cancelEdit
+
+		# Deletable button show/hide
+		if @getObject(@elem).data("deletable")
+			$(".editbar .delete").css("display", "").html("Delete "+@getObject(@elem).data("object").split(":")[0])
+		else
+			$(".editbar .delete").css("display", "none")
+
+
+		### Tab fix (not works with contenteditable)
+		@elem.on 'keydown', (e) =>
+			if e.which == 9 # Tab fix
+				e.preventDefault();
+				s = @elem.selectionStart;
+				val = @elem.html()
+				debugger
+				@elem.html val.substring(0,@elem[0].selectionStart) + "\t" + val.substring(@elem[0].selectionEnd)
+				@elem[0].selectionEnd = s+1; 
+		###
+		
+		return false
+
+
+	stopEdit: =>
+		$(".editable-edit").css("display", "")
+		@elem.attr("contenteditable", "false")
+		@elem.removeClass("editing")
+		@elem.off "blur"
+
+		$(".editbar").cssLater("display", "none", 1000).removeClass("visible") # Hide editbar
+		$(".publishbar").css("opacity", 1) # Show publishbar
+
+
+	saveEdit: =>
+		content = @editableToMarkdown(@elem.html())
+		$(".editbar .save").addClass("loading")
+		@saveContent @elem, content, (content_html) =>
+			if content_html # File write ok
+				$(".editbar .save").removeClass("loading")
+				@stopEdit()
+				@elem.html content_html
+
+				$('pre code').each (i, block) -> # Higlight code blocks
+					hljs.highlightBlock(block)
+			else
+				$(".editbar .save").removeClass("loading")
+
+		return false
+
+
+	deletePost: =>
+		object_type = @getObject(@elem).data("object").split(":")[0]
+		window.zero_blog.cmd "wrapperConfirm", ["Are you sure you sure to delete this #{object_type}?", "Delete"], (confirmed) => 
+			@stopEdit()
+			@saveContent @getObject(@elem), null
+		return false
+
+
+	cancelEdit: =>
+		@stopEdit()
+		@elem.html @content_before
+
+		$('pre code').each (i, block) -> # Higlight code blocks
+			hljs.highlightBlock(block)
+
+		return false
+
+
+	editableToMarkdown: (s) ->
+		s = s.replace(/<br><\/p>/g, "\n").replace(/<\/p>/g,"\n")# Convert newlines IE
+		s = s.replace(/<br><\/div>/g, "\n").replace(/<div>/g,"\n").replace(/<br.*?>/g, "\n")# Convert newlines
+		s = $("<div>"+s+"</div>").text() # Convert to text
+		return s
+
+
+	markdownToEditable: (s) ->
+		return s.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")
+
+ 
+window.InlineEditor = InlineEditor

+ 317 - 0
js/ZeroBlog.coffee

@@ -0,0 +1,317 @@
+class ZeroBlog extends ZeroFrame
+	init: ->
+		# Set avatar
+		address = document.location.href.match(/media\/(.*?)\//)[1]
+		imagedata = new Identicon(address, 70).toString();
+		$("body").append("<style>.avatar { background-image: url(data:image/png;base64,#{imagedata}) }</style>")
+
+		@data = null
+		@site_info = null
+		@server_info = null
+
+		@event_page_load = $.Deferred()
+		@event_site_info = $.Deferred()
+		@loadData()
+
+		# Editable items on own site
+		$.when(@event_page_load, @event_site_info).done =>
+			if @site_info.settings.own or @data.demo
+				@addInlineEditors()
+				@checkPublishbar()
+				$(".publishbar").on "click", @publish
+				$(".posts .button.new").css("display", "inline-block")
+
+		@log "inited!"
+
+
+	loadData: ->
+		$.get "#{window.media_root}/data.json", (data) =>
+			@data = data
+			$(".left h1 a").html(data.title)
+			$(".left h2").html(marked(data.description))
+			$(".left .links").html(marked(data.links))
+
+			# Show page based on url
+			@routeUrl(window.location.search.substring(1))
+
+
+	routeUrl: (url) ->
+		@log "Routing url:", url
+		if match = url.match /Post:([0-9]+)/
+			$("body").addClass("page-post")
+			@pagePost(parseInt(match[1]))
+		else
+			$("body").addClass("page-main")
+			@pageMain()
+
+
+	# - Pages -
+
+
+	pagePost: (post_id) ->
+		s = (+ new Date)
+		found = false
+		for post in @data.posts
+			if post.id == post_id
+				found = true
+				break
+
+		if found
+			@applyPostdata($(".post-full"), post, true)
+		else
+			$(".post-full").html("<h1>Not found</h1>")
+		@pageLoaded()
+		@log "Post loaded in", ((+ new Date)-s),"ms"
+
+
+
+	pageMain: ->
+		s = (+ new Date)
+		for post in @data.posts
+			elem = $(".post.template").clone().removeClass("template")
+			@applyPostdata(elem, post)
+			elem.appendTo(".posts")
+		@pageLoaded()
+		@log "Posts loaded in", ((+ new Date)-s),"ms"
+
+		$(".posts .new").on "click", => # Create new blog post
+			# Add to data
+			@data.posts.unshift
+				id: @data.next_id
+				title: "New blog post"
+				posted: (+ new Date)/1000
+				edited: false
+				body: "Blog post body"
+			@data.next_id += 1
+
+			# Create html elements
+			elem = $(".post.template").clone().removeClass("template")
+			@applyPostdata(elem, @data.posts[0])
+			elem.hide()
+			elem.prependTo(".posts").slideDown()
+			@addInlineEditors(elem)
+
+			@writeData()
+			return false
+
+
+
+	# - EOF Pages -
+
+
+	# All page content loaded
+	pageLoaded: ->
+		$("body").addClass("loaded") # Back/forward button keep position support
+		$('pre code').each (i, block) -> # Higlight code blocks
+			hljs.highlightBlock(block)
+		@event_page_load.resolve()
+
+
+	# Add inline editor markers
+	addInlineEditors: (parent) ->
+		elems = $("[data-editable]:visible", parent)
+		for elem in elems
+			new InlineEditor($(elem), @getContent, @saveContent, @getObject)
+
+
+	# Check if publishing is necessary
+	checkPublishbar: ->
+		if not @data.modified or @data.modified > @site_info.content.modified
+			$(".publishbar").addClass("visible")
+		else
+			$(".publishbar").removeClass("visible")
+
+
+	# Sign and Publish site
+	publish: =>
+		if not @server_info.ip_external # No port open
+			@cmd "wrapperNotification", ["error", "To publish the site please open port <b>#{@server_info.fileserver_port}</b> on your router"]
+			return false
+		@cmd "wrapperPrompt", ["Enter your private key:", "password"], (privatekey) => # Prompt the private key
+			$(".publishbar .button").addClass("loading")
+			@cmd "sitePublish", [privatekey], (res) =>
+				$(".publishbar .button").removeClass("loading")
+				@log "Publish result:", res
+
+		return false # Ignore link default event
+
+
+	# Apply from data to post html element
+	applyPostdata: (elem, post, full=false) ->
+		title_hash = post.title.replace(/[#?& ]/g, "+").replace(/[+]+/g, "+")
+		elem.data("object", "Post:"+post.id)
+		$(".title a", elem).html(post.title).attr("href", "?Post:#{post.id}:#{title_hash}")
+		details = @formatSince(post.posted)
+
+		if post.body.match /^---/m # Has more over fold
+			details += " &middot; #{@readtime(post.body)}" # If has break add readtime
+			$(".more", elem).css("display", "inline").attr("href", "?Post:#{post.id}:#{title_hash}")
+		$(".details", elem).html(details)
+
+		if full 
+			body = post.body
+		else # On main page only show post until the first --- hr separator
+			body = post.body.replace(/^([\s\S]*?)\n---\n[\s\S]*$/, "$1")
+
+		$(".body", elem).html(marked(body))
+
+
+	# Wrapper websocket connection ready
+	onOpenWebsocket: (e) =>
+		@cmd "siteInfo", {}, @setSiteinfo
+		@cmd "serverInfo", {}, (ret) => # Get server info
+			@server_info = ret
+			version = @server_info.version.split(".")
+			if version[0] == "0" and version[1] == "1" and parseInt(version[2]) < 6
+				@cmd "wrapperNotification", ["error", "ZeroBlog requires ZeroNet 0.1.6, please update!"]
+
+
+	# Returns the elem parent object
+	getObject: (elem) =>
+		return elem.parents("[data-object]")
+
+
+	# Get content from data.json
+	getContent: (elem, raw=false) =>
+		[type, id] = @getObject(elem).data("object").split(":")
+		id = parseInt(id)
+		@log "Editing", type, id
+		if type == "Post"
+			post = (post for post in @data.posts when post.id == id)[0]
+			content = post[elem.data("editable")]
+
+			if elem.data("editable-mode") == "timestamp" # Time hash
+				content = @formatDate(content, "full")
+		else if type == "Site"
+			content = @data[elem.data("editable")]
+		else
+			content = "Unknown"
+
+
+		if elem.data("editable-mode") == "simple" or raw # No markdown
+			return content
+		else
+			return marked(content)
+
+
+	# Save content to data.json
+	saveContent: (elem, content, cb=false) =>
+		if elem.data("deletable") and content == null then return @deleteObject(elem) # Its a delete request
+
+		[type, id] = @getObject(elem).data("object").split(":")
+		id = parseInt(id)
+		@log "Saving", type, id
+
+		if type == "Post"
+			post = (post for post in @data.posts when post.id == id)[0]
+
+			if elem.data("editable-mode") == "timestamp" # Time parse to timestamp
+				content = @timestamp(content)
+
+			post[elem.data("editable")] = content
+		else if type == "Site"
+			@data[elem.data("editable")] = content
+
+		@writeData (res) =>
+			if cb
+				if res == true # OK
+					if elem.data("editable-mode") == "simple" # No markdown
+						cb(content)
+					else if elem.data("editable-mode") == "timestamp" # Format timestamp
+						cb(@formatSince(content))
+					else
+						cb(marked(content))
+				else # Error
+					cb(false)
+
+
+	deleteObject: (elem) ->
+		[type, id] = elem.data("object").split(":")
+		id = parseInt(id)
+
+		if type == "Post"
+			post = (post for post in @data.posts when post.id == id)[0]
+			if not post then return false # No post found for this id
+			@data.posts.splice(@data.posts.indexOf(post), 1) # Remove from data
+
+			@writeData (res) ->
+				if res == true then window.open("?Home", "_top") # Go to home
+
+
+	writeData: (cb=null) ->
+		@data.modified = @timestamp()
+		json_raw = unescape(encodeURIComponent(JSON.stringify(@data, undefined, '\t'))) # Encode to json, encode utf8
+		@cmd "fileWrite", ["data.json", btoa(json_raw)], (res) => # Convert to to base64 and send
+			if res == "ok"
+				if cb then cb(true)
+			else
+				@cmd "wrapperNotification", ["error", "File write error: #{res}"]
+				if cb then cb(false)
+			@checkPublishbar()
+
+
+	# - Date -
+
+	formatSince: (time) ->
+		now = +(new Date)/1000
+		secs = now - time
+		if secs < 60
+			back = "Just now"
+		else if secs < 60*60
+			back = "#{Math.round(secs/60)} minutes ago"
+		else if secs < 60*60*24
+			back = "#{Math.round(secs/60/60)} hours ago"
+		else if secs < 60*60*24*3
+			back = "#{Math.round(secs/60/60/24)} days ago"
+		else
+			back = "on "+@formatDate(time)
+		back = back.replace(/1 ([a-z]+)s/, "1 $1") # 1 days ago fix
+		return back
+
+
+	# Get elistamated read time for post
+	readtime: (text) ->
+		chars = text.length
+		if chars > 1500
+			return parseInt(chars/1500)+" min read"
+		else
+			return "less than 1 min read"
+
+
+	formatDate: (timestamp, format="short") ->
+		parts = (new Date(timestamp*1000)).toString().split(" ")
+		if format == "short"
+			display = parts.slice(1, 4)
+		else
+			display = parts.slice(1, 5)
+		return display.join(" ").replace(/( [0-9]{4})/, ",$1")
+
+
+	timestamp: (date="") ->
+		if date == "now" or date == ""
+			return parseInt(+(new Date)/1000)
+		else
+			return parseInt(Date.parse(date)/1000)
+
+
+	# Route incoming requests
+	route: (cmd, message) ->
+		if cmd == "setSiteInfo" # Site updated
+			@actionSetSiteInfo(message)
+		else
+			@log "Unknown command", message
+
+
+	# Siteinfo changed
+	actionSetSiteInfo: (message) =>
+		@log "setSiteinfo", message
+		@setSiteinfo(message.params)
+		@checkPublishbar()
+
+
+	setSiteinfo: (site_info) =>
+		@site_info = site_info
+		@event_site_info.resolve(site_info)
+
+
+window.zero_blog = new ZeroBlog()

File diff suppressed because it is too large
+ 6 - 0
js/all.js


File diff suppressed because it is too large
+ 1 - 0
js/lib/00-jquery.min.js


+ 73 - 0
js/lib/ZeroFrame.coffee

@@ -0,0 +1,73 @@
+class ZeroFrame
+	constructor: (url) ->
+		@url = url
+		@waiting_cb = {}
+		@connect()
+		@next_message_id = 1
+		@init()
+
+
+	init: ->
+		@
+
+
+	connect: ->
+		@target = window.parent
+		window.addEventListener("message", @onMessage, false) 
+		@cmd("innerReady")
+
+
+	onMessage: (e) =>
+		message = e.data
+		cmd = message.cmd
+		if cmd == "response"
+			if @waiting_cb[message.to]?
+				@waiting_cb[message.to](message.result)
+			else
+				@log "Websocket callback not found:", message
+		else if cmd == "wrapperReady" # Wrapper inited later
+			@cmd("innerReady")
+		else if cmd == "ping"
+			@response message.id, "pong"
+		else if cmd == "wrapperOpenedWebsocket"
+			@onOpenWebsocket()
+		else if cmd == "wrapperClosedWebsocket"
+			@onCloseWebsocket()
+		else
+			@route cmd, message
+
+
+	route: (cmd, message) =>
+		@log "Unknown command", message
+
+
+	response: (to, result) ->
+		@send {"cmd": "response", "to": to, "result": result}
+
+
+	cmd: (cmd, params={}, cb=null) ->
+		@send {"cmd": cmd, "params": params}, cb
+
+
+	send: (message, cb=null) ->
+		message.id = @next_message_id
+		@next_message_id += 1
+		@target.postMessage(message, "*")
+		if cb
+			@waiting_cb[message.id] = cb
+
+
+	log: (args...) ->
+		console.log "[ZeroFrame]", args...
+
+
+	onOpenWebsocket: =>
+		@log "Websocket open"
+
+
+	onCloseWebsocket: =>
+		@log "Websocket close"
+
+
+
+window.ZeroFrame = ZeroFrame

File diff suppressed because it is too large
+ 6 - 0
js/lib/all.js


File diff suppressed because it is too large
+ 0 - 0
js/lib/highlight.pack.js


+ 92 - 0
js/lib/identicon.js

@@ -0,0 +1,92 @@
+/**
+ * Identicon.js v1.0
+ * http://github.com/stewartlord/identicon.js
+ *
+ * Requires PNGLib
+ * http://www.xarg.org/download/pnglib.js
+ *
+ * Copyright 2013, Stewart Lord
+ * Released under the BSD license
+ * http://www.opensource.org/licenses/bsd-license.php
+ */
+
+(function() {
+    Identicon = function(hash, size, margin){
+        this.hash   = hash;
+        this.size   = size   || 64;
+        this.margin = margin || .08;
+    }
+
+    Identicon.prototype = {
+        hash:   null,
+        size:   null,
+        margin: null,
+
+        render: function(){
+            var hash    = this.hash,
+                size    = this.size,
+                margin  = Math.floor(size * this.margin),
+                cell    = Math.floor((size - (margin * 2)) / 5),
+                image   = new PNGlib(size, size, 256);
+
+            // light-grey background
+            var bg      = image.color(240, 240, 240);
+
+            // foreground is last 7 chars as hue at 50% saturation, 70% brightness
+            var rgb     = this.hsl2rgb(parseInt(hash.substr(-7), 16) / 0xfffffff, .5, .7),
+                fg      = image.color(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255);
+
+            // the first 15 characters of the hash control the pixels (even/odd)
+            // they are drawn down the middle first, then mirrored outwards
+            var i, color;
+            for (i = 0; i < 15; i++) {
+                color = parseInt(hash.charAt(i), 16) % 2 ? bg : fg;
+                if (i < 5) {
+                    this.rectangle(2 * cell + margin, i * cell + margin, cell, cell, color, image);
+                } else if (i < 10) {
+                    this.rectangle(1 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image);
+                    this.rectangle(3 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image);
+                } else if (i < 15) {
+                    this.rectangle(0 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image);
+                    this.rectangle(4 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image);
+                }
+            }
+
+            return image;
+        },
+
+        rectangle: function(x, y, w, h, color, image) {
+            var i, j;
+            for (i = x; i < x + w; i++) {
+                for (j = y; j < y + h; j++) {
+                    image.buffer[image.index(i, j)] = color;
+                }
+            }
+        },
+
+        // adapted from: https://gist.github.com/aemkei/1325937
+        hsl2rgb: function(h, s, b){
+            h *= 6;
+            s = [
+                b += s *= b < .5 ? b : 1 - b,
+                b - h % 1 * s * 2,
+                b -= s *= 2,
+                b,
+                b + h % 1 * s,
+                b + s
+            ];
+
+            return[
+                s[ ~~h    % 6 ],  // red
+                s[ (h|16) % 6 ],  // green
+                s[ (h|8)  % 6 ]   // blue
+            ];
+        },
+
+        toString: function(){
+            return this.render().getBase64();
+        }
+    }
+
+    window.Identicon = Identicon;
+})();

+ 42 - 0
js/lib/jquery.csslater.coffee

@@ -0,0 +1,42 @@
+jQuery.fn.readdClass = (class_name) ->
+	elem = @
+	elem.removeClass class_name
+	setTimeout ( ->
+		elem.addClass class_name
+	), 1
+	return @
+
+jQuery.fn.removeLater = (time = 500) ->
+	elem = @
+	setTimeout ( ->
+		elem.remove()
+	), time
+	return @
+
+jQuery.fn.hideLater = (time = 500) ->
+	elem = @
+	setTimeout ( ->
+		elem.css("display", "none")
+	), time
+	return @
+
+jQuery.fn.addClassLater = (class_name, time = 5) ->
+	elem = @
+	setTimeout ( ->
+		elem.addClass(class_name)
+	), time
+	return @
+
+jQuery.fn.removeClassLater = (class_name, time = 500) ->
+	elem = @
+	setTimeout ( ->
+		elem.removeClass(class_name)
+	), time
+	return @
+
+jQuery.fn.cssLater = (name, val, time = 500) ->
+	elem = @
+	setTimeout ( ->
+		elem.css name, val
+	), time
+	return @

File diff suppressed because it is too large
+ 5 - 0
js/lib/marked.min.js


+ 207 - 0
js/lib/pnglib.js

@@ -0,0 +1,207 @@
+/**
+* A handy class to calculate color values.
+*
+* @version 1.0
+* @author Robert Eisele <robert@xarg.org>
+* @copyright Copyright (c) 2010, Robert Eisele
+* @link http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/
+* @license http://www.opensource.org/licenses/bsd-license.php BSD License
+*
+*/
+
+(function() {
+
+	// helper functions for that ctx
+	function write(buffer, offs) {
+		for (var i = 2; i < arguments.length; i++) {
+			for (var j = 0; j < arguments[i].length; j++) {
+				buffer[offs++] = arguments[i].charAt(j);
+			}
+		}
+	}
+
+	function byte2(w) {
+		return String.fromCharCode((w >> 8) & 255, w & 255);
+	}
+
+	function byte4(w) {
+		return String.fromCharCode((w >> 24) & 255, (w >> 16) & 255, (w >> 8) & 255, w & 255);
+	}
+
+	function byte2lsb(w) {
+		return String.fromCharCode(w & 255, (w >> 8) & 255);
+	}
+
+	window.PNGlib = function(width,height,depth) {
+
+		this.width   = width;
+		this.height  = height;
+		this.depth   = depth;
+
+		// pixel data and row filter identifier size
+		this.pix_size = height * (width + 1);
+
+		// deflate header, pix_size, block headers, adler32 checksum
+		this.data_size = 2 + this.pix_size + 5 * Math.floor((0xfffe + this.pix_size) / 0xffff) + 4;
+
+		// offsets and sizes of Png chunks
+		this.ihdr_offs = 0;									// IHDR offset and size
+		this.ihdr_size = 4 + 4 + 13 + 4;
+		this.plte_offs = this.ihdr_offs + this.ihdr_size;	// PLTE offset and size
+		this.plte_size = 4 + 4 + 3 * depth + 4;
+		this.trns_offs = this.plte_offs + this.plte_size;	// tRNS offset and size
+		this.trns_size = 4 + 4 + depth + 4;
+		this.idat_offs = this.trns_offs + this.trns_size;	// IDAT offset and size
+		this.idat_size = 4 + 4 + this.data_size + 4;
+		this.iend_offs = this.idat_offs + this.idat_size;	// IEND offset and size
+		this.iend_size = 4 + 4 + 4;
+		this.buffer_size  = this.iend_offs + this.iend_size;	// total PNG size
+
+		this.buffer  = new Array();
+		this.palette = new Object();
+		this.pindex  = 0;
+
+		var _crc32 = new Array();
+
+		// initialize buffer with zero bytes
+		for (var i = 0; i < this.buffer_size; i++) {
+			this.buffer[i] = "\x00";
+		}
+
+		// initialize non-zero elements
+		write(this.buffer, this.ihdr_offs, byte4(this.ihdr_size - 12), 'IHDR', byte4(width), byte4(height), "\x08\x03");
+		write(this.buffer, this.plte_offs, byte4(this.plte_size - 12), 'PLTE');
+		write(this.buffer, this.trns_offs, byte4(this.trns_size - 12), 'tRNS');
+		write(this.buffer, this.idat_offs, byte4(this.idat_size - 12), 'IDAT');
+		write(this.buffer, this.iend_offs, byte4(this.iend_size - 12), 'IEND');
+
+		// initialize deflate header
+		var header = ((8 + (7 << 4)) << 8) | (3 << 6);
+		header+= 31 - (header % 31);
+
+		write(this.buffer, this.idat_offs + 8, byte2(header));
+
+		// initialize deflate block headers
+		for (var i = 0; (i << 16) - 1 < this.pix_size; i++) {
+			var size, bits;
+			if (i + 0xffff < this.pix_size) {
+				size = 0xffff;
+				bits = "\x00";
+			} else {
+				size = this.pix_size - (i << 16) - i;
+				bits = "\x01";
+			}
+			write(this.buffer, this.idat_offs + 8 + 2 + (i << 16) + (i << 2), bits, byte2lsb(size), byte2lsb(~size));
+		}
+
+		/* Create crc32 lookup table */
+		for (var i = 0; i < 256; i++) {
+			var c = i;
+			for (var j = 0; j < 8; j++) {
+				if (c & 1) {
+					c = -306674912 ^ ((c >> 1) & 0x7fffffff);
+				} else {
+					c = (c >> 1) & 0x7fffffff;
+				}
+			}
+			_crc32[i] = c;
+		}
+
+		// compute the index into a png for a given pixel
+		this.index = function(x,y) {
+			var i = y * (this.width + 1) + x + 1;
+			var j = this.idat_offs + 8 + 2 + 5 * Math.floor((i / 0xffff) + 1) + i;
+			return j;
+		}
+
+		// convert a color and build up the palette
+		this.color = function(red, green, blue, alpha) {
+
+			alpha = alpha >= 0 ? alpha : 255;
+			var color = (((((alpha << 8) | red) << 8) | green) << 8) | blue;
+
+			if (typeof this.palette[color] == "undefined") {
+				if (this.pindex == this.depth) return "\x00";
+
+				var ndx = this.plte_offs + 8 + 3 * this.pindex;
+
+				this.buffer[ndx + 0] = String.fromCharCode(red);
+				this.buffer[ndx + 1] = String.fromCharCode(green);
+				this.buffer[ndx + 2] = String.fromCharCode(blue);
+				this.buffer[this.trns_offs+8+this.pindex] = String.fromCharCode(alpha);
+
+				this.palette[color] = String.fromCharCode(this.pindex++);
+			}
+			return this.palette[color];
+		}
+
+		// output a PNG string, Base64 encoded
+		this.getBase64 = function() {
+
+			var s = this.getDump();
+
+			var ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+			var c1, c2, c3, e1, e2, e3, e4;
+			var l = s.length;
+			var i = 0;
+			var r = "";
+
+			do {
+				c1 = s.charCodeAt(i);
+				e1 = c1 >> 2;
+				c2 = s.charCodeAt(i+1);
+				e2 = ((c1 & 3) << 4) | (c2 >> 4);
+				c3 = s.charCodeAt(i+2);
+				if (l < i+2) { e3 = 64; } else { e3 = ((c2 & 0xf) << 2) | (c3 >> 6); }
+				if (l < i+3) { e4 = 64; } else { e4 = c3 & 0x3f; }
+				r+= ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4);
+			} while ((i+= 3) < l);
+			return r;
+		}
+
+		// output a PNG string
+		this.getDump = function() {
+
+			// compute adler32 of output pixels + row filter bytes
+			var BASE = 65521; /* largest prime smaller than 65536 */
+			var NMAX = 5552;  /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */
+			var s1 = 1;
+			var s2 = 0;
+			var n = NMAX;
+
+			for (var y = 0; y < this.height; y++) {
+				for (var x = -1; x < this.width; x++) {
+					s1+= this.buffer[this.index(x, y)].charCodeAt(0);
+					s2+= s1;
+					if ((n-= 1) == 0) {
+						s1%= BASE;
+						s2%= BASE;
+						n = NMAX;
+					}
+				}
+			}
+			s1%= BASE;
+			s2%= BASE;
+			write(this.buffer, this.idat_offs + this.idat_size - 8, byte4((s2 << 16) | s1));
+
+			// compute crc32 of the PNG chunks
+			function crc32(png, offs, size) {
+				var crc = -1;
+				for (var i = 4; i < size-4; i += 1) {
+					crc = _crc32[(crc ^ png[offs+i].charCodeAt(0)) & 0xff] ^ ((crc >> 8) & 0x00ffffff);
+				}
+				write(png, offs+size-4, byte4(crc ^ -1));
+			}
+
+			crc32(this.buffer, this.ihdr_offs, this.ihdr_size);
+			crc32(this.buffer, this.plte_offs, this.plte_size);
+			crc32(this.buffer, this.trns_offs, this.trns_size);
+			crc32(this.buffer, this.idat_offs, this.idat_size);
+			crc32(this.buffer, this.iend_offs, this.iend_size);
+
+			// convert PNG to string
+			return "\211PNG\r\n\032\n"+this.buffer.join('');
+		}
+	}
+
+})();

Some files were not shown because too many files changed in this diff