mirror of
https://github.com/viq/NewsBlur.git
synced 2025-11-01 09:09:16 +00:00
Merge branch 'refs/heads/dejal' into catalyst
This commit is contained in:
commit
9466efe171
2444 changed files with 2101 additions and 86192 deletions
|
|
@ -137,7 +137,6 @@ these after the installation below.
|
|||
the `fabfile.py`. You should also have MySQL/PostgreSQL and MongoDB already installed.
|
||||
|
||||
fab -R local setup_python
|
||||
fab -R local setup_imaging
|
||||
fab -R local setup_mongoengine
|
||||
fab -R local setup_forked_mongoengine
|
||||
fab -R local setup_repo_local_settings
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class LoginForm(forms.Form):
|
|||
widget=forms.PasswordInput(attrs={'tabindex': 2, 'class': 'NB-input'}),
|
||||
required=False)
|
||||
# error_messages={'required': 'Please enter a password.'})
|
||||
add = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user_cache = None
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@ def login(request):
|
|||
code = 1
|
||||
else:
|
||||
logging.user(form.get_user(), "~FG~BBLogin~FW")
|
||||
next_url = request.POST.get('next', '')
|
||||
if next_url:
|
||||
return HttpResponseRedirect(next_url)
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
else:
|
||||
message = form.errors.items()[0][1][0]
|
||||
|
|
|
|||
|
|
@ -417,7 +417,7 @@ class Feed(models.Model):
|
|||
|
||||
if url and url.startswith('newsletter:'):
|
||||
return cls.objects.get(feed_address=url)
|
||||
if url and re.match('(https?://)?twitter.com/\w+/?$', url):
|
||||
if url and re.match('(https?://)?twitter.com/\w+/?', url):
|
||||
without_rss = True
|
||||
if url and re.match(r'(https?://)?(www\.)?facebook.com/\w+/?$', url):
|
||||
without_rss = True
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.newsblur"
|
||||
android:versionCode="163"
|
||||
android:versionName="9.0.1" >
|
||||
android:versionCode="165"
|
||||
android:versionName="10.0b2" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
|
|
@ -74,12 +74,18 @@
|
|||
<activity
|
||||
android:name=".activity.Settings"
|
||||
android:label="@string/settings"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.WidgetConfig"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/widget" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.FeedItemsList" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.AllStoriesItemsList" />
|
||||
android:name=".activity.AllStoriesItemsList"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.InfrequentItemsList" />
|
||||
|
|
@ -137,17 +143,11 @@
|
|||
<activity
|
||||
android:name=".activity.SocialFeedReading"/>
|
||||
|
||||
<activity android:name=".widget.ConfigureWidgetActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".service.NBSyncService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service android:name=".widget.BlurWidgetRemoteViewsService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||
/>
|
||||
<service android:name=".widget.WidgetRemoteViewsService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
<intent-filter>
|
||||
|
|
@ -158,13 +158,24 @@
|
|||
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
|
||||
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
|
||||
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />
|
||||
<receiver android:name=".widget.NewsBlurWidgetProvider" >
|
||||
<receiver android:name=".widget.WidgetProvider"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider"
|
||||
android:resource="@xml/newsblur_appwidget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".service.TimeChangeReceiver">
|
||||
<intent-filter >
|
||||
<action android:name="android.intent.action.TIME_SET"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".widget.WidgetUpdateReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</receiver>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="com.newsblur.fileprovider"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ExpandableListView
|
||||
android:id="@+id/list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:groupIndicator="@null" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_no_subscriptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:padding="32dp"
|
||||
android:text="@string/title_no_subscriptions"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:background="@color/white"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txt_feed_name"
|
||||
style="@style/rowItemHeaderBackground"
|
||||
tools:text="Coding Horror"
|
||||
android:paddingTop="9dp"
|
||||
android:paddingBottom="9dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:shadowDy="1"
|
||||
android:textStyle="bold"
|
||||
android:lines="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<ListView
|
||||
tools:listitem="@layout/newsblur_widget_item"
|
||||
android:id="@+id/widget_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Note that empty views must be siblings of the collection view
|
||||
for which the empty view represents empty state.-->
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/newsblur_widget_item"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:id="@+id/widget_item_title"
|
||||
android:paddingBottom="7dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
tools:text="The Next CEO of Stackoverflow"
|
||||
android:lines="1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:paddingEnd="8dp"
|
||||
android:id="@+id/widget_item_time"
|
||||
tools:text="30 min ago"
|
||||
android:textSize="12sp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_box"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img"
|
||||
android:layout_width="19dp"
|
||||
android:layout_height="19dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/description_row_feed_icon"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_details"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/container_folder"
|
||||
style="?selectorFolderBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_folder"
|
||||
android:layout_width="19dp"
|
||||
android:layout_height="19dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="2dp"
|
||||
android:contentDescription="@string/description_row_folder_icon"
|
||||
android:src="@drawable/g_icn_folder" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_folder_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingTop="9dp"
|
||||
android:paddingBottom="9dp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
54
clients/android/NewsBlur/res/layout/view_app_widget.xml
Normal file
54
clients/android/NewsBlur/res/layout/view_app_widget.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="@string/description_login_logo"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/logo" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="32dp"
|
||||
android:lines="1"
|
||||
android:text="@string/newsblur"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/widget_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/widget_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/view_widget_story_item"
|
||||
android:divider="@color/widget_divider"
|
||||
android:dividerHeight="0.5dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/title_widget_setup"
|
||||
android:textColor="@color/widget_feed_title" />
|
||||
|
||||
<!-- Note that empty views must be siblings of the collection view
|
||||
for which the empty view represents empty state.-->
|
||||
|
||||
</LinearLayout>
|
||||
116
clients/android/NewsBlur/res/layout/view_widget_story_item.xml
Normal file
116
clients/android/NewsBlur/res/layout/view_widget_story_item.xml
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/view_widget_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_item_favicon_borderbar_1"
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_item_favicon_borderbar_2"
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@id/story_item_favicon_borderbar_1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_item_feedicon"
|
||||
android:layout_width="19dp"
|
||||
android:layout_height="19dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_toRightOf="@+id/story_item_favicon_borderbar_2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_item_feedtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_toRightOf="@id/story_item_feedicon"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/widget_feed_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_item_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/story_item_feedicon"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_toLeftOf="@+id/story_item_thumbnail"
|
||||
android:layout_toRightOf="@id/story_item_favicon_borderbar_2"
|
||||
android:ellipsize="end"
|
||||
android:lines="2"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/widget_story_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_item_content"
|
||||
style="?storySnippetText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/story_item_title"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_toLeftOf="@+id/story_item_thumbnail"
|
||||
android:layout_toRightOf="@id/story_item_favicon_borderbar_2"
|
||||
android:ellipsize="end"
|
||||
android:lines="2"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/widget_story_content"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_item_thumbnail"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_below="@+id/story_item_feedtitle"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_item_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/story_item_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_toLeftOf="@id/story_item_date"
|
||||
android:layout_toRightOf="@id/story_item_favicon_borderbar_2"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/widget_story_author"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/story_item_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/story_item_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:gravity="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/widget_story_date_time"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
|
@ -21,6 +21,10 @@
|
|||
android:title="@string/settings"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_widget"
|
||||
android:title="@string/widget"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_feedback"
|
||||
android:title="@string/menu_feedback"
|
||||
android:showAsAction="never" >
|
||||
|
|
|
|||
63
clients/android/NewsBlur/res/menu/menu_widget.xml
Normal file
63
clients/android/NewsBlur/res/menu/menu_widget.xml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/menu_sort_by"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_sort_by">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/menu_sort_by_name"
|
||||
android:title="@string/menu_sort_by_name" />
|
||||
<item
|
||||
android:id="@+id/menu_sort_by_subs"
|
||||
android:title="@string/menu_sort_by_subs" />
|
||||
<item
|
||||
android:id="@+id/menu_sort_by_stories_month"
|
||||
android:title="@string/menu_sort_by_stories_month" />
|
||||
<item
|
||||
android:id="@+id/menu_sort_by_recent_story"
|
||||
android:title="@string/menu_sort_by_recent_story" />
|
||||
<item
|
||||
android:id="@+id/menu_sort_by_number_opens"
|
||||
android:title="@string/menu_sort_by_number_opens" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/menu_sort_order"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_sort_order">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/menu_sort_order_ascending"
|
||||
android:title="@string/menu_sort_order_ascending" />
|
||||
<item
|
||||
android:id="@+id/menu_sort_order_descending"
|
||||
android:title="@string/menu_sort_order_descending" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/menu_folder_view"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_folder_view">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/menu_folder_view_nested"
|
||||
android:title="@string/menu_folder_view_nested" />
|
||||
<item
|
||||
android:id="@+id/menu_folder_view_flat"
|
||||
android:title="@string/menu_folder_view_flat" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/menu_select_all"
|
||||
android:title="@string/menu_select_all" />
|
||||
<item
|
||||
android:id="@+id/menu_select_none"
|
||||
android:title="@string/menu_select_none" />
|
||||
</menu>
|
||||
|
|
@ -66,6 +66,14 @@
|
|||
<color name="dark_story_content_text">#CECECE</color>
|
||||
<color name="story_author_text">#959595</color>
|
||||
|
||||
<color name="widget_title">#8C8C8C</color>
|
||||
<color name="widget_divider">#42453E</color>
|
||||
<color name="widget_feed_title">#8C8C8C</color>
|
||||
<color name="widget_story_title">#CECECE</color>
|
||||
<color name="widget_story_content">#8C8C8C</color>
|
||||
<color name="widget_story_author">#404040</color>
|
||||
<color name="widget_story_date_time">#BDBDBD</color>
|
||||
|
||||
<color name="story_comment_divider">#F0F0F0</color>
|
||||
<color name="dark_story_comment_divider">#42453E</color>
|
||||
|
||||
|
|
@ -130,4 +138,6 @@
|
|||
<color name="refresh_3">#e9a941</color>
|
||||
<color name="refresh_4">#fae576</color>
|
||||
|
||||
<color name="widget_background">#C8000000</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@
|
|||
<string name="description_login_logo">NewsBlur Logo</string>
|
||||
<string name="description_profile_picture">The user\'s profile picture</string>
|
||||
<string name="description_row_folder_icon">folder icon</string>
|
||||
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
|
||||
<string name="description_follow_button">Follow or unfollow a user</string>
|
||||
<string name="description_comment_user">Comment user image</string>
|
||||
<string name="description_empty_list_image">Empty list placeholder</string>
|
||||
<string name="description_row_feed_icon">feed icon</string>
|
||||
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
|
||||
<string name="description_follow_button">Follow or unfollow a user</string>
|
||||
<string name="description_comment_user">Comment user image</string>
|
||||
<string name="description_empty_list_image">Empty list placeholder</string>
|
||||
<string name="description_menu">Menu</string>
|
||||
|
||||
|
||||
<string name="title_choose_folders">Choose Folders for Feed %s</string>
|
||||
<string name="title_rename_feed">Rename Feed %s</string>
|
||||
|
||||
|
|
@ -165,6 +166,21 @@
|
|||
<string name="list_style_grid_m">Grid (medium)</string>
|
||||
<string name="list_style_grid_c">Grid (coarse)</string>
|
||||
|
||||
<string name="menu_sort_by">Sort By</string>
|
||||
<string name="menu_sort_by_name">Name</string>
|
||||
<string name="menu_sort_by_subs">Subscribers</string>
|
||||
<string name="menu_sort_by_stories_month">Stories Per Month</string>
|
||||
<string name="menu_sort_by_recent_story">Most Recent Story</string>
|
||||
<string name="menu_sort_by_number_opens">Number of opens</string>
|
||||
<string name="menu_sort_order">Sort Order</string>
|
||||
<string name="menu_sort_order_ascending">Ascending</string>
|
||||
<string name="menu_sort_order_descending">Descending</string>
|
||||
<string name="menu_folder_view">Folder View</string>
|
||||
<string name="menu_folder_view_nested">Nested</string>
|
||||
<string name="menu_folder_view_flat">Flat</string>
|
||||
<string name="menu_select_all">Select All</string>
|
||||
<string name="menu_select_none">Select None</string>
|
||||
|
||||
<string name="toast_hold_to_select">Press and hold to select text</string>
|
||||
|
||||
<string name="logout_warning">Are you sure you want to log out?</string>
|
||||
|
|
@ -216,8 +232,14 @@
|
|||
<string name="unknown_user">Unknown User</string>
|
||||
<string name="delete_feed_message">Delete feed \"%s\"?</string>
|
||||
<string name="unfollow_message">Unfollow \"%s\"?</string>
|
||||
<string name="feed_subscribers">%s subscribers</string>
|
||||
<string name="feed_opens">%d opens</string>
|
||||
<string name="feed_stories_per_month">%d stories/month</string>
|
||||
|
||||
<string name="settings">Preferences</string>
|
||||
<string name="widget">Widget</string>
|
||||
<string name="title_widget_setup">Tap to setup in NewsBlur</string>
|
||||
<string name="title_no_subscriptions">No active subscriptions detected</string>
|
||||
|
||||
<string name="settings_cat_offline">Offline</string>
|
||||
<string name="settings_enable_offline">Download Stories</string>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="180dp"
|
||||
android:minWidth="320dp"
|
||||
android:minHeight="100dp"
|
||||
android:minResizeWidth="100dp"
|
||||
android:minResizeHeight="60dp"
|
||||
android:updatePeriodMillis="14400000"
|
||||
android:initialLayout="@layout/newsblur_widget"
|
||||
android:configure="com.newsblur.widget.ConfigureWidgetActivity"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:initialLayout="@layout/view_app_widget"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen">
|
||||
</appwidget-provider>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
|
@ -14,4 +15,13 @@ public class AllStoriesItemsList extends ItemsList {
|
|||
UIUtils.setCustomActionBar(this, R.drawable.ak_icon_allstories, getResources().getString(R.string.all_stories_title));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) {
|
||||
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
|
||||
UIUtils.startReadingActivity(fs, hash, true,this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ import com.newsblur.util.UIUtils;
|
|||
public abstract class ItemsList extends NbActivity implements StoryOrderChangedListener, ReadFilterChangedListener, OnSeekBarChangeListener {
|
||||
|
||||
public static final String EXTRA_FEED_SET = "feed_set";
|
||||
|
||||
public static final String EXTRA_STORY_HASH = "story_hash";
|
||||
public static final String EXTRA_WIDGET_STORY = "widget_story";
|
||||
private static final String STORY_ORDER = "storyOrder";
|
||||
private static final String READ_FILTER = "readFilter";
|
||||
private static final String DEFAULT_FEED_VIEW = "defaultFeedView";
|
||||
|
|
@ -67,9 +68,12 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
// reduce UI lag, or in case somehow we got redisplayed in a zero-story state
|
||||
FeedUtils.prepareReadingSession(fs, false);
|
||||
|
||||
if (PrefsUtils.isAutoOpenFirstUnread(this)) {
|
||||
if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) {
|
||||
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
|
||||
UIUtils.startReadingActivity(fs, hash, true, this);
|
||||
} else if (PrefsUtils.isAutoOpenFirstUnread(this)) {
|
||||
if (FeedUtils.dbHelper.getUnreadCount(fs, intelState) > 0) {
|
||||
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this);
|
||||
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, false,this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import com.newsblur.util.PrefsUtils;
|
|||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.view.StateToggleButton.StateChangedListener;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener, OnSeekBarChangeListener {
|
||||
|
||||
|
|
@ -294,6 +295,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
} else if (themeValue == ThemeValue.BLACK) {
|
||||
menu.findItem(R.id.menu_theme_black).setChecked(true);
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_widget).setVisible(WidgetUtils.hasActiveAppWidgets(this));
|
||||
|
||||
pm.setOnMenuItemClickListener(this);
|
||||
pm.show();
|
||||
|
|
@ -324,7 +327,11 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
Intent settingsIntent = new Intent(this, Settings.class);
|
||||
startActivity(settingsIntent);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_feedback_email) {
|
||||
} else if (item.getItemId() == R.id.menu_widget) {
|
||||
Intent widgetIntent = new Intent(this, WidgetConfig.class);
|
||||
startActivity(widgetIntent);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_feedback_email) {
|
||||
PrefsUtils.sendLogEmail(this);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_feedback_post) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
public static final String EXTRA_FEEDSET = "feed_set";
|
||||
public static final String EXTRA_POSITION = "feed_position";
|
||||
public static final String EXTRA_STORY_HASH = "story_hash";
|
||||
public static final String EXTRA_WAIT_REFRESH = "wait_refresh";
|
||||
private static final String BUNDLE_POSITION = "position";
|
||||
private static final String BUNDLE_STARTING_UNREAD = "starting_unread";
|
||||
private static final String BUNDLE_SELECTED_FEED_VIEW = "selectedFeedView";
|
||||
|
|
@ -104,6 +105,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
private int lastVScrollPos = 0;
|
||||
|
||||
private boolean unreadSearchActive = false;
|
||||
private boolean waitRefresh = false;
|
||||
|
||||
private List<Story> pageHistory;
|
||||
|
||||
|
|
@ -124,6 +126,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
try {
|
||||
fs = (FeedSet)getIntent().getSerializableExtra(EXTRA_FEEDSET);
|
||||
waitRefresh = getIntent().getBooleanExtra(EXTRA_WAIT_REFRESH, false);
|
||||
} catch (RuntimeException re) {
|
||||
// in the wild, the notification system likes to pass us an Intent that has missing or very stale
|
||||
// Serializable extras.
|
||||
|
|
@ -315,6 +318,11 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
emptyViewText.setVisibility(View.INVISIBLE);
|
||||
storyHash = null;
|
||||
return;
|
||||
} else if (waitRefresh) {
|
||||
// when deep linking from app widget the db might not have the latest stories
|
||||
// that we're looking to read so return and wait for updated cursor
|
||||
waitRefresh = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// if the story wasn't found, try to get more stories into the cursor
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ExpandableListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.domain.Folder;
|
||||
import com.newsblur.util.FeedOrderFilter;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.FolderViewFilter;
|
||||
import com.newsblur.util.ListOrderFilter;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
public class WidgetConfig extends NbActivity {
|
||||
|
||||
@Bind(R.id.list_view)
|
||||
ExpandableListView listView;
|
||||
@Bind(R.id.text_no_subscriptions)
|
||||
TextView textNoSubscriptions;
|
||||
|
||||
private WidgetConfigAdapter adapter;
|
||||
private ArrayList<Feed> feeds;
|
||||
private ArrayList<Folder> folders;
|
||||
private Map<String, Feed> feedMap = new HashMap<>();
|
||||
private ArrayList<String> folderNames = new ArrayList<>();
|
||||
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_widget_config);
|
||||
ButterKnife.bind(this);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setupList();
|
||||
loadFeeds();
|
||||
loadFolders();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// notify widget to refresh next time it's viewed
|
||||
WidgetUtils.notifyViewDataChanged(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_widget, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
ListOrderFilter listOrderFilter = PrefsUtils.getWidgetConfigListOrder(this);
|
||||
if (listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
menu.findItem(R.id.menu_sort_order_ascending).setChecked(true);
|
||||
} else if (listOrderFilter == ListOrderFilter.DESCENDING) {
|
||||
menu.findItem(R.id.menu_sort_order_descending).setChecked(true);
|
||||
}
|
||||
|
||||
FeedOrderFilter feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(this);
|
||||
if (feedOrderFilter == FeedOrderFilter.NAME) {
|
||||
menu.findItem(R.id.menu_sort_by_name).setChecked(true);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
|
||||
menu.findItem(R.id.menu_sort_by_subs).setChecked(true);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
|
||||
menu.findItem(R.id.menu_sort_by_stories_month).setChecked(true);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY) {
|
||||
menu.findItem(R.id.menu_sort_by_recent_story).setChecked(true);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.OPENS) {
|
||||
menu.findItem(R.id.menu_sort_by_number_opens).setChecked(true);
|
||||
}
|
||||
|
||||
FolderViewFilter folderViewFilter = PrefsUtils.getWidgetConfigFolderView(this);
|
||||
if (folderViewFilter == FolderViewFilter.NESTED) {
|
||||
menu.findItem(R.id.menu_folder_view_nested).setChecked(true);
|
||||
} else if (folderViewFilter == FolderViewFilter.FLAT) {
|
||||
menu.findItem(R.id.menu_folder_view_flat).setChecked(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.menu_sort_order_ascending:
|
||||
replaceListOrderFilter(ListOrderFilter.ASCENDING);
|
||||
return true;
|
||||
case R.id.menu_sort_order_descending:
|
||||
replaceListOrderFilter(ListOrderFilter.DESCENDING);
|
||||
return true;
|
||||
case R.id.menu_sort_by_name:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.NAME);
|
||||
return true;
|
||||
case R.id.menu_sort_by_subs:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
|
||||
return true;
|
||||
case R.id.menu_sort_by_recent_story:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
|
||||
return true;
|
||||
case R.id.menu_sort_by_stories_month:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
|
||||
return true;
|
||||
case R.id.menu_sort_by_number_opens:
|
||||
replaceFeedOrderFilter(FeedOrderFilter.OPENS);
|
||||
return true;
|
||||
case R.id.menu_folder_view_nested:
|
||||
replaceFolderView(FolderViewFilter.NESTED);
|
||||
return true;
|
||||
case R.id.menu_folder_view_flat:
|
||||
replaceFolderView(FolderViewFilter.FLAT);
|
||||
return true;
|
||||
case R.id.menu_select_all:
|
||||
selectAllFeeds();
|
||||
return true;
|
||||
case R.id.menu_select_none:
|
||||
replaceWidgetFeedIds(Collections.<String>emptySet());
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupList() {
|
||||
adapter = new WidgetConfigAdapter(this);
|
||||
listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void loadFeeds() {
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
|
||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
||||
@Override
|
||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
|
||||
processFeeds(cursor);
|
||||
}
|
||||
});
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
private void loadFolders() {
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
|
||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
||||
@Override
|
||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
|
||||
processFolders(cursor);
|
||||
}
|
||||
});
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
private void processFeeds(Cursor cursor) {
|
||||
ArrayList<Feed> feeds = new ArrayList<>();
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Feed feed = Feed.fromCursor(cursor);
|
||||
if (feed.active) {
|
||||
feeds.add(feed);
|
||||
feedMap.put(feed.feedId, feed);
|
||||
}
|
||||
}
|
||||
this.feeds = feeds;
|
||||
processData();
|
||||
}
|
||||
|
||||
private void processFolders(Cursor cursor) {
|
||||
ArrayList<Folder> folders = new ArrayList<>();
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Folder folder = Folder.fromCursor(cursor);
|
||||
if (!folder.feedIds.isEmpty()) {
|
||||
folders.add(folder);
|
||||
}
|
||||
}
|
||||
this.folders = folders;
|
||||
Collections.sort(this.folders, new Comparator<Folder>() {
|
||||
@Override
|
||||
public int compare(Folder o1, Folder o2) {
|
||||
return Folder.compareFolderNames(o1.flatName(), o2.flatName());
|
||||
}
|
||||
});
|
||||
processData();
|
||||
}
|
||||
|
||||
private void processData() {
|
||||
if (folders != null && feeds != null) {
|
||||
for (Folder folder : folders) {
|
||||
ArrayList<Feed> activeFeeds = new ArrayList<>();
|
||||
for (String feedId : folder.feedIds) {
|
||||
Feed feed = feedMap.get(feedId);
|
||||
if (feed != null && feed.active && !activeFeeds.contains(feed)) {
|
||||
activeFeeds.add(feed);
|
||||
}
|
||||
}
|
||||
folderNames.add(folder.flatName());
|
||||
folderChildren.add(activeFeeds);
|
||||
}
|
||||
|
||||
setSelectedFeeds();
|
||||
setAdapterData();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectAllFeeds() {
|
||||
Set<String> feedIds = new HashSet<>(this.feeds.size());
|
||||
for (Feed feed : this.feeds) {
|
||||
feedIds.add(feed.feedId);
|
||||
}
|
||||
replaceWidgetFeedIds(feedIds);
|
||||
}
|
||||
|
||||
private void replaceWidgetFeedIds(Set<String> feedIds) {
|
||||
PrefsUtils.setWidgetFeedIds(this, feedIds);
|
||||
adapter.replaceFeedIds(feedIds);
|
||||
}
|
||||
|
||||
private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
|
||||
PrefsUtils.setWidgetConfigFeedOrder(this, feedOrderFilter);
|
||||
adapter.replaceFeedOrder(feedOrderFilter);
|
||||
}
|
||||
|
||||
private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
|
||||
PrefsUtils.setWidgetConfigListOrder(this, listOrderFilter);
|
||||
adapter.replaceListOrder(listOrderFilter);
|
||||
}
|
||||
|
||||
private void replaceFolderView(FolderViewFilter folderViewFilter) {
|
||||
PrefsUtils.setWidgetConfigFolderView(this, folderViewFilter);
|
||||
adapter.replaceFolderView(folderViewFilter);
|
||||
setAdapterData();
|
||||
}
|
||||
|
||||
private void setSelectedFeeds() {
|
||||
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(this);
|
||||
// by default select all feeds
|
||||
if (feedIds == null) {
|
||||
feedIds = new HashSet<>(this.feeds.size());
|
||||
for (Feed feed : this.feeds) {
|
||||
feedIds.add(feed.feedId);
|
||||
}
|
||||
}
|
||||
adapter.setFeedIds(feedIds);
|
||||
}
|
||||
|
||||
private void setAdapterData() {
|
||||
adapter.setData(this.folderNames, this.folderChildren, this.feeds);
|
||||
|
||||
listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ExpandableListView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.FeedOrderFilter;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.FolderViewFilter;
|
||||
import com.newsblur.util.ListOrderFilter;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class WidgetConfigAdapter extends BaseExpandableListAdapter {
|
||||
|
||||
private final static int defaultTextSizeChild = 14;
|
||||
private final static int defaultTextSizeGroup = 13;
|
||||
|
||||
private Set<String> feedIds = new HashSet<>();
|
||||
private ArrayList<String> folderNames = new ArrayList<>();
|
||||
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
|
||||
|
||||
private FolderViewFilter folderViewFilter;
|
||||
private ListOrderFilter listOrderFilter;
|
||||
private FeedOrderFilter feedOrderFilter;
|
||||
|
||||
private float textSize;
|
||||
|
||||
WidgetConfigAdapter(Context context) {
|
||||
folderViewFilter = PrefsUtils.getWidgetConfigFolderView(context);
|
||||
listOrderFilter = PrefsUtils.getWidgetConfigListOrder(context);
|
||||
feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(context);
|
||||
textSize = PrefsUtils.getListTextSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return folderNames.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildrenCount(int groupPosition) {
|
||||
return folderChildren.get(groupPosition).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup(int groupPosition) {
|
||||
return folderNames.get(groupPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Feed getChild(int groupPosition, int childPosition) {
|
||||
return folderChildren.get(groupPosition).get(childPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupId(int groupPosition) {
|
||||
return folderNames.get(groupPosition).hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChildId(int groupPosition, int childPosition) {
|
||||
return folderChildren.get(groupPosition).get(childPosition).hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
|
||||
String folderName = folderNames.get(groupPosition);
|
||||
if (folderName.equals(AppConstants.ROOT_FOLDER)) {
|
||||
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_root_folder, parent, false);
|
||||
} else {
|
||||
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_folder, parent, false);
|
||||
TextView textName = convertView.findViewById(R.id.text_folder_name);
|
||||
textName.setTextSize(textSize * defaultTextSizeGroup);
|
||||
textName.setText(folderName);
|
||||
}
|
||||
|
||||
((ExpandableListView) parent).expandGroup(groupPosition);
|
||||
|
||||
convertView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ArrayList<Feed> folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
|
||||
// check all is selected
|
||||
boolean allSelected = true;
|
||||
for (Feed feed : folderChild) {
|
||||
if (!feedIds.contains(feed.feedId)) {
|
||||
allSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (Feed feed : folderChild) {
|
||||
if (allSelected) {
|
||||
feedIds.remove(feed.feedId);
|
||||
} else {
|
||||
feedIds.add(feed.feedId);
|
||||
}
|
||||
}
|
||||
setWidgetFeedIds(parent.getContext());
|
||||
notifyDataChanged();
|
||||
}
|
||||
});
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
|
||||
}
|
||||
|
||||
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
|
||||
TextView textTitle = convertView.findViewById(R.id.text_title);
|
||||
TextView textDetails = convertView.findViewById(R.id.text_details);
|
||||
final CheckBox checkBox = convertView.findViewById(R.id.check_box);
|
||||
ImageView img = convertView.findViewById(R.id.img);
|
||||
textTitle.setTextSize(textSize * defaultTextSizeChild);
|
||||
textDetails.setTextSize(textSize * defaultTextSizeChild);
|
||||
textTitle.setText(feed.title);
|
||||
checkBox.setChecked(feedIds.contains(feed.feedId));
|
||||
|
||||
if (feedOrderFilter == FeedOrderFilter.NAME || feedOrderFilter == FeedOrderFilter.OPENS) {
|
||||
textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
|
||||
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
|
||||
textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
|
||||
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
|
||||
textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
|
||||
} else {
|
||||
// FeedOrderFilter.RECENT_STORY
|
||||
try {
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
Date dateTime = dateFormat.parse(feed.lastStoryDate);
|
||||
CharSequence relativeTimeString = DateUtils.getRelativeTimeSpanString(dateTime.getTime(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
|
||||
textDetails.setText(relativeTimeString);
|
||||
} catch (Exception e) {
|
||||
textDetails.setText(feed.lastStoryDate);
|
||||
}
|
||||
}
|
||||
|
||||
FeedUtils.iconLoader.displayImage(feed.faviconUrl, img, 0, false, img.getHeight(), true);
|
||||
|
||||
convertView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
checkBox.setChecked(!checkBox.isChecked());
|
||||
if (checkBox.isChecked()) {
|
||||
feedIds.add(feed.feedId);
|
||||
} else {
|
||||
feedIds.remove(feed.feedId);
|
||||
}
|
||||
setWidgetFeedIds(parent.getContext());
|
||||
}
|
||||
});
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildSelectable(int groupPosition, int childPosition) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return super.areAllItemsEnabled();
|
||||
}
|
||||
|
||||
void setData(ArrayList<String> activeFoldersNames, ArrayList<ArrayList<Feed>> activeFolderChildren, ArrayList<Feed> feeds) {
|
||||
if (folderViewFilter == FolderViewFilter.NESTED) {
|
||||
this.folderNames = activeFoldersNames;
|
||||
this.folderChildren = activeFolderChildren;
|
||||
} else {
|
||||
this.folderNames = new ArrayList<>(1);
|
||||
this.folderNames.add(AppConstants.ROOT_FOLDER);
|
||||
this.folderChildren = new ArrayList<>();
|
||||
this.folderChildren.add(feeds);
|
||||
}
|
||||
this.notifyDataChanged();
|
||||
}
|
||||
|
||||
void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
|
||||
this.feedOrderFilter = feedOrderFilter;
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
void replaceListOrder(ListOrderFilter listOrderFilter) {
|
||||
this.listOrderFilter = listOrderFilter;
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
void replaceFolderView(FolderViewFilter folderViewFilter) {
|
||||
this.folderViewFilter = folderViewFilter;
|
||||
}
|
||||
|
||||
private void notifyDataChanged() {
|
||||
for (ArrayList<Feed> feedList : this.folderChildren) {
|
||||
Collections.sort(feedList, getListComparator());
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setFeedIds(Set<String> feedIds) {
|
||||
this.feedIds.clear();
|
||||
this.feedIds.addAll(feedIds);
|
||||
}
|
||||
|
||||
void replaceFeedIds(Set<String> feedIds) {
|
||||
setFeedIds(feedIds);
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void setWidgetFeedIds(Context context) {
|
||||
PrefsUtils.setWidgetFeedIds(context, feedIds);
|
||||
}
|
||||
|
||||
private Comparator<Feed> getListComparator() {
|
||||
return new Comparator<Feed>() {
|
||||
@Override
|
||||
public int compare(Feed o1, Feed o2) {
|
||||
if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
return o1.title.compareTo(o2.title);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.DESCENDING) {
|
||||
return o2.title.compareTo(o1.title);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
return Integer.valueOf(o1.subscribers).compareTo(Integer.valueOf(o2.subscribers));
|
||||
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.DESCENDING) {
|
||||
return Integer.valueOf(o2.subscribers).compareTo(Integer.valueOf(o1.subscribers));
|
||||
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
return Integer.compare(o1.feedOpens, o2.feedOpens);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.DESCENDING) {
|
||||
return Integer.compare(o2.feedOpens, o1.feedOpens);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.DESCENDING) {
|
||||
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
return Integer.compare(o1.storiesPerMonth, o2.storiesPerMonth);
|
||||
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.DESCENDING) {
|
||||
return Integer.compare(o2.storiesPerMonth, o1.storiesPerMonth);
|
||||
}
|
||||
return o1.title.compareTo(o2.title);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private int compareLastStoryDateTimes(String firstDateTime, String secondDateTime, ListOrderFilter listOrderFilter) {
|
||||
try {
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
// found null last story date times on feeds
|
||||
if (TextUtils.isEmpty(firstDateTime)) {
|
||||
firstDateTime = "2000-01-01 00:00:00";
|
||||
}
|
||||
if (TextUtils.isEmpty(secondDateTime)) {
|
||||
secondDateTime = "2000-01-01 00:00:00";
|
||||
}
|
||||
|
||||
Date firstDate = dateFormat.parse(firstDateTime);
|
||||
Date secondDate = dateFormat.parse(secondDateTime);
|
||||
if (listOrderFilter == ListOrderFilter.ASCENDING) {
|
||||
return firstDate.compareTo(secondDate);
|
||||
} else {
|
||||
return secondDate.compareTo(firstDate);
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import android.database.Cursor;
|
|||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.CancellationSignal;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.TextUtils;
|
||||
|
|
@ -1106,8 +1107,7 @@ public class BlurDatabaseHelper {
|
|||
};
|
||||
}
|
||||
|
||||
public Loader<Cursor> getStoriesLoader(final FeedSet fs) {
|
||||
// final StoryOrder order = PrefsUtils.getStoryOrder(context, fs);
|
||||
public Loader<Cursor> getStoriesLoader(@Nullable final FeedSet fs) {
|
||||
return new QueryCursorLoader(context) {
|
||||
@Override
|
||||
protected Cursor createCursor() {
|
||||
|
|
@ -1116,15 +1116,21 @@ public class BlurDatabaseHelper {
|
|||
};
|
||||
}
|
||||
|
||||
private Cursor getStoriesCursor(FeedSet fs, CancellationSignal cancellationSignal) {
|
||||
private Cursor getStoriesCursor(@Nullable FeedSet fs, CancellationSignal cancellationSignal) {
|
||||
StringBuilder q = new StringBuilder(DatabaseConstants.STORY_QUERY_BASE_0);
|
||||
|
||||
q.append(DatabaseConstants.STORY_FEED_ID);
|
||||
q.append(" = ");
|
||||
q.append(fs.getSingleFeed());
|
||||
if (fs != null && !TextUtils.isEmpty(fs.getSingleFeed())) {
|
||||
q.append(DatabaseConstants.STORY_FEED_ID);
|
||||
q.append(" = ");
|
||||
q.append(fs.getSingleFeed());
|
||||
} else {
|
||||
q.append(DatabaseConstants.FEED_ACTIVE);
|
||||
q.append(" = 1");
|
||||
}
|
||||
|
||||
q.append(" ORDER BY ");
|
||||
q.append(DatabaseConstants.STORY_TIMESTAMP);
|
||||
q.append(" DESC LIMIT 10");
|
||||
q.append(" DESC LIMIT 20");
|
||||
return rawQuery(q.toString(), null, cancellationSignal);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ public class DatabaseConstants {
|
|||
public static final String FEED_LINK = "link";
|
||||
public static final String FEED_ADDRESS = "address";
|
||||
public static final String FEED_SUBSCRIBERS = "subscribers";
|
||||
public static final String FEED_OPENS = "opens";
|
||||
public static final String FEED_LAST_STORY_DATE = "last_story_date";
|
||||
public static final String FEED_AVERAGE_STORIES_PER_MONTH = "average_stories_per_month";
|
||||
public static final String FEED_UPDATED_SECONDS = "updated_seconds";
|
||||
public static final String FEED_FAVICON_FADE = "favicon_fade";
|
||||
public static final String FEED_FAVICON_COLOR = "favicon_color";
|
||||
|
|
@ -183,6 +186,9 @@ public class DatabaseConstants {
|
|||
FEED_LINK + TEXT + ", " +
|
||||
FEED_SUBSCRIBERS + TEXT + ", " +
|
||||
FEED_TITLE + TEXT + ", " +
|
||||
FEED_OPENS + INTEGER + ", " +
|
||||
FEED_AVERAGE_STORIES_PER_MONTH + INTEGER + ", " +
|
||||
FEED_LAST_STORY_DATE + TEXT + ", " +
|
||||
FEED_UPDATED_SECONDS + INTEGER + ", " +
|
||||
FEED_NOTIFICATION_TYPES + TEXT + ", " +
|
||||
FEED_NOTIFICATION_FILTER + TEXT + ", " +
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
return;
|
||||
}
|
||||
if (gestureL2R || gestureR2L) return;
|
||||
UIUtils.startReadingActivity(fs, story.storyHash, context);
|
||||
UIUtils.startReadingActivity(fs, story.storyHash, false, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
@SerializedName("num_subscribers")
|
||||
public String subscribers;
|
||||
|
||||
@SerializedName("feed_opens")
|
||||
public int feedOpens;
|
||||
|
||||
@SerializedName("last_story_date")
|
||||
public String lastStoryDate;
|
||||
|
||||
@SerializedName("average_stories_per_month")
|
||||
public int storiesPerMonth;
|
||||
|
||||
@SerializedName("feed_title")
|
||||
public String title;
|
||||
|
||||
|
|
@ -85,6 +94,9 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
values.put(DatabaseConstants.FEED_FAVICON_URL, faviconUrl);
|
||||
values.put(DatabaseConstants.FEED_LINK, feedLink);
|
||||
values.put(DatabaseConstants.FEED_SUBSCRIBERS, subscribers);
|
||||
values.put(DatabaseConstants.FEED_OPENS, feedOpens);
|
||||
values.put(DatabaseConstants.FEED_LAST_STORY_DATE, lastStoryDate);
|
||||
values.put(DatabaseConstants.FEED_AVERAGE_STORIES_PER_MONTH, storiesPerMonth);
|
||||
values.put(DatabaseConstants.FEED_TITLE, title);
|
||||
values.put(DatabaseConstants.FEED_UPDATED_SECONDS, lastUpdated);
|
||||
values.put(DatabaseConstants.FEED_NOTIFICATION_TYPES, DatabaseConstants.flattenStringList(notificationTypes));
|
||||
|
|
@ -113,6 +125,9 @@ public class Feed implements Comparable<Feed>, Serializable {
|
|||
feed.neutralCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_NEUTRAL_COUNT));
|
||||
feed.positiveCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_POSITIVE_COUNT));
|
||||
feed.subscribers = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_SUBSCRIBERS));
|
||||
feed.feedOpens = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_OPENS));
|
||||
feed.storiesPerMonth = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_AVERAGE_STORIES_PER_MONTH));
|
||||
feed.lastStoryDate = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_LAST_STORY_DATE));
|
||||
feed.title = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
|
||||
feed.lastUpdated = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_UPDATED_SECONDS));
|
||||
feed.notificationTypes = DatabaseConstants.unflattenStringList(cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_NOTIFICATION_TYPES)));
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ public class Folder {
|
|||
* folder on top, and also the expectation that *despite locale*, folders
|
||||
* starting with an underscore should show up on top.
|
||||
*/
|
||||
private static int compareFolderNames(String s1, String s2) {
|
||||
public static int compareFolderNames(String s1, String s2) {
|
||||
if (TextUtils.equals(s1, s2)) return 0;
|
||||
if (s1.equals(AppConstants.ROOT_FOLDER)) return -1;
|
||||
if (s2.equals(AppConstants.ROOT_FOLDER)) return 1;
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ public abstract class ProfileActivityDetailsFragment extends Fragment implements
|
|||
//context.startActivity(intent);
|
||||
}
|
||||
} else if (activity.category == Category.STAR) {
|
||||
UIUtils.startReadingActivity(FeedSet.allSaved(), activity.storyHash, context);
|
||||
UIUtils.startReadingActivity(FeedSet.allSaved(), activity.storyHash, false, context);
|
||||
} else if (isSocialFeedCategory(activity)) {
|
||||
// Strip the social: prefix from feedId
|
||||
String socialFeedId = activity.feedId.substring(7);
|
||||
|
|
@ -161,7 +161,7 @@ public abstract class ProfileActivityDetailsFragment extends Fragment implements
|
|||
if (feed == null) {
|
||||
Toast.makeText(context, R.string.profile_do_not_follow, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
UIUtils.startReadingActivity(FeedSet.singleSocialFeed(feed.userId, feed.username), activity.storyHash, context);
|
||||
UIUtils.startReadingActivity(FeedSet.singleSocialFeed(feed.userId, feed.username), activity.storyHash, false, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuI
|
|||
registerForContextMenu(web);
|
||||
web.setCustomViewLayout(webviewCustomViewLayout);
|
||||
web.setWebviewWrapperLayout(fragmentScrollview);
|
||||
web.setBackgroundColor(Color.TRANSPARENT);
|
||||
web.fragment = this;
|
||||
web.activity = activity;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import com.newsblur.util.PrefConstants;
|
|||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
|
@ -296,7 +297,13 @@ public class APIManager {
|
|||
ValueMultimap values = new ValueMultimap();
|
||||
|
||||
// create the URI and populate request params depending on what kind of stories we want
|
||||
if (fs.getSingleFeed() != null) {
|
||||
if (fs.isForWidget()) {
|
||||
uri = Uri.parse(buildUrl(APIConstants.PATH_RIVER_STORIES));
|
||||
for (String feedId : fs.getAllFeeds()) values.put(APIConstants.PARAMETER_FEEDS, feedId);
|
||||
values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_FALSE);
|
||||
values.put(APIConstants.PARAMETER_INFREQUENT, APIConstants.VALUE_FALSE);
|
||||
values.put(APIConstants.PARAMETER_LIMIT, String.valueOf(WidgetUtils.STORIES_LIMIT));
|
||||
} else if (fs.getSingleFeed() != null) {
|
||||
uri = Uri.parse(buildUrl(APIConstants.PATH_FEED_STORIES)).buildUpon().appendPath(fs.getSingleFeed()).build();
|
||||
values.put(APIConstants.PARAMETER_FEEDS, fs.getSingleFeed());
|
||||
values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_TRUE);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
/**
|
||||
* First receiver in the chain that starts with the device. Simply schedules another broadcast
|
||||
* that will periodicaly start the sync service.
|
||||
* that will periodically start the sync service.
|
||||
*/
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ public class BootReceiver extends BroadcastReceiver {
|
|||
public void onReceive(Context context, Intent intent) {
|
||||
com.newsblur.util.Log.d(this, "triggering sync service from device boot");
|
||||
scheduleSyncService(context);
|
||||
resetWidgetSync(context);
|
||||
}
|
||||
|
||||
public static void scheduleSyncService(Context context) {
|
||||
|
|
@ -32,5 +34,9 @@ public class BootReceiver extends BroadcastReceiver {
|
|||
int result = sched.schedule(builder.build());
|
||||
com.newsblur.util.Log.d("BootReceiver", String.format("Scheduling result: %s - %s", result, result == 0 ? "Failure" : "Success"));
|
||||
}
|
||||
|
||||
|
||||
private static void resetWidgetSync(Context context) {
|
||||
com.newsblur.util.Log.d(BootReceiver.class.getName(), "Received " + Intent.ACTION_BOOT_COMPLETED + " - reset widget sync");
|
||||
WidgetUtils.resetWidgetUpdate(context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ package com.newsblur.service;
|
|||
import android.app.Service;
|
||||
import android.app.job.JobParameters;
|
||||
import android.app.job.JobService;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
|
@ -39,7 +37,7 @@ import com.newsblur.util.ReadingAction;
|
|||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.widget.NewsBlurWidgetProvider;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
|
@ -47,7 +45,6 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
|
@ -284,14 +281,11 @@ public class NBSyncService extends JobService {
|
|||
// on all devices
|
||||
housekeeping();
|
||||
|
||||
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
|
||||
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this, NewsBlurWidgetProvider.class));
|
||||
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
|
||||
|
||||
// check to see if we are on an allowable network only after ensuring we have CPU
|
||||
if (!( (NbActivity.getActiveActivityCount() > 0) ||
|
||||
PrefsUtils.isEnableNotifications(this) ||
|
||||
PrefsUtils.isBackgroundNetworkAllowed(this) )) {
|
||||
PrefsUtils.isBackgroundNetworkAllowed(this) ||
|
||||
WidgetUtils.hasActiveAppWidgets(this)) ) {
|
||||
Log.d(this.getClass().getName(), "Abandoning sync: app not active and network type not appropriate for background sync.");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
public class TimeChangeReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, @Nullable Intent intent) {
|
||||
if (intent != null && intent.getAction() != null
|
||||
&& intent.getAction().equals(Intent.ACTION_TIME_CHANGED)) {
|
||||
com.newsblur.util.Log.d(TimeChangeReceiver.class.getName(), "Received " + Intent.ACTION_TIME_CHANGED + " - reset widget sync");
|
||||
WidgetUtils.resetWidgetUpdate(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.newsblur.util;
|
||||
|
||||
public enum FeedOrderFilter {
|
||||
NAME,
|
||||
SUBSCRIBERS,
|
||||
STORIES_MONTH,
|
||||
RECENT_STORY,
|
||||
OPENS
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.newsblur.util;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -29,6 +30,7 @@ public class FeedSet implements Serializable {
|
|||
private boolean isAllRead;
|
||||
private boolean isGlobalShared;
|
||||
private boolean isInfrequent;
|
||||
private boolean isForWidget;
|
||||
|
||||
private String folderName;
|
||||
private String searchQuery;
|
||||
|
|
@ -73,6 +75,17 @@ public class FeedSet implements Serializable {
|
|||
return fs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for multiple feeds with IDs
|
||||
*/
|
||||
public static FeedSet multipleFeeds(Set<String> feedIds) {
|
||||
FeedSet fs = new FeedSet();
|
||||
fs.feeds = new HashSet<>(feedIds.size());
|
||||
fs.feeds.addAll(feedIds);
|
||||
fs.feeds = Collections.unmodifiableSet(fs.feeds);
|
||||
return fs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for all (non-social) feeds.
|
||||
*/
|
||||
|
|
@ -138,6 +151,22 @@ public class FeedSet implements Serializable {
|
|||
return fs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for widget feed.
|
||||
*/
|
||||
public static FeedSet widgetFeeds(@Nullable Set<String> feedIds){
|
||||
FeedSet fs = new FeedSet();
|
||||
fs.isForWidget = true;
|
||||
if (feedIds != null) {
|
||||
fs.feeds = new HashSet<>(feedIds.size());
|
||||
fs.feeds.addAll(feedIds);
|
||||
fs.feeds = Collections.unmodifiableSet(fs.feeds);
|
||||
} else {
|
||||
fs.feeds = Collections.EMPTY_SET;
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for a folder.
|
||||
*/
|
||||
|
|
@ -163,6 +192,13 @@ public class FeedSet implements Serializable {
|
|||
if (feeds != null && ((feeds.size() > 1) || (folderName != null))) return feeds; else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of all feed IDs if there are any or null otherwise.
|
||||
*/
|
||||
public Set<String> getAllFeeds() {
|
||||
if (feeds != null) return feeds; else return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single social feed ID and username iff there is only one or null otherwise.
|
||||
*/
|
||||
|
|
@ -201,6 +237,10 @@ public class FeedSet implements Serializable {
|
|||
return ((savedTags != null) && (savedTags.size() == 1));
|
||||
}
|
||||
|
||||
public boolean isForWidget() {
|
||||
return this.isForWidget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single saved tag iff there is only one or null otherwise.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.newsblur.util;
|
||||
|
||||
public enum FolderViewFilter {
|
||||
FLAT,
|
||||
NESTED
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import android.graphics.BitmapFactory;
|
|||
import android.os.Process;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.network.APIConstants;
|
||||
|
|
@ -72,15 +73,48 @@ public class ImageLoader {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous background call coming from app widget on home screen
|
||||
*/
|
||||
public void displayWidgetImage(String url, int imageViewId, int maxDimPX, RemoteViews remoteViews) {
|
||||
if (url == null) {
|
||||
remoteViews.setViewVisibility(imageViewId, View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
url = buildUrlIfNeeded(url);
|
||||
|
||||
// try from memory
|
||||
Bitmap bitmap = memoryCache.get(url);
|
||||
if (bitmap != null) {
|
||||
remoteViews.setImageViewBitmap(imageViewId, bitmap);
|
||||
remoteViews.setViewVisibility(imageViewId, View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// try from disk
|
||||
bitmap = getImageFromDisk(url, maxDimPX, false, 0f);
|
||||
if (bitmap == null) {
|
||||
// try for network
|
||||
bitmap = getImageFromNetwork(url, maxDimPX,false, 0f);
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
memoryCache.put(url, bitmap);
|
||||
remoteViews.setImageViewBitmap(imageViewId, bitmap);
|
||||
remoteViews.setViewVisibility(imageViewId, View.VISIBLE);
|
||||
} else {
|
||||
remoteViews.setViewVisibility(imageViewId, View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public PhotoToLoad displayImage(String url, ImageView imageView, float roundRadius, boolean cropSquare, int maxDimPX, boolean allowDelay) {
|
||||
if (url == null) {
|
||||
imageView.setImageResource(emptyRID);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.startsWith("/")) {
|
||||
url = APIConstants.buildUrl(url);
|
||||
}
|
||||
url = buildUrlIfNeeded(url);
|
||||
|
||||
imageViewMappings.put(imageView, url);
|
||||
PhotoToLoad photoToLoad = new PhotoToLoad(url, imageView, roundRadius, cropSquare, maxDimPX, allowDelay);
|
||||
|
|
@ -150,16 +184,11 @@ public class ImageLoader {
|
|||
}
|
||||
|
||||
// try from disk
|
||||
File f = fileCache.getCachedFile(photoToLoad.url);
|
||||
// the only reliable way to check a cached file is to try decoding it. the util method will
|
||||
// return null if it fails
|
||||
bitmap = UIUtils.decodeImage(f, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
|
||||
// try for network
|
||||
bitmap = getImageFromDisk(photoToLoad.url, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
|
||||
if (bitmap == null) {
|
||||
// try for network
|
||||
if (photoToLoad.cancel) return;
|
||||
fileCache.cacheFile(photoToLoad.url);
|
||||
f = fileCache.getCachedFile(photoToLoad.url);
|
||||
bitmap = UIUtils.decodeImage(f, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
|
||||
bitmap = getImageFromNetwork(photoToLoad.url, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
|
|
@ -227,4 +256,23 @@ public class ImageLoader {
|
|||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
private String buildUrlIfNeeded(String url) {
|
||||
if (url.startsWith("/")) {
|
||||
url = APIConstants.buildUrl(url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private Bitmap getImageFromDisk(String url, int maxDimPX, boolean cropSquare, float roundRadius) {
|
||||
// the only reliable way to check a cached file is to try decoding it. the util method will
|
||||
// return null if it fails
|
||||
File f = fileCache.getCachedFile(url);
|
||||
return UIUtils.decodeImage(f, maxDimPX, cropSquare, roundRadius);
|
||||
}
|
||||
|
||||
private Bitmap getImageFromNetwork(String url, int maxDimPX, boolean cropSquare, float roundRadius) {
|
||||
fileCache.cacheFile(url);
|
||||
File f = fileCache.getCachedFile(url);
|
||||
return UIUtils.decodeImage(f, maxDimPX, cropSquare, roundRadius);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.newsblur.util;
|
||||
|
||||
public enum ListOrderFilter {
|
||||
ASCENDING,
|
||||
DESCENDING
|
||||
}
|
||||
|
|
@ -106,6 +106,8 @@ public class PrefConstants {
|
|||
public static final String ENABLE_NOTIFICATIONS = "enable_notifications";
|
||||
|
||||
public static final String READING_FONT = "reading_font";
|
||||
public static final String WIDGET_FEED_ID = "WIDGET_FEED_ID";
|
||||
public static final String WIDGET_FEED_NAME = "WIDGET_FEED_NAME";
|
||||
public static final String WIDGET_FEED_SET = "widget_feed_set";
|
||||
public static final String WIDGET_CONFIG_LIST_ORDER = "widget_config_list_order";
|
||||
public static final String WIDGET_CONFIG_FEED_ORDER = "widget_config_feed_order";
|
||||
public static final String WIDGET_CONFIG_FOLDER_VIEW = "widget_config_folder_view";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import android.graphics.BitmapFactory;
|
|||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.FileProvider;
|
||||
import android.util.Log;
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ import com.newsblur.domain.UserDetails;
|
|||
import com.newsblur.network.APIConstants;
|
||||
import com.newsblur.util.PrefConstants.ThemeValue;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
public class PrefsUtils {
|
||||
|
||||
|
|
@ -161,6 +163,9 @@ public class PrefsUtils {
|
|||
// wipe the local DB
|
||||
FeedUtils.dropAndRecreateTables();
|
||||
|
||||
// disable widget
|
||||
WidgetUtils.disableWidgetUpdate(context);
|
||||
|
||||
// reset custom server
|
||||
APIConstants.unsetCustomServer();
|
||||
|
||||
|
|
@ -840,7 +845,7 @@ public class PrefsUtils {
|
|||
}
|
||||
|
||||
public static boolean isBackgroundNeeded(Context context) {
|
||||
return (isEnableNotifications(context) || isOfflineEnabled(context));
|
||||
return (isEnableNotifications(context) || isOfflineEnabled(context) || WidgetUtils.hasActiveAppWidgets(context));
|
||||
}
|
||||
|
||||
public static Font getFont(Context context) {
|
||||
|
|
@ -859,42 +864,70 @@ public class PrefsUtils {
|
|||
editor.commit();
|
||||
}
|
||||
|
||||
|
||||
public static void setWidgetFeed(Context context, int widgetId, String feedId, String name) {
|
||||
public static void setWidgetFeedIds(Context context, Set<String> feedIds) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
Editor editor = prefs.edit();
|
||||
editor.putString(PrefConstants.WIDGET_FEED_ID + widgetId, feedId)
|
||||
.putString(PrefConstants.WIDGET_FEED_NAME + widgetId, name);
|
||||
editor.putStringSet(PrefConstants.WIDGET_FEED_SET, feedIds);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets only the name, no id when it is a folder
|
||||
*/
|
||||
public static void setWidgetFolderName(Context context, int widgetId, String folderName) {
|
||||
@Nullable
|
||||
public static Set<String> getWidgetFeedIds(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return preferences.getStringSet(PrefConstants.WIDGET_FEED_SET, null);
|
||||
}
|
||||
|
||||
public static void removeWidgetData(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
Editor editor = prefs.edit();
|
||||
editor.remove(PrefConstants.WIDGET_FEED_ID + widgetId)
|
||||
.putString(PrefConstants.WIDGET_FEED_NAME + widgetId, folderName);
|
||||
editor.commit();
|
||||
|
||||
}
|
||||
public static String getWidgetFeed(Context context, int widgetId) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
String feedId = preferences.getString(PrefConstants.WIDGET_FEED_ID + widgetId, null);
|
||||
return feedId;
|
||||
}
|
||||
public static String getWidgetFeedName(Context context, int widgetId) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return preferences.getString(PrefConstants.WIDGET_FEED_NAME + widgetId, "-");
|
||||
}
|
||||
public static void removeWidgetFeed(Context context, int widgetId) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
if(prefs.contains(PrefConstants.WIDGET_FEED_ID + widgetId)){
|
||||
Editor editor = prefs.edit();
|
||||
editor.remove(PrefConstants.WIDGET_FEED_ID + widgetId);
|
||||
editor.remove(PrefConstants.WIDGET_FEED_NAME + widgetId);
|
||||
editor.apply();
|
||||
if (prefs.contains(PrefConstants.WIDGET_FEED_SET)) {
|
||||
editor.remove(PrefConstants.WIDGET_FEED_SET);
|
||||
}
|
||||
if (prefs.contains(PrefConstants.WIDGET_CONFIG_FEED_ORDER)) {
|
||||
editor.remove(PrefConstants.WIDGET_CONFIG_FEED_ORDER);
|
||||
}
|
||||
if (prefs.contains(PrefConstants.WIDGET_CONFIG_LIST_ORDER)) {
|
||||
editor.remove(PrefConstants.WIDGET_CONFIG_LIST_ORDER);
|
||||
}
|
||||
if (prefs.contains(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW)) {
|
||||
editor.remove(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW);
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static FeedOrderFilter getWidgetConfigFeedOrder(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return FeedOrderFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_FEED_ORDER, FeedOrderFilter.NAME.toString()));
|
||||
}
|
||||
|
||||
public static void setWidgetConfigFeedOrder(Context context, FeedOrderFilter feedOrderFilter) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
Editor editor = prefs.edit();
|
||||
editor.putString(PrefConstants.WIDGET_CONFIG_FEED_ORDER, feedOrderFilter.toString());
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static ListOrderFilter getWidgetConfigListOrder(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return ListOrderFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_LIST_ORDER, ListOrderFilter.ASCENDING.name()));
|
||||
}
|
||||
|
||||
public static void setWidgetConfigListOrder(Context context, ListOrderFilter listOrderFilter) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
Editor editor = prefs.edit();
|
||||
editor.putString(PrefConstants.WIDGET_CONFIG_LIST_ORDER, listOrderFilter.toString());
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static FolderViewFilter getWidgetConfigFolderView(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return FolderViewFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW, FolderViewFilter.NESTED.name()));
|
||||
}
|
||||
|
||||
public static void setWidgetConfigFolderView(Context context, FolderViewFilter folderViewFilter) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
Editor editor = prefs.edit();
|
||||
editor.putString(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW, folderViewFilter.toString());
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ public class UIUtils {
|
|||
});
|
||||
}
|
||||
|
||||
public static void startReadingActivity(FeedSet fs, String startingHash, Context context) {
|
||||
public static void startReadingActivity(FeedSet fs, String startingHash, boolean waitRefresh, Context context) {
|
||||
Class activityClass;
|
||||
if (fs.isAllSaved()) {
|
||||
activityClass = SavedStoriesReading.class;
|
||||
|
|
@ -299,6 +299,7 @@ public class UIUtils {
|
|||
Intent i = new Intent(context, activityClass);
|
||||
i.putExtra(Reading.EXTRA_FEEDSET, fs);
|
||||
i.putExtra(Reading.EXTRA_STORY_HASH, startingHash);
|
||||
i.putExtra(Reading.EXTRA_WAIT_REFRESH, waitRefresh);
|
||||
context.startActivity(i);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.RemoteViewsService;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.Log;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.StoryUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class BlurWidgetRemoteViewsService extends RemoteViewsService {
|
||||
private static String TAG = "BlurWidgetRemoteViewsFactory";
|
||||
@Override
|
||||
public RemoteViewsFactory onGetViewFactory(Intent intent) {
|
||||
Log.d(TAG, "onGetViewFactory");
|
||||
return new BlurWidgetRemoteViewsFactory(this.getApplicationContext(), intent);
|
||||
}
|
||||
}
|
||||
|
||||
class BlurWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
|
||||
private Context context;
|
||||
private static String TAG = "BlurWidgetRemoteViewsFactory";
|
||||
private List<Story> storyItems = new ArrayList<Story>();
|
||||
private FeedSet fs;
|
||||
private APIManager apiManager;
|
||||
public BlurWidgetRemoteViewsFactory(Context context, Intent intent) {
|
||||
Log.d(TAG, "Constructor");
|
||||
this.context = context;
|
||||
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
final String feedId = PrefsUtils.getWidgetFeed(context, appWidgetId);
|
||||
final String feedName = PrefsUtils.getWidgetFeedName(context, appWidgetId);
|
||||
|
||||
apiManager = new APIManager(context);
|
||||
Log.d(TAG, "Feed ID: " + feedId);
|
||||
|
||||
if(feedId != null){
|
||||
// this is a single feed
|
||||
fs = FeedSet.singleFeed(feedId);
|
||||
}else{
|
||||
// this is a folder
|
||||
fs = FeedUtils.feedSetFromFolderName(feedName);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* The system calls onCreate() when creating your factory for the first time.
|
||||
* This is where you set up any connections and/or cursors to your data source.
|
||||
*
|
||||
* Heavy lifting,
|
||||
* for example downloading or creating content etc, should be deferred to onDataSetChanged()
|
||||
* or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.d(TAG, "onCreate");
|
||||
}
|
||||
|
||||
private void fetchStories() {
|
||||
Log.d(TAG, String.format("Fetching stories %s", fs.hashCode()));
|
||||
StoriesResponse response =
|
||||
apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL);
|
||||
|
||||
if (response == null) {
|
||||
Log.e(TAG, "Response is null");
|
||||
return;
|
||||
} else if (response.stories == null) {
|
||||
Log.e(TAG, "Stories are empty");
|
||||
return;
|
||||
} else if (response.isError()) {
|
||||
String err = String.format("response error for feed %s", fs.hashCode());
|
||||
Log.e(TAG, response.getErrorMessage(err));
|
||||
return;
|
||||
}
|
||||
storyItems.clear();
|
||||
storyItems.addAll(Arrays.asList(response.stories));
|
||||
}
|
||||
/**
|
||||
* allowed to run synchronous calls
|
||||
*/
|
||||
@Override
|
||||
public RemoteViews getViewAt(int position) {
|
||||
Log.d(TAG, "getViewAt " + position);
|
||||
Story story = storyItems.get(position);
|
||||
// Construct a remote views item based on the app widget item XML file,
|
||||
// and set the text based on the position.
|
||||
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.newsblur_widget_item);
|
||||
rv.setTextViewText(R.id.widget_item_title, story.title);
|
||||
|
||||
CharSequence time = StoryUtils.formatRelativeTime(context, story.timestamp);
|
||||
|
||||
rv.setTextViewText(R.id.widget_item_time, time);
|
||||
|
||||
|
||||
|
||||
// Next, set a fill-intent, which will be used to fill in the pending intent template
|
||||
// that is set on the collection view in StackWidgetProvider.
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(NewsBlurWidgetProvider.EXTRA_ITEM_ID, story.storyHash);
|
||||
extras.putString(NewsBlurWidgetProvider.EXTRA_FEED_ID, story.feedId);
|
||||
Intent fillInIntent = new Intent();
|
||||
// fillInIntent.setAction(NewsBlurWidgetProvider.ACTION_OPEN_STORY);
|
||||
fillInIntent.putExtras(extras);
|
||||
// Make it possible to distinguish the individual on-click
|
||||
// action of a given item
|
||||
rv.setOnClickFillInIntent(R.id.newsblur_widget_item, fillInIntent);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* This allows for the use of a custom loading view which appears between the time that
|
||||
* {@link #getViewAt(int)} is called and returns. If null is returned, a default loading
|
||||
* view will be used.
|
||||
*
|
||||
* @return The RemoteViews representing the desired loading view.
|
||||
*/
|
||||
@Override
|
||||
public RemoteViews getLoadingView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The number of types of Views that will be returned by this factory.
|
||||
*/
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param position The position of the item within the data set whose row id we want.
|
||||
* @return The id of the item at the specified position.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return storyItems.get(position).hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if the same id always refers to the same object.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
// fetch any new data
|
||||
fetchStories();
|
||||
Log.d(TAG, "onDataSetChanged");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the last RemoteViewsAdapter that is associated with this factory is
|
||||
* unbound.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.d(TAG, "onDestroy");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Count of items.
|
||||
*/
|
||||
@Override
|
||||
public int getCount() {
|
||||
Log.d(TAG, "getCount: " + Math.min(storyItems.size(), 10));
|
||||
return Math.min(storyItems.size(), 10);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.NbActivity;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.domain.Folder;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.Log;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class ConfigureWidgetActivity extends NbActivity {
|
||||
|
||||
private int appWidgetId;
|
||||
private List<Feed> feeds = new ArrayList<>();
|
||||
private List<Folder> folders = new ArrayList<>();
|
||||
private static String TAG = "ConfigureWidgetActivity";
|
||||
private Feed selectedFeed = null;
|
||||
private Folder selectedFolder = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.activity_configure_widget);
|
||||
PrefsUtils.applyThemePreference(this);
|
||||
Intent intent = getIntent();
|
||||
Bundle extras = intent.getExtras();
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
}
|
||||
|
||||
PrefsUtils.removeWidgetFeed(this, appWidgetId);
|
||||
|
||||
folders = null;
|
||||
feeds = null;
|
||||
getAllFeeds();
|
||||
getAllFolders();
|
||||
// set result as cancelled in the case that we don't finish config
|
||||
Intent resultValue = new Intent();
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
setResult(RESULT_CANCELED, resultValue);
|
||||
}
|
||||
|
||||
private void getAllFeeds() {
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
|
||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
||||
@Override
|
||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
|
||||
processFeeds(data);
|
||||
}
|
||||
});
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
private void getAllFolders() {
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
|
||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
||||
@Override
|
||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
|
||||
processFolders(data);
|
||||
}
|
||||
});
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
private void processFolders(Cursor cursor) {
|
||||
List<Folder> folders = new ArrayList<>();
|
||||
while (cursor.moveToNext()) {
|
||||
Folder f = Folder.fromCursor(cursor);
|
||||
folders.add(f);
|
||||
}
|
||||
Collections.sort(folders, new Comparator<Folder>() {
|
||||
@Override
|
||||
public int compare(Folder o1, Folder o2) {
|
||||
return o1.name.compareTo(o2.name);
|
||||
}
|
||||
});
|
||||
this.folders = new ArrayList<>();
|
||||
this.folders.addAll(folders);
|
||||
requestFeedFromUser();
|
||||
}
|
||||
|
||||
private void processFeeds(Cursor cursor) {
|
||||
List<Feed> feeds = new ArrayList<>();
|
||||
while (cursor.moveToNext()) {
|
||||
Feed f = Feed.fromCursor(cursor);
|
||||
if (f.active) {
|
||||
feeds.add(f);
|
||||
}
|
||||
}
|
||||
this.feeds = new ArrayList<>();
|
||||
this.feeds.addAll(feeds);
|
||||
requestFeedFromUser();
|
||||
}
|
||||
|
||||
private void requestFeedFromUser() {
|
||||
if (feeds == null || folders == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<String> feedTitles = new ArrayList<>();
|
||||
for (Folder folder : folders) {
|
||||
feedTitles.add(String.format("Folder: %s", folder.name));
|
||||
}
|
||||
for (Feed feed : feeds) {
|
||||
feedTitles.add(String.format("Feed: %s", feed.title));
|
||||
}
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setTitle("Select a feed")
|
||||
.setItems(feedTitles.toArray(new String[feedTitles.size()]), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Log.d(TAG, "Selected " + which);
|
||||
if (which < folders.size()) {
|
||||
selectedFolder = folders.get(which);
|
||||
} else {
|
||||
selectedFeed = feeds.get(which);
|
||||
}
|
||||
saveWidget();
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void saveWidget() {
|
||||
if (selectedFeed == null && selectedFolder == null) {
|
||||
toastError("Please select a feed");
|
||||
return;
|
||||
}
|
||||
//update widget
|
||||
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
|
||||
RemoteViews rv = new RemoteViews(getPackageName(),
|
||||
R.layout.newsblur_widget);
|
||||
|
||||
Intent intent = new Intent(this, BlurWidgetRemoteViewsService.class);
|
||||
// Add the app widget ID to the intent extras.
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
|
||||
|
||||
String title;
|
||||
if (selectedFeed != null) {
|
||||
PrefsUtils.setWidgetFeed(this, appWidgetId, selectedFeed.feedId, selectedFeed.title);
|
||||
title = selectedFeed.title;
|
||||
} else {
|
||||
PrefsUtils.setWidgetFolderName(this, appWidgetId, selectedFolder.name);
|
||||
title = selectedFolder.name;
|
||||
}
|
||||
|
||||
rv.setTextViewText(R.id.txt_feed_name, title);
|
||||
rv.setRemoteAdapter(R.id.widget_list, intent);
|
||||
rv.setEmptyView(R.id.widget_list, R.id.empty_view);
|
||||
|
||||
|
||||
Intent touchIntent = new Intent(this, NewsBlurWidgetProvider.class);
|
||||
// Set the action for the intent.
|
||||
// When the user touches a particular view, it will have the effect of
|
||||
// broadcasting TOAST_ACTION.
|
||||
touchIntent.setAction(NewsBlurWidgetProvider.ACTION_OPEN_STORY);
|
||||
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
|
||||
PendingIntent touchIntentTemplate = PendingIntent.getBroadcast(this, 0, touchIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
rv.setPendingIntentTemplate(R.id.widget_list, touchIntentTemplate);
|
||||
|
||||
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, rv);
|
||||
|
||||
Intent resultValue = new Intent();
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
setResult(RESULT_OK, resultValue);
|
||||
finish();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -6,67 +6,57 @@ import android.appwidget.AppWidgetProvider;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.FeedReading;
|
||||
import com.newsblur.activity.Reading;
|
||||
import com.newsblur.database.BlurDatabaseHelper;
|
||||
import com.newsblur.activity.AllStoriesItemsList;
|
||||
import com.newsblur.activity.ItemsList;
|
||||
import com.newsblur.activity.WidgetConfig;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.Log;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
public class NewsBlurWidgetProvider extends AppWidgetProvider {
|
||||
public static String ACTION_OPEN_STORY = "ACTION_OPEN_STORY";
|
||||
public static String EXTRA_ITEM_ID = "EXTRA_ITEM_ID";
|
||||
public static String EXTRA_FEED_ID = "EXTRA_FEED_ID";
|
||||
public class WidgetProvider extends AppWidgetProvider {
|
||||
|
||||
private static String TAG = "WidgetProvider";
|
||||
|
||||
private static String TAG = "NewsBlurWidgetProvider";
|
||||
// Called when the BroadcastReceiver receives an Intent broadcast.
|
||||
// Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
|
||||
// displays a Toast message for the current item.
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "onReceive");
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
|
||||
if (intent.getAction().equals(ACTION_OPEN_STORY)) {
|
||||
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
String storyHash = intent.getStringExtra(EXTRA_ITEM_ID);
|
||||
String feedId = intent.getStringExtra(EXTRA_FEED_ID);
|
||||
FeedSet fs = FeedSet.singleFeed(feedId);
|
||||
Intent i = new Intent(context, FeedReading.class);
|
||||
i.putExtra(Reading.EXTRA_FEEDSET, fs);
|
||||
i.putExtra(Reading.EXTRA_STORY_HASH, storyHash);
|
||||
if (intent.getAction().equals(WidgetUtils.ACTION_OPEN_STORY)) {
|
||||
String storyHash = intent.getStringExtra(WidgetUtils.EXTRA_ITEM_ID);
|
||||
Intent i = new Intent(context, AllStoriesItemsList.class);
|
||||
i.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds());
|
||||
i.putExtra(ItemsList.EXTRA_STORY_HASH, storyHash);
|
||||
i.putExtra(ItemsList.EXTRA_WIDGET_STORY, true);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.getApplicationContext().startActivity(i);
|
||||
} else if (intent.getAction().equals(WidgetUtils.ACTION_OPEN_CONFIG)) {
|
||||
Intent i = new Intent(context, WidgetConfig.class);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.getApplicationContext().startActivity(i);
|
||||
|
||||
}
|
||||
super.onReceive(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update at regular interval
|
||||
* This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute
|
||||
*/
|
||||
@Override
|
||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
||||
// update each of the app widgets with the remote adapter
|
||||
Log.d(TAG, "onUpdate");
|
||||
for (int i = 0; i < appWidgetIds.length; ++i) {
|
||||
for (int appWidgetId : appWidgetIds) {
|
||||
|
||||
Log.d(TAG, "onUpdate iteration #" + i);
|
||||
// Set up the intent that starts the BlurWidgetRemoteViewService, which will
|
||||
// Set up the intent that starts the WidgetRemoteViewService, which will
|
||||
// provide the views for this collection.
|
||||
Intent intent = new Intent(context, BlurWidgetRemoteViewsService.class);
|
||||
Intent intent = new Intent(context, WidgetRemoteViewsService.class);
|
||||
// Add the app widget ID to the intent extras.
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
|
||||
|
||||
|
||||
// Instantiate the RemoteViews object for the app widget layout.
|
||||
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.newsblur_widget);
|
||||
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.view_app_widget);
|
||||
// Set up the RemoteViews object to use a RemoteViews adapter.
|
||||
// This adapter connects
|
||||
// to a RemoteViewsService through the specified intent.
|
||||
|
|
@ -76,37 +66,33 @@ public class NewsBlurWidgetProvider extends AppWidgetProvider {
|
|||
// The empty view is displayed when the collection has no items.
|
||||
// It should be in the same layout used to instantiate the RemoteViews
|
||||
// object above.
|
||||
rv.setEmptyView(R.id.widget_list, R.id.empty_view);
|
||||
rv.setEmptyView(R.id.widget_list, R.id.widget_empty_view);
|
||||
|
||||
rv.setTextViewText(R.id.txt_feed_name,
|
||||
PrefsUtils.getWidgetFeedName(context, appWidgetIds[i]));
|
||||
Intent configIntent = new Intent(context, WidgetProvider.class);
|
||||
configIntent.setAction(WidgetUtils.ACTION_OPEN_CONFIG);
|
||||
PendingIntent configIntentTemplate = PendingIntent.getBroadcast(context, WidgetUtils.RC_WIDGET_CONFIG, configIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
rv.setOnClickPendingIntent(R.id.widget_empty_view, configIntentTemplate);
|
||||
|
||||
//
|
||||
// Do additional processing specific to this app widget...
|
||||
//
|
||||
// This section makes it possible for items to have individualized behavior.
|
||||
// It does this by setting up a pending intent template. Individuals items of a collection
|
||||
// cannot set up their own pending intents. Instead, the collection as a whole sets
|
||||
// up a pending intent template, and the individual items set a fillInIntent
|
||||
// to create unique behavior on an item-by-item basis.
|
||||
Intent touchIntent = new Intent(context, NewsBlurWidgetProvider.class);
|
||||
Intent touchIntent = new Intent(context, WidgetProvider.class);
|
||||
// Set the action for the intent.
|
||||
// When the user touches a particular view, it will have the effect of
|
||||
// broadcasting TOAST_ACTION.
|
||||
touchIntent.setAction(NewsBlurWidgetProvider.ACTION_OPEN_STORY);
|
||||
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
|
||||
// broadcasting ACTION_OPEN_STORY.
|
||||
touchIntent.setAction(WidgetUtils.ACTION_OPEN_STORY);
|
||||
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
|
||||
PendingIntent touchIntentTemplate = PendingIntent.getBroadcast(context, 0, touchIntent,
|
||||
PendingIntent touchIntentTemplate = PendingIntent.getBroadcast(context, WidgetUtils.RC_WIDGET_STORY, touchIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
rv.setPendingIntentTemplate(R.id.widget_list, touchIntentTemplate);
|
||||
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
|
||||
appWidgetManager.updateAppWidget(appWidgetId, rv);
|
||||
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
|
||||
}
|
||||
|
||||
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
class WidgetRemoteViews extends RemoteViews {
|
||||
|
||||
WidgetRemoteViews(String packageName, int layoutId) {
|
||||
super(packageName, layoutId);
|
||||
}
|
||||
|
||||
void setViewBackgroundColor(int viewId, @ColorInt int color) {
|
||||
setInt(viewId, "setBackgroundColor", color);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.RemoteViewsService;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.Log;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadFilter;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
import com.newsblur.util.StoryUtils;
|
||||
import com.newsblur.util.ThumbnailStyle;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class WidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
|
||||
|
||||
private static String TAG = "WidgetRemoteViewsFactory";
|
||||
|
||||
private Context context;
|
||||
private APIManager apiManager;
|
||||
private List<Story> storyItems = new ArrayList<>();
|
||||
private FeedSet fs;
|
||||
private int appWidgetId;
|
||||
private boolean dataCompleted;
|
||||
|
||||
WidgetRemoteViewsFactory(Context context, Intent intent) {
|
||||
com.newsblur.util.Log.d(TAG, "Constructor");
|
||||
this.context = context;
|
||||
appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* The system calls onCreate() when creating your factory for the first time.
|
||||
* This is where you set up any connections and/or cursors to your data source.
|
||||
* <p>
|
||||
* Heavy lifting,
|
||||
* for example downloading or creating content etc, should be deferred to onDataSetChanged()
|
||||
* or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.d(TAG, "onCreate");
|
||||
this.apiManager = new APIManager(context);
|
||||
// widget could be created before app init
|
||||
// wait for the dbHelper to be ready for use
|
||||
while (FeedUtils.dbHelper == null) {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (FeedUtils.dbHelper == null) {
|
||||
FeedUtils.offerInitContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
WidgetUtils.enableWidgetUpdate(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowed to run synchronous calls
|
||||
*/
|
||||
@Override
|
||||
public RemoteViews getViewAt(int position) {
|
||||
com.newsblur.util.Log.d(TAG, "getViewAt " + position);
|
||||
Story story = storyItems.get(position);
|
||||
|
||||
WidgetRemoteViews rv = new WidgetRemoteViews(context.getPackageName(), R.layout.view_widget_story_item);
|
||||
rv.setTextViewText(R.id.story_item_title, story.title);
|
||||
rv.setTextViewText(R.id.story_item_content, story.shortContent);
|
||||
rv.setTextViewText(R.id.story_item_author, story.authors);
|
||||
rv.setTextViewText(R.id.story_item_feedtitle, story.extern_feedTitle);
|
||||
CharSequence time = StoryUtils.formatShortDate(context, story.timestamp);
|
||||
rv.setTextViewText(R.id.story_item_date, time);
|
||||
|
||||
// image dimensions same as R.layout.view_widget_story_item
|
||||
FeedUtils.iconLoader.displayWidgetImage(story.extern_faviconUrl, R.id.story_item_feedicon, UIUtils.dp2px(context, 19), rv);
|
||||
if (PrefsUtils.getThumbnailStyle(context) != ThumbnailStyle.OFF && !TextUtils.isEmpty(story.thumbnailUrl)) {
|
||||
FeedUtils.thumbnailLoader.displayWidgetImage(story.thumbnailUrl, R.id.story_item_thumbnail, UIUtils.dp2px(context, 64), rv);
|
||||
} else {
|
||||
rv.setViewVisibility(R.id.story_item_thumbnail, View.GONE);
|
||||
}
|
||||
|
||||
rv.setViewBackgroundColor(R.id.story_item_favicon_borderbar_1, UIUtils.decodeColourValue(story.extern_feedColor, Color.GRAY));
|
||||
rv.setViewBackgroundColor(R.id.story_item_favicon_borderbar_2, UIUtils.decodeColourValue(story.extern_feedFade, Color.LTGRAY));
|
||||
|
||||
// set fill-intent which is used to fill in the pending intent template
|
||||
// set on the collection view in WidgetProvider
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(WidgetUtils.EXTRA_ITEM_ID, story.storyHash);
|
||||
Intent fillInIntent = new Intent();
|
||||
fillInIntent.putExtras(extras);
|
||||
|
||||
rv.setOnClickFillInIntent(R.id.view_widget_item, fillInIntent);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* This allows for the use of a custom loading view which appears between the time that
|
||||
* {@link #getViewAt(int)} is called and returns. If null is returned, a default loading
|
||||
* view will be used.
|
||||
*
|
||||
* @return The RemoteViews representing the desired loading view.
|
||||
*/
|
||||
@Override
|
||||
public RemoteViews getLoadingView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of types of Views that will be returned by this factory.
|
||||
*/
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position The position of the item within the data set whose row id we want.
|
||||
* @return The id of the item at the specified position.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return storyItems.get(position).hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the same id always refers to the same object.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
com.newsblur.util.Log.d(TAG, "onDataSetChanged");
|
||||
// if user logged out don't try to update widget
|
||||
if (!WidgetUtils.isLoggedIn(context)) {
|
||||
com.newsblur.util.Log.d(TAG, "onDataSetChanged - not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataCompleted) {
|
||||
// we have all the stories data, just let the widget redraw
|
||||
com.newsblur.util.Log.d(TAG, "onDataSetChanged - redraw widget");
|
||||
dataCompleted = false;
|
||||
} else {
|
||||
setFeedSet();
|
||||
if (fs == null) {
|
||||
com.newsblur.util.Log.d(TAG, "onDataSetChanged - null feed set. Show empty view");
|
||||
setStories(new Story[]{}, new HashMap<String, Feed>(0));
|
||||
return;
|
||||
}
|
||||
|
||||
com.newsblur.util.Log.d(TAG, "onDataSetChanged - fetch stories");
|
||||
StoriesResponse response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL);
|
||||
|
||||
if (response == null || response.stories == null) {
|
||||
com.newsblur.util.Log.d(TAG, "Error fetching widget stories");
|
||||
} else {
|
||||
com.newsblur.util.Log.d(TAG, "Fetched widget stories");
|
||||
processStories(response.stories);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the last RemoteViewsAdapter that is associated with this factory is
|
||||
* unbound.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
com.newsblur.util.Log.d(TAG, "onDestroy");
|
||||
WidgetUtils.disableWidgetUpdate(context);
|
||||
PrefsUtils.removeWidgetData(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Count of items.
|
||||
*/
|
||||
@Override
|
||||
public int getCount() {
|
||||
return Math.min(storyItems.size(), WidgetUtils.STORIES_LIMIT);
|
||||
}
|
||||
|
||||
private void processStories(final Story[] stories) {
|
||||
com.newsblur.util.Log.d(TAG, "processStories");
|
||||
final HashMap<String, Feed> feedMap = new HashMap<>();
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
|
||||
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
|
||||
@Override
|
||||
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Feed feed = Feed.fromCursor(cursor);
|
||||
if (feed.active) {
|
||||
feedMap.put(feed.feedId, feed);
|
||||
}
|
||||
}
|
||||
setStories(stories, feedMap);
|
||||
}
|
||||
});
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
private void setStories(Story[] stories, HashMap<String, Feed> feedMap) {
|
||||
com.newsblur.util.Log.d(TAG, "setStories");
|
||||
for (Story story : stories) {
|
||||
Feed storyFeed = feedMap.get(story.feedId);
|
||||
if (storyFeed != null) {
|
||||
bindStoryValues(story, storyFeed);
|
||||
}
|
||||
}
|
||||
this.storyItems.clear();
|
||||
this.storyItems.addAll(Arrays.asList(stories));
|
||||
// we have the data, notify data set changed
|
||||
dataCompleted = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void bindStoryValues(Story story, Feed feed) {
|
||||
story.thumbnailUrl = Story.guessStoryThumbnailURL(story);
|
||||
story.extern_faviconBorderColor = feed.faviconBorder;
|
||||
story.extern_faviconUrl = feed.faviconUrl;
|
||||
story.extern_feedTitle = feed.title;
|
||||
story.extern_feedFade = feed.faviconFade;
|
||||
story.extern_feedColor = feed.faviconColor;
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
com.newsblur.util.Log.d(TAG, "Invalidate app widget with id: " + appWidgetId);
|
||||
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
|
||||
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
|
||||
}
|
||||
|
||||
private void setFeedSet() {
|
||||
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(context);
|
||||
if (feedIds == null || !feedIds.isEmpty()) {
|
||||
fs = FeedSet.widgetFeeds(feedIds);
|
||||
} else {
|
||||
// no feeds selected. Widget will show tap to config view
|
||||
fs = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.widget.RemoteViewsService;
|
||||
|
||||
import com.newsblur.util.Log;
|
||||
|
||||
public class WidgetRemoteViewsService extends RemoteViewsService {
|
||||
|
||||
private static String TAG = "WidgetRemoteViewsFactory";
|
||||
|
||||
@Override
|
||||
public RemoteViewsService.RemoteViewsFactory onGetViewFactory(Intent intent) {
|
||||
Log.d(TAG, "onGetViewFactory");
|
||||
return new WidgetRemoteViewsFactory(this.getApplicationContext(), intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.util.Log;
|
||||
|
||||
public class WidgetUpdateReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, @Nullable Intent intent) {
|
||||
if (intent != null && intent.getAction() != null &&
|
||||
intent.getAction().equals(WidgetUtils.ACTION_UPDATE_WIDGET)) {
|
||||
Log.d(this.getClass().getName(), "Received " + WidgetUtils.ACTION_UPDATE_WIDGET);
|
||||
AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
|
||||
int[] appWidgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
|
||||
widgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.newsblur.widget;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.util.Log;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
public class WidgetUtils {
|
||||
|
||||
private static String TAG = "WidgetUtils";
|
||||
|
||||
public static String ACTION_UPDATE_WIDGET = "ACTION_UPDATE_WIDGET";
|
||||
public static String ACTION_OPEN_STORY = "ACTION_OPEN_STORY";
|
||||
public static String ACTION_OPEN_CONFIG = "ACTION_OPEN_CONFIG";
|
||||
public static String EXTRA_ITEM_ID = "EXTRA_ITEM_ID";
|
||||
public static int RC_WIDGET_UPDATE = 1;
|
||||
public static int RC_WIDGET_STORY = 2;
|
||||
public static int RC_WIDGET_CONFIG = 3;
|
||||
public static int STORIES_LIMIT = 5;
|
||||
|
||||
static void enableWidgetUpdate(Context context) {
|
||||
Log.d(TAG, "enableWidgetUpdate");
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent intent = getUpdateIntent(context);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, RC_WIDGET_UPDATE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
int widgetUpdateInterval = 1000 * 60 * 5;
|
||||
long startAlarmAt = SystemClock.currentThreadTimeMillis() + widgetUpdateInterval;
|
||||
alarmManager.setInexactRepeating(AlarmManager.RTC, startAlarmAt, widgetUpdateInterval, pendingIntent);
|
||||
}
|
||||
|
||||
public static void disableWidgetUpdate(Context context) {
|
||||
Log.d(TAG, "disableWidgetUpdate");
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, RC_WIDGET_UPDATE, getUpdateIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
alarmManager.cancel(pendingIntent);
|
||||
}
|
||||
|
||||
public static void resetWidgetUpdate(Context context) {
|
||||
if (hasActiveAppWidgets(context)) {
|
||||
WidgetUtils.enableWidgetUpdate(context);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasActiveAppWidgets(Context context) {
|
||||
AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
|
||||
int[] appWidgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
|
||||
return appWidgetIds.length > 0;
|
||||
}
|
||||
|
||||
public static void notifyViewDataChanged(Context context) {
|
||||
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
|
||||
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
|
||||
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
|
||||
}
|
||||
|
||||
public static boolean isLoggedIn(Context context) {
|
||||
return PrefsUtils.getUniqueLoginKey(context) != null;
|
||||
}
|
||||
|
||||
private static Intent getUpdateIntent(Context context) {
|
||||
Intent intent = new Intent(context, WidgetUpdateReceiver.class);
|
||||
intent.setAction(ACTION_UPDATE_WIDGET);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
|
@ -270,10 +270,10 @@
|
|||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
return 12;
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
|
||||
return 13;
|
||||
} else {
|
||||
return 11;
|
||||
return 12;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +281,7 @@
|
|||
static NSString *CellIndentifier = @"Cell";
|
||||
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIndentifier];
|
||||
NSUInteger iPadOffset = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone ? 0 : 1;
|
||||
NSUInteger iPadOffset = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? 0 : 1;
|
||||
|
||||
if (indexPath.row == 7) {
|
||||
return [self makeFontSizeTableCell];
|
||||
|
|
@ -396,7 +396,7 @@
|
|||
}
|
||||
|
||||
|
||||
// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
|
||||
// if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
// if (indexPath.row != 2 && indexPath.row != 3) {
|
||||
// // if we're opening another popover, then don't animate out - it looks strange
|
||||
// [self.appDelegate.masterContainerViewController hidePopover];
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@
|
|||
@property (readwrite) BOOL isHidingStory;
|
||||
@property (readwrite) BOOL feedDetailIsVisible;
|
||||
@property (readwrite) BOOL keyboardIsShown;
|
||||
@property (nonatomic) UIBackgroundTaskIdentifier reorientBackgroundTask;
|
||||
|
||||
@end
|
||||
|
||||
|
|
@ -230,11 +229,9 @@
|
|||
if (self.feedDetailIsVisible) {
|
||||
// Defer this in the background, to avoid misaligning the detail views
|
||||
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
|
||||
self.reorientBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
||||
[[UIApplication sharedApplication] endBackgroundTask:self.reorientBackgroundTask];
|
||||
self.reorientBackgroundTask = UIBackgroundTaskInvalid;
|
||||
}];
|
||||
[self performSelector:@selector(delayedReorientPages) withObject:nil afterDelay:0.5];
|
||||
NSString *networkOperationIdentifier = [appDelegate beginNetworkOperation];
|
||||
|
||||
[self performSelector:@selector(delayedReorientPages:) withObject:networkOperationIdentifier afterDelay:0.5];
|
||||
} else {
|
||||
[self.storyPageControl reorientPages];
|
||||
}
|
||||
|
|
@ -268,10 +265,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)delayedReorientPages {
|
||||
- (void)delayedReorientPages:(NSString *)identifier {
|
||||
[self.storyPageControl reorientPages];
|
||||
[[UIApplication sharedApplication] endBackgroundTask:self.reorientBackgroundTask];
|
||||
self.reorientBackgroundTask = UIBackgroundTaskInvalid;
|
||||
[appDelegate endNetworkOperation:identifier];
|
||||
}
|
||||
|
||||
- (void)checkSize:(CGSize)size {
|
||||
|
|
|
|||
|
|
@ -342,6 +342,8 @@ SFSafariViewControllerDelegate> {
|
|||
- (void)recalculateIntelligenceScores:(id)feedId;
|
||||
|
||||
- (void)cancelRequests;
|
||||
- (NSString *)beginNetworkOperation;
|
||||
- (void)endNetworkOperation:(NSString *)networkOperationIdentifier;
|
||||
|
||||
- (void)GET:(NSString *)urlString parameters:(id)parameters
|
||||
success:(void (^)(NSURLSessionDataTask *, id))success
|
||||
|
|
|
|||
|
|
@ -3314,7 +3314,7 @@
|
|||
[gradientView addSubview:titleImageView];
|
||||
} else {
|
||||
gradientView = [NewsBlurAppDelegate
|
||||
makeGradientView:CGRectMake(0, -1, rect.size.width, 10)
|
||||
makeGradientView:CGRectMake(0, rect.origin.y, rect.size.width, 10)
|
||||
// hard coding the 1024 as a hack for window.frame.size.width
|
||||
startColor:[feed objectForKey:@"favicon_fade"]
|
||||
endColor:[feed objectForKey:@"favicon_color"]
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ static NSArray<NSString *> *NewsBlurTopSectionNames;
|
|||
// [self.feedTitlesTable selectRowAtIndexPath:self.currentRowAtIndexPath
|
||||
// animated:NO
|
||||
// scrollPosition:UITableViewScrollPositionNone];
|
||||
[self.notifier setNeedsLayout];
|
||||
[self hideNotifier];
|
||||
}
|
||||
|
||||
if (self.searchFeedIds) {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
doubleDoubleTapGesture.delegate = self;
|
||||
[self.webView addGestureRecognizer:doubleDoubleTapGesture];
|
||||
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
if ([UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(pinchGesture:)];
|
||||
[self.webView addGestureRecognizer:pinchGesture];
|
||||
|
|
@ -369,7 +369,7 @@
|
|||
}
|
||||
|
||||
- (BOOL)isPhoneOrCompact {
|
||||
return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone || self.appDelegate.isCompactWidth;
|
||||
return [UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone || self.appDelegate.isCompactWidth;
|
||||
}
|
||||
|
||||
// allow keyboard comands
|
||||
|
|
@ -607,6 +607,11 @@
|
|||
}
|
||||
|
||||
- (void)drawFeedGradient {
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
|
||||
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
|
||||
BOOL shouldOffsetFeedGradient = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && !UIInterfaceOrientationIsLandscape(orientation) && self.navigationController.navigationBarHidden && !shouldHideStatusBar;
|
||||
CGFloat yOffset = shouldOffsetFeedGradient ? appDelegate.storyPageControl.statusBarBackgroundView.bounds.size.height - 1 : -1;
|
||||
NSString *feedIdStr = [NSString stringWithFormat:@"%@",
|
||||
[self.activeStory
|
||||
objectForKey:@"story_feed_id"]];
|
||||
|
|
@ -619,7 +624,7 @@
|
|||
|
||||
self.feedTitleGradient = [appDelegate
|
||||
makeFeedTitleGradient:feed
|
||||
withRect:CGRectMake(0, -1, CGRectGetWidth(self.view.bounds), 21)]; // 1024 hack for self.webView.frame.size.width
|
||||
withRect:CGRectMake(0, yOffset, CGRectGetWidth(self.view.bounds), 21)]; // 1024 hack for self.webView.frame.size.width
|
||||
self.feedTitleGradient.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.feedTitleGradient.tag = FEED_TITLE_GRADIENT_TAG; // Not attached yet. Remove old gradients, first.
|
||||
|
||||
|
|
@ -640,7 +645,7 @@
|
|||
[self.webView insertSubview:feedTitleGradient aboveSubview:self.webView.scrollView];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if (self.view.safeAreaInsets.top > 0.0 && [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
if (self.view.safeAreaInsets.top > 0.0 && [UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone && shouldHideStatusBar) {
|
||||
feedTitleGradient.alpha = self.navigationController.navigationBarHidden ? 1 : 0;
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
|
|
@ -1409,7 +1414,11 @@
|
|||
// appDelegate.storyPageControl.traverseView.frame = CGRectMake(tvf.origin.x,
|
||||
// (webpageHeight - topPosition) - tvf.size.height - safeBottomMargin,
|
||||
// tvf.size.width, tvf.size.height);
|
||||
appDelegate.storyPageControl.traverseBottomConstraint.constant = viewportHeight - (webpageHeight - topPosition) + safeBottomMargin;
|
||||
if (webpageHeight > 0) {
|
||||
appDelegate.storyPageControl.traverseBottomConstraint.constant = viewportHeight - (webpageHeight - topPosition) + safeBottomMargin;
|
||||
} else {
|
||||
appDelegate.storyPageControl.traverseBottomConstraint.constant = safeBottomMargin;
|
||||
}
|
||||
// appDelegate.storyPageControl.traverseBottomConstraint.constant = safeBottomMargin;
|
||||
}
|
||||
} else if (!singlePage && (atTop && !atBottom)) {
|
||||
|
|
@ -1896,7 +1905,7 @@
|
|||
}
|
||||
|
||||
- (BOOL)canHideNavigationBar {
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPhone || self.presentedViewController != nil) {
|
||||
if ([UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPhone || self.presentedViewController != nil) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,15 +228,15 @@
|
|||
[self.view addConstraint:self.notifier.topOffsetConstraint];
|
||||
[self.notifier hideNow];
|
||||
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
self.traverseBottomConstraint.constant = 50;
|
||||
|
||||
if ([UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:
|
||||
originalStoryButton,
|
||||
separatorBarButton,
|
||||
fontSettingsButton, nil];
|
||||
}
|
||||
|
||||
[self updateTheme];
|
||||
|
||||
[self.scrollView addObserver:self forKeyPath:@"contentOffset"
|
||||
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
||||
context:nil];
|
||||
|
|
@ -266,6 +266,8 @@
|
|||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateTheme];
|
||||
|
||||
[self updateAutoscrollButtons];
|
||||
[self updateTraverseBackground];
|
||||
[self setNextPreviousButtons];
|
||||
|
|
@ -500,7 +502,7 @@
|
|||
|
||||
NSUserDefaults *userPreferences = [NSUserDefaults standardUserDefaults];
|
||||
BOOL swipeEnabled = [[userPreferences stringForKey:@"story_detail_swipe_left_edge"]
|
||||
isEqualToString:@"pop_to_story_list"];;
|
||||
isEqualToString:@"pop_to_story_list"];
|
||||
self.navigationController.interactivePopGestureRecognizer.enabled = swipeEnabled;
|
||||
|
||||
if (hide) {
|
||||
|
|
@ -1239,7 +1241,10 @@
|
|||
}
|
||||
self.scrollView.scrollsToTop = NO;
|
||||
|
||||
NSInteger topPosition = currentPage.webView.scrollView.contentOffset.y;
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
|
||||
NSInteger statusBarOffset = shouldHideStatusBar ? 0 : self.statusBarHeight;
|
||||
NSInteger topPosition = currentPage.webView.scrollView.contentOffset.y + statusBarOffset;
|
||||
BOOL canHide = currentPage.canHideNavigationBar && topPosition >= 0;
|
||||
|
||||
if (!canHide && self.isHorizontal && self.navigationController.navigationBarHidden) {
|
||||
|
|
|
|||
|
|
@ -2797,7 +2797,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1120;
|
||||
LastUpgradeCheck = 1140;
|
||||
LastUpgradeCheck = 1150;
|
||||
ORGANIZATIONNAME = NewsBlur;
|
||||
TargetAttributes = {
|
||||
1749390F1C251BFE003D98AA = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1140"
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1140"
|
||||
LastUpgradeVersion = "1150"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1140"
|
||||
LastUpgradeVersion = "1150"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1140"
|
||||
LastUpgradeVersion = "1150"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
@interface ShareViewController () <NSURLSessionDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSString *itemTitle;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ShareViewController
|
||||
|
|
@ -26,6 +28,8 @@
|
|||
}
|
||||
|
||||
- (void)didSelectPost {
|
||||
self.itemTitle = nil;
|
||||
|
||||
NSItemProvider *itemProvider = [self providerWithURL];
|
||||
|
||||
NSLog(@"ShareExt: didSelectPost");
|
||||
|
|
@ -55,6 +59,12 @@
|
|||
for (NSExtensionItem *extensionItem in self.extensionContext.inputItems) {
|
||||
for (NSItemProvider *itemProvider in extensionItem.attachments) {
|
||||
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {
|
||||
self.itemTitle = extensionItem.attributedTitle.string;
|
||||
|
||||
if (!self.itemTitle.length) {
|
||||
self.itemTitle = extensionItem.attributedContentText.string;
|
||||
}
|
||||
|
||||
return itemProvider;
|
||||
}
|
||||
}
|
||||
|
|
@ -79,14 +89,20 @@
|
|||
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.newsblur.NewsBlur-Group"];
|
||||
NSString *host = [defaults objectForKey:@"share:host"];
|
||||
NSString *token = [defaults objectForKey:@"share:token"];
|
||||
NSString *title = self.itemTitle;
|
||||
NSString *comments = self.contentText;
|
||||
|
||||
if (title && [comments isEqualToString:title]) {
|
||||
comments = @"";
|
||||
}
|
||||
|
||||
if (text && [comments isEqualToString:text]) {
|
||||
comments = @"";
|
||||
}
|
||||
|
||||
NSCharacterSet *characterSet = [NSCharacterSet URLQueryAllowedCharacterSet];
|
||||
NSString *encodedURL = url ? [url.absoluteString stringByAddingPercentEncodingWithAllowedCharacters:characterSet] : @"";
|
||||
NSString *encodedTitle = title ? [title stringByAddingPercentEncodingWithAllowedCharacters:characterSet] : @"";
|
||||
NSString *encodedContent = text ? [text stringByAddingPercentEncodingWithAllowedCharacters:characterSet] : @"";
|
||||
NSString *encodedComments = [comments stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
|
||||
// NSInteger time = [[NSDate date] timeIntervalSince1970];
|
||||
|
|
@ -96,7 +112,7 @@
|
|||
NSURL *requestURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/api/share_story/%@", host, token]];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL];
|
||||
request.HTTPMethod = @"POST";
|
||||
NSString *postBody = [NSString stringWithFormat:@"story_url=%@&title=&content=%@&comments=%@", encodedURL, encodedContent, encodedComments];
|
||||
NSString *postBody = [NSString stringWithFormat:@"story_url=%@&title=%@&content=%@&comments=%@", encodedURL, encodedTitle, encodedContent, encodedComments];
|
||||
request.HTTPBody = [postBody dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSURLSessionTask *myTask = [mySession dataTaskWithRequest:request];
|
||||
[myTask resume];
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ frontend public
|
|||
option http-server-close
|
||||
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
# redirect scheme https code 301 if !{ ssl_fc }
|
||||
redirect scheme https code 301 if !{ ssl_fc }
|
||||
|
||||
acl gunicorn_dead nbsrv(gunicorn) lt 1
|
||||
acl nginx_dead nbsrv(nginx) lt 1
|
||||
|
|
|
|||
51
fabfile.py
vendored
51
fabfile.py
vendored
|
|
@ -212,7 +212,6 @@ def setup_common():
|
|||
# setup_pymongo_repo()
|
||||
setup_logrotate()
|
||||
setup_nginx()
|
||||
# setup_imaging()
|
||||
setup_munin()
|
||||
|
||||
def setup_all():
|
||||
|
|
@ -458,25 +457,6 @@ def setup_libxml_code():
|
|||
def setup_psycopg():
|
||||
sudo('easy_install -U psycopg2')
|
||||
|
||||
# def setup_python():
|
||||
# # sudo('easy_install -U $(<%s)' %
|
||||
# # os.path.join(env.NEWSBLUR_PATH, 'config/requirements.txt'))
|
||||
# pip()
|
||||
# put('config/pystartup.py', '.pystartup')
|
||||
#
|
||||
# # with cd(os.path.join(env.NEWSBLUR_PATH, 'vendor/cjson')):
|
||||
# # sudo('python setup.py install')
|
||||
#
|
||||
# with settings(warn_only=True):
|
||||
# sudo('echo "import sys; sys.setdefaultencoding(\'utf-8\')" | sudo tee /usr/lib/python2.7/sitecustomize.py')
|
||||
# sudo("chmod a+r /usr/local/lib/python2.7/dist-packages/httplib2-0.8-py2.7.egg/EGG-INFO/top_level.txt")
|
||||
# sudo("chmod a+r /usr/local/lib/python2.7/dist-packages/python_dateutil-2.1-py2.7.egg/EGG-INFO/top_level.txt")
|
||||
# sudo("chmod a+r /usr/local/lib/python2.7/dist-packages/httplib2-0.8-py2.7.egg/httplib2/cacerts.txt")
|
||||
#
|
||||
# if env.user == 'ubuntu':
|
||||
# with settings(warn_only=True):
|
||||
# sudo('chown -R ubuntu.ubuntu /home/ubuntu/.python-eggs')
|
||||
|
||||
def setup_virtualenv():
|
||||
sudo('rm -fr ~/.cache') # Clean `sudo pip`
|
||||
sudo('pip install --upgrade virtualenv')
|
||||
|
|
@ -529,10 +509,6 @@ def solo_pip(role):
|
|||
pip()
|
||||
celery()
|
||||
|
||||
# PIL - Only if python-imaging didn't install through apt-get, like on Mac OS X.
|
||||
def setup_imaging():
|
||||
sudo('easy_install --always-unzip pil')
|
||||
|
||||
def setup_supervisor():
|
||||
sudo('apt-get -y install supervisor')
|
||||
put('config/supervisord.conf', '/etc/supervisor/supervisord.conf', use_sudo=True)
|
||||
|
|
@ -820,11 +796,11 @@ def assemble_certificates():
|
|||
local('cat STAR_newsblur_com.crt EssentialSSLCA_2.crt ComodoUTNSGCCA.crt UTNAddTrustSGCCA.crt AddTrustExternalCARoot.crt > newsblur.com.crt')
|
||||
|
||||
def copy_certificates():
|
||||
cert_path = '%s/config/certificates' % env.NEWSBLUR_PATH
|
||||
cert_path = os.path.join(env.NEWSBLUR_PATH, 'config/certificates')
|
||||
run('mkdir -p %s' % cert_path)
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/newsblur.com.crt'), cert_path)
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/newsblur.com.key'), cert_path)
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/newsblur.com.pem'), cert_path)
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/newsblur.com.crt'), os.path.join(cert_path, 'newsblur.com.pem')) # For backwards compatibility with hard-coded nginx configs
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/dhparams.pem'), cert_path)
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps_development.pem'), cert_path)
|
||||
# openssl x509 -in aps.cer -inform DER -outform PEM -out aps.pem
|
||||
|
|
@ -832,10 +808,21 @@ def copy_certificates():
|
|||
# Export aps.p12 from aps.cer using Keychain Assistant
|
||||
# openssl pkcs12 -in aps.p12 -out aps.p12.pem -nodes
|
||||
put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps.p12.pem'), cert_path)
|
||||
run('cat %s/newsblur.com.pem > %s/newsblur.pem' % (cert_path, cert_path))
|
||||
run('cat %s/newsblur.com.crt > %s/newsblur.pem' % (cert_path, cert_path))
|
||||
run('echo "\n" >> %s/newsblur.pem' % (cert_path))
|
||||
run('cat %s/newsblur.com.key >> %s/newsblur.pem' % (cert_path, cert_path))
|
||||
|
||||
def setup_certbot():
|
||||
sudo('add-apt-repository -y universe')
|
||||
sudo('add-apt-repository -y ppa:certbot/certbot')
|
||||
sudo('apt-get update')
|
||||
sudo('apt-get install -y certbot')
|
||||
sudo('apt-get install -y python3-certbot-dns-dnsimple')
|
||||
run('echo "dns_dnsimple_token = %s" > dnsimple.ini')
|
||||
run('chmod 0400 dnsimple.ini')
|
||||
sudo('certbot certonly -n --agree-tos --email samuel@newsblur.com --domains "*.newsblur.com" --dns-dnsimple --dns-dnsimple-credentials %s' % (settings.DNSIMPLE_TOKEN))
|
||||
run('rm dnsimple.ini')
|
||||
|
||||
@parallel
|
||||
def maintenance_on():
|
||||
role = role_for_host()
|
||||
|
|
@ -1392,6 +1379,16 @@ def setup_usage_monitor():
|
|||
sudo('ln -fs %s/utils/monitor_disk_usage.py /etc/cron.daily/monitor_disk_usage' % env.NEWSBLUR_PATH)
|
||||
sudo('/etc/cron.daily/monitor_disk_usage')
|
||||
|
||||
@parallel
|
||||
def setup_feeds_fetched_monitor():
|
||||
sudo('ln -fs %s/utils/monitor_task_fetches.py /etc/cron.hourly/monitor_task_fetches' % env.NEWSBLUR_PATH)
|
||||
sudo('/etc/cron.hourly/monitor_task_fetches')
|
||||
|
||||
@parallel
|
||||
def setup_newsletter_monitor():
|
||||
sudo('ln -fs %s/utils/monitor_newsletter_delivery.py /etc/cron.hourly/monitor_newsletter_delivery' % env.NEWSBLUR_PATH)
|
||||
sudo('/etc/cron.hourly/monitor_newsletter_delivery')
|
||||
|
||||
@parallel
|
||||
def setup_redis_monitor():
|
||||
run('sleep 5') # Wait for redis to startup so the log file is there
|
||||
|
|
|
|||
|
|
@ -135,3 +135,26 @@ if len(logging._handlerList) < 1:
|
|||
datefmt='%b %d %H:%M:%S',
|
||||
handler=logging.StreamHandler)
|
||||
|
||||
S3_ACCESS_KEY = '000000000000000000000'
|
||||
S3_SECRET = '000000000000000000000000/0000000000000000'
|
||||
S3_BACKUP_BUCKET = 'newsblur_backups'
|
||||
S3_PAGES_BUCKET_NAME = 'pages-dev.newsblur.com'
|
||||
S3_ICONS_BUCKET_NAME = 'icons-dev.newsblur.com'
|
||||
S3_AVATARS_BUCKET_NAME = 'avatars-dev.newsblur.com'
|
||||
|
||||
MAILGUN_ACCESS_KEY = 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
MAILGUN_SERVER_NAME = 'newsblur.com'
|
||||
|
||||
DO_TOKEN_LOG = '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
DO_TOKEN_FABRIC = '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
|
||||
SERVER_NAME = "nblocalhost"
|
||||
NEWSBLUR_URL = 'http://nb.local.com'
|
||||
|
||||
SESSION_ENGINE = 'redis_sessions.session'
|
||||
|
||||
# CORS_ORIGIN_REGEX_WHITELIST = ('^(https?://)?(\w+\.)?nb.local\.com$', )
|
||||
|
||||
YOUTUBE_API_KEY = "000000000000000000000000000000000000000"
|
||||
RECAPTCHA_SECRET_KEY = "0000000000000000000000000000000000000000"
|
||||
IMAGES_SECRET_KEY = "0000000000000000000000000000000"
|
||||
|
|
|
|||
|
|
@ -1537,12 +1537,16 @@
|
|||
this.flags['loaded_next_after_load'] = true;
|
||||
|
||||
var next = $.getQueryString('next') || $.getQueryString('test');
|
||||
var add_url = $.getQueryString('add') || $.getQueryString('url');
|
||||
var story = $.getQueryString('story');
|
||||
if (next == 'notifications') {
|
||||
_.defer(function() {
|
||||
NEWSBLUR.reader.open_notifications_modal(NEWSBLUR.assets.active_feed && NEWSBLUR.assets.active_feed.id);
|
||||
});
|
||||
}
|
||||
if (add_url) {
|
||||
NEWSBLUR.reader.open_add_feed_modal({url: url});
|
||||
}
|
||||
if (story) {
|
||||
this.flags['select_story_in_feed'] = story;
|
||||
}
|
||||
|
|
@ -3180,7 +3184,11 @@
|
|||
clearInterval(this.flags['bouncing_callout']);
|
||||
$.modal.close();
|
||||
|
||||
NEWSBLUR.add_feed = NEWSBLUR.ReaderAddFeed.create(options);
|
||||
if (NEWSBLUR.Globals.is_anonymous && NEWSBLUR.welcome) {
|
||||
NEWSBLUR.welcome.show_signin_form();
|
||||
} else {
|
||||
NEWSBLUR.add_feed = NEWSBLUR.ReaderAddFeed.create(options);
|
||||
}
|
||||
},
|
||||
|
||||
open_manage_feed_modal: function(feed_id) {
|
||||
|
|
@ -6726,6 +6734,12 @@
|
|||
self.reload_feed();
|
||||
}
|
||||
});
|
||||
$document.bind('keydown', 'shift+r', function(e) {
|
||||
e.preventDefault();
|
||||
if (self.active_feed) {
|
||||
self.force_instafetch_stories();
|
||||
}
|
||||
});
|
||||
$document.bind('keydown', 'enter', function(e) {
|
||||
e.preventDefault();
|
||||
if (self.flags['feed_view_showing_story_view']) {
|
||||
|
|
|
|||
|
|
@ -305,8 +305,10 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
|
|||
var url = $.getQueryString('url') || $.getQueryString('add');
|
||||
if (url) {
|
||||
NEWSBLUR.reader.open_add_feed_modal({url: url});
|
||||
route_found = true;
|
||||
}
|
||||
|
||||
// This removes the query string from the URL.
|
||||
if (!route_found && window.history.replaceState && !$.getQueryString('test')) {
|
||||
// In case this needs to be found again: window.location.href = BACKBONE
|
||||
window.history.replaceState({}, null, '/');
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ NEWSBLUR.Welcome = Backbone.View.extend({
|
|||
this.flags.on_header_caption = true;
|
||||
this.flags.on_signin = true;
|
||||
|
||||
var add_url = $.getQueryString('add') || $.getQueryString('url');
|
||||
if (add_url) {
|
||||
this.$("input[name=next]").val("/?add=" + add_url);
|
||||
}
|
||||
|
||||
this.$el.scrollTo(0, 500, {queue: false, easing: 'easeInOutQuint'});
|
||||
|
||||
_.delay(_.bind(function() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "Servers used in running NewsBlur",
|
||||
"main": "favicons.js",
|
||||
"dependencies": {
|
||||
"@postlight/mercury-parser": "^2.1.1",
|
||||
"@postlight/mercury-parser": "^2.2.0",
|
||||
"connect-busboy": "^0.0.2",
|
||||
"esm": "^3.2.22",
|
||||
"express": "^4.16.4",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ NEWSBLUR_URL = 'http://www.newsblur.com'
|
|||
IMAGES_URL = 'https://imageproxy.newsblur.com'
|
||||
SECRET_KEY = 'YOUR_SECRET_KEY'
|
||||
IMAGES_SECRET_KEY = "YOUR_SECRET_IMAGE_KEY"
|
||||
DNSIMPLE_TOKEN = "YOUR_DNSIMPLE_TOKEN"
|
||||
RECAPTCHA_SECRET_KEY = "YOUR_RECAPTCHA_KEY"
|
||||
YOUTUBE_API_KEY = "YOUR_YOUTUBE_API_KEY"
|
||||
IMAGES_SECRET_KEY = "YOUR_IMAGES_SECRET_KEY"
|
||||
|
||||
# ===================
|
||||
# = Global Settings =
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@
|
|||
{{ login_form.password.label_tag }}
|
||||
{{ login_form.password }}
|
||||
</div>
|
||||
|
||||
{{ login_form.add }}
|
||||
<input name="submit" type="submit" class="NB-modal-submit-button NB-modal-submit-green" value="log in" />
|
||||
<input type="hidden" name="next" value="/" />
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1122,6 +1122,38 @@
|
|||
desc: "Object with folders and feed ids."
|
||||
required: true
|
||||
example: "[12, 24, 36, <br>{'Blogs': [56, 67, 78,<br />{'Photoblogs': [42]}]}]"
|
||||
|
||||
- url: /reader/save_search
|
||||
method: POST
|
||||
short_desc: "Add a new saved search feed."
|
||||
long_desc:
|
||||
- "Add a new saved search feed, which is a shortcut to a search query on a specific feed."
|
||||
params:
|
||||
- key: feed_id
|
||||
desc: "A single feed id of feed to search."
|
||||
required: true
|
||||
example: "42"
|
||||
- key: query
|
||||
desc: >
|
||||
Search for a keyword or phrase in the feed. Note that only premium users can search feeds.
|
||||
required: true
|
||||
example: "pizza"
|
||||
|
||||
- url: /reader/delete_search
|
||||
method: POST
|
||||
short_desc: "Removes a saved search feed."
|
||||
long_desc:
|
||||
- "Deletes a saved search feed."
|
||||
params:
|
||||
- key: feed_id
|
||||
desc: "A single feed id of feed to search."
|
||||
required: true
|
||||
example: "42"
|
||||
- key: query
|
||||
desc: >
|
||||
The query originally used to save the search.
|
||||
required: true
|
||||
example: "pizza"
|
||||
|
||||
|
||||
- "intelligence classifiers":
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class FetchFeed:
|
|||
(self.feed.log_title[:30], address))
|
||||
return FEED_ERRHTTP, None
|
||||
self.fpf = feedparser.parse(youtube_feed)
|
||||
elif re.match(r'(https?)?://twitter.com/\w+/?$', qurl(address, remove=['_'])):
|
||||
elif re.match(r'(https?)?://twitter.com/\w+/?', qurl(address, remove=['_'])):
|
||||
twitter_feed = self.fetch_twitter(address)
|
||||
if not twitter_feed:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter fetch failed: %s' %
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@ def main():
|
|||
device, size, used, available, percent, mountpoint = output.split("\n")[1].split()
|
||||
hostname = socket.gethostname()
|
||||
percent = int(percent.strip('%'))
|
||||
admin_email = settings.ADMINS[0][1]
|
||||
|
||||
if percent > 95:
|
||||
requests.post(
|
||||
"https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME,
|
||||
auth=("api", settings.MAILGUN_ACCESS_KEY),
|
||||
data={"from": "NewsBlur Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
|
||||
"to": [settings.ADMINS[0][1]],
|
||||
data={"from": "NewsBlur Disk Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
|
||||
"to": [admin_email],
|
||||
"subject": "%s hit %s%% disk usage!" % (hostname, percent),
|
||||
"text": "Usage on %s: %s" % (hostname, output)})
|
||||
print " ---> Disk usage is NOT fine: %s / %s%% used" % (hostname, percent)
|
||||
else:
|
||||
print " ---> Disk usage is fine: %s / %s%% used" % (hostname, percent)
|
||||
|
||||
|
|
|
|||
40
utils/monitor_newsletter_delivery.py
Executable file
40
utils/monitor_newsletter_delivery.py
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/srv/newsblur/venv/newsblur/bin/python
|
||||
|
||||
import sys
|
||||
sys.path.append('/srv/newsblur')
|
||||
|
||||
import subprocess
|
||||
import requests
|
||||
import settings
|
||||
import socket
|
||||
|
||||
def main():
|
||||
df = subprocess.Popen(["df", "/"], stdout=subprocess.PIPE)
|
||||
output = df.communicate()[0]
|
||||
device, size, used, available, percent, mountpoint = output.split("\n")[1].split()
|
||||
hostname = socket.gethostname()
|
||||
admin_email = settings.ADMINS[0][1]
|
||||
|
||||
r = requests.get("https://api.mailgun.net/v3/newsletters.newsblur.com/stats/total",
|
||||
auth=("api", settings.MAILGUN_ACCESS_KEY),
|
||||
params={"event": ["accepted", "delivered", "failed"],
|
||||
"duration": "2h"})
|
||||
stats = r.json()['stats'][0]
|
||||
delivered = stats['delivered']['total']
|
||||
accepted = stats['delivered']['total']
|
||||
bounced = stats['failed']['permanent']['total'] + stats['failed']['temporary']['total']
|
||||
|
||||
if bounced / float(delivered) > 0.5:
|
||||
requests.post(
|
||||
"https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME,
|
||||
auth=("api", settings.MAILGUN_ACCESS_KEY),
|
||||
data={"from": "NewsBlur Newsletter Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
|
||||
"to": [admin_email],
|
||||
"subject": "%s newsletters bounced: %s > %s > %s" % (hostname, accepted, delivered, bounced),
|
||||
"text": "Newsletters are not being delivered! %s delivered, %s bounced" % (delivered, bounced)})
|
||||
print " ---> %s newsletters bounced: %s > %s > %s" % (hostname, accepted, delivered, bounced)
|
||||
else:
|
||||
print " ---> %s newsletters OK: %s > %s > %s" % (hostname, accepted, delivered, bounced)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
44
utils/monitor_task_fetches.py
Executable file
44
utils/monitor_task_fetches.py
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/srv/newsblur/venv/newsblur/bin/python
|
||||
|
||||
import sys
|
||||
sys.path.append('/srv/newsblur')
|
||||
|
||||
import subprocess
|
||||
import requests
|
||||
import settings
|
||||
import socket
|
||||
import pymongo
|
||||
|
||||
def main():
|
||||
df = subprocess.Popen(["df", "/"], stdout=subprocess.PIPE)
|
||||
output = df.communicate()[0]
|
||||
device, size, used, available, percent, mountpoint = output.split("\n")[1].split()
|
||||
hostname = socket.gethostname()
|
||||
percent = int(percent.strip('%'))
|
||||
admin_email = settings.ADMINS[0][1]
|
||||
failed = False
|
||||
feeds_fetched = 0
|
||||
|
||||
try:
|
||||
client = pymongo.MongoClient('mongodb://%s' % settings.MONGO_DB['host'])
|
||||
feeds_fetched = client.newsblur.statistics.find_one({"key": "feeds_fetched"})['value']
|
||||
except Exception, e:
|
||||
failed = e
|
||||
|
||||
if feeds_fetched < 5000000:
|
||||
failed = True
|
||||
|
||||
if failed:
|
||||
requests.post(
|
||||
"https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME,
|
||||
auth=("api", settings.MAILGUN_ACCESS_KEY),
|
||||
data={"from": "NewsBlur Task Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
|
||||
"to": [admin_email],
|
||||
"subject": "%s feeds fetched falling: %s" % (hostname, feeds_fetched),
|
||||
"text": "Feed fetches are falling (%s): %s" % (hostname, feeds_fetched, failed)})
|
||||
print(" ---> Feeds fetched falling! %s" % (feeds_fetched))
|
||||
else:
|
||||
print(" ---> Feeds fetched OK: %s" % (feeds_fetched))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -18,25 +18,38 @@ class TwitterFetcher:
|
|||
self.options = options or {}
|
||||
|
||||
def fetch(self, address=None):
|
||||
data = {}
|
||||
if not address:
|
||||
address = self.feed.feed_address
|
||||
self.address = address
|
||||
twitter_user = None
|
||||
|
||||
username = self.extract_username()
|
||||
if not username:
|
||||
return
|
||||
|
||||
twitter_user = self.fetch_user(username)
|
||||
if not twitter_user:
|
||||
return
|
||||
if '/lists/' in address:
|
||||
list_id = self.extract_list_id()
|
||||
if not list_id:
|
||||
return
|
||||
|
||||
tweets = self.user_timeline(twitter_user)
|
||||
tweets, list_info = self.fetch_list_timeline(list_id)
|
||||
if not tweets:
|
||||
return
|
||||
|
||||
data['title'] = "%s on Twitter" % list_info.full_name
|
||||
data['link'] = "https://twitter.com%s" % list_info.uri
|
||||
data['description'] = "%s on Twitter" % list_info.full_name
|
||||
else:
|
||||
username = self.extract_username()
|
||||
if not username:
|
||||
return
|
||||
|
||||
data = {}
|
||||
data['title'] = "%s on Twitter" % username
|
||||
data['link'] = "https://twitter.com/%s" % username
|
||||
data['description'] = "%s on Twitter" % username
|
||||
twitter_user = self.fetch_user(username)
|
||||
if not twitter_user:
|
||||
return
|
||||
tweets = self.user_timeline(twitter_user)
|
||||
|
||||
data['title'] = "%s on Twitter" % username
|
||||
data['link'] = "https://twitter.com/%s" % username
|
||||
data['description'] = "%s on Twitter" % username
|
||||
|
||||
data['lastBuildDate'] = datetime.datetime.utcnow()
|
||||
data['generator'] = 'NewsBlur Twitter API Decrapifier - %s' % settings.NEWSBLUR_URL
|
||||
data['docs'] = None
|
||||
|
|
@ -52,7 +65,7 @@ class TwitterFetcher:
|
|||
def extract_username(self):
|
||||
username = None
|
||||
try:
|
||||
username_groups = re.search('twitter.com/(\w+)/?', self.address)
|
||||
username_groups = re.search('twitter.com/(\w+)/?$', self.address)
|
||||
if not username_groups:
|
||||
return
|
||||
username = username_groups.group(1)
|
||||
|
|
@ -60,8 +73,20 @@ class TwitterFetcher:
|
|||
return
|
||||
|
||||
return username
|
||||
|
||||
def fetch_user(self, username):
|
||||
|
||||
def extract_list_id(self):
|
||||
list_id = None
|
||||
try:
|
||||
list_groups = re.search('twitter.com/i/lists/(\w+)/?', self.address)
|
||||
if not list_groups:
|
||||
return
|
||||
list_id = list_groups.group(1)
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
return list_id
|
||||
|
||||
def twitter_api(self):
|
||||
twitter_api = None
|
||||
social_services = None
|
||||
if self.options.get('requesting_user_id', None):
|
||||
|
|
@ -97,6 +122,13 @@ class TwitterFetcher:
|
|||
(self.feed.log_title[:30], self.address, usersubs[0].user.username))
|
||||
return
|
||||
|
||||
return twitter_api
|
||||
|
||||
def fetch_user(self, username):
|
||||
twitter_api = self.twitter_api()
|
||||
if not twitter_api:
|
||||
return
|
||||
|
||||
try:
|
||||
twitter_user = twitter_api.get_user(username)
|
||||
except TypeError, e:
|
||||
|
|
@ -170,6 +202,89 @@ class TwitterFetcher:
|
|||
if not tweets:
|
||||
return []
|
||||
return tweets
|
||||
|
||||
def fetch_list_timeline(self, list_id):
|
||||
twitter_api = self.twitter_api()
|
||||
if not twitter_api:
|
||||
return
|
||||
|
||||
try:
|
||||
list_timeline = twitter_api.list_timeline(list_id=list_id, tweet_mode='extended')
|
||||
except TypeError, e:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter list fetch failed, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: %s" % (e))
|
||||
return
|
||||
except tweepy.error.TweepError, e:
|
||||
message = str(e).lower()
|
||||
if ((len(e.args) >= 2 and e.args[2] == 63) or
|
||||
('temporarily locked' in message)):
|
||||
# Suspended
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter failed, user suspended, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: User suspended")
|
||||
return
|
||||
elif 'suspended' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: User suspended")
|
||||
return
|
||||
elif 'expired token' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: Expired token")
|
||||
social_services.disconnect_twitter()
|
||||
return
|
||||
elif 'not found' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: User not found")
|
||||
return
|
||||
elif 'over capacity' in message or 'Max retries' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(460, "Twitter Error: Over capacity")
|
||||
return
|
||||
else:
|
||||
raise e
|
||||
|
||||
list_info = twitter_api.get_list(list_id=list_id)
|
||||
|
||||
if not list_timeline:
|
||||
return [], list_info
|
||||
return list_timeline, list_info
|
||||
|
||||
def tweets_from_list(self, list_timeline):
|
||||
try:
|
||||
tweets = twitter_user.timeline(tweet_mode='extended')
|
||||
except tweepy.error.TweepError, e:
|
||||
message = str(e).lower()
|
||||
if 'not authorized' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter timeline failed, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: Not authorized")
|
||||
return []
|
||||
elif 'user not found' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: User not found")
|
||||
return []
|
||||
elif '429' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter rate limited: %s: %s' %
|
||||
(self.feed.log_title[:30], self.address, e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: Rate limited")
|
||||
return []
|
||||
elif 'blocked from viewing' in message:
|
||||
logging.debug(u' ***> [%-30s] ~FRTwitter user blocked, ignoring: %s' %
|
||||
(self.feed.log_title[:30], e))
|
||||
self.feed.save_feed_history(560, "Twitter Error: Blocked from viewing")
|
||||
return []
|
||||
else:
|
||||
raise e
|
||||
|
||||
if not tweets:
|
||||
return []
|
||||
return tweets
|
||||
|
||||
def tweet_story(self, user_tweet):
|
||||
categories = set()
|
||||
|
|
|
|||
290
vendor/feedvalidator/__init__.py
vendored
290
vendor/feedvalidator/__init__.py
vendored
|
|
@ -1,290 +0,0 @@
|
|||
"""$Id: __init__.py 699 2006-09-25 02:01:18Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 699 $"
|
||||
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
import socket
|
||||
if hasattr(socket, 'setdefaulttimeout'):
|
||||
socket.setdefaulttimeout(10)
|
||||
Timeout = socket.timeout
|
||||
else:
|
||||
import timeoutsocket
|
||||
timeoutsocket.setDefaultSocketTimeout(10)
|
||||
Timeout = timeoutsocket.Timeout
|
||||
|
||||
import urllib2
|
||||
import logging
|
||||
from logging import *
|
||||
from xml.sax import SAXException
|
||||
from xml.sax.xmlreader import InputSource
|
||||
import re
|
||||
import xmlEncoding
|
||||
import mediaTypes
|
||||
from httplib import BadStatusLine
|
||||
|
||||
MAXDATALENGTH = 200000
|
||||
|
||||
def _validate(aString, firstOccurrenceOnly, loggedEvents, base, encoding, selfURIs=None):
|
||||
"""validate RSS from string, returns validator object"""
|
||||
from xml.sax import make_parser, handler
|
||||
from base import SAXDispatcher
|
||||
from exceptions import UnicodeError
|
||||
from cStringIO import StringIO
|
||||
|
||||
# By now, aString should be Unicode
|
||||
source = InputSource()
|
||||
source.setByteStream(StringIO(xmlEncoding.asUTF8(aString)))
|
||||
|
||||
validator = SAXDispatcher(base, selfURIs or [base], encoding)
|
||||
validator.setFirstOccurrenceOnly(firstOccurrenceOnly)
|
||||
|
||||
validator.loggedEvents += loggedEvents
|
||||
|
||||
# experimental RSS-Profile draft 1.06 support
|
||||
validator.setLiterals(re.findall('&(\w+);',aString))
|
||||
|
||||
xmlver = re.match("^<\?\s*xml\s+version\s*=\s*['\"]([-a-zA-Z0-9_.:]*)['\"]",aString)
|
||||
if xmlver and xmlver.group(1)<>'1.0':
|
||||
validator.log(logging.BadXmlVersion({"version":xmlver.group(1)}))
|
||||
|
||||
try:
|
||||
from xml.sax.expatreader import ExpatParser
|
||||
class fake_dtd_parser(ExpatParser):
|
||||
def reset(self):
|
||||
ExpatParser.reset(self)
|
||||
self._parser.UseForeignDTD(1)
|
||||
parser = fake_dtd_parser()
|
||||
except:
|
||||
parser = make_parser()
|
||||
|
||||
parser.setFeature(handler.feature_namespaces, 1)
|
||||
parser.setContentHandler(validator)
|
||||
parser.setErrorHandler(validator)
|
||||
parser.setEntityResolver(validator)
|
||||
if hasattr(parser, '_ns_stack'):
|
||||
# work around bug in built-in SAX parser (doesn't recognize xml: namespace)
|
||||
# PyXML doesn't have this problem, and it doesn't have _ns_stack either
|
||||
parser._ns_stack.append({'http://www.w3.org/XML/1998/namespace':'xml'})
|
||||
|
||||
def xmlvalidate(log):
|
||||
import libxml2
|
||||
from StringIO import StringIO
|
||||
from random import random
|
||||
|
||||
prefix="...%s..." % str(random()).replace('0.','')
|
||||
msg=[]
|
||||
libxml2.registerErrorHandler(lambda msg,str: msg.append(str), msg)
|
||||
|
||||
input = libxml2.inputBuffer(StringIO(xmlEncoding.asUTF8(aString)))
|
||||
reader = input.newTextReader(prefix)
|
||||
reader.SetParserProp(libxml2.PARSER_VALIDATE, 1)
|
||||
ret = reader.Read()
|
||||
while ret == 1: ret = reader.Read()
|
||||
|
||||
msg=''.join(msg)
|
||||
for line in msg.splitlines():
|
||||
if line.startswith(prefix): log(line.split(':',4)[-1].strip())
|
||||
validator.xmlvalidator=xmlvalidate
|
||||
|
||||
try:
|
||||
parser.parse(source)
|
||||
except SAXException:
|
||||
pass
|
||||
except UnicodeError:
|
||||
import sys
|
||||
exctype, value = sys.exc_info()[:2]
|
||||
validator.log(logging.UnicodeError({"exception":value}))
|
||||
|
||||
if validator.getFeedType() == TYPE_RSS1:
|
||||
try:
|
||||
from rdflib.syntax.parsers.RDFXMLHandler import RDFXMLHandler
|
||||
|
||||
class Handler(RDFXMLHandler):
|
||||
ns_prefix_map = {}
|
||||
prefix_ns_map = {}
|
||||
def add(self, triple): pass
|
||||
def __init__(self, dispatcher):
|
||||
RDFXMLHandler.__init__(self, self)
|
||||
self.dispatcher=dispatcher
|
||||
def error(self, message):
|
||||
self.dispatcher.log(InvalidRDF({"message": message}))
|
||||
|
||||
source.getByteStream().reset()
|
||||
parser.reset()
|
||||
parser.setContentHandler(Handler(parser.getContentHandler()))
|
||||
parser.setErrorHandler(handler.ErrorHandler())
|
||||
parser.parse(source)
|
||||
except:
|
||||
pass
|
||||
|
||||
return validator
|
||||
|
||||
def validateStream(aFile, firstOccurrenceOnly=0, contentType=None, base=""):
|
||||
loggedEvents = []
|
||||
|
||||
if contentType:
|
||||
(mediaType, charset) = mediaTypes.checkValid(contentType, loggedEvents)
|
||||
else:
|
||||
(mediaType, charset) = (None, None)
|
||||
|
||||
rawdata = aFile.read(MAXDATALENGTH)
|
||||
if aFile.read(1):
|
||||
raise ValidationFailure(logging.ValidatorLimit({'limit': 'feed length > ' + str(MAXDATALENGTH) + ' bytes'}))
|
||||
|
||||
encoding, rawdata = xmlEncoding.decode(mediaType, charset, rawdata, loggedEvents, fallback='utf-8')
|
||||
|
||||
validator = _validate(rawdata, firstOccurrenceOnly, loggedEvents, base, encoding)
|
||||
|
||||
if mediaType and validator.feedType:
|
||||
mediaTypes.checkAgainstFeedType(mediaType, validator.feedType, validator.loggedEvents)
|
||||
|
||||
return {"feedType":validator.feedType, "loggedEvents":validator.loggedEvents}
|
||||
|
||||
def validateString(aString, firstOccurrenceOnly=0, fallback=None, base=""):
|
||||
loggedEvents = []
|
||||
if type(aString) != unicode:
|
||||
encoding, aString = xmlEncoding.decode("", None, aString, loggedEvents, fallback)
|
||||
else:
|
||||
encoding = "utf-8" # setting a sane (?) default
|
||||
|
||||
if aString is not None:
|
||||
validator = _validate(aString, firstOccurrenceOnly, loggedEvents, base, encoding)
|
||||
return {"feedType":validator.feedType, "loggedEvents":validator.loggedEvents}
|
||||
else:
|
||||
return {"loggedEvents": loggedEvents}
|
||||
|
||||
def validateURL(url, firstOccurrenceOnly=1, wantRawData=0):
|
||||
"""validate RSS from URL, returns events list, or (events, rawdata) tuple"""
|
||||
loggedEvents = []
|
||||
request = urllib2.Request(url)
|
||||
request.add_header("Accept-encoding", "gzip, deflate")
|
||||
request.add_header("User-Agent", "FeedValidator/1.3")
|
||||
usock = None
|
||||
try:
|
||||
try:
|
||||
usock = urllib2.urlopen(request)
|
||||
rawdata = usock.read(MAXDATALENGTH)
|
||||
if usock.read(1):
|
||||
raise ValidationFailure(logging.ValidatorLimit({'limit': 'feed length > ' + str(MAXDATALENGTH) + ' bytes'}))
|
||||
|
||||
# check for temporary redirects
|
||||
if usock.geturl()<>request.get_full_url():
|
||||
from httplib import HTTPConnection
|
||||
spliturl=url.split('/',3)
|
||||
if spliturl[0]=="http:":
|
||||
conn=HTTPConnection(spliturl[2])
|
||||
conn.request("GET",'/'+spliturl[3].split("#",1)[0])
|
||||
resp=conn.getresponse()
|
||||
if resp.status<>301:
|
||||
loggedEvents.append(TempRedirect({}))
|
||||
|
||||
except BadStatusLine, status:
|
||||
raise ValidationFailure(logging.HttpError({'status': status.__class__}))
|
||||
|
||||
except urllib2.HTTPError, status:
|
||||
rawdata = status.read()
|
||||
lastline = rawdata.strip().split('\n')[-1].strip()
|
||||
if lastline in ['</rss>','</feed>','</rdf:RDF>']:
|
||||
loggedEvents.append(logging.HttpError({'status': status}))
|
||||
usock = status
|
||||
else:
|
||||
raise ValidationFailure(logging.HttpError({'status': status}))
|
||||
except urllib2.URLError, x:
|
||||
raise ValidationFailure(logging.HttpError({'status': x.reason}))
|
||||
except Timeout, x:
|
||||
raise ValidationFailure(logging.IOError({"message": 'Server timed out', "exception":x}))
|
||||
|
||||
if usock.headers.get('content-encoding', None) == None:
|
||||
loggedEvents.append(Uncompressed({}))
|
||||
|
||||
if usock.headers.get('content-encoding', None) == 'gzip':
|
||||
import gzip, StringIO
|
||||
try:
|
||||
rawdata = gzip.GzipFile(fileobj=StringIO.StringIO(rawdata)).read()
|
||||
except:
|
||||
import sys
|
||||
exctype, value = sys.exc_info()[:2]
|
||||
event=logging.IOError({"message": 'Server response declares Content-Encoding: gzip', "exception":value})
|
||||
raise ValidationFailure(event)
|
||||
|
||||
if usock.headers.get('content-encoding', None) == 'deflate':
|
||||
import zlib
|
||||
try:
|
||||
rawdata = zlib.decompress(rawdata, -zlib.MAX_WBITS)
|
||||
except:
|
||||
import sys
|
||||
exctype, value = sys.exc_info()[:2]
|
||||
event=logging.IOError({"message": 'Server response declares Content-Encoding: deflate', "exception":value})
|
||||
raise ValidationFailure(event)
|
||||
|
||||
mediaType = None
|
||||
charset = None
|
||||
|
||||
# Is the Content-Type correct?
|
||||
contentType = usock.headers.get('content-type', None)
|
||||
if contentType:
|
||||
(mediaType, charset) = mediaTypes.checkValid(contentType, loggedEvents)
|
||||
|
||||
# Check for malformed HTTP headers
|
||||
for (h, v) in usock.headers.items():
|
||||
if (h.find(' ') >= 0):
|
||||
loggedEvents.append(HttpProtocolError({'header': h}))
|
||||
|
||||
selfURIs = [request.get_full_url()]
|
||||
baseURI = usock.geturl()
|
||||
if not baseURI in selfURIs: selfURIs.append(baseURI)
|
||||
|
||||
# Get baseURI from content-location and/or redirect information
|
||||
if usock.headers.get('content-location', None):
|
||||
from urlparse import urljoin
|
||||
baseURI=urljoin(baseURI,usock.headers.get('content-location', ""))
|
||||
elif usock.headers.get('location', None):
|
||||
from urlparse import urljoin
|
||||
baseURI=urljoin(baseURI,usock.headers.get('location', ""))
|
||||
|
||||
if not baseURI in selfURIs: selfURIs.append(baseURI)
|
||||
usock.close()
|
||||
usock = None
|
||||
|
||||
mediaTypes.contentSniffing(mediaType, rawdata, loggedEvents)
|
||||
|
||||
encoding, rawdata = xmlEncoding.decode(mediaType, charset, rawdata, loggedEvents, fallback='utf-8')
|
||||
|
||||
if rawdata is None:
|
||||
return {'loggedEvents': loggedEvents}
|
||||
|
||||
rawdata = rawdata.replace('\r\n', '\n').replace('\r', '\n') # normalize EOL
|
||||
validator = _validate(rawdata, firstOccurrenceOnly, loggedEvents, baseURI, encoding, selfURIs)
|
||||
|
||||
# Warn about mismatches between media type and feed version
|
||||
if mediaType and validator.feedType:
|
||||
mediaTypes.checkAgainstFeedType(mediaType, validator.feedType, validator.loggedEvents)
|
||||
|
||||
params = {"feedType":validator.feedType, "loggedEvents":validator.loggedEvents}
|
||||
if wantRawData:
|
||||
params['rawdata'] = rawdata
|
||||
return params
|
||||
|
||||
finally:
|
||||
try:
|
||||
if usock: usock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
__all__ = ['base',
|
||||
'channel',
|
||||
'compatibility',
|
||||
'image',
|
||||
'item',
|
||||
'logging',
|
||||
'rdf',
|
||||
'root',
|
||||
'rss',
|
||||
'skipHours',
|
||||
'textInput',
|
||||
'util',
|
||||
'validators',
|
||||
'validateURL',
|
||||
'validateString']
|
||||
53
vendor/feedvalidator/author.py
vendored
53
vendor/feedvalidator/author.py
vendored
|
|
@ -1,53 +0,0 @@
|
|||
"""$Id: author.py 699 2006-09-25 02:01:18Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 699 $"
|
||||
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
from base import validatorBase
|
||||
from validators import *
|
||||
|
||||
#
|
||||
# author element.
|
||||
#
|
||||
class author(validatorBase):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', u'parseType')]
|
||||
|
||||
def validate(self):
|
||||
if not "name" in self.children and not "atom_name" in self.children:
|
||||
self.log(MissingElement({"parent":self.name, "element":"name"}))
|
||||
|
||||
def do_name(self):
|
||||
return nonhtml(), nonemail(), nonblank(), noduplicates()
|
||||
|
||||
def do_email(self):
|
||||
return addr_spec(), noduplicates()
|
||||
|
||||
def do_uri(self):
|
||||
return nonblank(), rfc3987(), nows(), noduplicates()
|
||||
|
||||
def do_foaf_workplaceHomepage(self):
|
||||
return rdfResourceURI()
|
||||
|
||||
def do_foaf_homepage(self):
|
||||
return rdfResourceURI()
|
||||
|
||||
def do_foaf_weblog(self):
|
||||
return rdfResourceURI()
|
||||
|
||||
def do_foaf_plan(self):
|
||||
return text()
|
||||
|
||||
def do_foaf_firstName(self):
|
||||
return text()
|
||||
|
||||
def do_xhtml_div(self):
|
||||
from content import diveater
|
||||
return diveater()
|
||||
|
||||
# RSS/Atom support
|
||||
do_atom_name = do_name
|
||||
do_atom_email = do_email
|
||||
do_atom_uri = do_uri
|
||||
511
vendor/feedvalidator/base.py
vendored
511
vendor/feedvalidator/base.py
vendored
|
|
@ -1,511 +0,0 @@
|
|||
"""$Id: base.py 744 2007-03-24 11:57:16Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 744 $"
|
||||
__date__ = "$Date: 2007-03-24 11:57:16 +0000 (Sat, 24 Mar 2007) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
from xml.sax.handler import ContentHandler
|
||||
from xml.sax.xmlreader import Locator
|
||||
from logging import NonCanonicalURI, NotUTF8
|
||||
import re
|
||||
|
||||
# references:
|
||||
# http://web.resource.org/rss/1.0/modules/standard.html
|
||||
# http://web.resource.org/rss/1.0/modules/proposed.html
|
||||
# http://dmoz.org/Reference/Libraries/Library_and_Information_Science/Technical_Services/Cataloguing/Metadata/RDF/Applications/RSS/Specifications/RSS1.0_Modules/
|
||||
namespaces = {
|
||||
"http://www.bloglines.com/about/specs/fac-1.0": "access",
|
||||
"http://webns.net/mvcb/": "admin",
|
||||
"http://purl.org/rss/1.0/modules/aggregation/": "ag",
|
||||
"http://purl.org/rss/1.0/modules/annotate/": "annotate",
|
||||
"http://media.tangent.org/rss/1.0/": "audio",
|
||||
"http://backend.userland.com/blogChannelModule": "blogChannel",
|
||||
"http://web.resource.org/cc/": "cc",
|
||||
"http://www.microsoft.com/schemas/rss/core/2005": "cf",
|
||||
"http://backend.userland.com/creativeCommonsRssModule": "creativeCommons",
|
||||
"http://purl.org/rss/1.0/modules/company": "company",
|
||||
"http://purl.org/rss/1.0/modules/content/": "content",
|
||||
"http://my.theinfo.org/changed/1.0/rss/": "cp",
|
||||
"http://purl.org/dc/elements/1.1/": "dc",
|
||||
"http://purl.org/dc/terms/": "dcterms",
|
||||
"http://purl.org/rss/1.0/modules/email/": "email",
|
||||
"http://purl.org/rss/1.0/modules/event/": "ev",
|
||||
"http://www.w3.org/2003/01/geo/wgs84_pos#": "geo",
|
||||
"http://geourl.org/rss/module/": "geourl",
|
||||
"http://www.georss.org/georss": "georss",
|
||||
"http://www.opengis.net/gml": "gml",
|
||||
"http://postneo.com/icbm": "icbm",
|
||||
"http://purl.org/rss/1.0/modules/image/": "image",
|
||||
"http://www.itunes.com/dtds/podcast-1.0.dtd": "itunes",
|
||||
"http://xmlns.com/foaf/0.1/": "foaf",
|
||||
"http://purl.org/rss/1.0/modules/link/": "l",
|
||||
"http://search.yahoo.com/mrss/": "media",
|
||||
"http://a9.com/-/spec/opensearch/1.1/": "opensearch",
|
||||
"http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
|
||||
"http://www.w3.org/2000/01/rdf-schema#": "rdfs",
|
||||
"http://purl.org/rss/1.0/modules/reference/": "ref",
|
||||
"http://purl.org/rss/1.0/modules/richequiv/": "reqv",
|
||||
"http://purl.org/rss/1.0/modules/rss091#": "rss091",
|
||||
"http://purl.org/rss/1.0/modules/search/": "search",
|
||||
"http://purl.org/rss/1.0/modules/slash/": "slash",
|
||||
"http://purl.org/rss/1.0/modules/servicestatus/": "ss",
|
||||
"http://hacks.benhammersley.com/rss/streaming/": "str",
|
||||
"http://purl.org/rss/1.0/modules/subscription/": "sub",
|
||||
"http://purl.org/rss/1.0/modules/syndication/": "sy",
|
||||
"http://purl.org/rss/1.0/modules/taxonomy/": "taxo",
|
||||
"http://purl.org/rss/1.0/modules/threading/": "thr",
|
||||
"http://purl.org/syndication/thread/1.0": "thr",
|
||||
"http://madskills.com/public/xml/rss/module/trackback/": "trackback",
|
||||
"http://wellformedweb.org/CommentAPI/": "wfw",
|
||||
"http://purl.org/rss/1.0/modules/wiki/": "wiki",
|
||||
"http://www.usemod.com/cgi-bin/mb.pl?ModWiki": "wiki",
|
||||
"http://schemas.xmlsoap.org/soap/envelope/": "soap",
|
||||
"http://www.w3.org/2005/Atom": "atom",
|
||||
"http://www.w3.org/1999/xhtml": "xhtml",
|
||||
"http://my.netscape.com/rdf/simple/0.9/": "rss090",
|
||||
"http://purl.org/net/rss1.1#": "rss11",
|
||||
"http://base.google.com/ns/1.0": "g",
|
||||
"http://www.w3.org/XML/1998/namespace": "xml",
|
||||
"http://openid.net/xmlns/1.0": "openid",
|
||||
"xri://$xrd*($v*2.0)": "xrd",
|
||||
"xri://$xrds": "xrds",
|
||||
}
|
||||
|
||||
def near_miss(ns):
|
||||
try:
|
||||
return re.match(".*\w", ns).group().lower()
|
||||
except:
|
||||
return ns
|
||||
|
||||
nearly_namespaces = dict([(near_miss(u),p) for u,p in namespaces.items()])
|
||||
|
||||
stdattrs = [(u'http://www.w3.org/XML/1998/namespace', u'base'),
|
||||
(u'http://www.w3.org/XML/1998/namespace', u'lang'),
|
||||
(u'http://www.w3.org/XML/1998/namespace', u'space')]
|
||||
|
||||
#
|
||||
# From the SAX parser's point of view, this class is the one responsible for
|
||||
# handling SAX events. In actuality, all this class does is maintain a
|
||||
# pushdown stack of the *real* content handlers, and delegates sax events
|
||||
# to the current one.
|
||||
#
|
||||
class SAXDispatcher(ContentHandler):
|
||||
|
||||
firstOccurrenceOnly = 0
|
||||
|
||||
def __init__(self, base, selfURIs, encoding):
|
||||
from root import root
|
||||
ContentHandler.__init__(self)
|
||||
self.lastKnownLine = 1
|
||||
self.lastKnownColumn = 0
|
||||
self.loggedEvents = []
|
||||
self.feedType = 0
|
||||
try:
|
||||
self.xmlBase = base.encode('idna')
|
||||
except:
|
||||
self.xmlBase = base
|
||||
self.selfURIs = selfURIs
|
||||
self.encoding = encoding
|
||||
self.handler_stack=[[root(self, base)]]
|
||||
self.literal_entities=[]
|
||||
self.defaultNamespaces = []
|
||||
|
||||
# experimental RSS-Profile draft 1.06 support
|
||||
def setLiterals(self, literals):
|
||||
for literal in literals:
|
||||
if literal not in self.literal_entities:
|
||||
self.literal_entities.append(literal)
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
self.locator = locator
|
||||
ContentHandler.setDocumentLocator(self, self.locator)
|
||||
|
||||
def setFirstOccurrenceOnly(self, firstOccurrenceOnly=1):
|
||||
self.firstOccurrenceOnly = firstOccurrenceOnly
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
for handler in iter(self.handler_stack[-1]):
|
||||
handler.namespace[prefix] = uri
|
||||
if uri and len(uri.split())>1:
|
||||
from xml.sax import SAXException
|
||||
self.error(SAXException('Invalid Namespace: %s' % uri))
|
||||
if prefix in namespaces.values():
|
||||
if not namespaces.get(uri,'') == prefix and prefix:
|
||||
from logging import ReservedPrefix
|
||||
preferredURI = [key for key, value in namespaces.items() if value == prefix][0]
|
||||
self.log(ReservedPrefix({'prefix':prefix, 'ns':preferredURI}))
|
||||
elif prefix=='wiki' and uri.find('usemod')>=0:
|
||||
from logging import ObsoleteWikiNamespace
|
||||
self.log(ObsoleteWikiNamespace({'preferred':namespaces[uri], 'ns':uri}))
|
||||
elif namespaces.has_key(uri):
|
||||
if not namespaces[uri] == prefix and prefix:
|
||||
from logging import NonstdPrefix
|
||||
self.log(NonstdPrefix({'preferred':namespaces[uri], 'ns':uri}))
|
||||
|
||||
def namespaceFor(self, prefix):
|
||||
return None
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
self.lastKnownLine = self.locator.getLineNumber()
|
||||
self.lastKnownColumn = self.locator.getColumnNumber()
|
||||
qname, name = name
|
||||
for handler in iter(self.handler_stack[-1]):
|
||||
handler.startElementNS(name, qname, attrs)
|
||||
|
||||
if len(attrs):
|
||||
present = attrs.getNames()
|
||||
unexpected = filter(lambda x: x not in stdattrs, present)
|
||||
for handler in iter(self.handler_stack[-1]):
|
||||
ean = handler.getExpectedAttrNames()
|
||||
if ean: unexpected = filter(lambda x: x not in ean, unexpected)
|
||||
for u in unexpected:
|
||||
if u[0] and near_miss(u[0]) not in nearly_namespaces:
|
||||
feedtype=self.getFeedType()
|
||||
if (not qname) and feedtype and (feedtype==TYPE_RSS2):
|
||||
from logging import InvalidExtensionAttr
|
||||
self.log(InvalidExtensionAttr({"attribute":u, "element":name}))
|
||||
continue
|
||||
from logging import UnexpectedAttribute
|
||||
if not u[0]: u=u[1]
|
||||
self.log(UnexpectedAttribute({"parent":name, "attribute":u, "element":name}))
|
||||
|
||||
def resolveEntity(self, publicId, systemId):
|
||||
if not publicId and not systemId:
|
||||
import cStringIO
|
||||
return cStringIO.StringIO()
|
||||
|
||||
try:
|
||||
def log(exception):
|
||||
from logging import SAXError
|
||||
self.log(SAXError({'exception':str(exception)}))
|
||||
if self.xmlvalidator:
|
||||
self.xmlvalidator(log)
|
||||
self.xmlvalidator=0
|
||||
except:
|
||||
pass
|
||||
|
||||
if (publicId=='-//Netscape Communications//DTD RSS 0.91//EN' and
|
||||
systemId=='http://my.netscape.com/publish/formats/rss-0.91.dtd'):
|
||||
from logging import ValidDoctype, DeprecatedDTD
|
||||
self.log(ValidDoctype({}))
|
||||
self.log(DeprecatedDTD({}))
|
||||
else:
|
||||
from logging import ContainsSystemEntity
|
||||
self.lastKnownLine = self.locator.getLineNumber()
|
||||
self.lastKnownColumn = self.locator.getColumnNumber()
|
||||
self.log(ContainsSystemEntity({}))
|
||||
from StringIO import StringIO
|
||||
return StringIO()
|
||||
|
||||
def skippedEntity(self, name):
|
||||
from logging import ValidDoctype
|
||||
if [e for e in self.loggedEvents if e.__class__ == ValidDoctype]:
|
||||
from htmlentitydefs import name2codepoint
|
||||
if name in name2codepoint: return
|
||||
from logging import UndefinedNamedEntity
|
||||
self.log(UndefinedNamedEntity({'value':name}))
|
||||
|
||||
def characters(self, string):
|
||||
self.lastKnownLine = self.locator.getLineNumber()
|
||||
self.lastKnownColumn = self.locator.getColumnNumber()
|
||||
for handler in iter(self.handler_stack[-1]):
|
||||
handler.characters(string)
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
self.lastKnownLine = self.locator.getLineNumber()
|
||||
self.lastKnownColumn = self.locator.getColumnNumber()
|
||||
qname, name = name
|
||||
for handler in iter(self.handler_stack[-1]):
|
||||
handler.endElementNS(name, qname)
|
||||
del self.handler_stack[-1]
|
||||
|
||||
def push(self, handlers, name, attrs, parent):
|
||||
if hasattr(handlers,'__iter__'):
|
||||
for handler in iter(handlers):
|
||||
handler.setElement(name, attrs, parent)
|
||||
handler.value=""
|
||||
handler.prevalidate()
|
||||
else:
|
||||
handlers.setElement(name, attrs, parent)
|
||||
handlers.value=""
|
||||
handlers.prevalidate()
|
||||
handlers = [handlers]
|
||||
self.handler_stack.append(handlers)
|
||||
|
||||
def log(self, event, offset=(0,0)):
|
||||
def findDuplicate(self, event):
|
||||
duplicates = [e for e in self.loggedEvents if e.__class__ == event.__class__]
|
||||
if duplicates and (event.__class__ in [NonCanonicalURI]):
|
||||
return duplicates[0]
|
||||
|
||||
for dup in duplicates:
|
||||
for k, v in event.params.items():
|
||||
if k != 'value':
|
||||
if not k in dup.params or dup.params[k] != v: break
|
||||
else:
|
||||
return dup
|
||||
|
||||
if event.params.has_key('element') and event.params['element']:
|
||||
if not isinstance(event.params['element'],tuple):
|
||||
event.params['element']=':'.join(event.params['element'].split('_', 1))
|
||||
elif event.params['element'][0]==u'http://www.w3.org/XML/1998/namespace':
|
||||
event.params['element'] = 'xml:' + event.params['element'][-1]
|
||||
if self.firstOccurrenceOnly:
|
||||
dup = findDuplicate(self, event)
|
||||
if dup:
|
||||
dup.params['msgcount'] = dup.params['msgcount'] + 1
|
||||
return
|
||||
event.params['msgcount'] = 1
|
||||
try:
|
||||
line = self.locator.getLineNumber() + offset[0]
|
||||
backupline = self.lastKnownLine
|
||||
column = (self.locator.getColumnNumber() or 0) + offset[1]
|
||||
backupcolumn = self.lastKnownColumn
|
||||
except AttributeError:
|
||||
line = backupline = column = backupcolumn = 1
|
||||
event.params['line'] = line
|
||||
event.params['backupline'] = backupline
|
||||
event.params['column'] = column
|
||||
event.params['backupcolumn'] = backupcolumn
|
||||
self.loggedEvents.append(event)
|
||||
|
||||
def error(self, exception):
|
||||
from logging import SAXError
|
||||
self.log(SAXError({'exception':str(exception)}))
|
||||
raise exception
|
||||
fatalError=error
|
||||
warning=error
|
||||
|
||||
def getFeedType(self):
|
||||
return self.feedType
|
||||
|
||||
def setFeedType(self, feedType):
|
||||
self.feedType = feedType
|
||||
|
||||
#
|
||||
# This base class for content handlers keeps track of such administrative
|
||||
# details as the parent of the current element, and delegating both log
|
||||
# and push events back up the stack. It will also concatenate up all of
|
||||
# the SAX events associated with character data into a value, handing such
|
||||
# things as CDATA and entities.
|
||||
#
|
||||
# Subclasses are expected to declare "do_name" methods for every
|
||||
# element that they support. These methods are expected to return the
|
||||
# appropriate handler for the element.
|
||||
#
|
||||
# The name of the element and the names of the children processed so
|
||||
# far are also maintained.
|
||||
#
|
||||
# Hooks are also provided for subclasses to do "prevalidation" and
|
||||
# "validation".
|
||||
#
|
||||
from logging import TYPE_RSS2
|
||||
|
||||
class validatorBase(ContentHandler):
|
||||
|
||||
def __init__(self):
|
||||
ContentHandler.__init__(self)
|
||||
self.value = ""
|
||||
self.attrs = None
|
||||
self.children = []
|
||||
self.isValid = 1
|
||||
self.name = None
|
||||
self.itunes = False
|
||||
self.namespace = {}
|
||||
|
||||
def setElement(self, name, attrs, parent):
|
||||
self.name = name
|
||||
self.attrs = attrs
|
||||
self.parent = parent
|
||||
self.dispatcher = parent.dispatcher
|
||||
self.line = self.dispatcher.locator.getLineNumber()
|
||||
self.col = self.dispatcher.locator.getColumnNumber()
|
||||
self.xmlLang = parent.xmlLang
|
||||
|
||||
if attrs and attrs.has_key((u'http://www.w3.org/XML/1998/namespace', u'base')):
|
||||
self.xmlBase=attrs.getValue((u'http://www.w3.org/XML/1998/namespace', u'base'))
|
||||
from validators import rfc3987
|
||||
self.validate_attribute((u'http://www.w3.org/XML/1998/namespace',u'base'),
|
||||
rfc3987)
|
||||
from urlparse import urljoin
|
||||
self.xmlBase = urljoin(parent.xmlBase, self.xmlBase)
|
||||
else:
|
||||
self.xmlBase = parent.xmlBase
|
||||
|
||||
return self
|
||||
|
||||
def simplename(self, name):
|
||||
if not name[0]: return name[1]
|
||||
return namespaces.get(name[0], name[0]) + ":" + name[1]
|
||||
|
||||
def namespaceFor(self, prefix):
|
||||
if self.namespace.has_key(prefix):
|
||||
return self.namespace[prefix]
|
||||
elif self.parent:
|
||||
return self.parent.namespaceFor(prefix)
|
||||
else:
|
||||
return None
|
||||
|
||||
def validate_attribute(self, name, rule):
|
||||
if not isinstance(rule,validatorBase): rule = rule()
|
||||
if isinstance(name,str): name = (None,name)
|
||||
rule.setElement(self.simplename(name), {}, self)
|
||||
rule.value=self.attrs.getValue(name)
|
||||
rule.validate()
|
||||
|
||||
def validate_required_attribute(self, name, rule):
|
||||
if self.attrs and self.attrs.has_key(name):
|
||||
self.validate_attribute(name, rule)
|
||||
else:
|
||||
from logging import MissingAttribute
|
||||
self.log(MissingAttribute({"attr": self.simplename(name)}))
|
||||
|
||||
def validate_optional_attribute(self, name, rule):
|
||||
if self.attrs and self.attrs.has_key(name):
|
||||
self.validate_attribute(name, rule)
|
||||
|
||||
def getExpectedAttrNames(self):
|
||||
None
|
||||
|
||||
def unknown_starttag(self, name, qname, attrs):
|
||||
from validators import any
|
||||
return any(self, name, qname, attrs)
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
if attrs.has_key((u'http://www.w3.org/XML/1998/namespace', u'lang')):
|
||||
self.xmlLang=attrs.getValue((u'http://www.w3.org/XML/1998/namespace', u'lang'))
|
||||
if self.xmlLang:
|
||||
from validators import iso639_validate
|
||||
iso639_validate(self.log, self.xmlLang, "xml:lang", name)
|
||||
|
||||
from validators import eater
|
||||
feedtype=self.getFeedType()
|
||||
if (not qname) and feedtype and (feedtype!=TYPE_RSS2):
|
||||
from logging import UndeterminableVocabulary
|
||||
self.log(UndeterminableVocabulary({"parent":self.name, "element":name, "namespace":'""'}))
|
||||
qname="null"
|
||||
if qname in self.dispatcher.defaultNamespaces: qname=None
|
||||
|
||||
nm_qname = near_miss(qname)
|
||||
if nearly_namespaces.has_key(nm_qname):
|
||||
prefix = nearly_namespaces[nm_qname]
|
||||
qname, name = None, prefix + "_" + name
|
||||
if prefix == 'itunes' and not self.itunes and not self.parent.itunes:
|
||||
if hasattr(self, 'setItunes'): self.setItunes(True)
|
||||
|
||||
# ensure all attribute namespaces are properly defined
|
||||
for (namespace,attr) in attrs.keys():
|
||||
if ':' in attr and not namespace:
|
||||
from logging import MissingNamespace
|
||||
self.log(MissingNamespace({"parent":self.name, "element":attr}))
|
||||
|
||||
if qname=='http://purl.org/atom/ns#':
|
||||
from logging import ObsoleteNamespace
|
||||
self.log(ObsoleteNamespace({"element":"feed"}))
|
||||
|
||||
for key, string in attrs.items():
|
||||
for c in string:
|
||||
if 0x80 <= ord(c) <= 0x9F or c == u'\ufffd':
|
||||
from validators import BadCharacters
|
||||
self.log(BadCharacters({"parent":name, "element":key[-1]}))
|
||||
|
||||
if qname:
|
||||
handler = self.unknown_starttag(name, qname, attrs)
|
||||
name="unknown_"+name
|
||||
else:
|
||||
try:
|
||||
self.child=name
|
||||
if name.startswith('dc_'):
|
||||
# handle "Qualified" Dublin Core
|
||||
handler = getattr(self, "do_" + name.replace("-","_").split('.')[0])()
|
||||
else:
|
||||
handler = getattr(self, "do_" + name.replace("-","_"))()
|
||||
except AttributeError:
|
||||
if name.find(':') != -1:
|
||||
from logging import MissingNamespace
|
||||
self.log(MissingNamespace({"parent":self.name, "element":name}))
|
||||
handler = eater()
|
||||
elif name.startswith('xhtml_'):
|
||||
from logging import MisplacedXHTMLContent
|
||||
self.log(MisplacedXHTMLContent({"parent": ':'.join(self.name.split("_",1)), "element":name}))
|
||||
handler = eater()
|
||||
else:
|
||||
from logging import UndefinedElement
|
||||
self.log(UndefinedElement({"parent": ':'.join(self.name.split("_",1)), "element":name}))
|
||||
handler = eater()
|
||||
|
||||
self.push(handler, name, attrs)
|
||||
|
||||
# MAP - always append name, even if already exists (we need this to
|
||||
# check for too many hour elements in skipHours, and it doesn't
|
||||
# hurt anything else)
|
||||
self.children.append(name)
|
||||
|
||||
def normalizeWhitespace(self):
|
||||
self.value = self.value.strip()
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
self.normalizeWhitespace()
|
||||
self.validate()
|
||||
if self.isValid and self.name:
|
||||
from validators import ValidElement
|
||||
self.log(ValidElement({"parent":self.parent.name, "element":name}))
|
||||
|
||||
def textOK(self):
|
||||
from validators import UnexpectedText
|
||||
self.log(UnexpectedText({"element":self.name,"parent":self.parent.name}))
|
||||
|
||||
def characters(self, string):
|
||||
if string.strip(): self.textOK()
|
||||
|
||||
line=column=0
|
||||
pc=' '
|
||||
for c in string:
|
||||
|
||||
# latin characters double encoded as utf-8
|
||||
if 0x80 <= ord(c) <= 0xBF:
|
||||
if 0xC2 <= ord(pc) <= 0xC3:
|
||||
try:
|
||||
string.encode('iso-8859-1').decode('utf-8')
|
||||
from validators import BadCharacters
|
||||
self.log(BadCharacters({"parent":self.parent.name, "element":self.name}), offset=(line,max(1,column-1)))
|
||||
except:
|
||||
pass
|
||||
pc = c
|
||||
|
||||
# win1252
|
||||
if 0x80 <= ord(c) <= 0x9F or c == u'\ufffd':
|
||||
from validators import BadCharacters
|
||||
self.log(BadCharacters({"parent":self.parent.name, "element":self.name}), offset=(line,column))
|
||||
column=column+1
|
||||
if ord(c) in (10,13):
|
||||
column=0
|
||||
line=line+1
|
||||
|
||||
self.value = self.value + string
|
||||
|
||||
def log(self, event, offset=(0,0)):
|
||||
if not event.params.has_key('element'):
|
||||
event.params['element'] = self.name
|
||||
self.dispatcher.log(event, offset)
|
||||
self.isValid = 0
|
||||
|
||||
def setFeedType(self, feedType):
|
||||
self.dispatcher.setFeedType(feedType)
|
||||
|
||||
def getFeedType(self):
|
||||
return self.dispatcher.getFeedType()
|
||||
|
||||
def push(self, handler, name, value):
|
||||
self.dispatcher.push(handler, name, value, self)
|
||||
|
||||
def leaf(self):
|
||||
from validators import text
|
||||
return text()
|
||||
|
||||
def prevalidate(self):
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
30
vendor/feedvalidator/category.py
vendored
30
vendor/feedvalidator/category.py
vendored
|
|
@ -1,30 +0,0 @@
|
|||
"""$Id: category.py 699 2006-09-25 02:01:18Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 699 $"
|
||||
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
from base import validatorBase
|
||||
from validators import *
|
||||
|
||||
#
|
||||
# author element.
|
||||
#
|
||||
class category(validatorBase, rfc3987_full, nonhtml):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(None,u'term'),(None,u'scheme'),(None,u'label')]
|
||||
|
||||
def prevalidate(self):
|
||||
self.children.append(True) # force warnings about "mixed" content
|
||||
|
||||
if not self.attrs.has_key((None,"term")):
|
||||
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"term"}))
|
||||
|
||||
if self.attrs.has_key((None,"scheme")):
|
||||
self.value=self.attrs.getValue((None,"scheme"))
|
||||
rfc3987_full.validate(self, extraParams={"element": "scheme"})
|
||||
|
||||
if self.attrs.has_key((None,"label")):
|
||||
self.value=self.attrs.getValue((None,"label"))
|
||||
nonhtml.validate(self)
|
||||
20
vendor/feedvalidator/cf.py
vendored
20
vendor/feedvalidator/cf.py
vendored
|
|
@ -1,20 +0,0 @@
|
|||
# http://msdn.microsoft.com/XML/rss/sle/default.aspx
|
||||
|
||||
from base import validatorBase
|
||||
from validators import eater, text
|
||||
|
||||
class sort(validatorBase):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(None,u'data-type'),(None,u'default'),(None,u'element'),(None, u'label'),(None,u'ns')]
|
||||
|
||||
class group(validatorBase):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(None,u'element'),(None, u'label'),(None,u'ns')]
|
||||
|
||||
class listinfo(validatorBase):
|
||||
def do_cf_sort(self):
|
||||
return sort()
|
||||
def do_cf_group(self):
|
||||
return group()
|
||||
|
||||
class treatAs(text): pass
|
||||
279
vendor/feedvalidator/channel.py
vendored
279
vendor/feedvalidator/channel.py
vendored
|
|
@ -1,279 +0,0 @@
|
|||
"""$Id: channel.py 711 2006-10-25 00:43:41Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 711 $"
|
||||
__date__ = "$Date: 2006-10-25 00:43:41 +0000 (Wed, 25 Oct 2006) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
from base import validatorBase
|
||||
from logging import *
|
||||
from validators import *
|
||||
from itunes import itunes_channel
|
||||
from extension import *
|
||||
|
||||
#
|
||||
# channel element.
|
||||
#
|
||||
class channel(validatorBase, rfc2396, extension_channel, itunes_channel):
|
||||
def __init__(self):
|
||||
self.link=None
|
||||
validatorBase.__init__(self)
|
||||
def validate(self):
|
||||
if not "description" in self.children:
|
||||
self.log(MissingDescription({"parent":self.name,"element":"description"}))
|
||||
if not "link" in self.children:
|
||||
self.log(MissingLink({"parent":self.name, "element":"link"}))
|
||||
if not "title" in self.children:
|
||||
self.log(MissingTitle({"parent":self.name, "element":"title"}))
|
||||
if not "dc_language" in self.children and not "language" in self.children:
|
||||
if not self.xmlLang:
|
||||
self.log(MissingDCLanguage({"parent":self.name, "element":"language"}))
|
||||
if self.children.count("image") > 1:
|
||||
self.log(DuplicateElement({"parent":self.name, "element":"image"}))
|
||||
if self.children.count("textInput") > 1:
|
||||
self.log(DuplicateElement({"parent":self.name, "element":"textInput"}))
|
||||
if self.children.count("skipHours") > 1:
|
||||
self.log(DuplicateElement({"parent":self.name, "element":"skipHours"}))
|
||||
if self.children.count("skipDays") > 1:
|
||||
self.log(DuplicateElement({"parent":self.name, "element":"skipDays"}))
|
||||
if self.attrs.has_key((rdfNS,"about")):
|
||||
self.value = self.attrs.getValue((rdfNS, "about"))
|
||||
rfc2396.validate(self, extraParams={"attr": "rdf:about"})
|
||||
if not "items" in self.children:
|
||||
self.log(MissingElement({"parent":self.name, "element":"items"}))
|
||||
|
||||
if self.itunes: itunes_channel.validate(self)
|
||||
|
||||
def do_image(self):
|
||||
from image import image
|
||||
return image(), noduplicates()
|
||||
|
||||
def do_textInput(self):
|
||||
from textInput import textInput
|
||||
return textInput(), noduplicates()
|
||||
|
||||
def do_textinput(self):
|
||||
if not self.attrs.has_key((rdfNS,"about")):
|
||||
# optimize for RSS 2.0. If it is not valid RDF, assume that it is
|
||||
# a simple misspelling (in other words, the error message will be
|
||||
# less than helpful on RSS 1.0 feeds.
|
||||
self.log(UndefinedElement({"parent":self.name, "element":"textinput"}))
|
||||
return eater(), noduplicates()
|
||||
|
||||
def do_link(self):
|
||||
return link(), noduplicates()
|
||||
|
||||
def do_title(self):
|
||||
return nonhtml(), noduplicates(), nonblank()
|
||||
|
||||
def do_description(self):
|
||||
return nonhtml(), noduplicates()
|
||||
|
||||
def do_blink(self):
|
||||
return blink(), noduplicates()
|
||||
|
||||
def do_atom_author(self):
|
||||
from author import author
|
||||
return author()
|
||||
|
||||
def do_atom_category(self):
|
||||
from category import category
|
||||
return category()
|
||||
|
||||
def do_atom_contributor(self):
|
||||
from author import author
|
||||
return author()
|
||||
|
||||
def do_atom_generator(self):
|
||||
from generator import generator
|
||||
return generator(), nonblank(), noduplicates()
|
||||
|
||||
def do_atom_id(self):
|
||||
return rfc2396_full(), noduplicates()
|
||||
|
||||
def do_atom_icon(self):
|
||||
return nonblank(), rfc2396(), noduplicates()
|
||||
|
||||
def do_atom_link(self):
|
||||
from link import link
|
||||
return link()
|
||||
|
||||
def do_atom_logo(self):
|
||||
return nonblank(), rfc2396(), noduplicates()
|
||||
|
||||
def do_atom_title(self):
|
||||
from content import textConstruct
|
||||
return textConstruct(), noduplicates()
|
||||
|
||||
def do_atom_subtitle(self):
|
||||
from content import textConstruct
|
||||
return textConstruct(), noduplicates()
|
||||
|
||||
def do_atom_rights(self):
|
||||
from content import textConstruct
|
||||
return textConstruct(), noduplicates()
|
||||
|
||||
def do_atom_updated(self):
|
||||
return rfc3339(), noduplicates()
|
||||
|
||||
def do_dc_creator(self):
|
||||
if "managingEditor" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"managingEditor", "ext":"dc:creator"}))
|
||||
return text() # duplicates allowed
|
||||
|
||||
def do_dc_subject(self):
|
||||
if "category" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"category", "ext":"dc:subject"}))
|
||||
return text() # duplicates allowed
|
||||
|
||||
def do_dc_date(self):
|
||||
if "pubDate" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"pubDate", "ext":"dc:date"}))
|
||||
return w3cdtf(), noduplicates()
|
||||
|
||||
def do_cc_license(self):
|
||||
if "creativeCommons_license" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"creativeCommons:license", "ext":"cc:license"}))
|
||||
return eater()
|
||||
|
||||
def do_creativeCommons_license(self):
|
||||
if "cc_license" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"creativeCommons:license", "ext":"cc:license"}))
|
||||
return rfc2396_full()
|
||||
|
||||
class rss20Channel(channel):
|
||||
def do_item(self):
|
||||
from item import rss20Item
|
||||
return rss20Item()
|
||||
|
||||
def do_category(self):
|
||||
return category()
|
||||
|
||||
def do_cloud(self):
|
||||
return cloud(), noduplicates()
|
||||
|
||||
do_rating = validatorBase.leaf # TODO test cases?!?
|
||||
|
||||
def do_ttl(self):
|
||||
return positiveInteger(), nonblank(), noduplicates()
|
||||
|
||||
def do_docs(self):
|
||||
return rfc2396_full(), noduplicates()
|
||||
|
||||
def do_generator(self):
|
||||
if "admin_generatorAgent" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"generator", "ext":"admin:generatorAgent"}))
|
||||
return text(), noduplicates()
|
||||
|
||||
def do_pubDate(self):
|
||||
if "dc_date" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"pubDate", "ext":"dc:date"}))
|
||||
return rfc822(), noduplicates()
|
||||
|
||||
def do_managingEditor(self):
|
||||
if "dc_creator" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"managingEditor", "ext":"dc:creator"}))
|
||||
return email(), noduplicates()
|
||||
|
||||
def do_webMaster(self):
|
||||
if "dc_publisher" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"webMaster", "ext":"dc:publisher"}))
|
||||
return email(), noduplicates()
|
||||
|
||||
def do_language(self):
|
||||
if "dc_language" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"language", "ext":"dc:language"}))
|
||||
return iso639(), noduplicates()
|
||||
|
||||
def do_copyright(self):
|
||||
if "dc_rights" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"copyright", "ext":"dc:rights"}))
|
||||
return nonhtml(), noduplicates()
|
||||
|
||||
def do_lastBuildDate(self):
|
||||
if "dcterms_modified" in self.children:
|
||||
self.log(DuplicateSemantics({"core":"lastBuildDate", "ext":"dcterms:modified"}))
|
||||
return rfc822(), noduplicates()
|
||||
|
||||
def do_skipHours(self):
|
||||
from skipHours import skipHours
|
||||
return skipHours()
|
||||
|
||||
def do_skipDays(self):
|
||||
from skipDays import skipDays
|
||||
return skipDays()
|
||||
|
||||
class rss10Channel(channel):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', u'about'),
|
||||
(u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', u'about')]
|
||||
|
||||
def prevalidate(self):
|
||||
if self.attrs.has_key((rdfNS,"about")):
|
||||
if not "abouts" in self.dispatcher.__dict__:
|
||||
self.dispatcher.__dict__["abouts"] = []
|
||||
self.dispatcher.__dict__["abouts"].append(self.attrs[(rdfNS,"about")])
|
||||
|
||||
def do_items(self): # this actually should be from the rss1.0 ns
|
||||
if not self.attrs.has_key((rdfNS,"about")):
|
||||
self.log(MissingAttribute({"parent":self.name, "element":self.name, "attr":"rdf:about"}))
|
||||
from item import items
|
||||
return items(), noduplicates()
|
||||
|
||||
def do_rdfs_label(self):
|
||||
return text()
|
||||
|
||||
def do_rdfs_comment(self):
|
||||
return text()
|
||||
|
||||
|
||||
class link(rfc2396_full):
|
||||
def validate(self):
|
||||
self.parent.link = self.value
|
||||
rfc2396_full.validate(self)
|
||||
|
||||
class blink(text):
|
||||
def validate(self):
|
||||
self.log(NoBlink({}))
|
||||
|
||||
class category(nonhtml):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(None, u'domain')]
|
||||
|
||||
class cloud(validatorBase):
|
||||
def getExpectedAttrNames(self):
|
||||
return [(None, u'domain'), (None, u'path'), (None, u'registerProcedure'),
|
||||
(None, u'protocol'), (None, u'port')]
|
||||
def prevalidate(self):
|
||||
if (None, 'domain') not in self.attrs.getNames():
|
||||
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"domain"}))
|
||||
else:
|
||||
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"domain"}))
|
||||
|
||||
try:
|
||||
if int(self.attrs.getValue((None, 'port'))) <= 0:
|
||||
self.log(InvalidIntegerAttribute({"parent":self.parent.name, "element":self.name, "attr":'port'}))
|
||||
else:
|
||||
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":'port'}))
|
||||
except KeyError:
|
||||
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":'port'}))
|
||||
except ValueError:
|
||||
self.log(InvalidIntegerAttribute({"parent":self.parent.name, "element":self.name, "attr":'port'}))
|
||||
|
||||
if (None, 'path') not in self.attrs.getNames():
|
||||
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"path"}))
|
||||
else:
|
||||
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"path"}))
|
||||
|
||||
if (None, 'registerProcedure') not in self.attrs.getNames():
|
||||
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"registerProcedure"}))
|
||||
else:
|
||||
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"registerProcedure"}))
|
||||
|
||||
if (None, 'protocol') not in self.attrs.getNames():
|
||||
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"protocol"}))
|
||||
else:
|
||||
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"protocol"}))
|
||||
## TODO - is there a list of accepted protocols for this thing?
|
||||
|
||||
return validatorBase.prevalidate(self)
|
||||
37
vendor/feedvalidator/compatibility.py
vendored
37
vendor/feedvalidator/compatibility.py
vendored
|
|
@ -1,37 +0,0 @@
|
|||
"""$Id: compatibility.py 699 2006-09-25 02:01:18Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 699 $"
|
||||
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
from logging import *
|
||||
|
||||
def _must(event):
|
||||
return isinstance(event, Error)
|
||||
|
||||
def _should(event):
|
||||
return isinstance(event, Warning)
|
||||
|
||||
def _may(event):
|
||||
return isinstance(event, Info)
|
||||
|
||||
def A(events):
|
||||
return [event for event in events if _must(event)]
|
||||
|
||||
def AA(events):
|
||||
return [event for event in events if _must(event) or _should(event)]
|
||||
|
||||
def AAA(events):
|
||||
return [event for event in events if _must(event) or _should(event) or _may(event)]
|
||||
|
||||
def AAAA(events):
|
||||
return events
|
||||
|
||||
def analyze(events, rawdata):
|
||||
for event in events:
|
||||
if isinstance(event,UndefinedElement):
|
||||
if event.params['parent'] == 'root':
|
||||
if event.params['element'].lower() in ['html','xhtml:html']:
|
||||
return "html"
|
||||
return None
|
||||
151
vendor/feedvalidator/content.py
vendored
151
vendor/feedvalidator/content.py
vendored
|
|
@ -1,151 +0,0 @@
|
|||
"""$Id: content.py 699 2006-09-25 02:01:18Z rubys $"""
|
||||
|
||||
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
|
||||
__version__ = "$Revision: 699 $"
|
||||
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
|
||||
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
|
||||
|
||||
from base import validatorBase
|
||||
from validators import *
|
||||
from logging import *
|
||||
#
|
||||
# item element.
|
||||
#
|
||||
class textConstruct(validatorBase,rfc2396,nonhtml):
|
||||
from validators import mime_re
|
||||
import re
|
||||
|
||||
def getExpectedAttrNames(self):
|
||||
return [(None, u'type'),(None, u'src')]
|
||||
|
||||
def normalizeWhitespace(self):
|
||||
pass
|
||||
|
||||
def maptype(self):
|
||||
if self.type.find('/') > -1:
|
||||
self.log(InvalidTextType({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
|
||||
|
||||
def prevalidate(self):
|
||||
if self.attrs.has_key((None,"src")):
|
||||
self.type=''
|
||||
else:
|
||||
self.type='text'
|
||||
if self.getFeedType() == TYPE_RSS2 and self.name != 'atom_summary':
|
||||
self.log(DuplicateDescriptionSemantics({"element":self.name}))
|
||||
|
||||
if self.attrs.has_key((None,"type")):
|
||||
self.type=self.attrs.getValue((None,"type"))
|
||||
if not self.type:
|
||||
self.log(AttrNotBlank({"parent":self.parent.name, "element":self.name, "attr":"type"}))
|
||||
|
||||
self.maptype()
|
||||
|
||||
if self.attrs.has_key((None,"src")):
|
||||
self.children.append(True) # force warnings about "mixed" content
|
||||
self.value=self.attrs.getValue((None,"src"))
|
||||
rfc2396.validate(self, errorClass=InvalidURIAttribute, extraParams={"attr": "src"})
|
||||
self.value=""
|
||||
|
||||
if not self.attrs.has_key((None,"type")):
|
||||
self.log(MissingTypeAttr({"parent":self.parent.name, "element":self.name, "attr":"type"}))
|
||||
|
||||
if self.type in ['text','html','xhtml'] and not self.attrs.has_key((None,"src")):
|
||||
pass
|
||||
elif self.type and not self.mime_re.match(self.type):
|
||||
self.log(InvalidMIMEType({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
|
||||
else:
|
||||
self.log(ValidMIMEAttribute({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
|
||||
|
||||
if not self.xmlLang:
|
||||
self.log(MissingDCLanguage({"parent":self.name, "element":"xml:lang"}))
|
||||
|
||||
def validate(self):
|
||||
if self.type in ['text','xhtml']:
|
||||
if self.type=='xhtml':
|
||||
nonhtml.validate(self, NotInline)
|
||||
else:
|
||||
nonhtml.validate(self, ContainsUndeclaredHTML)
|
||||
else:
|
||||
if self.type.find('/') > -1 and not (
|
||||
self.type.endswith('+xml') or self.type.endswith('/xml') or
|
||||
self.type.startswith('text/')):
|
||||
import base64
|
||||
try:
|
||||
self.value=base64.decodestring(self.value)
|
||||
if self.type.endswith('/html'): self.type='html'
|
||||
except:
|
||||
self.log(NotBase64({"parent":self.parent.name, "element":self.name,"value":self.value}))
|
||||
|
||||
if self.type=='html' or self.type.endswith("/html"):
|
||||
self.validateSafe(self.value)
|
||||
|
||||
if self.type.endswith("/html"):
|
||||
if self.value.find("<html")<0 and not self.attrs.has_key((None,"src")):
|
||||
self.log(HtmlFragment({"parent":self.parent.name, "element":self.name,"value":self.value, "type":self.type}))
|
||||
else:
|
||||
nonhtml.validate(self, ContainsUndeclaredHTML)
|
||||
|
||||
if not self.value and len(self.children)==0 and not self.attrs.has_key((None,"src")):
|
||||
self.log(NotBlank({"parent":self.parent.name, "element":self.name}))
|
||||
|
||||
def textOK(self):
|
||||
if self.children: validatorBase.textOK(self)
|
||||
|
||||
def characters(self, string):
|
||||
for c in string:
|
||||
if 0x80 <= ord(c) <= 0x9F or c == u'\ufffd':
|
||||
from validators import BadCharacters
|
||||
self.log(BadCharacters({"parent":self.parent.name, "element":self.name}))
|
||||
if (self.type=='xhtml') and string.strip() and not self.value.strip():
|
||||
self.log(MissingXhtmlDiv({"parent":self.parent.name, "element":self.name}))
|
||||
validatorBase.characters(self,string)
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
if (self.type<>'xhtml') and not (
|
||||
self.type.endswith('+xml') or self.type.endswith('/xml')):
|
||||
self.log(UndefinedElement({"parent":self.name, "element":name}))
|
||||
|
||||
if self.type=="xhtml":
|
||||
if name<>'div' and not self.value.strip():
|
||||
self.log(MissingXhtmlDiv({"parent":self.parent.name, "element":self.name}))
|
||||
elif qname not in ["http://www.w3.org/1999/xhtml"]:
|
||||
self.log(NotHtml({"parent":self.parent.name, "element":self.name, "message":"unexpected namespace: %s" % qname}))
|
||||
|
||||
if self.type=="application/xhtml+xml":
|
||||
if name<>'html':
|
||||
self.log(HtmlFragment({"parent":self.parent.name, "element":self.name,"value":self.value, "type":self.type}))
|
||||
elif qname not in ["http://www.w3.org/1999/xhtml"]:
|
||||
self.log(NotHtml({"parent":self.parent.name, "element":self.name, "message":"unexpected namespace: %s" % qname}))
|
||||
|
||||
if self.attrs.has_key((None,"mode")):
|
||||
if self.attrs.getValue((None,"mode")) == 'escaped':
|
||||
self.log(NotEscaped({"parent":self.parent.name, "element":self.name}))
|
||||
|
||||
if name=="div" and qname=="http://www.w3.org/1999/xhtml":
|
||||
handler=diveater()
|
||||
else:
|
||||
handler=eater()
|
||||
self.children.append(handler)
|
||||
self.push(handler, name, attrs)
|
||||
|
||||
# treat xhtml:div as part of the content for purposes of detecting escaped html
|
||||
class diveater(eater):
|
||||
def __init__(self):
|
||||
eater.__init__(self)
|
||||
self.mixed = False
|
||||
def textOK(self):
|
||||
pass
|
||||
def characters(self, string):
|
||||
validatorBase.characters(self, string)
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
if not qname:
|
||||
self.log(MissingNamespace({"parent":"xhtml:div", "element":name}))
|
||||
self.mixed = True
|
||||
eater.startElementNS(self, name, qname, attrs)
|
||||
def validate(self):
|
||||
if not self.mixed: self.parent.value += self.value
|
||||
|
||||
class content(textConstruct):
|
||||
def maptype(self):
|
||||
if self.type == 'multipart/alternative':
|
||||
self.log(InvalidMIMEType({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
|
||||
50
vendor/feedvalidator/demo/.htaccess
vendored
50
vendor/feedvalidator/demo/.htaccess
vendored
|
|
@ -1,50 +0,0 @@
|
|||
Options +ExecCGI -MultiViews
|
||||
AddHandler cgi-script .cgi
|
||||
AddType image/x-icon ico
|
||||
|
||||
RewriteRule ^check$ check.cgi
|
||||
|
||||
DirectoryIndex index.html check.cgi
|
||||
|
||||
# MAP - bad bots are killing the server (everybody links to their validation results and the bots dutifully crawl it)
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP_USER_AGENT} BecomeBot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} Crawler [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} FatBot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} Feed24 [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} Gigabot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} Googlebot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} htdig [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} HttpClient
|
||||
RewriteCond %{HTTP_USER_AGENT} HTTrack [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} IQSearch [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} msnbot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} NaverBot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} OmniExplorer [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} SietsCrawler [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} Thunderbird [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} TurnitinBot [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} User-Agent [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} Yahoo!.Slurp [OR]
|
||||
RewriteRule check - [F,L]
|
||||
|
||||
# fastcgi
|
||||
RewriteCond /home/rubys/public_html/fvstat/status -f
|
||||
RewriteRule check.cgi(.*)$ http://localhost:8080/rubys/feedvalidator.org/$1 [P]
|
||||
|
||||
<Files check.cgi>
|
||||
Deny from feeds01.archive.org
|
||||
Deny from feedvalidator.org
|
||||
Deny from new.getfilesfast.com
|
||||
Deny from gnat.yodlee.com
|
||||
Deny from master.macworld.com
|
||||
Deny from 62.244.248.104
|
||||
Deny from 207.97.204.219
|
||||
Deny from ik63025.ikexpress.com
|
||||
Deny from 65-86-180-70.client.dsl.net
|
||||
Deny from vanadium.sabren.com
|
||||
</Files>
|
||||
|
||||
<Files config.py>
|
||||
ForceType text/plain
|
||||
</Files>
|
||||
26
vendor/feedvalidator/demo/LICENSE
vendored
26
vendor/feedvalidator/demo/LICENSE
vendored
|
|
@ -1,26 +0,0 @@
|
|||
The Feed Validator (includig all code, tests, and documentation) is released
|
||||
under the following license:
|
||||
|
||||
----- begin license block -----
|
||||
|
||||
Copyright (c) 2002-2006, Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
----- end license block -----
|
||||
9
vendor/feedvalidator/demo/README
vendored
9
vendor/feedvalidator/demo/README
vendored
|
|
@ -1,9 +0,0 @@
|
|||
Some tests, and some functionality, will not be enabled unless a full set
|
||||
of 32-bit character encodings are available through Python.
|
||||
|
||||
The package 'iconvcodec' provides the necessary codecs, if your underlying
|
||||
operating system supports them. Its web page is at
|
||||
<http://cjkpython.i18n.org/#iconvcodec>, and a range of packages are
|
||||
provided.
|
||||
|
||||
Python 2.3.x is required, for its Unicode support.
|
||||
76
vendor/feedvalidator/demo/RSSValidator.wsdl
vendored
76
vendor/feedvalidator/demo/RSSValidator.wsdl
vendored
|
|
@ -1,76 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<definitions
|
||||
targetNamespace="http://feedvalidator.org/"
|
||||
xmlns:validator="http://feedvalidator.org/"
|
||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.xmlsoap.org/wsdl/">
|
||||
|
||||
<types>
|
||||
<xsd:schema elementFormDefault="qualified"
|
||||
targetNamespace="http://feedvalidator.org/">
|
||||
|
||||
<xsd:complexType name="Request">
|
||||
<xsd:sequence>
|
||||
<xsd:any namespace="##other"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="Message">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="level" type="xsd:string"/>
|
||||
<xsd:element name="type" type="xsd:string"/>
|
||||
<xsd:element name="line" type="xsd:string"/>
|
||||
<xsd:element name="column" type="xsd:string"/>
|
||||
<xsd:element name="msgcount" type="xsd:string"/>
|
||||
<xsd:element name="text" type="xsd:string"/>
|
||||
<xsd:any minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="Response">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="message" type="validator:Message"
|
||||
minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:element name="request" type="validator:Request"/>
|
||||
<xsd:element name="response" type="validator:Response"/>
|
||||
</xsd:schema>
|
||||
</types>
|
||||
|
||||
<message name="validateIn">
|
||||
<part name="request" element="validator:request" />
|
||||
</message>
|
||||
<message name="validateOut">
|
||||
<part name="response" element="validator:response" />
|
||||
</message>
|
||||
|
||||
<portType name="RSSValidatorSoap">
|
||||
<operation name="validate">
|
||||
<input message="validator:validateIn" />
|
||||
<output message="validator:validateOut" />
|
||||
</operation>
|
||||
</portType>
|
||||
|
||||
<binding name="soap" type="validator:RSSValidatorSoap">
|
||||
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
|
||||
style="document" />
|
||||
<operation name="validate">
|
||||
<soap:operation soapAction="urn:validate" style="document" />
|
||||
<input>
|
||||
<soap:body use="literal" />
|
||||
</input>
|
||||
<output>
|
||||
<soap:body use="literal" />
|
||||
</output>
|
||||
</operation>
|
||||
</binding>
|
||||
|
||||
<service name="RSSValidator">
|
||||
<port name="RSSValidatorSoap" binding="validator:soap">
|
||||
<soap:address location="http://feedvalidator.org/" />
|
||||
</port>
|
||||
</service>
|
||||
</definitions>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue