diff options
-rw-r--r-- | css/news.css | 357 | ||||
-rw-r--r-- | index.php | 2 | ||||
-rw-r--r-- | js/main.js | 96 | ||||
-rw-r--r-- | js/menu.js | 683 | ||||
-rw-r--r-- | js/news.js | 50 | ||||
-rw-r--r-- | templates/main.php | 12 | ||||
-rw-r--r-- | templates/part.feeds.php | 47 | ||||
-rw-r--r-- | templates/part.listfeed.php | 17 | ||||
-rw-r--r-- | templates/part.listfolder.php | 23 |
9 files changed, 515 insertions, 772 deletions
diff --git a/css/news.css b/css/news.css index 595d13fdf..730fa6039 100644 --- a/css/news.css +++ b/css/news.css @@ -59,286 +59,222 @@ div.add_parentfolder { } /* feed/folder list */ -#leftcontent { +#leftcontent_news { width: 20em; margin: 0; - z-index: 0; + height: 100%; position: relative; overflow: hidden; - top: 0; - bottom: 0; + border-right: 1px solid #ccc; + background-color: #f8f8f8; + box-sizing: border-box; + -moz-box-sizing: border-box; } -#feeds { +#feed_wrapper { background-color: #f8f8f8; - position: absolute; + position: relative; top: 0; - bottom: 2.8em; - left: 0; - right: 0; + height: 100%; + padding-bottom: 2.8em; box-sizing: border-box; -moz-box-sizing: border-box; } +#feeds { + height: 100%; + overflow: auto; +} + #feeds > ul { height: 100%; - overflow: auto; + overflow-x: hidden; } - + .dnd_over { - -moz-transition: background-color 200ms ease 0s; - transition: background-color 200ms ease 0s; - background-color: #d0d0d0! important; + -moz-transition: padding-bottom 500ms ease 0s; + transition: padding-bottom 500ms ease 0s; + } + + #feeds > ul.dnd_over { + } .dnd_over > ul { - padding-bottom: 2.5em; - -moz-transition: padding-bottom 500ms ease 0s; - transition: padding-bottom 500ms ease 0s; + padding-bottom: 2.5em! important; + } + #feeds > ul ul.dnd_over { + padding-bottom: 2.5em !important; + } - #feeds .folder { - box-sizing: border-box; - -moz-box-sizing: border-box; - padding: .5em 0 .5em 2.5em !important; - background-color: #dfdfdf !important; - font-weight: bold ; - border-bottom: 1px solid #bbb !important; - position: relative; - z-index: 1; + .all_read { + font-weight: normal; } - #feeds .folder a { - float: none; - display: inline-block; - position: relative; + + #feeds .buttons { + position: absolute; + right: 0; + top: 0; + bottom: 0; + display: none; + } + + #feeds .buttons button { + margin: .65em .3em 0 0; + background-position: center; + background-repeat: no-repeat; } - #feeds .folder:hover { - background-color: #d0d0d0 !important; - -moz-transition: background-color 200ms ease 0s; - transition: background-color 200ms ease 0s; + #feeds li:hover > .buttons { + display: block; } - #feeds .subscriptions, #feeds .subscriptions:hover { - background-image: url('%appswebroot%/news/img/rss.svg'); - background-repeat: no-repeat; - background-size: 16px 16px; - background-position: .8em center; + #feeds .buttons button.feeds_delete { + background-image: url('%webroot%/core/img/actions/delete.svg'); + } + + #feeds .buttons button.feeds_edit { + background-image: url('%webroot%/core/img/actions/rename.svg'); + } + + #feeds .buttons button.feeds_markread { + background-image: url('%appswebroot%/news/img/mark_read.svg'); + } + + #feeds button.collapsable_trigger { + position: absolute; + left: .5em; + top: .4em; + background: none; + background-image: url('%webroot%/core/img/actions/triangle-s.svg'); + border: none; + border-radius: 0; + outline: none !important; + box-shadow: none; + -moz-box-shadow: none; + display: none; } - #feeds .starred, #feeds .starred:hover { - background-image: url('%appswebroot%/news/img/starred.svg'); - background-repeat: no-repeat; - background-size: 16px 16px; - background-position: .8em center; - + #feeds button.collapsable_trigger.triggered { + -moz-transform: rotate(-90deg); + transform: rotate(-90deg); } - #feeds .subscriptions a, #feeds .starred a { - display: inline-block; - width: 75%; + #feeds .folder.collapsable:hover button.collapsable_trigger { + display: block; } - #feeds li.feed { - position: relative; + #feeds li { + border-bottom: 1px solid #bbb; + padding: 0 .5em; + background-color: #dfdfdf; + font-weight: bold ; box-sizing: border-box; -moz-box-sizing: border-box; - border-bottom: 1px solid #ccc; - padding-left: 2.5em; - -moz-transition: background-color 200ms ease 0s; - transition: background-color 200ms ease 0s; - background-color: #f1f1f1; - z-index: 1; - background-size:16px 16px; - background-repeat: no-repeat; - background-position: .8em center; + width: 100%; + position: relative; } - - #feeds li.feed a { - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - text-align: left; - width: 85%; - font-weight: bold; - } - - #feeds li.feed a.all_read, #feeds .all_read { - font-weight: normal; - } - #feeds li#selected_feed { - background-color: #ffffff !important; - font-style: oblique; - } + #feeds li#selected_feed { + background-color: #ffffff !important; + font-style: oblique; + } - #feeds li.feed.updating { - padding-right: 21px; - background-image: url('%webroot%/core/img/loader.gif'); + #feeds li a.title { + box-sizing: border-box; + -moz-box-sizing: border-box; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; + padding-left: 2em; background-size: 16px 16px; background-repeat: no-repeat; - background-position: right center; + background-position: .2em center; + line-height: 2.5em; } - - #feeds li.feed:hover { - background-color: #ddd; - } - - #feeds li.feed .feeds_delete, - #feeds li.feed .feeds_edit { - display: none; - border: none; - } - - #feeds li.feed:hover .unreaditemcounter { - display: none; - } - #feeds li.feed:hover .feeds_delete, - #feeds li.feed:hover .feeds_edit, - #feeds .collapsable:hover .feeds_delete, - #feeds .collapsable:hover .feeds_edit { - display: inline-block; + #feeds li:hover > a.title { + width: 75%; } - #feeds .feeds_delete { - position: absolute; - right: 0.2em; - background: url('%webroot%/core/img/actions/delete.svg') no-repeat center; - display: none; - padding: 0; - margin: -8px 0 0 0; - top: 50%; - } - - #feeds .feeds_edit { - position: absolute; - right: 1.6em; - background: url('%webroot%/core/img/actions/rename.svg') no-repeat center; - display: none; - padding: 0; - margin: -8px 0 0 0; - top: 50%; - } - - #feeds .unreaditemcounter { - position: relative; - top: .3em; - right: .3em; - background: #5E5E5E; - border-radius: 5px; - padding: 2px 5px; - color: white; - text-align: center; - font-weight: normal; - margin: 0; - display: none; + #feeds .starred a.title { + background-image: url('%appswebroot%/news/img/starred.svg'); } - - #feeds .unreaditemcounter.all_read { - display: none !important; - } - #feeds li.collapsable_container { - background-color: #dfdfdf; - font-weight: bold; - padding: 0; - border: 0; - border-bottom: 1px solid #bbb; - position: relative; - z-index: 1; + #feeds .subscriptions a.title { + background-image: url('%appswebroot%/news/img/rss.svg'); } - #feeds li.collapsable_container.open { - background-image: linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%); - background-image: -o-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%); - background-image: -moz-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%); - background-image: -webkit-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%); - background-image: -ms-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%); - font-weight: bold; - padding: 0; - border: 0; - border-bottom: 1px solid #bbb; - border-top: 1px solid #bbb; - -webkit-box-shadow: inset 0 0 5px -1px #bbb; - box-shadow: inset 0 0 5px -1px #bbb; - } + #feeds .feed { + border-bottom: 1px solid #ccc; + background-color: #f1f1f1; + } - #feeds li.collapsable_container:hover { - background-color: #d0d0d0; - -moz-transition: background-color 200ms ease 0s; - transition: background-color 200ms ease 0s; - } - + #feeds .feed:hover { + background-color: #ddd; + } + #feeds .folder { + padding: 0 .5em; + } - #feeds li.collapsable_container.open > ul { - margin: 0 8px 8px 8px; - position: relative; - z-index: 1; + #feeds .folder:hover, #feeds .starred:hover, #feeds .subscriptions:hover { + background-color: #d0d0d0; } - #feeds li.collapsable_container > ul > li { - border: 1px solid #ccc; - border-bottom: 0; - } - - #feeds li.collapsable_container > ul > li:last-child { - border: 1px solid #ccc; - } - - #feeds div.collapsable { - position: relative; - z-index: 1; - border-radius: 5px; + #feeds .folder a.title { + background-image: url('%webroot%/core/img/places/folder.svg'); } - - #feeds .collapsable_trigger { - border: none ; - margin: 0 0 0 .8em; - padding: 0; - border-radius: 0; - box-shadow: none; - vertical-align: middle !important; - background: url('%webroot%/core/img/places/folder.svg') no-repeat; - outline: none; - opacity: 1; + #feeds .folder.collapsable:hover a { + background-image: none; } - #feeds .collapsable_container:hover .collapsable_trigger.triggerable { - background-image: url('%webroot%/core/img/actions/triangle-s.svg'); - -moz-transform: none; - transform: none; - } + #feeds .folder.collapsable.open { + background-image: linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%), + background-image: url('%webroot%/core/img/places/folder.svg'); + background-image: -o-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%), + background-image: url('%webroot%/core/img/places/folder.svg'); + background-image: -moz-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%), + background-image: url('%webroot%/core/img/places/folder.svg'); + background-image: -webkit-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%), + background-image: url('%webroot%/core/img/places/folder.svg'); + background-image: -ms-linear-gradient(bottom, rgb(208,208,208) 0%, rgb(222,222,222) 100%), + background-image: url('%webroot%/core/img/places/folder.svg'); + border-bottom: 1px solid #bbb; + border-top: 1px solid #bbb; + -webkit-box-shadow: inset 0 0 5px -1px #bbb; + box-shadow: inset 0 0 5px -1px #bbb; + padding-bottom: .5em; + } - #feeds .collapsable_container:hover .collapsable_trigger.triggered { - background-image: url('%webroot%/core/img/actions/triangle-s.svg'); - -moz-transform: rotate(-90deg); - transform: rotate(-90deg); + #feeds .folder.collapsable.open ul { + border: 1px solid #ccc; } - - #feeds .collapsable_title { - padding: .5em 0 .5em .5em; - line-height: 1.5em; - float: none; - display: inline-block; - position: relative; + + #feeds .folder.open ul li:last-child { + border-bottom: 0; } /* feed settings */ #feed_settings { padding: 0; - bottom: 0px; + bottom: 0; overflow:visible; height:2.8em; width: 20em; margin:0; + z-index: 2; background:#eee; - border-top:1px - solid #ccc; + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; + box-sizing: border-box; + -moz-box-sizing: border-box; position:fixed; } @@ -410,6 +346,7 @@ div.add_parentfolder { background:-o-linear-gradient(#DCDCDC, #EEEEEE); background:linear-gradient(#DCDCDC, #EEEEEE); position:fixed; + z-index: 6; margin-left: 0; bottom: 2.8em; border-left:1px solid #ccc; @@ -17,7 +17,9 @@ OCP\User::checkLoggedIn(); OCP\App::checkAppEnabled('news'); OCP\App::setActiveNavigationEntry('news'); +OCP\Util::addScript('news','main'); OCP\Util::addScript('news','news'); +OCP\Util::addScript('news','menu'); OCP\Util::addScript('news','jquery.timeago'); OCP\Util::addScript('news','jquery.onscreen'); diff --git a/js/main.js b/js/main.js new file mode 100644 index 000000000..b6d21a5b2 --- /dev/null +++ b/js/main.js @@ -0,0 +1,96 @@ +/** +* ownCloud - News app +* +* @author Bernhard Posselt +* Copyright (c) 2012 - Bernhard Posselt <nukeawhale@gmail.com> +* +* This file is licensed under the Affero General Public License version 3 or later. +* See the COPYING-README file +* +*/ + + +var News = News || {}; + +$(document).ready(function(){ + + // basic setup + News.Feed.updateAll(); + var updateInterval = 200000; //how often the feeds should update (in msec) + setInterval('News.Feed.updateAll()', updateInterval); + + // bind listeners on the menu + var menu = new News.Menu(false); + menu.bindOn('#feeds ul'); + + + /* first run script begins */ + $('#browsebtn_firstrun, #cloudbtn_firstrun, #importbtn_firstrun').hide(); + + /* first run script ends */ + + $('#addfeed').click(function() { + News.UI.overview('#addfeed_dialog','feeddialog.php'); + }); + + $('#addfeedbtn').click(function() { + $(this).hide(); + $('#addfeed_dialog_firstrun').show(); + }); + + $('#addfolder').click(function() { + News.UI.overview('#addfolder_dialog','folderdialog.php'); + }); + + $('#addfeedfolder').click(function(event) { + News.DropDownMenu.fade($(this).children('ul')); + event.stopPropagation(); + }); + + $('#settingsbtn').on('click keydown', function() { + try { + OC.appSettings({appid:'news', loadJS:true, cache:false}); + } catch(e) { + alert(e); + } + }); + + $('#view').click(function(){ + var term; + if($(this).hasClass('show_all')){ + term = 'unread'; + $(this).addClass('show_unread').removeClass('show_all'); + } else { + term = 'all'; + $(this).addClass('show_all').removeClass('show_unread'); + } + News.Feed.filter(term); + }); + + // mark items whose title was hid under the top edge as read + // when the bottom is reached, mark all items as read + $('#feed_items').scroll(function(){ + var boxHeight = $(this).height(); + var scrollHeight = $(this).prop('scrollHeight'); + var scrolled = $(this).scrollTop() + boxHeight; + var scrollArea = this; + $(this).children('ul').children('.feed_item:not(.read)').each(function(){ + var item = this; + var itemOffset = $(this).position().top; + if(itemOffset <= 0 || scrolled >= scrollHeight){ + // wait and check if the item is still under the top edge + setTimeout(function(){ markItemAsRead(scrollArea, item);}, 1000); + } + }) + + }); + + $('#feed_items').scrollTop(0); + + $(document).keydown(function(e) { + if ((e.keyCode || e.which) == 74) { // 'j' key shortcut + + } + }); + +}); diff --git a/js/menu.js b/js/menu.js index 32d528156..19a869e35 100644 --- a/js/menu.js +++ b/js/menu.js @@ -18,79 +18,11 @@ * HOWTO * -We create a new instance of the menu. The first argument is its class. - - var menu = new News.Menu('feedlist'); - - -To fill it with items we use JSON. Hint: If no icons are given, default icons -are being used. A typical JSON array would look like this: - - var menuStructure = [ - { - id: 1, - title: 'New Articles', - type: News.MenuNodeType.New, - children: [], - unreadCount: 4, - }, - { - id: 1, - title: 'Starred', - type: News.MenuNodeType.Starred, - children: [], - unreadCount: 1, - }, - { - id: 1, - title: 'hi', - type: News.MenuNodeType.Folder, - unreadCount: 7, - children: [ - { - id: 2, - title: 'hi too', - type: News.MenuNodeType.Feed, - unreadCount: 4, - children: [], - icon: '/test/test.png' - }, - { - id: 2, - title: 'hi 3', - type: News.MenuNodeType.Feed, - children: [], - unreadCount: 3, - }, - ] - }, - { - id: 4, - title: 'hi 4', - type: News.MenuNodeType.Feed, - children: [], - unreadCount: 1, - }, - { - id: 114, - title: 'hi 3', - type: News.MenuNodeType.Folder, - children: [], - unreadCount: 1, - }, - ]; - - menu.populateFromJSON(menuStructure, menu); - - -Now that the menu is populated, we can render, append it into the place we'd -like to and select the current selected item - - $('#leftcontent').append(menu.render()); - var selectedType = News.MenuNodeType.Feed; - var selectedId = 3; - menu.setSelected(selectedType, selectedId); +We create a new instance of the menu. Then we need to bind it on an ul which contains +all the items: + var menu = new News.Menu(); + menu.bindOn('#feeds ul'); Updating nodes (you dont have to set all values in data): @@ -164,16 +96,15 @@ var t = t || function(app, string){ return string; }; // mock translation for lo 'Feed': 0, 'Folder': 1, 'Starred': 2, - 'New': 3 + 'Subscriptions': 3 } - // TODO: set paths for icons - MenuNodeTypeDefaultIcon = { - 'Feed': '', - 'Folder': '', - 'Starred': '', - 'New': '' - } + // map css classes to MenuNodeTypes + MenuNodeTypeClass = {}; + MenuNodeTypeClass[MenuNodeType.Feed] = 'feed'; + MenuNodeTypeClass[MenuNodeType.Folder] = 'folder'; + MenuNodeTypeClass[MenuNodeType.Starred] = 'starred'; + MenuNodeTypeClass[MenuNodeType.Subscriptions] = 'subscriptions'; News.MenuNodeType = MenuNodeType; @@ -183,255 +114,248 @@ var t = t || function(app, string){ return string; }; // mock translation for lo *#########################################################################/ /** * This is the basic menu used to construct and maintain the menu - * @param cls the css class of the element + * @param showAll if all items should be shown by default */ - Menu = function(cls){ - this._class = cls; - this._children = []; - this._parent = false; - this._id = 0; - this._$htmlElement = $('<ul>'); - this._$htmlElement.attr('data-id', this._id); - this._bindDroppable(this._$htmlElement); - this._selectedNode = undefined; - this._showAll = false; + Menu = function(showAll){ + this._showAll = showAll; + this._unreadCount = { + Feed: {}, + Folder: {}, + Starred: 0, + Subscriptions: 0 + }; } News.Menu = Menu; /** - * Attaches a MenuNode to a node and renders it in the dom - * @param parentType the type of the parent node - * @param parentId the id of the parent node, if 0 the top menu is used - * @param node the MenuNode that should be created + * */ - Menu.prototype.createNode = function(parentType, parentId, node){ - var parentNode = this._findNode(parentType, parentId); - parentNode._addChildNode(node); - parentNode._$htmlElement.append(node.render()); + Menu.prototype.removeNode = function(type, id){ + } /** - * Recursively remove all occurences of the node from the dom and - * from the datastructure - * @param type the type of the node (MenuNodeType) - * @param id the id of the node - * @param removeDom if true, also remove the dom - * @return the childelemnt or undefined if not found + * A node can only be added to a folder or to the root */ - Menu.prototype.removeNode = function(type, id, removeDom){ - for(var i=0; i<this._children.length; i++){ - var child = this._children[i]; - if(child._type === type && child._id === id){ - var nodeIndex = i; - // if we have children, we need to remove their - // html from the dom first then we need to - if(removeDom){ - child._$htmlElement.remove(); - } - this._children.splice(nodeIndex, 1); - return child; - } else { - var child = child.removeNode(type, id); - if(child !== undefined){ - return child; - } - } - } - return undefined; + Menu.prototype.addNode = function(parentId, type, id, data){ + } /** - * Updates a node in the menu - * @param type the type of the node (MenuNodeType) - * @param id the id of the node - * @param data the data array like {title: 'title', unreadCount: 1, icon: 'path/icon.png'} + * */ Menu.prototype.updateNode = function(type, id, data){ - var node = this._findNode(type, id); - node.update(data); + } /** - * Creates the menu from a json structure - * @param json the json looks like this - [ - { - id: 1,, - title: 'hi', - type: MenuNodeType.Folder, - icon: 'url/to/jpg.png', - children: [ - { - id: 1, - title: 'hi too', - type: MenuNodeType.Feed, - children: [] - }, - { - ... - } - ] - }, - { - ... - } - ] - * @param attachToNode used for recursion. to set the current - * element to the node, pass the current structure + * Binds the menu on an existing menu + * @param css Selector the selector to get the element with jquery */ - Menu.prototype.populateFromJSON = function(json, attachToNode){ - for(var i=0; i<json.length; i++){ - var nodeInfo = json[i]; - var nodeData = { - title: nodeInfo.title, - icon: nodeInfo.icon, - unreadCount: nodeInfo.unreadCount - }; - var node = new MenuNode(nodeInfo.type, nodeInfo.id, nodeData); - attachToNode._addChildNode(node); - this.populateFromJSON(nodeInfo.children, node); - } + Menu.prototype.bindOn = function(cssSelector){ + var self = this; + this._$root = $(cssSelector); + this._id = this._$root.data('id'); + this._$root.children('li').each(function(){ + self._bindMenuItem($(this)); + }); + this._bindDroppable(this._$root); } /** - * This function creates the html of the menu and its children - * @return the menu of the node and its children + * Binds the according handlers and reads in the meta data for each node + * @param $listItem the jquery list element */ - Menu.prototype.render = function(){ - var $html = this._$htmlElement.addClass(this._class).data('id', this._id); - for(var i=0; i<this._children.length; i++){ - var child = this._children[i]; - var childHTML = child.render(); - $html.append(childHTML); + Menu.prototype._bindMenuItem = function($listItem){ + switch(this._listItemToMenuNodeType($listItem)){ + case MenuNodeType.Feed: + this._bindFeed($listItem); + break; + case MenuNodeType.Folder: + this._bindFolder($listItem); + break; + case MenuNodeType.Starred: + this._bindStarred($listItem); + break; + case MenuNodeType.Subscriptions: + this._bindSubscriptions($listItem); + break; + default: + console.log('Found unknown MenuNodeType'); + console.log($listItem); + break; } - this._rendered = true; - - return $html; } /** - * Returns the number of elements in the menu - * @return the number of all children + * Returns the MenuNodeType of a list item + * @param $listItem the jquery list element + * @return the MenuNodeType of the jquery element */ - Menu.prototype.getSize = function(){ - var size = this._children.length; - for(var i=0; i<this._children.length; i++){ - size += this._children[i].getSize(); + Menu.prototype._listItemToMenuNodeType = function($listItem){ + if($listItem.hasClass(this._menuNodeTypeToClass(MenuNodeType.Feed))){ + return MenuNodeType.Feed; + } else if($listItem.hasClass(this._menuNodeTypeToClass(MenuNodeType.Folder))){ + return MenuNodeType.Folder; + } else if($listItem.hasClass(this._menuNodeTypeToClass(MenuNodeType.Starred))){ + return MenuNodeType.Starred; + } else if($listItem.hasClass(this._menuNodeTypeToClass(MenuNodeType.Subscriptions))){ + return MenuNodeType.Subscriptions; } - return size; } /** - * Shortcut for intially setting the selected node - * @param type the type of the node (MenuNodeType) - * @param id the id of the node + * Returns the classname of the MenuNodeType + * @param menuNodeType the type of the menu node + * @return the class of the MenuNodeType */ - Menu.prototype.setSelected = function(type, id){ - this._setSelected(this._findNode(type, id)); + Menu.prototype._menuNodeTypeToClass = function(menuNodeType){ + return MenuNodeTypeClass[menuNodeType]; } /** - * Elements should only be set as hidden if the user clicked on a new entry - * Then all all_read entries should be marked as hidden - * This function is used to hide all the read ones if showAll is false, - * otherwise shows all + * Binds event listeners to the folder and its subcontents + * @param $listItem the jquery list element */ - Menu.prototype.triggerHideRead = function(){ - // only trigger in the root menu - if(this._parent === false){ - if(this._showAll){ - $(this._$htmlElement).find('.hidden').each(function(){ - $(this).removeClass('hidden'); - }); - } else { - $(this._$htmlElement).find('.all_read').each(function(){ - if(!$(this).hasClass('hidden')){ - $(this).addClass('hidden'); - } - }); - } - - } - } + Menu.prototype._bindFolder = function($listItem){ + var self = this; + var id = $listItem.data('id'); |