/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.Drawing; using System.Globalization; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Windows.Forms; using System.Xml; using JetBrains.DataStructures; using JetBrains.Omea.Base; using JetBrains.Omea.Contacts; using JetBrains.Omea.FiltersManagement; using JetBrains.Omea.GUIControls; using JetBrains.Omea.HttpTools; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.ResourceTools; using JetBrains.Omea.RSSPlugin.SubscribeWizard; namespace JetBrains.Omea.RSSPlugin { [PluginDescription("RSS & Atom Feeds", "JetBrains Inc.", "Support for RSS/Atom subscriptions.", PluginDescriptionFormat.PlainText, "Icons/RssPluginIcon.png")] public class RSSPlugin : IPlugin, IResourceDisplayer, IResourceUIHandler, IResourceTextProvider, IRssService { private ResourceTreePaneBase _rssTreePane; private IResource _feedRoot; private IResource _lastSelectedFeed; private IResource _lastDisplayedFeed; private bool _lastDisplayUnread; private bool _lastDisplayThreaded; private bool _lastDisplayNewspaper; private IResourceList _lastSelectedFeedList; private IResourceList _lastDisplayedGroupWatcher; private GroupUnreadCountDecorator _groupUnreadCountDecorator; public static readonly string _NetworkUnavailable = "Network is unavailable"; private static RSSPlugin _thePlugin; private static FeedUnreadsFilter _feedsPaneUnreadFilter = new FeedUnreadsFilter(); private static ErrorFeedFilter _feedsPaneErrorFilter = new ErrorFeedFilter(); private static String _savedOrder; private static readonly PlaneListProvider _feedsPlaneListProvider = new PlaneListProvider(); private static readonly FeedUpdateQueue _updateQueue = new FeedUpdateQueue(); private BlogExtensionManager _blogExtensionManager; private CommentThreadingHandler _commentThreadingHandler; private readonly IntHashTableOfInt _selectionMap = new IntHashTableOfInt(); // feed ID -> item ID private ImportManager _importManager; readonly Hashtable _feedImporters = new Hashtable(); protected IStatusWriter _statuswriter; public event ResourceEventHandler FeedUpdated; public event EventHandler UpdateAllStarted; public event EventHandler UpdateAllFinished; /// /// True if the “Update All Feeds” action is currently in progress. /// Is reset to False automatically when all the pending updates finish. /// Does not set to True if a schedulled update or a manual update of a single feed occurs. /// public bool _isUpdatingAll; public RSSPlugin() { _thePlugin = this; _statuswriter = Core.UIManager.GetStatusWriter( this, StatusPane.UI ); } public static RSSPlugin GetInstance() { return _thePlugin; } static private void CreateSearchEngine( string name, string url ) { Guard.NullArgument( name, "name" ); Guard.NullArgument( url, "url" ); IResource engine = Core.ResourceStore.FindUniqueResource( Props.RSSSearchEngineResource, Core.Props.Name, name ); if ( engine == null ) { engine = Core.ResourceStore.BeginNewResource( Props.RSSSearchEngineResource ); engine.SetProp( Core.Props.Name, name ); } else { engine.BeginUpdate(); } engine.SetProp( Props.URL, url ); engine.EndUpdate(); } static private void CreateSearchEngines() { CreateSearchEngine( "Google News", "http://news.google.com/news?output=rss&scoring=d&ie=UTF-8&q=" ); CreateSearchEngine( "MSN Search", "http://search.msn.com:80/results.aspx?format=rss&FORM=R0RE&q=" ); CreateSearchEngine( "Yahoo! Search", "http://api.search.yahoo.com/WebSearchService/rss/webSearch.xml?adult_ok=1&query=" ); CreateSearchEngine( "Feedster", "http://feedster.com/search.php?sort=date&ie=UTF-8&hl=&content=full&type=rss&limit=15&q=" ); CreateSearchEngine( "Blogdigger", "http://www.blogdigger.com/rss.jsp?sortby=date&q=" ); CreateSearchEngine( "Google Blog Search", "http://blogsearch.google.com/blogsearch_feeds?hl=en&btnG=Search+Blogs&scoring=d&num=20&output=rss&ie=UTF-8&q=" ); CreateSearchEngine( "BlogPulse", "http://www.blogpulse.com/rss?sort=date&operator=and&query=" ); CreateSearchEngine( "blogs.yandex.ru", "http://blogs.yandex.ru/search.rss?how=tm&rd=2&charset=UTF-8&no_group=1&text=" ); CreateSearchEngine( "IceRocket Blog Search", "http://www.icerocket.com/search?tab=blog&rss=1&q=" ); CreateSearchEngine( "Sphere", "http://www.sphere.com/rss?datedrop=0&sortby=date&histdays=120&q=" ); } public void Register() { Props.Register( this ); Core.ResourceStore.RegisterUniqueRestriction( Props.RSSSearchEngineResource, Core.Props.Name ); CreateSearchEngines(); IUIManager uiMgr = Core.UIManager; uiMgr.RegisterOptionsGroup( "Internet", "The Internet options enable you to control how [product name] works with several types of online content." ); uiMgr.RegisterOptionsPane( "Internet", "Feeds", CreateRSSOptionsPane, "The Feeds options enable you to control when and how posts to RSS and Atom feeds are downloaded (and subsequently indexed)." ); uiMgr.RegisterOptionsPane( "Internet", "Feeds Enclosures", CreateRSSEnclosureOptionsPane, "The Feeds Enclosures options enable you to control when and where downloads RSS enclosures." ); InitRootFeedGroup(); Core.TabManager.RegisterResourceTypeTab( "Feeds", "Feeds", new[] {"RSSItem", "RSSFeed", "RSSFeedGroup"}, 4 ); _rssTreePane = new JetResourceTreePane(); _rssTreePane.RootResourceType = "RSSFeed"; Image img = Utils.TryGetEmbeddedResourceImageFromAssembly( Assembly.GetExecutingAssembly(), "RSSPlugin.Icons.RSSfeeds24.png" ); Core.LeftSidebar.RegisterResourceStructurePane( "Feeds", "Feeds", "Feeds", img, _rssTreePane ); _rssTreePane.WorkspaceFilterTypes = new[] {"RSSFeed", "RSSFeedGroup"}; _groupUnreadCountDecorator = new GroupUnreadCountDecorator(); _rssTreePane.AddNodeDecorator( _groupUnreadCountDecorator ); _rssTreePane.AddNodeDecorator( new FeedActivenessDecorator() ); _rssTreePane.AddNodeDecorator( new TotalCountDecorator( "RSSFeed", Props.RSSItem, Core.Props.Parent ) ); _rssTreePane.ToolTipCallback = HandleToolTipCallback; _feedsPaneUnreadFilter = new FeedUnreadsFilter(); _feedsPaneUnreadFilter.Hide = Core.SettingStore.ReadBool( IniKeys.Section, IniKeys.FilterUnreadFeeds, false ); _rssTreePane.AddNodeFilter( _feedsPaneUnreadFilter ); _feedsPaneErrorFilter = new ErrorFeedFilter(); _feedsPaneErrorFilter.Hide = Core.SettingStore.ReadBool( IniKeys.Section, IniKeys.FilterErrorFeeds, false ); _rssTreePane.AddNodeFilter( _feedsPaneErrorFilter ); Core.LeftSidebar.RegisterViewPaneShortcut( "Feeds", Keys.Control | Keys.Alt | Keys.F ); Core.PluginLoader.RegisterViewsConstructor( new RSSUgrade1ViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new RSSDataUpgrade() ); Core.PluginLoader.RegisterViewsConstructor( new RSSDataUpgrade2() ); Core.PluginLoader.RegisterViewsConstructor( new RSSViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new RSSUgrade2ViewsConstructor() ); Core.PluginLoader.RegisterViewsConstructor( new RSSUgrade3ViewsConstructor() ); //----------------------------------------------------------------- // Register Search Extensions to narrow the list of results using // simple phrases in search queries: // - two synonyms for restricting the resource type to feed articles; // - restriction to those posts which are comments to others; // - restriction to those posts which have enclosures. //----------------------------------------------------------------- Core.SearchQueryExtensions.RegisterResourceTypeRestriction( "in", "feeds", "RSSItem" ); Core.SearchQueryExtensions.RegisterResourceTypeRestriction( "in", "rss", "RSSItem" ); IResource cond = Core.ResourceStore.FindUniqueResource( FilterManagerProps.ConditionResName, "DeepName", RSSViewsConstructor.PostIsACommentDeep ); if( cond != null ) Core.SearchQueryExtensions.RegisterSingleTokenRestriction( "in", "comments", cond ); cond = Core.ResourceStore.FindUniqueResource( FilterManagerProps.ConditionResName, "DeepName", RSSViewsConstructor.PostHasEnclosuredDeep ); if( cond != null ) Core.SearchQueryExtensions.RegisterSingleTokenRestriction( "in", "enclosured", cond ); //----------------------------------------------------------------- uiMgr.RegisterResourceLocationLink( "RSSItem", -Props.RSSItem, "RSSFeed" ); uiMgr.RegisterResourceLocationLink( "RSSFeedGroup", 0, "RSSFeedGroup" ); uiMgr.RegisterResourceSelectPane( "RSSFeed", typeof (ResourceTreeSelectPane) ); IWorkspaceManager workspaceMgr = Core.WorkspaceManager; workspaceMgr.RegisterWorkspaceType( "RSSFeed", new[] {Props.RSSItem}, WorkspaceResourceType.Container ); workspaceMgr.RegisterWorkspaceFolderType( "RSSFeedGroup", "RSSFeed", new[] {Props.RSSItem} ); workspaceMgr.RegisterWorkspaceType( "RSSItem", new[] { -Props.ItemComment }, WorkspaceResourceType.None ); Core.PluginLoader.RegisterResourceTextProvider( "RSSItem", this ); Core.PluginLoader.RegisterResourceDisplayer( "RSSItem", this ); Core.PluginLoader.RegisterResourceUIHandler( "RSSFeed", this ); Core.PluginLoader.RegisterResourceUIHandler( "RSSFeedGroup", this ); Core.PluginLoader.RegisterNewspaperProvider( "RSSItem", new RssNewspaperProvider() ); // Drag'n'drop RSSDragDropHandler dragDropHandler = new RSSDragDropHandler(); Core.PluginLoader.RegisterResourceDragDropHandler( Core.ResourceTreeManager.ResourceTreeRoot.Type, dragDropHandler ); Core.PluginLoader.RegisterResourceDragDropHandler( "RSSFeed", new DragDropLinkAdapter( dragDropHandler ) ); Core.PluginLoader.RegisterResourceDragDropHandler( "RSSFeedGroup", new DragDropLinkAdapter( dragDropHandler ) ); RSSRenameHandler renameHandler = new RSSRenameHandler(); Core.PluginLoader.RegisterResourceRenameHandler( "RSSFeed", renameHandler ); Core.PluginLoader.RegisterResourceRenameHandler( "RSSFeedGroup", renameHandler ); Core.PluginLoader.RegisterPluginService( this ); // Register default importers new FeedDemonImporter(); new RssBanditImporter(); new SharpReaderImporter(); new BloglinesImporter(); new OPMLImporter(); // After service _importManager = new ImportManager( null, _feedImporters ); if( Core.ProductFullName.EndsWith( "Reader" ) ) { uiMgr.RegisterWizardPane( ImportManager.ImportPaneName, _importManager.GetImportWizardPane, -1 ); } Core.ResourceBrowser.RegisterLinksPaneFilter( "RSSItem", new ItemRecipientsFilter() ); Core.ResourceBrowser.RegisterLinksPaneFilter( "RSSItem", new WeblogFromFilter() ); _blogExtensionManager = new BlogExtensionManager(); _blogExtensionManager.LoadExtensions(); _updateQueue.FeedUpdated += _updateQueue_OnFeedUpdated; _updateQueue.QueueGotEmpty += OnFeedUpdateQueueGotEmpty; Core.RemoteControllerManager.AddRemoteCall( "RSSPlugin.SubscribeToFeed.1", new RemoteSubscribeToFeedDelegate( RemoteSubscribeToFeed ) ); Core.PluginLoader.RegisterResourceDeleter( "RSSItem", new RSSItemDeleter() ); // Register Link Id which serves as an anchor for tracing the events // when new article is created and linked to its folder. int linkId = Core.ResourceStore.GetPropId( "RSSItem" ); Core.ExpirationRuleManager.RegisterResourceType( -linkId, "RSSFeed", "RSSItem" ); Core.ProtocolHandlerManager.RegisterProtocolHandler( "feed", "feed aggregator", RemoteSubscribeToFeed ); RSSFeedIconProvider rssFeedIconProvider = new RSSFeedIconProvider(); Core.ResourceIconManager.RegisterResourceIconProvider( "RSSFeed", rssFeedIconProvider ); Core.ResourceIconManager.RegisterOverlayIconProvider( "RSSFeed", rssFeedIconProvider ); Core.ResourceIconManager.RegisterOverlayIconProvider( "RSSFeedGroup", rssFeedIconProvider ); Core.ResourceIconManager.RegisterResourceIconProvider( "RSSItem", new RSSItemIconProvider(rssFeedIconProvider) ); _commentThreadingHandler = new CommentThreadingHandler(); Core.PluginLoader.RegisterResourceThreadingHandler( "RSSItem", _commentThreadingHandler ); EnclosureDownloadStateColumn.Register(); Core.DisplayColumnManager.RegisterPropertyToTextCallback( Props.EnclosureDownloadedSize, OnPropertyToSize ); Core.DisplayColumnManager.RegisterPropertyToTextCallback( Props.EnclosureSize, OnPropertyToSize ); Core.ResourceBrowser.RegisterLinksGroup( "Addresses", new[] { Props.RSSItem }, ListAnchor.First ); Core.ResourceBrowser.SetDefaultViewSettings( "Feeds", AutoPreviewMode.Off, true ); } /// /// Converts an integer property to a string containing its size representation. /// public static string OnPropertyToSize( IResource res, int propId ) { return Utils.SizeToString( res.GetIntProp( propId ) ); } private static AbstractOptionsPane CreateRSSOptionsPane() { return new RSSOptionPane(); } private static AbstractOptionsPane CreateRSSEnclosureOptionsPane() { return new RSSEnclosureOptionPane(); } private static void UpdateSubjectAndBodyCRC() { if ( Core.ResourceStore.FindResourcesWithProp( "RSSItem", Props.RssLongBodyCRC ).Count != 0 ) { return; } IResourceList list = Core.ResourceStore.GetAllResources( "RSSItem" ); foreach ( IResource item in list ) { int crc = Utils.GetHashCodeInLowerCase( item.GetPropText( Core.Props.Subject ), item.GetPropText( Core.Props.LongBody ) ); item.SetProp( Props.RssLongBodyCRC, crc ); } } public void Startup() { UpdateSubjectAndBodyCRC(); foreach ( IResource feedGroup in Core.ResourceStore.GetAllResources( "RSSFeedGroup" ) ) { if ( !feedGroup.HasProp( Core.Props.Parent ) || feedGroup.GetLinkProp( Core.Props.Parent ) == feedGroup ) { feedGroup.AddLink( Core.Props.Parent, _feedRoot ); } } if ( !Core.SettingStore.ReadBool( IniKeys.Section, "DefaultSubscriptionCreated", false ) ) { Core.SettingStore.WriteBool( IniKeys.Section, "DefaultSubscriptionCreated", true ); FindOrCreateFeed( "Omea News", "http://jetbrains.com/omearss.xml" ); FindOrCreateFeed( "Omea Tips and Tricks", "http://blogs.jetbrains.com/omea/wp-rss2.php" ); FindOrCreateFeed( "JetBrains News", "http://jetbrains.com/rss.xml" ); } string newSummaryStyle = Core.SettingStore.ReadString( IniKeys.Section, "SummaryStyle", string.Empty ); if( newSummaryStyle.Length > 0 ) { RSSItemView.SummaryStyle = newSummaryStyle; } // No preview possible, do all work by hands _importManager.DoImport( _feedRoot, true ); _importManager.DoImportCache(); Core.StateChanged += Core_StateChanged; PerformStructureCorrections(); _groupUnreadCountDecorator.UpdateGroupUnreadCount( false ); } private void FindOrCreateFeed(string name, string url ) { if( Core.ResourceStore.FindResources( null, Props.URL, url ).Count == 0 ) { IResource feed = CreateFeed( name, url, null ); if( feed != null ) { QueueFeedUpdate( feed ); } } } private void PerformStructureCorrections() { IResourceList allFeeds = Core.ResourceStore.GetAllResources( "RSSFeed" ); foreach ( IResource feed in allFeeds ) { IResource commentOwnerItem = feed.GetLinkProp( Props.ItemCommentFeed ); if ( !feed.HasProp( Core.Props.Parent ) && commentOwnerItem == null ) { feed.AddLink( Core.Props.Parent, _feedRoot ); } IResource parentFeed = feed.GetLinkProp( Props.FeedComment2Feed ); if ( commentOwnerItem != null && parentFeed == null ) { IResource pFeed = commentOwnerItem.GetLinkProp( -Props.RSSItem ); if ( pFeed != null ) { pFeed.SetProp( Props.FeedComment2Feed, feed ); } } // delete leftover transient feeds if ( feed.GetIntProp( Props.Transient ) == 1 ) { new ResourceProxy( feed ).DeleteAsync(); } else { if ( feed.GetStringProp( Props.UpdateStatus ) == "(updating)" ) { new ResourceProxy( feed ).DeleteProp( Props.UpdateStatus ); } UpgradeDeletedItems( feed ); } } if( Core.ResourceStore.PropTypes.Exist( "DeletedItems" ) ) { int propId = Core.ResourceStore.PropTypes[ "DeletedItems" ].Id; Core.ResourceStore.PropTypes.Delete( propId ); } } private static void Core_StateChanged( object sender, EventArgs e ) { if ( Core.State == CoreState.Running ) { Utils.NetworkConnectedStateChanged += NetworkConnectedStateChanged; Core.NetworkAP.QueueJobAt( DateTime.Now.AddSeconds( 5 ), "Update Feeds", ScheduleUpdate ); EnclosureDownloadManager.DownloadNextEnclosure(); bool treeFormat = Core.SettingStore.ReadBool( IniKeys.Section, IniKeys.ShowPlaneList, false ); UpdateSortFilter( treeFormat ); } } private static void NetworkConnectedStateChanged() { if( Utils.IsNetworkConnectedLight() ) { Core.NetworkAP.QueueJob( "Update Feeds", ScheduleUpdate ); } } private static void ScheduleUpdate() { foreach ( IResource feed in Core.ResourceStore.GetAllResources( "RSSFeed" ) ) { _updateQueue.ScheduleFeedUpdate( feed ); } } private static void UpgradeDeletedItems( IResource feed ) { if ( Core.ResourceStore.PropTypes.Exist( "DeletedItems" ) && feed.HasProp( "DeletedItems" ) ) { string deletedItems = feed.GetStringProp( "DeletedItems" ); IStringList deletedItemList = feed.GetStringListProp( Props.DeletedItemHashList ); foreach ( string itemHash in deletedItems.Split( ';' ) ) { deletedItemList.Add( itemHash ); } feed.DeleteProp( "DeletedItems" ); } } private void InitRootFeedGroup() { _feedRoot = Core.ResourceTreeManager.GetRootForType( "RSSFeed" ); Core.ResourceTreeManager.SetResourceNodeSort( _feedRoot, "Type- Name" ); // groups above feeds _feedRoot.DisplayName = "All Feeds"; } internal static void UpdateUnreadPaneFilter( bool hide ) { _feedsPaneUnreadFilter.Hide = hide; RSSTreePane.UpdateNodeFilter( true ); } internal static void UpdateErrorPaneFilter( bool hide ) { _feedsPaneErrorFilter.Hide = hide; RSSTreePane.UpdateNodeFilter( true ); } internal static void UpdateSortFilter( bool show ) { if( show ) { Core.UIManager.RunWithProgressWindow( "Sorting feeds", SortFeeds ); } else { IResource root = ((ResourceTreePaneBase)RSSTreePane).RootResource; ResourceTreeDataProvider provider = ((ResourceTreePaneBase)RSSTreePane).DataProvider; provider.ResourceChildProvider = null; new ResourceProxy( root ).SetProp( Core.Props.UserResourceOrder, _savedOrder ); provider.RebuildTree(); } } private static void SortFeeds() { Core.ProgressWindow.UpdateProgress( 0, "Sorting feeds", null ); IResource root = ((ResourceTreePaneBase)RSSTreePane).RootResource; ResourceTreeDataProvider provider = ((ResourceTreePaneBase)RSSTreePane).DataProvider; provider.ResourceChildProvider = _feedsPlaneListProvider; _savedOrder = root.GetStringProp( Core.Props.UserResourceOrder ); IResourceList feeds = _feedsPlaneListProvider.GetChildResources( root ); feeds.Sort( new LastPostComparer(), true ); UserResourceOrder uro = new UserResourceOrder( root ); uro.WriteSortOrder( feeds.ResourceIds ); provider.RebuildTree(); } private class PlaneListProvider : IJetResourceChildProvider { public IResourceList GetChildResources( IResource parent ) { IResourceList allFeeds = null; if( parent.Id == ((ResourceTreePaneBase)RSSTreePane).RootResource.Id ) { allFeeds = Core.ResourceStore.GetAllResourcesLive( Props.RSSFeedResource ); allFeeds = allFeeds.Minus( Core.ResourceStore.FindResourcesWithPropLive( Props.RSSFeedResource, Props.FeedComment2Feed ) ); } return allFeeds; } } private class LastPostComparer : IResourceComparer { private readonly Hashtable hash = new Hashtable(); #region IResourceComparer Members public int CompareResources( IResource r1, IResource r2 ) { DateTime r1Time, r2Time; if( hash.Contains( r1.Id )) r1Time = (DateTime)hash[ r1.Id ]; else { r1Time = CalcTime( r1 ); hash[ r1.Id ] = r1Time; } if( hash.Contains( r2.Id )) r2Time = (DateTime)hash[ r2.Id ]; else { r2Time = CalcTime( r2 ); hash[ r2.Id ] = r2Time; } return r1Time.CompareTo( r2Time ); } #endregion internal static DateTime CalcTime( IResource feed ) { DateTime time = DateTime.MinValue; if( feed.Type == Props.RSSFeedResource ) { IResourceList posts = feed.GetLinksOfType( Props.RSSItemResource, Props.RSSItem ); posts.Sort( new[] { Core.Props.Date }, false ); if( posts.Count > 0 ) time = posts[ 0 ].GetDateProp( Core.Props.Date ); } return time; } } internal static IResource RootFeedGroup { get { return _thePlugin._feedRoot; } } internal static IResourceTreePane RSSTreePane { get { return _thePlugin._rssTreePane; } } internal static BlogExtensionManager ExtensionManager { get { return _thePlugin._blogExtensionManager; } } internal Hashtable FeedImporters { get { return _feedImporters; } } /** * Creates a feed group with the specified parent and name. */ internal static IResource CreateFeedGroup( IResource parent, string name ) { ResourceProxy proxy = ResourceProxy.BeginNewResource( "RSSFeedGroup" ); proxy.SetProp( Core.Props.Name, name ); proxy.AddLink( Core.Props.Parent, parent ); proxy.EndUpdate(); Core.ResourceTreeManager.SetResourceNodeSort( proxy.Resource, "Type- Name" ); // groups above feeds Core.WorkspaceManager.AddToActiveWorkspaceRecursive( proxy.Resource ); return proxy.Resource; } public IResourceList GetAllFeeds() { // transient feeds do not have the Parent property return Core.ResourceStore.FindResourcesWithProp( "RSSFeed", Core.Props.Parent ); } public static IResource GetExistingFeed( string url ) { foreach ( IResource res in Core.ResourceStore.FindResources( "RSSFeed", Props.URL, url ) ) { if ( res.HasProp( Core.Props.Parent ) ) { return res; } } return null; } bool IResourceTextProvider.ProcessResourceText( IResource res, IResourceTextConsumer consumer ) { if ( res.Type == "RSSItem" ) { string title = res.GetPropText( Core.Props.Subject ); if ( title.Length > 0 ) { consumer.AddDocumentHeading( res.Id, title ); } string body = res.GetPropText( Core.Props.LongBody ); if ( body.Length > 0 ) { consumer.RestartOffsetCounting(); HtmlIndexer.IndexHtml( res, body, consumer, null ); } IResourceList parent = res.GetLinksTo( "RSSFeed", "RSSItem" ); if ( parent.Count == 1 ) { string author = ""; if ( parent[ 0 ].HasProp( Props.Author ) ) { author += parent[ 0 ].GetStringProp( Props.Author ) + " "; } if ( parent[ 0 ].HasProp( Core.Props.Name ) ) { author += parent[ 0 ].GetStringProp( Core.Props.Name ); } if ( author.Length > 0 ) { consumer.AddDocumentFragment( res.Id, author, DocumentSection.SourceSection ); } } } return true; } public void Shutdown() {} #region IResourceDisplayer Members public IDisplayPane CreateDisplayPane( string resourceType ) { if ( resourceType == "RSSItem" ) { return new RSSItemView(); } return null; } #endregion internal static Icon LoadIconFromAssembly( string name ) { Assembly assembly = Assembly.GetExecutingAssembly(); Stream stream = assembly.GetManifestResourceStream( "RSSPlugin.Icons." + name ); return stream != null ? new Icon( stream ) : null; } public void ResourceNodeSelected( IResource res ) { if ( res.Type == "RSSFeed" ) { DisplayFeedItemList( res ); } else if ( res.Type == "RSSFeedGroup" ) { DisplayGroupItemList( res ); } } private void DisplayFeedItemList( IResource res ) { bool displayUnread = res.HasProp( Core.Props.DisplayUnread ); bool displayNewspaper = res.HasProp( Core.Props.DisplayNewspaper ); if ( res != _lastSelectedFeed || displayUnread != _lastDisplayUnread || displayNewspaper != _lastDisplayNewspaper ) { _lastSelectedFeed = res; _lastDisplayUnread = displayUnread; bool haveComments = false; _lastSelectedFeedList = ItemsInFeed( res, !displayNewspaper, ref haveComments ); _lastDisplayThreaded = haveComments; _lastDisplayNewspaper = displayNewspaper; if ( displayUnread ) { _lastSelectedFeedList = GetUnreadResources( _lastSelectedFeedList ); } } string captionTemplate = "%OWNER%"; if ( displayUnread ) { captionTemplate = "Unread Posts in " + captionTemplate; } DisplayRSSItemList( res, _lastSelectedFeedList, captionTemplate, _lastDisplayThreaded ); _lastDisplayedFeed = res; Core.ResourceBrowser.ContentChanged += HandleBrowserContentChanged; } private IResource GetRememberedSelection( IResource res ) { IResource selResource = null; if ( Settings.RememberSelection ) { selResource = res.GetLinkProp( Props.SelectedRSSItem ); } else if ( _selectionMap.ContainsKey( res.Id ) ) { int selResourceId = _selectionMap[ res.Id ]; selResource = Core.ResourceStore.TryLoadResource( selResourceId ); } return selResource; } private void HandleBrowserContentChanged( object sender, EventArgs e ) { Core.ResourceBrowser.ContentChanged -= HandleBrowserContentChanged; if ( _lastDisplayedGroupWatcher != null ) { _lastDisplayedGroupWatcher.ResourceChanged -= HandleDisplayedFeedChanged; _lastDisplayedGroupWatcher.Dispose(); _lastDisplayedGroupWatcher = null; } if ( _lastDisplayedFeed != null && _lastDisplayedFeed.HasProp( Props.MarkReadOnLeave ) ) { MarkAsReadAction.DoMarkAsRead( _lastDisplayedFeed.ToResourceList() ); } } private void HandleDisplayedFeedChanged( object sender, ResourcePropIndexEventArgs e ) { if ( e.ChangeSet.IsPropertyChanged( -Core.Props.Parent ) ) { Core.UIManager.QueueUIJob( new MethodInvoker( RedisplaySelectedFeed ) ); } } private void DisplayGroupItemList( IResource res ) { bool displayUnread = res.HasProp( Core.Props.DisplayUnread ); bool displayThreaded = false; IResourceList itemList = ItemsInGroupRecursive( res, !res.HasProp( Core.Props.DisplayNewspaper ), ref displayThreaded ); if ( itemList != null ) { if ( displayUnread ) { itemList = GetUnreadResources( itemList ); } string captionTemplate = "%OWNER%"; if ( displayUnread ) { captionTemplate = "Unread Posts in " + captionTemplate; } DisplayRSSItemList( res, itemList, captionTemplate, displayThreaded ); _lastDisplayedGroupWatcher = res.ToResourceListLive(); _lastDisplayedGroupWatcher.ResourceChanged += HandleDisplayedFeedChanged; Core.ResourceBrowser.ContentChanged += HandleBrowserContentChanged; } else { Core.ResourceBrowser.DisplayResourceList( null, Core.ResourceStore.EmptyResourceList, res.GetPropText( "Name" ), null, null ); } } public static IResourceList ItemsInFeed( IResource res, bool includeComments, ref bool haveComments ) { IResourceList result = res.GetLinksOfTypeLive( "RSSItem", Props.RSSItem ); if ( includeComments ) { // to avoid scanning all items, enable threading if only first item has comment URL if ( result.Count > 0 && result[ 0 ].GetPropText( Props.CommentRSS ).Length > 0 ) { haveComments = true; } IResourceList comments = res.GetLinksOfTypeLive( "RSSItem", Props.FeedComment ); if ( comments.Count > 0 || haveComments ) { result = result.Union( comments ); haveComments = true; } else { comments.Dispose(); } } return result; } public static IResourceList ItemsInGroupRecursive( IResource res, bool includeComments, ref bool haveComments ) { IResourceList feedsInGroup = res.GetLinksTo( "RSSFeed", Core.Props.Parent ); IResourceList itemList = null; foreach ( IResource feed in feedsInGroup ) { itemList = ItemsInFeed( feed, includeComments, ref haveComments ).Union( itemList ); } foreach ( IResource childGroup in res.GetLinksTo( "RSSFeedGroup", Core.Props.Parent ) ) { IResourceList childList = ItemsInGroupRecursive( childGroup, includeComments, ref haveComments ); itemList = (itemList == null) ? childList : itemList.Union( childList ); } return itemList ?? Core.ResourceStore.EmptyResourceList; } private static IResourceList GetUnreadResources( IResourceList resList ) { return resList.Intersect( Core.ResourceStore.FindResourcesWithProp( SelectionType.LiveSnapshot, null, Core.Props.IsUnread ), true ); } private void DisplayRSSItemList( IResource ownerResource, IResourceList itemList, string captionTemplate, bool displayThreaded ) { ResourceListDisplayOptions options = new ResourceListDisplayOptions(); options.CaptionTemplate = captionTemplate; options.SelectedResource = GetRememberedSelection( ownerResource ); if ( displayThreaded ) { options.ThreadingHandler = _commentThreadingHandler; } options.SortSettings = new SortSettings( Core.Props.Date, false ); options.ShowNewspaper = ownerResource.HasProp( Core.Props.DisplayNewspaper ); if ( ownerResource.HasProp( Core.Props.LastError ) ) { options.StatusLine = ownerResource.GetStringProp( Core.Props.LastError ); } if ( ownerResource.HasProp( Props.RSSSearchPhrase ) ) { options.HighlightDataProvider = new HighlightDataProvider( ownerResource.GetPropText( Props.RSSSearchPhrase ) ); options.SuppressContexts = true; } Core.ResourceBrowser.DisplayResourceList( ownerResource, itemList, options ); } public bool CanRenameResource( IResource res ) { // obsolete return false; } public bool ResourceRenamed( IResource res, string newName ) { // obsolete return false; } public void ResourcesDropped( IResource targetResource, IResourceList droppedResources ) { // obsolete } public bool CanDropResources( IResource targetResource, IResourceList dragResources ) { // obsolete return false; } internal void RememberSelection( IResource feed, IResource item ) { _selectionMap[ feed.Id ] = item.Id; if ( Settings.RememberSelection ) { new ResourceProxy( feed ).SetPropAsync( Props.SelectedRSSItem, item ); } } /** * Initiates an immediate update of the selected feed. */ public void QueueFeedUpdate( IResource feed, JobPriority jobPriority ) { _updateQueue.QueueFeedUpdate( feed, jobPriority ); } public void QueueFeedUpdate( IResource feed ) { QueueFeedUpdate( feed, JobPriority.Normal ); } /** * Initiates an update of the selected feed at a time determined by its * last update time and update frequency. */ public void ScheduleFeedUpdate( IResource feed ) { _updateQueue.ScheduleFeedUpdate( feed ); } private void _updateQueue_OnFeedUpdated( object sender, ResourceEventArgs e ) { Core.UIManager.QueueUIJob( new ResourceDelegate( ProcessFeedUpdated ), e.Resource ); if ( FeedUpdated != null ) { FeedUpdated( this, new ResourceEventArgs( e.Resource ) ); } } private void ProcessFeedUpdated( IResource res ) { if ( Core.ResourceBrowser.OwnerResource == res ) { if ( !Core.ResourceBrowser.IsThreaded && !Core.ResourceBrowser.NewspaperVisible ) { IResourceList result = res.GetLinksOfTypeLive( "RSSItem", Props.RSSItem ); if ( result.Count > 0 && result[ 0 ].GetPropText( Props.CommentRSS ).Length > 0 ) { // update selection to ensure comments are shown (OM-10527) RedisplaySelectedFeed(); } } string lastError = res.GetStringProp( Core.Props.LastError ); if ( lastError == null ) { Core.ResourceBrowser.HideStatusLine(); } else { Core.ResourceBrowser.AddStatusLine( lastError, null ); } } } private void RedisplaySelectedFeed() { _lastSelectedFeed = null; _rssTreePane.UpdateSelection(); } public static int UpdatePeriodToIndex( string updatePeriod ) { for ( int i = 0; i < _updatePeriods.Length; i++ ) { if ( String.Compare( _updatePeriods[ i ], updatePeriod, true, CultureInfo.InvariantCulture ) == 0 ) { return i; } } return 1; } public static string IndexToUpdatePeriod( int index ) { return _updatePeriods[ index ]; } private static readonly string[] _updatePeriods = new[] { "minutely", "hourly", "daily", "weekly" }; public static bool IsFeedsOrGroups( IResourceList resources ) { string[] allTypes = resources.GetAllTypes(); return ( allTypes.Length == 1 && ( allTypes[ 0 ] == "RSSFeed" || allTypes[ 0 ] == "RSSFeedGroup" ) ) || ( allTypes.Length == 2 && allTypes[ 0 ] == "RSSFeed" && allTypes[ 1 ] == "RSSFeedGroup" ); } public static bool IsFeedOrGroup( IResource resource ) { return resource != null && ( resource.Type == "RSSFeed" || resource.Type == "RSSFeedGroup" ); } internal static bool HasComments( IResource rssItem ) { if ( rssItem.HasProp( Props.CommentCount ) && rssItem.GetProp( Props.CommentCount ) == 0 ) { return false; } string commentRss = rssItem.GetPropText( Props.CommentRSS ); if ( commentRss.Length == 0 ) { return false; } return true; } public IResource FindOrCreateGroup( string name, IResource parent ) { if ( parent == null ) { parent = _feedRoot; } foreach ( IResource group in parent.GetLinksTo( "RSSFeedGroup", Core.Props.Parent ) ) { if ( String.Compare( group.DisplayName, name, true ) == 0 ) { return group; } } ResourceProxy groupProxy = ResourceProxy.BeginNewResource( "RSSFeedGroup" ); groupProxy.SetProp( Core.Props.Name, name ); groupProxy.AddLink( Core.Props.Parent, parent ); groupProxy.EndUpdate(); return groupProxy.Resource; } public IResource CreateFeed( string name, string url, IResource parent ) { return CreateFeed( name, url, parent, null, null ); } public IResource CreateFeed( string name, string url, IResource parent, string httpLogin, string httpPassword ) { ResourceProxy newFeedProxy = ResourceProxy.BeginNewResource( "RSSFeed" ); newFeedProxy.SetProp( Core.Props.Name, name ); newFeedProxy.SetProp( Props.URL, url ); if ( parent != null ) { newFeedProxy.AddLink( Core.Props.Parent, parent ); } else { newFeedProxy.AddLink( Core.Props.Parent, _feedRoot ); } if ( httpLogin != null ) { newFeedProxy.SetProp( Props.HttpUserName, httpLogin ); newFeedProxy.SetProp( Props.HttpPassword, httpPassword ); } newFeedProxy.EndUpdate(); return newFeedProxy.Resource; } public void ImportOpmlStream( Stream importStream, IResource importRoot, string importFileName, bool importPreview ) { if ( importStream == null ) { throw new ArgumentNullException( "importStream" ); } if ( importRoot == null ) { importRoot = RootFeedGroup; } ImportFeedsOperation importOperation = new ImportFeedsOperation( importStream, importRoot, importFileName, importPreview ); if ( Core.ResourceStore.IsOwnerThread() ) { importOperation.ExecuteOperation(); } else { importOperation.Enqueue(); } } public void ExportOpmlFile( IResource exportRoot, string exportFileName ) { ExportOpmlFileImpl( exportRoot, exportFileName ); } private static void ExportOpmlFileImpl( IResource exportRoot, string exportFileName ) { if ( exportRoot == null ) { exportRoot = RootFeedGroup; } OPMLProcessor.Export( exportRoot, exportFileName ); } public void ShowAddFeedWizard( string defaultUrl, IResource defaultGroup ) { SubscribeForm form = new SubscribeForm(); form.ShowAddFeedWizard( defaultUrl, defaultGroup ); form.Activate(); } public void RegisterChannelElementParser( FeedType feedType, string xmlNameSpace, string elementName, IFeedElementParser parser ) { RSSParser.RegisterChannelElementParser( feedType, xmlNameSpace, elementName, parser ); } public void RegisterItemElementParser( FeedType feedType, string xmlNameSpace, string elementName, IFeedElementParser parser ) { RSSParser.RegisterItemElementParser( feedType, xmlNameSpace, elementName, parser ); } /// /// Register new subscription importer. Importer will be available to user in startup wizard /// and options pane. /// /// the name of importer, will be shown to user. /// The importer instance. public void RegisterFeedImporter( string name, IFeedImporter importer ) { _feedImporters[ name ] = importer; } public void RemoteSubscribeToFeed( string url ) { if ( url.StartsWith( "//" ) ) { url = url.Substring( 2 ); } Core.UIManager.QueueUIJob( new ShowSubscribeWizardDelegate( ShowAddFeedWizard ), url, RootFeedGroup ); } private delegate void ShowSubscribeWizardDelegate( string url, IResource defaultGroup ); private delegate void RemoteSubscribeToFeedDelegate( string url ); private static string HandleToolTipCallback( IResource res ) { if( Core.SettingStore.ReadBool( IniKeys.Section, IniKeys.ShowPlaneList, false ) ) { DateTime time = LastPostComparer.CalcTime( res ); return "Last updated: " + time.ToShortDateString(); } return res.GetStringProp( Core.Props.LastError ); } /// /// The queue of feeds-to-update has gotten empty, and all the pending updates are thru. /// Exit the “Is Updating All” state. /// protected void OnFeedUpdateQueueGotEmpty(object sender, EventArgs e) { bool bWasUpdatingAll; lock( this ) { bWasUpdatingAll = _isUpdatingAll; _isUpdatingAll = false; } // Show the message and fire an event if the queue's gotten empty after updating all the feeds if(bWasUpdatingAll) { _statuswriter.ShowStatus( "Finished updating all feeds.", 10 ); if( UpdateAllFinished != null ) { try { UpdateAllFinished( this, EventArgs.Empty ); } catch( Exception ex ) { Core.ReportException( ex, ExceptionReportFlags.AttachLog ); } } } } /// /// Fires the event that notifies that an update-all action has started. /// protected void FireUpdateAllStarted() { if( UpdateAllStarted != null ) { try { UpdateAllStarted( this, EventArgs.Empty ); } catch( Exception ex ) { Core.ReportException( ex, ExceptionReportFlags.AttachLog ); } } } /// /// Tries to set the flag and reports whether it was successful. /// Succeeds only if update-all is not already running. /// /// public bool TrySetIsDoingUpdateAll() { lock(this) { if(_isUpdatingAll) { _statuswriter.ShowStatus( "“Update All Feeds” is already running.", 5 ); return false; } Core.UserInterfaceAP.QueueJob( "Started Updating All Feeds.", new MethodInvoker(FireUpdateAllStarted) ); _statuswriter.ShowStatus( "Updating all feeds…", 1 ); return _isUpdatingAll = true; } } public delegate void StringDelegate(string param); /// /// Gets whether the “Update All Feeds” action is currently updating any of the feeds. /// public bool IsDoingUpdateAll { get { return _isUpdatingAll;} } /// /// Attempts to start updating all the feeds. /// /// Whether an update-all was initiated successfully. /// The attempt succeeds if an “update all feeds” process is not currently running. In this case the function returns True. /// Otherwise, all the feeds are not queued for update again and False is returned. public bool UpdateAll() { // Check if we're allowed to update-all at the moment // (we're not if an update-all is already in progress) if(!TrySetIsDoingUpdateAll()) return false; // Cannot // Get the list of feeds and queue the update IResourceList resFeeds = GetAllFeeds().Minus( Core.ResourceStore.FindResourcesWithProp( null, Props.IsPaused )); foreach(IResource res in resFeeds) QueueFeedUpdate( res); // TODO: should we update feeds if the “Update Every” is not checked?.. // res.GetIntProp( Props.UpdateFrequency ) >= 0 return true; } internal static void SaveSubscription() { string dbPath = RegUtil.DatabasePath ?? Application.StartupPath; string fileName = Path.Combine( dbPath, ".Subscription.opml.sav" ); ExportOpmlFileImpl( RootFeedGroup, fileName ); } } /// /// Links pane filter which hides the From link if it has the same value as the Weblog link. /// internal class WeblogFromFilter : ILinksPaneFilter { public bool AcceptLinkType( IResource displayedResource, int propId, ref string displayName ) { return true; } public bool AcceptLink( IResource displayedResource, int propId, IResource targetResource, ref string linkTooltip ) { if ( propId == Core.ContactManager.Props.LinkFrom ) { IResource weblog = displayedResource.GetLinkProp( -Props.RSSItem ); if ( weblog == targetResource ) { return false; } } return true; } public bool AcceptAction( IResource displayedResource, IAction action ) { return true; } } internal class RSSItemDeleter : DefaultResourceDeleter { public override void DeleteResource( IResource res ) { base.DeleteResource( res ); IResourceList comments = res.GetLinksTo( "RSSItem", Props.ItemComment ); foreach( IResource comment in comments.ValidResources ) { DeleteResource( comment ); } } public override void DeleteResourcePermanent( IResource res ) { IResourceList feedList = res.GetLinksTo( null, "RSSItem" ); if ( feedList.Count > 0 ) { IResource feed = feedList[ 0 ]; IStringList delItemList = feed.GetStringListProp( Props.DeletedItemHashList ); delItemList.Add( RSSParser.GetRSSItemMD5( res ) ); } IResource commentFeed = res.GetLinkProp( -Props.ItemCommentFeed ); if ( commentFeed != null ) { RemoveFeedsAndGroupsAction.DeleteFeedsAndGroups( commentFeed.ToResourceList() ); } res.Delete(); } } public class WebPost { private static void AddItem( XmlWriter xmlWriter, string tag, string value ) { xmlWriter.WriteStartElement( tag ); xmlWriter.WriteString( value ); xmlWriter.WriteEndElement(); } public static void PostNewComment( string url, string title, string author, string link, string body ) { HttpWebRequest req = (HttpWebRequest)WebRequest.Create( url ); req.Method = "POST"; req.ContentType = "text/xml"; req.UserAgent = HttpReader.UserAgent; StringBuilder xml = new StringBuilder(); XmlTextWriter xmlWriter = new XmlTextWriter( new StringWriter( xml ) ); try { xmlWriter.WriteStartElement( "item" ); AddItem( xmlWriter, "title", title ); AddItem( xmlWriter, "author", author ); AddItem( xmlWriter, "link", link ); AddItem( xmlWriter, "description", body ); xmlWriter.WriteEndElement(); xmlWriter.Flush(); } finally { xmlWriter.Close(); } byte[] utf8Bytes = Encoding.UTF8.GetBytes( xml.ToString() ); req.ContentLength = utf8Bytes.Length; try { Stream stream = req.GetRequestStream(); stream.Write( utf8Bytes, 0, utf8Bytes.Length ); stream.Close(); } catch ( WebException ) {} } } internal class FeedUnreadsFilter : IResourceNodeFilter { private bool _hidden; private readonly Hashtable _feedStati = new Hashtable(); public bool Hide { set { _hidden = value; _feedStati.Clear(); } } public bool AcceptNode( IResource node, int level ) { bool accept = true; if( _hidden ) { if( _feedStati.ContainsKey( node.Id )) { accept = (bool) _feedStati[ node.Id ]; } else if( node.Type == Props.RSSFeedResource ) { IResourceList unreads = Core.ResourceStore.FindResourcesWithProp( Props.RSSItemResource, Core.Props.IsUnread ); accept = node.GetLinksOfType( Props.RSSItemResource, Props.RSSItem ).Intersect( unreads ).Count > 0; _feedStati[ node.Id ] = accept; } else { IResourceList children = node.GetLinksTo( null, Core.Props.Parent ); foreach( IResource child in children ) accept = accept || AcceptNode( child, level + 1 ); } } return accept; } } internal class ErrorFeedFilter : IResourceNodeFilter { private bool _hidden; private readonly Hashtable _feedStati = new Hashtable(); public bool Hide { set { _hidden = value; _feedStati.Clear(); } } public bool AcceptNode(IResource node, int level) { bool accept = true; if(_hidden) { if(_feedStati.ContainsKey(node.Id)) accept = (bool)_feedStati[node.Id]; else if(node.Type == Props.RSSFeedResource) _feedStati[node.Id] = accept = node.HasProp(Core.Props.LastError); else { IResourceList children = node.GetLinksTo(null, Core.Props.Parent); foreach(IResource child in children) accept = accept || AcceptNode(child, level + 1); } } return accept; } } internal class RSSRenameHandler : IResourceRenameHandler { public bool CanRenameResource( IResource res, ref string editText ) { if ( res.Type == Props.RSSFeedResource || res.Type == Props.RSSFeedGroupResource ) { editText = res.GetPropText( Core.Props.Name ); return true; } return false; } public bool ResourceRenamed( IResource res, string newName ) { if ( newName == "" ) { MessageBox.Show( Core.MainWindow, "Please specify a name." ); return false; } ResourceProxy proxy = new ResourceProxy( res ); proxy.AsyncPriority = JobPriority.Immediate; proxy.SetPropAsync( Core.Props.Name, newName ); return true; } } internal class EnclosureDownloadStateColumn : ImageListColumn { /// /// Singleton instance. /// private static EnclosureDownloadStateColumn _instance; // Do not initialize here, so that .cctor won't create an object /// /// Gets the singleton instance. /// public static EnclosureDownloadStateColumn Instance { get { if( _instance == null ) _instance = new EnclosureDownloadStateColumn(); return _instance; } } private EnclosureDownloadStateColumn() : base( Props.EnclosureDownloadingState ) { } public static void Register() { EnclosureDownloadStateColumn instance = Instance; // Add enclosure state icons for( int a = (int)EnclosureDownloadState.MinValue; a < (int)EnclosureDownloadState.MaxValue; a++ ) instance.AddIconValue( EnclosureDownloadManager.GetEnclosureStateIcon( (EnclosureDownloadState)a ), a ); instance.SetHeaderIcon( RSSPlugin.LoadIconFromAssembly( "downloadColumn.ico" ) ); instance.ShowTooltips = true; Core.DisplayColumnManager.RegisterCustomColumn( Props.EnclosureDownloadingState, instance ); IResourceList list = Core.ResourceStore.FindResources( "RSSItem", Props.EnclosureSize, -1 ); foreach( IResource resource in list.ValidResources ) { resource.SetProp( Props.EnclosureSize, 0 ); } } public override string GetTooltip( IResource res ) { if( res.HasProp( Props.EnclosureDownloadingState ) ) { switch( res.GetIntProp( Props.EnclosureDownloadingState ) ) { case DownloadState.NotDownloaded: return "Not Downloaded"; case DownloadState.Planned: return "Planned For Downloading"; case DownloadState.Completed: return "Download Completed"; case DownloadState.Failed: string tooltip = "Download Failed"; if( res.HasProp( Props.EnclosureFailureReason ) ) { tooltip += ": " + res.GetPropText( Props.EnclosureFailureReason ); } return tooltip; case DownloadState.InProgress: float size = res.GetIntProp( Props.EnclosureSize ); float downloaded = res.GetIntProp( Props.EnclosureDownloadedSize ); if( size > 0.0 && downloaded > 0.0 ) { int percent = (int)((100.0 * downloaded) / size); if( percent > 100 ) { percent = 100; } return "Downloaded " + percent + "%"; } return "Download In Progress"; } } return string.Empty; } public override void MouseClicked( IResource res, Point pt ) { if( res.HasProp( Props.EnclosureDownloadingState ) ) { switch( res.GetIntProp( Props.EnclosureDownloadingState ) ) { case DownloadState.Planned: new ResourceProxy( res ).SetProp( Props.EnclosureDownloadingState, DownloadState.NotDownloaded ); break; case DownloadState.NotDownloaded: EnclosureDownloadManager.PlanToDownload( res ); break; case DownloadState.Failed: EnclosureDownloadManager.PlanToDownload( res ); break; } } } } internal class CommentThreadingHandler : DefaultThreadingHandler { public CommentThreadingHandler() : base( Props.ItemComment ) {} public override bool CanExpandThread( IResource res, ThreadExpandReason reason ) { if ( reason == ThreadExpandReason.Enumerate ) { return base.CanExpandThread( res, reason ); } return base.CanExpandThread( res, reason ) || RSSPlugin.HasComments( res ); } public override bool HandleThreadExpand( IResource res, ThreadExpandReason reason ) { if ( reason == ThreadExpandReason.Expand ) { // if feed update was started during last run of Omea, but never completed, // we need to retry it now if ( RSSPlugin.HasComments( res ) && ( !res.HasProp( -Props.ItemCommentFeed ) || ReadCommentsAction.FindDownloadingCommentsItem( res ) != null ) ) { ReadCommentsAction.DownloadComments( res ); } return true; } return false; } } public class HighlightDataProvider : IHighlightDataProvider { private readonly WordPtr[] _words; public HighlightDataProvider( string phrase ) { Guard.EmptyStringArgument( phrase, "phrase" ); string[] strWords = phrase.Split( ' ' ); if ( strWords.Length > 0 ) { _words = new WordPtr[strWords.Length]; for ( int i = 0; i < strWords.Length; ++i ) { _words[i].Section = DocumentSection.BodySection; _words[i].StartOffset = 0; _words[i].Original = strWords[i]; _words[i].Text = strWords[i]; } } } public bool GetHighlightData( IResource res, out WordPtr[] words ) { Guard.NullArgument( res, "res" ); words = _words; return _words != null; } public void RequestContexts( int[] resourceIDs ) { } public string GetContext( IResource res ) { return null; } public OffsetData[] GetContextHighlightData( IResource res ) { return null; } } public class DateIndexComparer: IResourceComparer { public int CompareResources( IResource r1, IResource r2 ) { int result = r1.GetDateProp( Core.Props.Date ).CompareTo( r2.GetDateProp( Core.Props.Date ) ); if ( result == 0 ) { // assume the items in the feed go in the same order as in main page => // newest items on top => smaller feed index means later date result = r2.GetIntProp( Props.IndexInFeed ) - r1.GetIntProp( Props.IndexInFeed ); } return result; } } }