penguin/AstroJS

Javascript framework for my blog

commit 3e8e6362b2002bb916f1081b0684f9f20932c3ea

author斟酌 鵬兄 <tgckpg@gmail.com>
date2015-09-29T16:46:54Z
subjectEarly album draft
commit 3e8e6362b2002bb916f1081b0684f9f20932c3ea
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2015-09-29T16:46:54Z

    Early album draft
---
 botanjs/src/Astro/Blog/AstroEdit/SiteLibrary.js    |   7 +-
 .../Blog/AstroEdit/Visualizer/Snippet/SiteFile.js  |  55 ++++--
 botanjs/src/Astro/Blog/Components/Album.css        |   4 +
 botanjs/src/Astro/Blog/Components/Album.js         |  92 +++++++++
 botanjs/src/Astro/Blog/Components/Entry/Block.css  |  65 ++++---
 botanjs/src/Astro/Blog/Components/Entry/Block.js   |  91 ++++++++-
 botanjs/src/Astro/Blog/Components/SiteFile.js      | 207 +++++++++++----------
 .../src/externs/Astro.Blog.Components.SiteFile.js  |  12 ++
 8 files changed, 387 insertions(+), 146 deletions(-)

diff --git a/botanjs/src/Astro/Blog/AstroEdit/SiteLibrary.js b/botanjs/src/Astro/Blog/AstroEdit/SiteLibrary.js
index 8949ce5..ebf699b 100644
--- a/botanjs/src/Astro/Blog/AstroEdit/SiteLibrary.js
+++ b/botanjs/src/Astro/Blog/AstroEdit/SiteLibrary.js
@@ -83,6 +83,7 @@
 				}.bind(
 					{
 						stage: source
+						, id: data.id
 						, href: data.src_location
 						, hash: data.hash
 						, author: data.author
@@ -307,7 +308,11 @@
 			}
 
 			, copyItemLink = function () { return config.f_host + canvasView.idata.href; }
-			, copyHash = function () { return canvasView.idata.hash; }
+			, copyHash = function ()
+			{
+				var data = canvasView.idata;
+				return data.isCollection ? data.id : data.hash;
+			}
 
 			, openAction = function ()
 			{
diff --git a/botanjs/src/Astro/Blog/AstroEdit/Visualizer/Snippet/SiteFile.js b/botanjs/src/Astro/Blog/AstroEdit/Visualizer/Snippet/SiteFile.js
index 00cb176..15bf019 100644
--- a/botanjs/src/Astro/Blog/AstroEdit/Visualizer/Snippet/SiteFile.js
+++ b/botanjs/src/Astro/Blog/AstroEdit/Visualizer/Snippet/SiteFile.js
@@ -45,10 +45,25 @@
 
 		, __applyData = function (e)
 		{
-			var finfo = JSON.parse(e).file, s, m, l
-				, content = this.stage.firstChild
-				, desc = this.stage.lastChild
-				, _hash = this.hash;
+			if( typeof( e ) == "string" )
+				e = JSON.parse(e);
+
+			var _stage = IDOMElement( this.stage );
+
+			var isAlbum = false;
+			// Check if this is an album
+			if( isAlbum = ( "files" in e ) )
+			{
+				e.file = e.files[0];
+				_stage.setAttribute( new DataKey( "collection", 1 ) );
+			}
+
+			var s, m, l;
+
+			var finfo = e.file;
+			var content = this.stage.firstChild;
+			var desc = this.stage.lastChild;
+			var _hash = finfo.hash;
 
 			switch ( finfo.type )
 			{
@@ -56,34 +71,33 @@
 
 					this.stage.removeChild(content);
 					// Default size is large
-					var _image = Dand.wrapna("img", new IKey("src", config.path.image.large + _hash + ".jpg"))
-						, _stage = IDOMElement(this.stage)
-						, keys = [ new IKey( "type", "radio" ), new IKey( "name", "size_grp" + Perf.uuid ) ]
-						, sid
-						, selectionChanged = function ()
+					var _image = Dand.wrapna( "img", new IKey("src", config.path.image.large + _hash + ".jpg") );
+					var keys = [ new IKey( "type", "radio" ), new IKey( "name", "size_grp" + Perf.uuid ) ];
+					var sid;
+					var selectionChanged = function ()
 						{
-							_stage.setAttribute(new DataKey("size", this.size));
+							_stage.setAttribute( new DataKey( "size", this.size ) );
 							//// Handles the size selection
 							switch(this.size)
 							{
 								case "small":
-									_image.setAttribute("src", config.path.image.small + _hash + ".jpg");
+									_image.setAttribute( "src", config.path.image.small + _hash + ".jpg" );
 									break;
 								case "medium":
-									_image.setAttribute("src", config.path.image.medium + _hash + ".jpg");
+									_image.setAttribute( "src", config.path.image.medium + _hash + ".jpg" );
 									break;
 								case "large":
-									_image.setAttribute("src", config.path.image.large + _hash + ".jpg");
+									_image.setAttribute( "src", config.path.image.large + _hash + ".jpg" );
 									break;
 							}
 						};
 
-					this.stage.insertBefore(_image, desc);
+					this.stage.insertBefore( _image, desc );
 
-					desc.removeChild(desc.firstChild);
+					desc.removeChild( desc.firstChild );
 					desc.appendChild(
 						Dand.wrape([
-							Dand.textNode("Size: ")
+							Dand.textNode( ( isAlbum ? "[Album] " : "" ) + "Size: ")
 							, s = Dand.wrapna("input", keys.concat( new IKey("id", sid = "size_" + Perf.uuid) ))
 							, Dand.wrapne("label", "small", new IKey("for", sid))
 							, m = Dand.wrapna("input", keys.concat( new IKey("id", sid = "size_" + Perf.uuid) ))
@@ -112,7 +126,7 @@
 
 		, loadFailed = function (e) { }
 
-		, visualizer = function (submitted, override)
+		, visualizer = function ( submitted, override )
 		{
 
 			var hash = override ? override.value : this.code.value
@@ -128,9 +142,9 @@
 						, Dand.wrapc("v_description", "Site file (hash): " + hash)
 					]
 					, [
-						new DataKey("value", hash)
-						, new DataKey("size", "large")
-						, new IKey("style", "max-height: 150px;")
+						new DataKey( "value", hash )
+						, new DataKey( "size", "large" )
+						, new IKey( "style", "max-height: 150px;" )
 					]
 				);
 
@@ -162,6 +176,7 @@
 		stage = IDOMElement( stage );
 		// [html][/html]
 		return "[sitefile"
+			+ " collection=\"" + stage.getDAttribute( "collection" ) + "\""
 			+ " size=\"" + stage.getDAttribute( "size" ) + "\"" +  "]"
 			+ escapeStr( stage.getDAttribute( "value" ) )
 			+ "[/sitefile]";
diff --git a/botanjs/src/Astro/Blog/Components/Album.css b/botanjs/src/Astro/Blog/Components/Album.css
new file mode 100644
index 0000000..57c4c9f
--- /dev/null
+++ b/botanjs/src/Astro/Blog/Components/Album.css
@@ -0,0 +1,4 @@
+.site_file[data-type="album"] > div {
+	display: inline-block;
+	margin: 0.5em;
+}
diff --git a/botanjs/src/Astro/Blog/Components/Album.js b/botanjs/src/Astro/Blog/Components/Album.js
new file mode 100644
index 0000000..4753011
--- /dev/null
+++ b/botanjs/src/Astro/Blog/Components/Album.js
@@ -0,0 +1,92 @@
+(function(){
+	var ns = __namespace( "Astro.Blog.Components" );
+
+	/** @type {System.Cycle} */
+	var Cycle                            = __import( "System.Cycle" );
+	/** @type {System.utils.IKey} */
+	var IKey                             = __import( "System.utils.IKey" );
+	/** @type {System.utils.DataKey} */
+	var DataKey                          = __import( "System.utils.DataKey" );
+	/** @type {Dandelion} */
+	var Dand                             = __import( "Dandelion" );
+	/** @type {Dandelion.IDOMElement} */
+	var IDOMElement                      = __import( "Dandelion.IDOMElement" );
+	/** @type {Astro.Bootstrap} */
+	var Bootstrap                        = __import( "Astro.Bootstrap" );
+	/** @type {System.utils.Perf} **/
+	var Perf                             = __import( "System.utils.Perf" );
+	/** @type {Astro.Blog.Config} */
+	var Conf                             = __import( "Astro.Blog.Config" );
+
+	/** @type {_AstConf_.SiteFile} */
+	var config = null;
+
+	var SiteFile = ns[ NS_INVOKE ]( "SiteFile" );
+
+	var Album = function ( id, hash )
+	{
+		if( !config ) throw new Error( "config is not defined" );
+		var stage = Dand.id( id, true );
+
+		var sf = new SiteFile( id, hash );
+		if( sf.type != "album" ) return;
+
+		var ostage = stage.element;
+
+		var applyStructure = function( obj )
+		{
+			// remove loading bubbles
+			while( ostage.hasChildNodes() ) ostage.removeChild( ostage.firstChild );
+
+			/** @type {_AstJson_.SiteFile} */
+			var finfo = JSON.parse( obj );
+
+			var files = finfo.files;
+			var l = files.length;
+			for( var i = 0; i < l; i ++ )
+			{
+				/** @type {_AstJson_.SiteFile} */
+				var file = files[i];
+				var uuid = Perf.uuid;
+
+				ostage.appendChild(
+					Dand.wrapna( "div", [
+						new IKey( "id", uuid )
+						, new DataKey( "size", "medium" )
+					] )
+				);
+
+				Cycle.next( function()
+				{
+					new SiteFile( this.id, this.hash );
+				}.bind({ id: uuid, hash: file.hash }) );
+			}
+
+			console.log( finfo );
+		};
+
+		var loadFailed = function( obj )
+		{
+			console.log( obj );
+		};
+
+		sf.loadInfo( applyStructure, loadFailed );
+	};
+
+	var init = function()
+	{
+		config = Conf.get( "SiteFile" );
+		if( config )
+		{
+			for( var i in config.files )
+			{
+				var f = config.files[i];
+				new Album( f[0], f[1] );
+			}
+		}
+	};
+
+	Bootstrap.regInit( init );
+
+	ns[ NS_EXPORT ]( EX_CLASS, "Album", Album );
+})();
diff --git a/botanjs/src/Astro/Blog/Components/Entry/Block.css b/botanjs/src/Astro/Blog/Components/Entry/Block.css
index 588da7c..3232879 100644
--- a/botanjs/src/Astro/Blog/Components/Entry/Block.css
+++ b/botanjs/src/Astro/Blog/Components/Entry/Block.css
@@ -1,4 +1,4 @@
-.bk_block {
+.block-entries .block {
 	float: left;
 
 	width: 225px;
@@ -7,33 +7,38 @@
 	margin: 1em;
 
 	position: relative;
-	overflow: hidden;
 
 	color: #666;
-	background-color: white;
-	border: 1px solid #D4D7C9;
+	border: 2px solid transparent;
 
 	display: block;
 }
 
-.bk_block:hover {
-	text-decoration: none;
+.block-entries .block:hover {
+	border-width: 2px;
+	border-color: purple;
 }
 
-.bk_SlideWrapper {
+.block-entries .slide-wrapper {
 	position: absolute;
 	overflow: hidden;
-	top: 0.5em;
 	width: 100%;
 	height: 100%;
+
+	border: 1px solid #D4D7C9;
+	margin: -1px;
 }
 
-.bk_date {
+.block-entries .slide-wrapper:hover {
+	border-color: purple;
+}
+
+.block-entries .date {
 	font-size: 0.8em;
 	text-align: right;
 }
 
-.bk_title {
+.block-entries .title {
 	font-size: 1.2em;
 	max-height: 2.4em;
 	overflow: hidden;
@@ -41,17 +46,17 @@
 	color: #444;
 }
 
-.bk_content {
+.block-entries .content {
 	text-overflow: ellipsis;
 	overflow: hidden;
 }
 
-.bk_author {
+.block-entries .author {
 	color: crimson;
 	text-align: right;
 }
 
-.bk_cCount {
+.block-entries .c-count {
 	position: absolute;
 	bottom: 1em;
 	left: 0.5em;
@@ -62,7 +67,7 @@
 }
 
 
-.bk_blockSlide {
+.block-entries .slide {
 	position: absolute;
 	width: 100%;
 	height: 150%;
@@ -70,7 +75,7 @@
 	top: 0;
 }
 
-.bk_blockSlide:after {
+.block-entries .slide:after, .block-entries[on] .slide {
 	content: attr(data-content);
 
 	position: absolute;
@@ -82,9 +87,6 @@
 
 	padding: 0 0.5em;
 
-	font-size: 0.8em;
-	font-family: custom-serif;
-
 	color: #EEE;
 	background: purple;
 
@@ -112,16 +114,16 @@
 	transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); /* easeOutCubic */
 }
 
-.bk_blockSlide:hover:after {
+.block-entries .slide:hover:after, .slide[on]:after {
 	top: 62.5%;
 	padding: 0.5em;
 }
 
-.bk_blockSlide:hover {
+.block-entries .slide:hover, .slide[on] {
 	top: -50%;
 }
 
-.bk_blockFront {
+.block-entries .front {
 	position: absolute;
 	background: white;
 
@@ -129,19 +131,30 @@
 	height: 100%;
 }
 
-.bk_blockWrapper {
+.block-entries .block-wrapper {
 	position: relative;
-	padding: 0 0.5em;
+	background-image: repeating-linear-gradient(
+		135deg
+		, transparent, transparent 1px
+		, rgba( 85, 85, 85, .1 ) 1px
+		, rgba( 85, 85, 85, .1 ) 30px
+	);
+	background-color: black;
 }
 
-.bk_banner {
+.block-entries .block-info {
+	padding: 0.5em;
+	background: white;
+}
+
+.block-entries .banner {
 	width: 50%;
 	padding: 50%;
 
-	background-image: url(http://file.astropenguin.net/blog/layout-images/featured.png);
 	background-position: center;
 	background-repeat: no-repeat;
-	background-color: white;
+
+	border-bottom: 1px solid #D4D7C9;
 
 	-webkit-box-sizing: border-box;
 	-moz-box-sizing: border-box;
diff --git a/botanjs/src/Astro/Blog/Components/Entry/Block.js b/botanjs/src/Astro/Blog/Components/Entry/Block.js
index e406e74..8df6cdd 100644
--- a/botanjs/src/Astro/Blog/Components/Entry/Block.js
+++ b/botanjs/src/Astro/Blog/Components/Entry/Block.js
@@ -1 +1,90 @@
-// __namespace( "Astro.Blog.Components.Entry.Block" );
+(function(){
+	var ns = __namespace( "Astro.Blog.Components.Entry" );
+
+	/** @type {System.Cycle} */
+	var Cycle                            = __import( "System.Cycle" );
+	/** @type {System.utils.Perf} */
+	var Perf                             = __import( "System.utils.Perf" );
+	/** @type {Dandelion} */
+	var Dand                             = __import( "Dandelion" );
+	/** @type {Astro.Bootstrap} */
+	var Bootstrap                        = __import( "Astro.Bootstrap" );
+
+	/** @type {Astro.Blog.Config} */
+	var Conf                                      = __import( "Astro.Blog.Config" );
+	/** @type {Astro.Blog.Components.SiteFile} */
+	var SiteFile                                  = __import( "Astro.Blog.Components.SiteFile" );
+
+	/** @type {_AstConf_.SiteFile} */
+	var config = null;
+
+	var Block  = function ( element )
+	{
+		if( !config ) throw new Error( "Config is not defined" );
+		var slide = Dand.glass( "slide", false, element )[0];
+		var delayCont = function( firstTime )
+		{
+			Cycle.delay(
+				function()
+				{
+					slide.getAttribute( "on" )
+						? slide.removeAttribute( "on" )
+						: slide.setAttribute( "on", 1 )
+						;
+					delayCont( false );
+				}
+				, firstTime
+					? ( 500 + Math.random()*4500 )
+					: ( 5500 + Math.random()*4500 )
+			);
+		};
+
+		delayCont( true );
+
+		var banner = Dand.glass( "banner", false, element )[0];
+		var bId = banner.getAttribute( "id" );
+
+		var placeBackground = function( obj )
+		{
+			/** @type {_AstJson_.SiteFile} */
+			var finfo = JSON.parse( obj );
+
+			/** @type {_AstJson_.SiteFile} */
+			var file = finfo.files[0];
+
+			var path = config.path.image.medium + file.hash + ".jpg";
+			banner.style.backgroundImage = "url(" + path  + ")"; 
+		};
+
+		var placeError = function( obj )
+		{
+			throw new Error( obj );
+		};
+
+		for( var i in config.files )
+		{
+			var f = config.files[i];
+			var id = f[0];
+			if( id == bId )
+			{
+				new SiteFile( id, f[1] ).loadInfo( placeBackground, placeError );
+			}
+		}
+	};
+
+	var init = function()
+	{
+		config = Conf.get( "SiteFile" );
+
+		var stage = Dand.glass( "block-entries" )[0];
+		var everyEntries = Dand.glass( "block", false, stage );
+		for( var i = 0; i < everyEntries.length; i ++ )
+		{
+			new Block( everyEntries[ i ] );
+		}
+	};
+
+	Bootstrap.regInit( init );
+
+	ns[ NS_EXPORT ]( EX_CLASS, "Block", Block );
+})();
diff --git a/botanjs/src/Astro/Blog/Components/SiteFile.js b/botanjs/src/Astro/Blog/Components/SiteFile.js
index ad2a7bd..f1b9a32 100644
--- a/botanjs/src/Astro/Blog/Components/SiteFile.js
+++ b/botanjs/src/Astro/Blog/Components/SiteFile.js
@@ -1,7 +1,7 @@
 (function(){
 	var ns = __namespace( "Astro.Blog.Components" );
 
-	/** @type {System.utils.EventKey} */
+	/** @type {System.utils.IKey} */
 	var IKey                             = __import( "System.utils.IKey" );
 	/** @type {Dandelion} */
 	var Dand                             = __import( "Dandelion" );
@@ -22,110 +22,121 @@
 	{
 		if( !config ) throw new Error( "config is not defined" );
 		// TODO: Make a trigger for downloading from server
-		var stage = Dand.id( id )
+		var stage = Dand.id( id );
+		this.id = id;
+		this.hash = hash;
+		this.type =  IDOMElement( stage ).getDAttribute( "type" );
 
-			, applyStructure = function( obj )
-			{
-				// remove loading bubbles
-				while( stage.hasChildNodes() ) stage.removeChild(stage.firstChild);
-
-				/** @type {_AstJson_.SiteFile} */
-				var finfo = JSON.parse( obj ).file;
-
-				switch( finfo.type ) {
-
-					case "image":
-						var node = Dand.wrap('img');
-						var k = new IKey( "src", null );
-
-						switch( IDOMElement(stage).getDAttribute('size') )
-						{
-							case "small":
-								k.keyValue = config.path.image.small + hash + '.jpg';
-								break;
-							case "medium":
-								k.keyValue = config.path.image.medium + hash + '.jpg';
-								break;
-							default: // large
-								k.keyValue = config.path.image.large + hash + '.jpg';
-						}
-						IDOMElement( node ).setAttribute( k );
+		if(!( this.type == "default" || this.type == null )) return this;
+		if( stage.getAttribute( "noauto" ) != null ) return this;
 
-						stage.appendChild(Dand.wrapne(
-							'a', node
+		var applyStructure = function( obj )
+		{
+			// remove loading bubbles
+			while( stage.hasChildNodes() ) stage.removeChild(stage.firstChild);
+
+			/** @type {_AstJson_.SiteFile} */
+			var finfo = JSON.parse( obj ).file;
+
+			switch( finfo.type ) {
+
+				case "image":
+					var node = Dand.wrap('img');
+					var k = new IKey( "src", null );
+
+					switch( IDOMElement(stage).getDAttribute('size') )
+					{
+						case "small":
+							k.keyValue = config.path.image.small + hash + '.jpg';
+							break;
+						case "medium":
+							k.keyValue = config.path.image.medium + hash + '.jpg';
+							break;
+						default: // large
+							k.keyValue = config.path.image.large + hash + '.jpg';
+					}
+					IDOMElement( node ).setAttribute( k );
+
+					stage.appendChild(Dand.wrapne(
+						'a', node
+						, [
+							new IKey( 'href', config.f_host + finfo.src_location )
+							, new IKey( 'title', finfo.name )
+							, new IKey( 'target', '_blank' )
+						]
+					));
+
+					break;
+
+				case "audio":
+					// TODO
+					break;
+
+				default:
+					// create a form to post hash string to php
+					var	hash_field = Dand.wrapna(
+							'input', [ new IKey( 'type', 'hidden' ), new IKey( 'name', 'hash' ) ]
+						)
+						, name_field = Dand.wrapna(
+							'input', [ new IKey( 'type', 'hidden' ), new IKey( 'name', 'name' ) ]
+						)
+						, link = Dand.wrapne(
+							'a', 'download', new IKey( 'href', config.path.download + finfo.name )
+						)
+
+						, form = Dand.wrap(
+							'form', null, 'sf_regular'
+							, [ name_field, hash_field ]
 							, [
-								new IKey( 'href', config.f_host + finfo.src_location )
-								, new IKey( 'title', finfo.name )
-								, new IKey( 'target', '_blank' )
+								new IKey('target', '_blank')
+								, new IKey('action', config.path.download + finfo.name)
+								, new IKey('method', 'POST')
 							]
-						));
-
-						break;
-
-					case "audio":
-						// TODO
-						break;
-
-					default:
-						// create a form to post hash string to php
-						var	hash_field = Dand.wrapna(
-								'input', [ new IKey( 'type', 'hidden' ), new IKey( 'name', 'hash' ) ]
-							)
-							, name_field = Dand.wrapna(
-								'input', [ new IKey( 'type', 'hidden' ), new IKey( 'name', 'name' ) ]
-							)
-							, link = Dand.wrapne(
-								'a', 'download', new IKey( 'href', config.path.download + finfo.name )
-							)
-
-							, form = Dand.wrap(
-								'form', null, 'sf_regular'
-								, [ name_field, hash_field ]
-								, [
-									new IKey('target', '_blank')
-									, new IKey('action', config.path.download + finfo.name)
-									, new IKey('method', 'POST')
-								]
-							)
-						;
-
-						hash_field.value = hash;
-						name_field.value = finfo.name;
-
-						IDOMElement(link).addEventListener(
-							'Click'
-							, function(e) {
-								form.submit();
-								e.preventDefault();
-								return false;
-							}
-						);
-
-						// file name
-						form.appendChild( Dand.wrapne( 'span', 'File: ' + finfo.name) );
-
-						// download, submit button
-						form.appendChild( Dand.wrapne( 'sup', [ Dand.textNode(" ["), link, Dand.textNode("]") ] ) );
-
-						// hash
-						form.appendChild( Dand.wrapne( 'sup', Dand.wrape( 'MD5: ' + hash ) ) );
-						// date
-						form.appendChild( Dand.wrapne( 'sup', Dand.wrape( 'Date: ' + getSMStamp( new Date( finfo.date_created ) ) ) ) );
-
-
-						stage.appendChild(form);
-
-				}
+						)
+					;
+
+					hash_field.value = hash;
+					name_field.value = finfo.name;
+
+					IDOMElement(link).addEventListener(
+						'Click'
+						, function(e) {
+							form.submit();
+							e.preventDefault();
+							return false;
+						}
+					);
+
+					// file name
+					form.appendChild( Dand.wrapne( 'span', 'File: ' + finfo.name) );
+
+					// download, submit button
+					form.appendChild( Dand.wrapne( 'sup', [ Dand.textNode(" ["), link, Dand.textNode("]") ] ) );
+
+					// hash
+					form.appendChild( Dand.wrapne( 'sup', Dand.wrape( 'MD5: ' + hash ) ) );
+					// date
+					form.appendChild( Dand.wrapne( 'sup', Dand.wrape( 'Date: ' + getSMStamp( new Date( finfo.date_created ) ) ) ) );
+
+
+					stage.appendChild(form);
 
 			}
-			, loadFailed = function( obj )
-			{
-				while( stage.hasChildNodes() ) stage.removeChild( stage.firstChild );
-				stage.appendChild( Dand.wrapc( "sf_failed", Dand.textNode( "Error: Failed to get resources info" ) ) );
-			}
-			;
 
-		getData( config.path.info + hash, applyStructure, loadFailed );
+		};
+
+		var loadFailed = function( obj )
+		{
+			while( stage.hasChildNodes() ) stage.removeChild( stage.firstChild );
+			stage.appendChild( Dand.wrapc( "sf_failed", Dand.textNode( "Error: Failed to get resources info" ) ) );
+		};
+
+		this.loadInfo( applyStructure, loadFailed );
+	};
+
+	SiteFile.prototype.loadInfo = function( success, failed )
+	{
+		getData( config.path.info + this.hash, success, failed );
 	};
 
 	var init = function()
diff --git a/botanjs/src/externs/Astro.Blog.Components.SiteFile.js b/botanjs/src/externs/Astro.Blog.Components.SiteFile.js
new file mode 100644
index 0000000..b3123e9
--- /dev/null
+++ b/botanjs/src/externs/Astro.Blog.Components.SiteFile.js
@@ -0,0 +1,12 @@
+/** @constructor */
+Astro.Blog.Components.SiteFile = function() {};
+
+/** @type {Function} */
+Astro.Blog.Components.SiteFile.loadInfo;
+
+/** @type {String} */
+Astro.Blog.Components.SiteFile.hash;
+/** @type {String} */
+Astro.Blog.Components.SiteFile.id;
+/** @type {String} */
+Astro.Blog.Components.SiteFile.type;