mirror of
https://github.com/viq/NewsBlur.git
synced 2025-04-13 09:38:09 +00:00
Merge branch 'django3.0' into docker_django3.0
* django3.0: (184 commits) Removing log override Moving logging over to the newsblur log. Fixing search indexer background task for new celery. Attempting to add gunicorn errors to console/log. Better handling of missing subs. Handling missing user sub on feed delete. Correct encoding for strings on systems that don't have utf-8 as default encoding. Writing in the real urllib3 dependency for requests. Upgrading requests due to urllib3 incompatibility. Login required should use the next parameter. Upgrading django oauth toolkit for django 1.11. Handling newsletters with multiple recipients. Extracting image urls sometimes fails. Handling ajax errors in json views. Adding timeouts to most outbound requests. Sentry SDK 0.19.4. Removing imperfect proxy warning for every story. Found four more GET/POST crosses. Feed unread count may need a POST. Namespacing settings. ...
This commit is contained in:
commit
05756155b1
7261 changed files with 4918 additions and 940647 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -12,6 +12,7 @@ media/iphone/build
|
|||
build/
|
||||
.DS_Store
|
||||
**/*.perspectivev*
|
||||
.vscode/*
|
||||
data/
|
||||
config/certificates
|
||||
**/*.xcuserstate
|
||||
|
@ -71,3 +72,5 @@ clients/android/NewsBlur/build.gradle
|
|||
clients/android/NewsBlur/gradle*
|
||||
clients/android/NewsBlur/settings.gradle
|
||||
/docker/volumes/*
|
||||
|
||||
**/node_modules
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from utils import log as logging
|
||||
|
||||
class EmailPopularityQuery(Task):
|
||||
@app.task()
|
||||
def EmailPopularityQuery(pk):
|
||||
from apps.analyzer.models import MPopularityQuery
|
||||
|
||||
query = MPopularityQuery.objects.get(pk=pk)
|
||||
logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query)
|
||||
|
||||
query.send_email()
|
||||
|
||||
def run(self, pk):
|
||||
from apps.analyzer.models import MPopularityQuery
|
||||
|
||||
query = MPopularityQuery.objects.get(pk=pk)
|
||||
logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query)
|
||||
|
||||
query.send_email()
|
||||
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from django.contrib.auth.models import User
|
||||
from apps.feed_import.models import UploadedOPML, OPMLImporter
|
||||
from apps.reader.models import UserSubscription
|
||||
from utils import log as logging
|
||||
|
||||
|
||||
class ProcessOPML(Task):
|
||||
@app.task()
|
||||
def ProcessOPML(user_id):
|
||||
user = User.objects.get(pk=user_id)
|
||||
logging.user(user, "~FR~SBOPML upload (task) starting...")
|
||||
|
||||
opml = UploadedOPML.objects.filter(user_id=user_id).first()
|
||||
opml_importer = OPMLImporter(opml.opml_file, user)
|
||||
opml_importer.process()
|
||||
|
||||
def run(self, user_id):
|
||||
user = User.objects.get(pk=user_id)
|
||||
logging.user(user, "~FR~SBOPML upload (task) starting...")
|
||||
|
||||
opml = UploadedOPML.objects.filter(user_id=user_id).first()
|
||||
opml_importer = OPMLImporter(opml.opml_file, user)
|
||||
opml_importer.process()
|
||||
|
||||
feed_count = UserSubscription.objects.filter(user=user).count()
|
||||
user.profile.send_upload_opml_finished_email(feed_count)
|
||||
logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))
|
||||
feed_count = UserSubscription.objects.filter(user=user).count()
|
||||
user.profile.send_upload_opml_finished_email(feed_count)
|
||||
logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))
|
||||
|
||||
|
|
|
@ -31,9 +31,26 @@ class EmailNewsletter:
|
|||
return
|
||||
usf.add_folder('', 'Newsletters')
|
||||
|
||||
# First look for the email address
|
||||
try:
|
||||
feed = Feed.objects.get(feed_address=feed_address)
|
||||
except Feed.MultipleObjectsReturned:
|
||||
feeds = Feed.objects.filter(feed_address=feed_address)[:1]
|
||||
if feeds.count():
|
||||
feed = feeds[0]
|
||||
except Feed.DoesNotExist:
|
||||
feed = None
|
||||
|
||||
# If not found, check among titles user has subscribed to
|
||||
if not feed:
|
||||
newsletter_subs = UserSubscription.objects.filter(user=user, feed__feed_address__contains="newsletter:").only('feed')
|
||||
newsletter_feed_ids = [us.feed.pk for us in newsletter_subs]
|
||||
feeds = Feed.objects.filter(feed_title__iexact=sender_name, pk__in=newsletter_feed_ids)
|
||||
if feeds.count():
|
||||
feed = feeds[0]
|
||||
|
||||
# Create a new feed if it doesn't exist by sender name or email
|
||||
if not feed:
|
||||
feed = Feed.objects.create(feed_address=feed_address,
|
||||
feed_link='http://' + sender_domain,
|
||||
feed_title=sender_name,
|
||||
|
@ -148,8 +165,8 @@ class EmailNewsletter:
|
|||
|
||||
return profile.user
|
||||
|
||||
def _feed_address(self, user, sender):
|
||||
return 'newsletter:%s:%s' % (user.pk, sender)
|
||||
def _feed_address(self, user, sender_email):
|
||||
return 'newsletter:%s:%s' % (user.pk, sender_email)
|
||||
|
||||
def _split_sender(self, sender):
|
||||
tokens = re.search('(.*?) <(.*?)@(.*?)>', sender)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from django.contrib.auth.models import User
|
||||
from apps.notifications.models import MUserFeedNotification
|
||||
from utils import log as logging
|
||||
|
||||
|
||||
class QueueNotifications(Task):
|
||||
|
||||
def run(self, feed_id, new_stories):
|
||||
MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
|
||||
@app.task()
|
||||
def QueueNotifications(feed_id, new_stories):
|
||||
MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
|
||||
|
|
|
@ -9,8 +9,6 @@ from apps.profile.models import change_password, blank_authenticate, MGiftCode,
|
|||
from apps.social.models import MSocialProfile
|
||||
|
||||
PLANS = [
|
||||
("newsblur-premium-12", mark_safe("$12 / year <span class='NB-small'>($1/month)</span>")),
|
||||
("newsblur-premium-24", mark_safe("$24 / year <span class='NB-small'>($2/month)</span>")),
|
||||
("newsblur-premium-36", mark_safe("$36 / year <span class='NB-small'>($3/month)</span>")),
|
||||
]
|
||||
|
||||
|
@ -35,7 +33,7 @@ class StripePlusPaymentForm(StripePaymentForm):
|
|||
email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)),
|
||||
label='Email address',
|
||||
required=False)
|
||||
plan = forms.ChoiceField(required=False, widget=HorizRadioRenderer,
|
||||
plan = forms.ChoiceField(required=False, widget=forms.RadioSelect,
|
||||
choices=PLANS, label='Plan')
|
||||
|
||||
|
||||
|
|
|
@ -492,7 +492,7 @@ class Profile(models.Model):
|
|||
|
||||
return ipn[0].payer_email
|
||||
|
||||
def activate_ios_premium(self, product_identifier, transaction_identifier, amount=36):
|
||||
def activate_ios_premium(self, transaction_identifier=None, amount=36):
|
||||
payments = PaymentHistory.objects.filter(user=self.user,
|
||||
payment_identifier=transaction_identifier,
|
||||
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
|
||||
|
@ -512,7 +512,30 @@ class Profile(models.Model):
|
|||
if not self.is_premium:
|
||||
self.activate_premium()
|
||||
|
||||
logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % product_identifier)
|
||||
logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % amount)
|
||||
return True
|
||||
|
||||
def activate_android_premium(self, order_id=None, amount=36):
|
||||
payments = PaymentHistory.objects.filter(user=self.user,
|
||||
payment_identifier=order_id,
|
||||
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
|
||||
if len(payments):
|
||||
# Already paid
|
||||
logging.user(self.user, "~FG~BBAlready paid Android premium subscription: $%s~FW" % amount)
|
||||
return False
|
||||
|
||||
PaymentHistory.objects.create(user=self.user,
|
||||
payment_date=datetime.datetime.now(),
|
||||
payment_amount=amount,
|
||||
payment_provider='android-subscription',
|
||||
payment_identifier=order_id)
|
||||
|
||||
self.setup_premium_history()
|
||||
|
||||
if not self.is_premium:
|
||||
self.activate_premium()
|
||||
|
||||
logging.user(self.user, "~FG~BBNew Android premium subscription: $%s~FW" % amount)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -1,90 +1,76 @@
|
|||
import datetime
|
||||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from apps.profile.models import Profile, RNewUserQueue
|
||||
from utils import log as logging
|
||||
from apps.reader.models import UserSubscription, UserSubscriptionFolders
|
||||
from apps.social.models import MSocialServices, MActivity, MInteraction
|
||||
|
||||
class EmailNewUser(Task):
|
||||
|
||||
def run(self, user_id):
|
||||
user_profile = Profile.objects.get(user__pk=user_id)
|
||||
user_profile.send_new_user_email()
|
||||
@app.task(name="email-new-user")
|
||||
def EmailNewUser(user_id):
|
||||
user_profile = Profile.objects.get(user__pk=user_id)
|
||||
user_profile.send_new_user_email()
|
||||
|
||||
class EmailNewPremium(Task):
|
||||
|
||||
def run(self, user_id):
|
||||
user_profile = Profile.objects.get(user__pk=user_id)
|
||||
user_profile.send_new_premium_email()
|
||||
@app.task(name="email-new-premium")
|
||||
def EmailNewPremium(user_id):
|
||||
user_profile = Profile.objects.get(user__pk=user_id)
|
||||
user_profile.send_new_premium_email()
|
||||
|
||||
class PremiumExpire(Task):
|
||||
name = 'premium-expire'
|
||||
|
||||
def run(self, **kwargs):
|
||||
# Get expired but grace period users
|
||||
two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
|
||||
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
|
||||
expired_profiles = Profile.objects.filter(is_premium=True,
|
||||
premium_expire__lte=two_days_ago,
|
||||
premium_expire__gt=thirty_days_ago)
|
||||
logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
|
||||
for profile in expired_profiles:
|
||||
if profile.grace_period_email_sent():
|
||||
continue
|
||||
profile.setup_premium_history()
|
||||
if profile.premium_expire < two_days_ago:
|
||||
profile.send_premium_expire_grace_period_email()
|
||||
|
||||
# Get fully expired users
|
||||
expired_profiles = Profile.objects.filter(is_premium=True,
|
||||
premium_expire__lte=thirty_days_ago)
|
||||
logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
|
||||
for profile in expired_profiles:
|
||||
profile.setup_premium_history()
|
||||
if profile.premium_expire < thirty_days_ago:
|
||||
profile.send_premium_expire_email()
|
||||
profile.deactivate_premium()
|
||||
|
||||
|
||||
class ActivateNextNewUser(Task):
|
||||
name = 'activate-next-new-user'
|
||||
|
||||
def run(self):
|
||||
RNewUserQueue.activate_next()
|
||||
|
||||
|
||||
class CleanupUser(Task):
|
||||
name = 'cleanup-user'
|
||||
|
||||
def run(self, user_id):
|
||||
UserSubscription.trim_user_read_stories(user_id)
|
||||
UserSubscription.verify_feeds_scheduled(user_id)
|
||||
Profile.count_all_feed_subscribers_for_user(user_id)
|
||||
MInteraction.trim(user_id)
|
||||
MActivity.trim(user_id)
|
||||
UserSubscriptionFolders.add_missing_feeds_for_user(user_id)
|
||||
UserSubscriptionFolders.compact_for_user(user_id)
|
||||
# UserSubscription.refresh_stale_feeds(user_id)
|
||||
@app.task(name="premium-expire")
|
||||
def PremiumExpire(**kwargs):
|
||||
# Get expired but grace period users
|
||||
two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
|
||||
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
|
||||
expired_profiles = Profile.objects.filter(is_premium=True,
|
||||
premium_expire__lte=two_days_ago,
|
||||
premium_expire__gt=thirty_days_ago)
|
||||
logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
|
||||
for profile in expired_profiles:
|
||||
if profile.grace_period_email_sent():
|
||||
continue
|
||||
profile.setup_premium_history()
|
||||
if profile.premium_expire < two_days_ago:
|
||||
profile.send_premium_expire_grace_period_email()
|
||||
|
||||
try:
|
||||
ss = MSocialServices.objects.get(user_id=user_id)
|
||||
except MSocialServices.DoesNotExist:
|
||||
logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
|
||||
return
|
||||
ss.sync_twitter_photo()
|
||||
# Get fully expired users
|
||||
expired_profiles = Profile.objects.filter(is_premium=True,
|
||||
premium_expire__lte=thirty_days_ago)
|
||||
logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
|
||||
for profile in expired_profiles:
|
||||
profile.setup_premium_history()
|
||||
if profile.premium_expire < thirty_days_ago:
|
||||
profile.send_premium_expire_email()
|
||||
profile.deactivate_premium()
|
||||
|
||||
class CleanSpam(Task):
|
||||
name = 'clean-spam'
|
||||
@app.task(name="activate-next-new-user")
|
||||
def ActivateNextNewUser():
|
||||
RNewUserQueue.activate_next()
|
||||
|
||||
def run(self, **kwargs):
|
||||
logging.debug(" ---> Finding spammers...")
|
||||
Profile.clear_dead_spammers(confirm=True)
|
||||
@app.task(name="cleanup-user")
|
||||
def CleanupUser(user_id):
|
||||
UserSubscription.trim_user_read_stories(user_id)
|
||||
UserSubscription.verify_feeds_scheduled(user_id)
|
||||
Profile.count_all_feed_subscribers_for_user(user_id)
|
||||
MInteraction.trim(user_id)
|
||||
MActivity.trim(user_id)
|
||||
UserSubscriptionFolders.add_missing_feeds_for_user(user_id)
|
||||
UserSubscriptionFolders.compact_for_user(user_id)
|
||||
# UserSubscription.refresh_stale_feeds(user_id)
|
||||
|
||||
try:
|
||||
ss = MSocialServices.objects.get(user_id=user_id)
|
||||
except MSocialServices.DoesNotExist:
|
||||
logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
|
||||
return
|
||||
ss.sync_twitter_photo()
|
||||
|
||||
class ReimportStripeHistory(Task):
|
||||
name = 'reimport-stripe-history'
|
||||
@app.task(name="clean-spam")
|
||||
def CleanSpam():
|
||||
logging.debug(" ---> Finding spammers...")
|
||||
Profile.clear_dead_spammers(confirm=True)
|
||||
|
||||
def run(self, **kwargs):
|
||||
logging.debug(" ---> Reimporting Stripe history...")
|
||||
Profile.reimport_stripe_history(limit=10, days=1)
|
||||
@app.task(name="reimport-stripe-history")
|
||||
def ReimportStripeHistory():
|
||||
logging.debug(" ---> Reimporting Stripe history...")
|
||||
Profile.reimport_stripe_history(limit=10, days=1)
|
||||
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ urlpatterns = [
|
|||
url(r'^never_expire_premium/?', views.never_expire_premium, name='profile-never-expire-premium'),
|
||||
url(r'^upgrade_premium/?', views.upgrade_premium, name='profile-upgrade-premium'),
|
||||
url(r'^save_ios_receipt/?', views.save_ios_receipt, name='save-ios-receipt'),
|
||||
url(r'^save_android_receipt/?', views.save_android_receipt, name='save-android-receipt'),
|
||||
url(r'^update_payment_history/?', views.update_payment_history, name='profile-update-payment-history'),
|
||||
url(r'^delete_account/?', views.delete_account, name='profile-delete-account'),
|
||||
url(r'^forgot_password_return/?', views.forgot_password_return, name='profile-forgot-password-return'),
|
||||
|
@ -30,4 +31,5 @@ urlpatterns = [
|
|||
url(r'^delete_all_sites/?', views.delete_all_sites, name='profile-delete-all-sites'),
|
||||
url(r'^email_optout/?', views.email_optout, name='profile-email-optout'),
|
||||
url(r'^ios_subscription_status/?', views.ios_subscription_status, name='profile-ios-subscription-status'),
|
||||
url(r'debug/?', views.trigger_error, name='trigger-error'),
|
||||
]
|
||||
|
|
|
@ -98,7 +98,8 @@ def login(request):
|
|||
|
||||
return render(request, 'accounts/login.html', {
|
||||
'form': form,
|
||||
'next': request.POST.get('next', "")})
|
||||
'next': request.POST.get('next', "") or request.GET.get('next', "")
|
||||
})
|
||||
|
||||
@csrf_exempt
|
||||
def signup(request):
|
||||
|
@ -106,16 +107,17 @@ def signup(request):
|
|||
recaptcha = request.POST.get('g-recaptcha-response', None)
|
||||
recaptcha_error = None
|
||||
|
||||
if not recaptcha:
|
||||
recaptcha_error = "Please hit the \"I'm not a robot\" button."
|
||||
else:
|
||||
response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
|
||||
'secret': settings.RECAPTCHA_SECRET_KEY,
|
||||
'response': recaptcha,
|
||||
})
|
||||
result = response.json()
|
||||
if not result['success']:
|
||||
recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
|
||||
if settings.ENFORCE_SIGNUP_CAPTCHA:
|
||||
if not recaptcha:
|
||||
recaptcha_error = "Please hit the \"I'm not a robot\" button."
|
||||
else:
|
||||
response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
|
||||
'secret': settings.RECAPTCHA_SECRET_KEY,
|
||||
'response': recaptcha,
|
||||
})
|
||||
result = response.json()
|
||||
if not result['success']:
|
||||
recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
|
||||
|
||||
if request.method == "POST":
|
||||
form = SignupForm(data=request.POST, prefix="signup")
|
||||
|
@ -331,7 +333,7 @@ def save_ios_receipt(request):
|
|||
|
||||
logging.user(request, "~BM~FBSaving iOS Receipt: %s %s" % (product_identifier, transaction_identifier))
|
||||
|
||||
paid = request.user.profile.activate_ios_premium(product_identifier, transaction_identifier)
|
||||
paid = request.user.profile.activate_ios_premium(transaction_identifier)
|
||||
if paid:
|
||||
logging.user(request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier))
|
||||
subject = "iOS Premium: %s (%s)" % (request.user.profile, product_identifier)
|
||||
|
@ -343,13 +345,32 @@ def save_ios_receipt(request):
|
|||
|
||||
return request.user.profile
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def save_android_receipt(request):
|
||||
order_id = request.POST.get('order_id')
|
||||
product_id = request.POST.get('product_id')
|
||||
|
||||
logging.user(request, "~BM~FBSaving Android Receipt: %s %s" % (product_id, order_id))
|
||||
|
||||
paid = request.user.profile.activate_android_premium(order_id)
|
||||
if paid:
|
||||
logging.user(request, "~BM~FBSending Android Receipt email: %s %s" % (product_id, order_id))
|
||||
subject = "Android Premium: %s (%s)" % (request.user.profile, product_id)
|
||||
message = """User: %s (%s) -- Email: %s, product: %s, order: %s""" % (request.user.username, request.user.pk, request.user.email, product_id, order_id)
|
||||
mail_admins(subject, message, fail_silently=True)
|
||||
else:
|
||||
logging.user(request, "~BM~FBNot sending Android Receipt email, already paid: %s %s" % (product_id, order_id))
|
||||
|
||||
|
||||
return request.user.profile
|
||||
|
||||
@login_required
|
||||
def stripe_form(request):
|
||||
user = request.user
|
||||
success_updating = False
|
||||
stripe.api_key = settings.STRIPE_SECRET
|
||||
plan = int(request.GET.get('plan', 2))
|
||||
plan = PLANS[plan-1][0]
|
||||
plan = PLANS[0][0]
|
||||
renew = is_true(request.GET.get('renew', False))
|
||||
error = None
|
||||
|
||||
|
@ -692,4 +713,9 @@ def ios_subscription_status(request):
|
|||
|
||||
return {
|
||||
"code": 1
|
||||
}
|
||||
}
|
||||
|
||||
def trigger_error(request):
|
||||
logging.user(request.user, "~BR~FW~SBTriggering divison by zero")
|
||||
division_by_zero = 1 / 0
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
|
|
@ -52,11 +52,11 @@ def push_callback(request, push_id):
|
|||
# XXX TODO: Optimize this by removing feedparser. It just needs to find out
|
||||
# the hub_url or topic has changed. ElementTree could do it.
|
||||
if random.random() < 0.1:
|
||||
parsed = feedparser.parse(request.raw_post_data)
|
||||
parsed = feedparser.parse(request.body)
|
||||
subscription.check_urls_against_pushed_data(parsed)
|
||||
|
||||
# Don't give fat ping, just fetch.
|
||||
# subscription.feed.queue_pushed_feed_xml(request.raw_post_data)
|
||||
# subscription.feed.queue_pushed_feed_xml(request.body)
|
||||
if subscription.feed.active_premium_subscribers >= 1:
|
||||
subscription.feed.queue_pushed_feed_xml("Fetch me", latest_push_date_delta=latest_push_date_delta)
|
||||
MFetchHistory.add(feed_id=subscription.feed_id,
|
||||
|
|
|
@ -154,7 +154,8 @@ class SignupForm(forms.Form):
|
|||
|
||||
new_user = User(username=username)
|
||||
new_user.set_password(password)
|
||||
new_user.is_active = False
|
||||
if not getattr(settings, 'AUTO_ENABLE_NEW_USERS', True):
|
||||
new_user.is_active = False
|
||||
new_user.email = email
|
||||
new_user.last_login = datetime.datetime.now()
|
||||
new_user.save()
|
||||
|
@ -184,4 +185,4 @@ class FeatureForm(forms.Form):
|
|||
feature = Feature(description=self.cleaned_data['description'],
|
||||
date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1))
|
||||
feature.save()
|
||||
return feature
|
||||
return feature
|
||||
|
|
|
@ -771,6 +771,9 @@ class UserSubscription(models.Model):
|
|||
except pymongo.errors.OperationFailure as e:
|
||||
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:100]
|
||||
stories = Feed.format_stories(stories_db, self.feed_id)
|
||||
except pymongo.errors.OperationFailure as e:
|
||||
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:25]
|
||||
stories = Feed.format_stories(stories_db, self.feed_id)
|
||||
|
||||
unread_stories = []
|
||||
for story in stories:
|
||||
|
@ -1192,7 +1195,7 @@ class RUserStory:
|
|||
redis_commands(read_story_key)
|
||||
|
||||
read_stories_list_key = 'lRS:%s' % user_id
|
||||
r.lrem(read_stories_list_key, story_hash)
|
||||
r.lrem(read_stories_list_key, 1, story_hash)
|
||||
|
||||
if ps and username:
|
||||
ps.publish(username, 'story:unread:%s' % story_hash)
|
||||
|
@ -1428,15 +1431,16 @@ class UserSubscriptionFolders(models.Model):
|
|||
self.save()
|
||||
|
||||
if not multiples_found and deleted and commit_delete:
|
||||
user_sub = None
|
||||
try:
|
||||
user_sub = UserSubscription.objects.get(user=self.user, feed=feed_id)
|
||||
except Feed.DoesNotExist:
|
||||
except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
|
||||
duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id)
|
||||
if duplicate_feed:
|
||||
try:
|
||||
user_sub = UserSubscription.objects.get(user=self.user,
|
||||
feed=duplicate_feed[0].feed)
|
||||
except Feed.DoesNotExist:
|
||||
except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
|
||||
return
|
||||
if user_sub:
|
||||
user_sub.delete()
|
||||
|
|
|
@ -1,46 +1,40 @@
|
|||
import datetime
|
||||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from utils import log as logging
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from apps.reader.models import UserSubscription
|
||||
from apps.social.models import MSocialSubscription
|
||||
|
||||
|
||||
class FreshenHomepage(Task):
|
||||
name = 'freshen-homepage'
|
||||
|
||||
def run(self, **kwargs):
|
||||
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||
user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
|
||||
user.profile.last_seen_on = datetime.datetime.utcnow()
|
||||
user.profile.save()
|
||||
@app.task(name='freshen-homepage')
|
||||
def FreshenHomepage():
|
||||
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||
user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
|
||||
user.profile.last_seen_on = datetime.datetime.utcnow()
|
||||
user.profile.save()
|
||||
|
||||
usersubs = UserSubscription.objects.filter(user=user)
|
||||
logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
|
||||
for sub in usersubs:
|
||||
sub.mark_read_date = day_ago
|
||||
sub.needs_unread_recalc = True
|
||||
sub.save()
|
||||
sub.calculate_feed_scores(silent=True)
|
||||
|
||||
usersubs = UserSubscription.objects.filter(user=user)
|
||||
logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
|
||||
for sub in usersubs:
|
||||
sub.mark_read_date = day_ago
|
||||
sub.needs_unread_recalc = True
|
||||
sub.save()
|
||||
sub.calculate_feed_scores(silent=True)
|
||||
|
||||
socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
|
||||
logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
|
||||
for sub in socialsubs:
|
||||
sub.mark_read_date = day_ago
|
||||
sub.needs_unread_recalc = True
|
||||
sub.save()
|
||||
sub.calculate_feed_scores(silent=True)
|
||||
socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
|
||||
logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
|
||||
for sub in socialsubs:
|
||||
sub.mark_read_date = day_ago
|
||||
sub.needs_unread_recalc = True
|
||||
sub.save()
|
||||
sub.calculate_feed_scores(silent=True)
|
||||
|
||||
class CleanAnalytics(Task):
|
||||
name = 'clean-analytics'
|
||||
hard = 720*10
|
||||
|
||||
def run(self, **kwargs):
|
||||
logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
|
||||
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
|
||||
))
|
||||
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
|
||||
"date": {"$lt": day_ago},
|
||||
})
|
||||
@app.task(name='clean_analytics', time_limit=720*10)
|
||||
def CleanAnalytics():
|
||||
logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
|
||||
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
|
||||
))
|
||||
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
|
||||
"date": {"$lt": day_ago},
|
||||
})
|
||||
|
|
|
@ -559,9 +559,11 @@ def interactions_count(request):
|
|||
@ajax_login_required
|
||||
@json.json_view
|
||||
def feed_unread_count(request):
|
||||
get_post = getattr(request, request.method)
|
||||
start = time.time()
|
||||
user = request.user
|
||||
feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]')
|
||||
feed_ids = get_post.getlist('feed_id') or get_post.getlist('feed_id[]')
|
||||
|
||||
force = request.GET.get('force', False)
|
||||
social_feed_ids = [feed_id for feed_id in feed_ids if 'social:' in feed_id]
|
||||
feed_ids = list(set(feed_ids) - set(social_feed_ids))
|
||||
|
@ -1024,10 +1026,15 @@ def starred_story_hashes(request):
|
|||
|
||||
mstories = MStarredStory.objects(
|
||||
user_id=user.pk
|
||||
).only('story_hash', 'starred_date').order_by('-starred_date')
|
||||
).only('story_hash', 'starred_date', 'starred_updated').order_by('-starred_date')
|
||||
|
||||
if include_timestamps:
|
||||
story_hashes = [(s.story_hash, s.starred_date.strftime("%s")) for s in mstories]
|
||||
story_hashes = []
|
||||
for s in mstories:
|
||||
date = s.starred_date
|
||||
if s.starred_updated:
|
||||
date = s.starred_updated
|
||||
story_hashes.append((s.story_hash, date.strftime("%s")))
|
||||
else:
|
||||
story_hashes = [s.story_hash for s in mstories]
|
||||
|
||||
|
@ -1315,28 +1322,32 @@ def load_read_stories(request):
|
|||
|
||||
@json.json_view
|
||||
def load_river_stories__redis(request):
|
||||
limit = int(request.GET.get('limit', 12))
|
||||
# get_post is request.REQUEST, since this endpoint needs to handle either
|
||||
# GET or POST requests, since the parameters for this endpoint can be
|
||||
# very long, at which point the max size of a GET url request is exceeded.
|
||||
get_post = getattr(request, request.method)
|
||||
limit = int(get_post.get('limit', 12))
|
||||
start = time.time()
|
||||
user = get_user(request)
|
||||
message = None
|
||||
feed_ids = request.GET.getlist('feeds') or request.GET.getlist('feeds[]')
|
||||
feed_ids = get_post.getlist('feeds') or get_post.getlist('feeds[]')
|
||||
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
|
||||
if not feed_ids:
|
||||
feed_ids = request.GET.getlist('f') or request.GET.getlist('f[]')
|
||||
feed_ids = [int(feed_id) for feed_id in request.GET.getlist('f') if feed_id]
|
||||
story_hashes = request.GET.getlist('h') or request.GET.getlist('h[]')
|
||||
feed_ids = get_post.getlist('f') or get_post.getlist('f[]')
|
||||
feed_ids = [int(feed_id) for feed_id in get_post.getlist('f') if feed_id]
|
||||
story_hashes = get_post.getlist('h') or get_post.getlist('h[]')
|
||||
story_hashes = story_hashes[:100]
|
||||
original_feed_ids = list(feed_ids)
|
||||
page = int(request.GET.get('page', 1))
|
||||
order = request.GET.get('order', 'newest')
|
||||
read_filter = request.GET.get('read_filter', 'unread')
|
||||
query = request.GET.get('query', '').strip()
|
||||
include_hidden = is_true(request.GET.get('include_hidden', False))
|
||||
include_feeds = is_true(request.GET.get('include_feeds', False))
|
||||
initial_dashboard = is_true(request.GET.get('initial_dashboard', False))
|
||||
infrequent = is_true(request.GET.get('infrequent', False))
|
||||
page = int(get_post.get('page', 1))
|
||||
order = get_post.get('order', 'newest')
|
||||
read_filter = get_post.get('read_filter', 'unread')
|
||||
query = get_post.get('query', '').strip()
|
||||
include_hidden = is_true(get_post.get('include_hidden', False))
|
||||
include_feeds = is_true(get_post.get('include_feeds', False))
|
||||
initial_dashboard = is_true(get_post.get('initial_dashboard', False))
|
||||
infrequent = is_true(get_post.get('infrequent', False))
|
||||
if infrequent:
|
||||
infrequent = request.GET.get('infrequent')
|
||||
infrequent = get_post.get('infrequent')
|
||||
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
|
||||
usersubs = []
|
||||
code = 1
|
||||
|
@ -1567,9 +1578,9 @@ def complete_river(request):
|
|||
@json.json_view
|
||||
def unread_story_hashes__old(request):
|
||||
user = get_user(request)
|
||||
feed_ids = request.POST.getlist('feed_id') or request.POST.getlist('feed_id[]')
|
||||
feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]')
|
||||
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
|
||||
include_timestamps = is_true(request.POST.get('include_timestamps', False))
|
||||
include_timestamps = is_true(request.GET.get('include_timestamps', False))
|
||||
usersubs = {}
|
||||
|
||||
if not feed_ids:
|
||||
|
@ -2661,8 +2672,8 @@ def send_story_email(request):
|
|||
|
||||
share_user_profile.save_sent_email()
|
||||
|
||||
logging.user(request, '~BMSharing story by email to %s recipient%s: ~FY~SB%s~SN~BM~FY/~SB%s' %
|
||||
(len(to_addresses), '' if len(to_addresses) == 1 else 's',
|
||||
logging.user(request, '~BMSharing story by email to %s recipient%s (%s): ~FY~SB%s~SN~BM~FY/~SB%s' %
|
||||
(len(to_addresses), '' if len(to_addresses) == 1 else 's', to_addresses,
|
||||
story['story_title'][:50], feed and feed.feed_title[:50]))
|
||||
|
||||
return {'code': code, 'message': message}
|
||||
|
|
|
@ -110,4 +110,4 @@ def decline_feed(request):
|
|||
recommended_feed.declined_date = datetime.datetime.now()
|
||||
recommended_feed.save()
|
||||
|
||||
return load_recommended_feed(request)
|
||||
return load_recommended_feed(request)
|
||||
|
|
|
@ -215,7 +215,7 @@ class IconImporter(object):
|
|||
url = self._url_from_html(content)
|
||||
if not url:
|
||||
try:
|
||||
content = requests.get(self.cleaned_feed_link).content
|
||||
content = requests.get(self.cleaned_feed_link, timeout=10).content
|
||||
url = self._url_from_html(content)
|
||||
except (AttributeError, SocketError, requests.ConnectionError,
|
||||
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
||||
|
@ -224,6 +224,7 @@ class IconImporter(object):
|
|||
requests.models.ChunkedEncodingError,
|
||||
requests.models.ContentDecodingError,
|
||||
http.client.IncompleteRead,
|
||||
requests.adapters.ReadTimeout,
|
||||
LocationParseError, OpenSSLError, PyAsn1Error,
|
||||
ValueError) as e:
|
||||
logging.debug(" ---> ~SN~FRFailed~FY to fetch ~FGfeed icon~FY: %s" % e)
|
||||
|
@ -276,14 +277,12 @@ class IconImporter(object):
|
|||
@timelimit(30)
|
||||
def _1(url):
|
||||
headers = {
|
||||
'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s '
|
||||
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
|
||||
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
|
||||
'Safari/534.48.3)' %
|
||||
'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s %s' %
|
||||
(
|
||||
self.feed.num_subscribers,
|
||||
's' if self.feed.num_subscribers != 1 else '',
|
||||
self.feed.permalink
|
||||
self.feed.permalink,
|
||||
self.feed.fake_user_agent,
|
||||
),
|
||||
'Connection': 'close',
|
||||
'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8'
|
||||
|
|
|
@ -409,6 +409,10 @@ class Feed(models.Model):
|
|||
def favicon_fetching(self):
|
||||
return bool(not (self.favicon_not_found or self.favicon_color))
|
||||
|
||||
@classmethod
|
||||
def get_feed_by_url(self, *args, **kwargs):
|
||||
return self.get_feed_from_url(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False):
|
||||
feed = None
|
||||
|
@ -416,7 +420,10 @@ class Feed(models.Model):
|
|||
original_url = url
|
||||
|
||||
if url and url.startswith('newsletter:'):
|
||||
return cls.objects.get(feed_address=url)
|
||||
try:
|
||||
return cls.objects.get(feed_address=url)
|
||||
except cls.MultipleObjectsReturned:
|
||||
return cls.objects.filter(feed_address=url)[0]
|
||||
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):
|
||||
|
@ -1116,20 +1123,20 @@ class Feed(models.Model):
|
|||
# A known workaround is using facebook's user agent.
|
||||
return 'facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)'
|
||||
|
||||
ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s '
|
||||
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/56.0.2924.87 Safari/537.36)' % (
|
||||
ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s %s' % (
|
||||
self.num_subscribers,
|
||||
's' if self.num_subscribers != 1 else '',
|
||||
self.permalink,
|
||||
self.fake_user_agent,
|
||||
))
|
||||
|
||||
return ua
|
||||
|
||||
@property
|
||||
def fake_user_agent(self):
|
||||
ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:49.0) Gecko/20100101 Firefox/49.0"
|
||||
ua = ('("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
|
||||
'AppleWebKit/605.1.15 (KHTML, like Gecko) '
|
||||
'Version/14.0.1 Safari/605.1.15")')
|
||||
|
||||
return ua
|
||||
|
||||
|
@ -2796,7 +2803,17 @@ class MStory(mongo.Document):
|
|||
|
||||
if len(image_urls):
|
||||
self.image_urls = [u for u in image_urls if u]
|
||||
else:
|
||||
return
|
||||
|
||||
max_length = MStory.image_urls.field.max_length
|
||||
while len(''.join(self.image_urls)) > max_length:
|
||||
if len(self.image_urls) <= 1:
|
||||
self.image_urls[0] = self.image_urls[0][:max_length-1]
|
||||
break
|
||||
else:
|
||||
self.image_urls.pop()
|
||||
|
||||
return self.image_urls
|
||||
|
||||
def fetch_original_text(self, force=False, request=None, debug=False):
|
||||
|
@ -2833,6 +2850,7 @@ class MStarredStory(mongo.DynamicDocument):
|
|||
mongoengine's inheritance model on every single row."""
|
||||
user_id = mongo.IntField(unique_with=('story_guid',))
|
||||
starred_date = mongo.DateTimeField()
|
||||
starred_updated = mongo.DateTimeField()
|
||||
story_feed_id = mongo.IntField()
|
||||
story_date = mongo.DateTimeField()
|
||||
story_title = mongo.StringField(max_length=1024)
|
||||
|
@ -2879,7 +2897,8 @@ class MStarredStory(mongo.DynamicDocument):
|
|||
self.story_original_content_z = zlib.compress(self.story_original_content)
|
||||
self.story_original_content = None
|
||||
self.story_hash = self.feed_guid_hash
|
||||
|
||||
self.starred_updated = datetime.datetime.now()
|
||||
|
||||
return super(MStarredStory, self).save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
@ -3010,6 +3029,11 @@ class MStarredStoryCounts(mongo.Document):
|
|||
secret_token = user.profile.secret_token
|
||||
|
||||
slug = self.slug if self.slug else ""
|
||||
if not self.slug and self.tag:
|
||||
slug = slugify(self.tag)
|
||||
self.slug = slug
|
||||
self.save()
|
||||
|
||||
return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id,
|
||||
secret_token, slug)
|
||||
|
||||
|
|
|
@ -51,13 +51,11 @@ class PageImporter(object):
|
|||
@property
|
||||
def headers(self):
|
||||
return {
|
||||
'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s '
|
||||
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
|
||||
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
|
||||
'Safari/534.48.3)' % (
|
||||
'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s %s' % (
|
||||
self.feed.num_subscribers,
|
||||
's' if self.feed.num_subscribers != 1 else '',
|
||||
self.feed.permalink,
|
||||
self.feed.fake_user_agent,
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -92,11 +90,12 @@ class PageImporter(object):
|
|||
data = response.read()
|
||||
else:
|
||||
try:
|
||||
response = requests.get(feed_link, headers=self.headers)
|
||||
response = requests.get(feed_link, headers=self.headers, timeout=10)
|
||||
response.connection.close()
|
||||
except requests.exceptions.TooManyRedirects:
|
||||
response = requests.get(feed_link)
|
||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError) as e:
|
||||
response = requests.get(feed_link, timeout=10)
|
||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError,
|
||||
requests.adapters.ReadTimeout) as e:
|
||||
logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed.log_title[:30], e))
|
||||
self.save_no_page()
|
||||
return
|
||||
|
@ -186,12 +185,18 @@ class PageImporter(object):
|
|||
return
|
||||
|
||||
try:
|
||||
response = requests.get(story_permalink, headers=self.headers)
|
||||
response = requests.get(story_permalink, headers=self.headers, timeout=10)
|
||||
response.connection.close()
|
||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as e:
|
||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.TooManyRedirects,
|
||||
requests.adapters.ReadTimeout) as e:
|
||||
try:
|
||||
response = requests.get(story_permalink)
|
||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as e:
|
||||
response = requests.get(story_permalink, timeout=10)
|
||||
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.TooManyRedirects,
|
||||
requests.adapters.ReadTimeout) as e:
|
||||
logging.debug(' ***> [%-30s] Original story fetch failed using requests: %s' % (self.feed.log_title[:30], e))
|
||||
return
|
||||
try:
|
||||
|
@ -293,7 +298,8 @@ class PageImporter(object):
|
|||
feed_page.page_data = zlib.compress(html)
|
||||
feed_page.save()
|
||||
except MFeedPage.DoesNotExist:
|
||||
feed_page = MFeedPage.objects.create(feed_id=self.feed.pk, page_data=html)
|
||||
feed_page = MFeedPage.objects.create(feed_id=self.feed.pk,
|
||||
page_data=zlib.compress(html))
|
||||
return feed_page
|
||||
|
||||
def save_page_node(self, html):
|
||||
|
|
|
@ -3,7 +3,7 @@ import os
|
|||
import shutil
|
||||
import time
|
||||
import redis
|
||||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
from utils import log as logging
|
||||
from utils import s3_utils as s3
|
||||
|
@ -13,259 +13,230 @@ from utils.mongo_raw_log_middleware import MongoDumpMiddleware
|
|||
from utils.redis_raw_log_middleware import RedisDumpMiddleware
|
||||
FEED_TASKING_MAX = 10000
|
||||
|
||||
class TaskFeeds(Task):
|
||||
name = 'task-feeds'
|
||||
|
||||
def run(self, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
settings.LOG_TO_STREAM = True
|
||||
now = datetime.datetime.utcnow()
|
||||
start = time.time()
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
tasked_feeds_size = r.zcard('tasked_feeds')
|
||||
|
||||
hour_ago = now - datetime.timedelta(hours=1)
|
||||
r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
|
||||
|
||||
now_timestamp = int(now.strftime("%s"))
|
||||
queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp)
|
||||
r.zremrangebyscore('scheduled_updates', 0, now_timestamp)
|
||||
if not queued_feeds:
|
||||
logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...")
|
||||
return
|
||||
|
||||
r.sadd('queued_feeds', *queued_feeds)
|
||||
logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
len(queued_feeds),
|
||||
r.zcard('tasked_feeds'),
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('scheduled_updates')))
|
||||
|
||||
# Regular feeds
|
||||
if tasked_feeds_size < FEED_TASKING_MAX:
|
||||
feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX)
|
||||
Feed.task_feeds(feeds, verbose=True)
|
||||
active_count = len(feeds)
|
||||
else:
|
||||
logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
|
||||
active_count = 0
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
active_count,
|
||||
int((time.time() - start)),
|
||||
r.zcard('tasked_feeds'),
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('scheduled_updates')))
|
||||
|
||||
class TaskBrokenFeeds(Task):
|
||||
name = 'task-broken-feeds'
|
||||
max_retries = 0
|
||||
ignore_result = True
|
||||
@app.task(name='task-feeds')
|
||||
def TaskFeeds():
|
||||
from apps.rss_feeds.models import Feed
|
||||
settings.LOG_TO_STREAM = True
|
||||
now = datetime.datetime.utcnow()
|
||||
start = time.time()
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
tasked_feeds_size = r.zcard('tasked_feeds')
|
||||
|
||||
def run(self, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
settings.LOG_TO_STREAM = True
|
||||
now = datetime.datetime.utcnow()
|
||||
start = time.time()
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
|
||||
logging.debug(" ---> ~SN~FBQueuing broken feeds...")
|
||||
|
||||
# Force refresh feeds
|
||||
refresh_feeds = Feed.objects.filter(
|
||||
active=True,
|
||||
fetched_once=False,
|
||||
active_subscribers__gte=1
|
||||
).order_by('?')[:100]
|
||||
refresh_count = refresh_feeds.count()
|
||||
cp1 = time.time()
|
||||
|
||||
logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count)
|
||||
|
||||
# Mistakenly inactive feeds
|
||||
hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s')
|
||||
old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago)
|
||||
inactive_count = len(old_tasked_feeds)
|
||||
if inactive_count:
|
||||
r.zremrangebyscore('tasked_feeds', 0, hours_ago)
|
||||
# r.sadd('queued_feeds', *old_tasked_feeds)
|
||||
for feed_id in old_tasked_feeds:
|
||||
r.zincrby('error_feeds', 1, feed_id)
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
feed.set_next_scheduled_update()
|
||||
logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % (
|
||||
inactive_count,
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('tasked_feeds')))
|
||||
cp2 = time.time()
|
||||
|
||||
old = now - datetime.timedelta(days=1)
|
||||
old_feeds = Feed.objects.filter(
|
||||
next_scheduled_update__lte=old,
|
||||
active_subscribers__gte=1
|
||||
).order_by('?')[:500]
|
||||
old_count = old_feeds.count()
|
||||
cp3 = time.time()
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % (
|
||||
refresh_count,
|
||||
inactive_count,
|
||||
old_count,
|
||||
cp1 - start,
|
||||
cp2 - cp1,
|
||||
cp3 - cp2,
|
||||
))
|
||||
|
||||
Feed.task_feeds(refresh_feeds, verbose=False)
|
||||
Feed.task_feeds(old_feeds, verbose=False)
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
int((time.time() - start)),
|
||||
r.zcard('tasked_feeds'),
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('scheduled_updates')))
|
||||
|
||||
class UpdateFeeds(Task):
|
||||
name = 'update-feeds'
|
||||
max_retries = 0
|
||||
ignore_result = True
|
||||
time_limit = 10*60
|
||||
soft_time_limit = 9*60
|
||||
|
||||
def run(self, feed_pks, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
from apps.statistics.models import MStatistics
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
|
||||
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
|
||||
compute_scores = bool(mongodb_replication_lag < 10)
|
||||
|
||||
profiler = DBProfilerMiddleware()
|
||||
profiler_activated = profiler.process_celery()
|
||||
if profiler_activated:
|
||||
mongo_middleware = MongoDumpMiddleware()
|
||||
mongo_middleware.process_celery(profiler)
|
||||
redis_middleware = RedisDumpMiddleware()
|
||||
redis_middleware.process_celery(profiler)
|
||||
|
||||
options = {
|
||||
'quick': float(MStatistics.get('quick_fetch', 0)),
|
||||
'updates_off': MStatistics.get('updates_off', False),
|
||||
'compute_scores': compute_scores,
|
||||
'mongodb_replication_lag': mongodb_replication_lag,
|
||||
}
|
||||
|
||||
if not isinstance(feed_pks, list):
|
||||
feed_pks = [feed_pks]
|
||||
|
||||
for feed_pk in feed_pks:
|
||||
feed = Feed.get_by_id(feed_pk)
|
||||
if not feed or feed.pk != int(feed_pk):
|
||||
logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk))
|
||||
r.zrem('tasked_feeds', feed_pk)
|
||||
if not feed:
|
||||
continue
|
||||
try:
|
||||
feed.update(**options)
|
||||
except SoftTimeLimitExceeded as e:
|
||||
feed.save_feed_history(505, 'Timeout', e)
|
||||
logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
|
||||
if profiler_activated: profiler.process_celery_finished()
|
||||
|
||||
class NewFeeds(Task):
|
||||
name = 'new-feeds'
|
||||
max_retries = 0
|
||||
ignore_result = True
|
||||
time_limit = 10*60
|
||||
soft_time_limit = 9*60
|
||||
|
||||
def run(self, feed_pks, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
if not isinstance(feed_pks, list):
|
||||
feed_pks = [feed_pks]
|
||||
|
||||
options = {}
|
||||
for feed_pk in feed_pks:
|
||||
feed = Feed.get_by_id(feed_pk)
|
||||
if not feed: continue
|
||||
feed.update(options=options)
|
||||
|
||||
class PushFeeds(Task):
|
||||
name = 'push-feeds'
|
||||
max_retries = 0
|
||||
ignore_result = True
|
||||
|
||||
def run(self, feed_id, xml, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
from apps.statistics.models import MStatistics
|
||||
|
||||
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
|
||||
compute_scores = bool(mongodb_replication_lag < 60)
|
||||
|
||||
options = {
|
||||
'feed_xml': xml,
|
||||
'compute_scores': compute_scores,
|
||||
'mongodb_replication_lag': mongodb_replication_lag,
|
||||
}
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
if feed:
|
||||
feed.update(options=options)
|
||||
|
||||
class BackupMongo(Task):
|
||||
name = 'backup-mongo'
|
||||
max_retries = 0
|
||||
ignore_result = True
|
||||
hour_ago = now - datetime.timedelta(hours=1)
|
||||
r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
|
||||
|
||||
def run(self, **kwargs):
|
||||
COLLECTIONS = "classifier_tag classifier_author classifier_feed classifier_title userstories starred_stories shared_stories category category_site sent_emails social_profile social_subscription social_services statistics feedback"
|
||||
now_timestamp = int(now.strftime("%s"))
|
||||
queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp)
|
||||
r.zremrangebyscore('scheduled_updates', 0, now_timestamp)
|
||||
if not queued_feeds:
|
||||
logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...")
|
||||
return
|
||||
|
||||
r.sadd('queued_feeds', *queued_feeds)
|
||||
logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
len(queued_feeds),
|
||||
r.zcard('tasked_feeds'),
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('scheduled_updates')))
|
||||
|
||||
# Regular feeds
|
||||
if tasked_feeds_size < FEED_TASKING_MAX:
|
||||
feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX)
|
||||
Feed.task_feeds(feeds, verbose=True)
|
||||
active_count = len(feeds)
|
||||
else:
|
||||
logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
|
||||
active_count = 0
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
active_count,
|
||||
int((time.time() - start)),
|
||||
r.zcard('tasked_feeds'),
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('scheduled_updates')))
|
||||
|
||||
date = time.strftime('%Y-%m-%d-%H-%M')
|
||||
collections = COLLECTIONS.split(' ')
|
||||
db_name = 'newsblur'
|
||||
dir_name = 'backup_mongo_%s' % date
|
||||
filename = '%s.tgz' % dir_name
|
||||
@app.task(name='task-broken-feeds')
|
||||
def TaskBrokenFeeds():
|
||||
from apps.rss_feeds.models import Feed
|
||||
settings.LOG_TO_STREAM = True
|
||||
now = datetime.datetime.utcnow()
|
||||
start = time.time()
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
|
||||
logging.debug(" ---> ~SN~FBQueuing broken feeds...")
|
||||
|
||||
# Force refresh feeds
|
||||
refresh_feeds = Feed.objects.filter(
|
||||
active=True,
|
||||
fetched_once=False,
|
||||
active_subscribers__gte=1
|
||||
).order_by('?')[:100]
|
||||
refresh_count = refresh_feeds.count()
|
||||
cp1 = time.time()
|
||||
|
||||
logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count)
|
||||
|
||||
os.mkdir(dir_name)
|
||||
# Mistakenly inactive feeds
|
||||
hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s')
|
||||
old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago)
|
||||
inactive_count = len(old_tasked_feeds)
|
||||
if inactive_count:
|
||||
r.zremrangebyscore('tasked_feeds', 0, hours_ago)
|
||||
# r.sadd('queued_feeds', *old_tasked_feeds)
|
||||
for feed_id in old_tasked_feeds:
|
||||
r.zincrby('error_feeds', 1, feed_id)
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
feed.set_next_scheduled_update()
|
||||
logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % (
|
||||
inactive_count,
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('tasked_feeds')))
|
||||
cp2 = time.time()
|
||||
|
||||
old = now - datetime.timedelta(days=1)
|
||||
old_feeds = Feed.objects.filter(
|
||||
next_scheduled_update__lte=old,
|
||||
active_subscribers__gte=1
|
||||
).order_by('?')[:500]
|
||||
old_count = old_feeds.count()
|
||||
cp3 = time.time()
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % (
|
||||
refresh_count,
|
||||
inactive_count,
|
||||
old_count,
|
||||
cp1 - start,
|
||||
cp2 - cp1,
|
||||
cp3 - cp2,
|
||||
))
|
||||
|
||||
Feed.task_feeds(refresh_feeds, verbose=False)
|
||||
Feed.task_feeds(old_feeds, verbose=False)
|
||||
|
||||
logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
|
||||
int((time.time() - start)),
|
||||
r.zcard('tasked_feeds'),
|
||||
r.scard('queued_feeds'),
|
||||
r.zcard('scheduled_updates')))
|
||||
|
||||
@app.task(name='update-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True)
|
||||
def UpdateFeeds(feed_pks):
|
||||
from apps.rss_feeds.models import Feed
|
||||
from apps.statistics.models import MStatistics
|
||||
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
|
||||
|
||||
for collection in collections:
|
||||
cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
|
||||
logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
|
||||
os.system(cmd)
|
||||
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
|
||||
compute_scores = bool(mongodb_replication_lag < 10)
|
||||
|
||||
profiler = DBProfilerMiddleware()
|
||||
profiler_activated = profiler.process_celery()
|
||||
if profiler_activated:
|
||||
mongo_middleware = MongoDumpMiddleware()
|
||||
mongo_middleware.process_celery(profiler)
|
||||
redis_middleware = RedisDumpMiddleware()
|
||||
redis_middleware.process_celery(profiler)
|
||||
|
||||
options = {
|
||||
'quick': float(MStatistics.get('quick_fetch', 0)),
|
||||
'updates_off': MStatistics.get('updates_off', False),
|
||||
'compute_scores': compute_scores,
|
||||
'mongodb_replication_lag': mongodb_replication_lag,
|
||||
}
|
||||
|
||||
if not isinstance(feed_pks, list):
|
||||
feed_pks = [feed_pks]
|
||||
|
||||
for feed_pk in feed_pks:
|
||||
feed = Feed.get_by_id(feed_pk)
|
||||
if not feed or feed.pk != int(feed_pk):
|
||||
logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk))
|
||||
r.zrem('tasked_feeds', feed_pk)
|
||||
if not feed:
|
||||
continue
|
||||
try:
|
||||
feed.update(**options)
|
||||
except SoftTimeLimitExceeded as e:
|
||||
feed.save_feed_history(505, 'Timeout', e)
|
||||
logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
|
||||
if profiler_activated: profiler.process_celery_finished()
|
||||
|
||||
cmd = 'tar -jcf %s %s' % (filename, dir_name)
|
||||
@app.task(name='new-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True)
|
||||
def NewFeeds(feed_pks):
|
||||
from apps.rss_feeds.models import Feed
|
||||
if not isinstance(feed_pks, list):
|
||||
feed_pks = [feed_pks]
|
||||
|
||||
options = {}
|
||||
for feed_pk in feed_pks:
|
||||
feed = Feed.get_by_id(feed_pk)
|
||||
if not feed: continue
|
||||
feed.update(options=options)
|
||||
|
||||
@app.task(name='push-feeds', ignore_result=True)
|
||||
def PushFeeds(feed_id, xml):
|
||||
from apps.rss_feeds.models import Feed
|
||||
from apps.statistics.models import MStatistics
|
||||
|
||||
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
|
||||
compute_scores = bool(mongodb_replication_lag < 60)
|
||||
|
||||
options = {
|
||||
'feed_xml': xml,
|
||||
'compute_scores': compute_scores,
|
||||
'mongodb_replication_lag': mongodb_replication_lag,
|
||||
}
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
if feed:
|
||||
feed.update(options=options)
|
||||
|
||||
@app.task(name='backup-mongo', ignore_result=True)
|
||||
def BackupMongo():
|
||||
COLLECTIONS = "classifier_tag classifier_author classifier_feed classifier_title userstories starred_stories shared_stories category category_site sent_emails social_profile social_subscription social_services statistics feedback"
|
||||
|
||||
date = time.strftime('%Y-%m-%d-%H-%M')
|
||||
collections = COLLECTIONS.split(' ')
|
||||
db_name = 'newsblur'
|
||||
dir_name = 'backup_mongo_%s' % date
|
||||
filename = '%s.tgz' % dir_name
|
||||
|
||||
os.mkdir(dir_name)
|
||||
|
||||
for collection in collections:
|
||||
cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
|
||||
logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
|
||||
os.system(cmd)
|
||||
|
||||
logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
|
||||
s3.save_file_in_s3(filename)
|
||||
shutil.rmtree(dir_name)
|
||||
os.remove(filename)
|
||||
logging.debug(' ---> ~FRFinished uploading ~SB~FM%s~SN~FR to S3.' % filename)
|
||||
cmd = 'tar -jcf %s %s' % (filename, dir_name)
|
||||
os.system(cmd)
|
||||
|
||||
logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
|
||||
s3.save_file_in_s3(filename)
|
||||
shutil.rmtree(dir_name)
|
||||
os.remove(filename)
|
||||
logging.debug(' ---> ~FRFinished uploading ~SB~FM%s~SN~FR to S3.' % filename)
|
||||
|
||||
|
||||
class ScheduleImmediateFetches(Task):
|
||||
@app.task()
|
||||
def ScheduleImmediateFetches(feed_ids, user_id=None):
|
||||
from apps.rss_feeds.models import Feed
|
||||
|
||||
def run(self, feed_ids, user_id=None, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
|
||||
if not isinstance(feed_ids, list):
|
||||
feed_ids = [feed_ids]
|
||||
|
||||
Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
|
||||
if not isinstance(feed_ids, list):
|
||||
feed_ids = [feed_ids]
|
||||
|
||||
Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
|
||||
|
||||
|
||||
class SchedulePremiumSetup(Task):
|
||||
@app.task()
|
||||
def SchedulePremiumSetup(feed_ids):
|
||||
from apps.rss_feeds.models import Feed
|
||||
|
||||
def run(self, feed_ids, **kwargs):
|
||||
from apps.rss_feeds.models import Feed
|
||||
|
||||
if not isinstance(feed_ids, list):
|
||||
feed_ids = [feed_ids]
|
||||
|
||||
Feed.setup_feeds_for_premium_subscribers(feed_ids)
|
||||
|
||||
class ScheduleCountTagsForUser(Task):
|
||||
if not isinstance(feed_ids, list):
|
||||
feed_ids = [feed_ids]
|
||||
|
||||
def run(self, user_id):
|
||||
from apps.rss_feeds.models import MStarredStoryCounts
|
||||
|
||||
MStarredStoryCounts.count_for_user(user_id)
|
||||
Feed.setup_feeds_for_premium_subscribers(feed_ids)
|
||||
|
||||
@app.task()
|
||||
def ScheduleCountTagsForUser(user_id):
|
||||
from apps.rss_feeds.models import MStarredStoryCounts
|
||||
|
||||
MStarredStoryCounts.count_for_user(user_id)
|
||||
|
|
|
@ -37,13 +37,11 @@ class TextImporter:
|
|||
def headers(self):
|
||||
num_subscribers = getattr(self.feed, 'num_subscribers', 0)
|
||||
return {
|
||||
'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s '
|
||||
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
|
||||
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
|
||||
'Safari/534.48.3)' % (
|
||||
'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s %s' % (
|
||||
num_subscribers,
|
||||
's' if num_subscribers != 1 else '',
|
||||
getattr(self.feed, 'permalink', '')
|
||||
getattr(self.feed, 'permalink', ''),
|
||||
getattr(self.feed, 'fake_user_agent', ''),
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -203,7 +201,7 @@ class TextImporter:
|
|||
url = "https://www.newsblur.com/rss_feeds/original_text_fetcher?url=%s" % url
|
||||
|
||||
try:
|
||||
r = requests.get(url, headers=headers)
|
||||
r = requests.get(url, headers=headers, verify=False, timeout=15)
|
||||
r.connection.close()
|
||||
except (AttributeError, SocketError, requests.ConnectionError,
|
||||
requests.models.MissingSchema, requests.sessions.InvalidSchema,
|
||||
|
@ -211,6 +209,7 @@ class TextImporter:
|
|||
requests.models.InvalidURL,
|
||||
requests.models.ChunkedEncodingError,
|
||||
requests.models.ContentDecodingError,
|
||||
requests.adapters.ReadTimeout,
|
||||
urllib3.exceptions.LocationValueError,
|
||||
LocationParseError, OpenSSLError, PyAsn1Error) as e:
|
||||
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % e)
|
||||
|
|
|
@ -185,7 +185,7 @@ def load_feed_statistics_embedded(request, feed_id):
|
|||
)
|
||||
|
||||
def assemble_statistics(user, feed_id):
|
||||
timezone = user.profile.timezone
|
||||
user_timezone = user.profile.timezone
|
||||
stats = dict()
|
||||
feed = get_object_or_404(Feed, pk=feed_id)
|
||||
feed.update_all_statistics()
|
||||
|
@ -201,7 +201,7 @@ def assemble_statistics(user, feed_id):
|
|||
if feed.is_push:
|
||||
try:
|
||||
stats['push_expires'] = localtime_for_timezone(feed.push.lease_expires,
|
||||
timezone).strftime("%Y-%m-%d %H:%M:%S")
|
||||
user_timezone).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except PushSubscription.DoesNotExist:
|
||||
stats['push_expires'] = 'Missing push'
|
||||
feed.is_push = False
|
||||
|
@ -233,7 +233,7 @@ def assemble_statistics(user, feed_id):
|
|||
stats['story_count_history'] = story_count_history
|
||||
|
||||
# Rotate hours to match user's timezone offset
|
||||
localoffset = timezone.utcoffset(datetime.datetime.utcnow())
|
||||
localoffset = user_timezone.utcoffset(datetime.datetime.utcnow())
|
||||
hours_offset = int(localoffset.total_seconds() / 3600)
|
||||
rotated_hours = {}
|
||||
for hour, value in list(stats['story_hours_history'].items()):
|
||||
|
@ -253,7 +253,7 @@ def assemble_statistics(user, feed_id):
|
|||
stats['classifier_counts'] = json.decode(feed.data.feed_classifier_counts)
|
||||
|
||||
# Fetch histories
|
||||
fetch_history = MFetchHistory.feed(feed_id, timezone=timezone)
|
||||
fetch_history = MFetchHistory.feed(feed_id, timezone=user_timezone)
|
||||
stats['feed_fetch_history'] = fetch_history['feed_fetch_history']
|
||||
stats['page_fetch_history'] = fetch_history['page_fetch_history']
|
||||
stats['feed_push_history'] = fetch_history['push_history']
|
||||
|
@ -515,11 +515,13 @@ def status(request):
|
|||
|
||||
@json.json_view
|
||||
def original_text(request):
|
||||
story_id = request.GET.get('story_id')
|
||||
feed_id = request.GET.get('feed_id')
|
||||
story_hash = request.GET.get('story_hash', None)
|
||||
force = request.GET.get('force', False)
|
||||
debug = request.GET.get('debug', False)
|
||||
# iOS sends a POST, web sends a GET
|
||||
GET_POST = getattr(request, request.method)
|
||||
story_id = GET_POST.get('story_id')
|
||||
feed_id = GET_POST.get('feed_id')
|
||||
story_hash = GET_POST.get('story_hash', None)
|
||||
force = GET_POST.get('force', False)
|
||||
debug = GET_POST.get('debug', False)
|
||||
|
||||
if story_hash:
|
||||
story, _ = MStory.find_story(story_hash=story_hash)
|
||||
|
@ -542,7 +544,7 @@ def original_text(request):
|
|||
'failed': not original_text or len(original_text) < 100,
|
||||
}
|
||||
|
||||
@required_params('story_hash')
|
||||
@required_params('story_hash', method="GET")
|
||||
def original_story(request):
|
||||
story_hash = request.GET.get('story_hash')
|
||||
force = request.GET.get('force', False)
|
||||
|
@ -559,7 +561,7 @@ def original_story(request):
|
|||
|
||||
return HttpResponse(original_page or "")
|
||||
|
||||
@required_params('story_hash')
|
||||
@required_params('story_hash', method="GET")
|
||||
@json.json_view
|
||||
def story_changes(request):
|
||||
story_hash = request.GET.get('story_hash', None)
|
||||
|
|
|
@ -78,7 +78,7 @@ class MUserSearch(mongo.Document):
|
|||
logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." %
|
||||
(total, len(feed_id_chunks)))
|
||||
|
||||
tasks = [IndexSubscriptionsChunkForSearch().s(feed_ids=feed_id_chunk,
|
||||
tasks = [IndexSubscriptionsChunkForSearch.s(feed_ids=feed_id_chunk,
|
||||
user_id=self.user_id
|
||||
).set(queue='search_indexer')
|
||||
for feed_id_chunk in feed_id_chunks]
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from utils import log as logging
|
||||
|
||||
class IndexSubscriptionsForSearch(Task):
|
||||
@app.task()
|
||||
def IndexSubscriptionsForSearch(user_id):
|
||||
from apps.search.models import MUserSearch
|
||||
|
||||
def run(self, user_id):
|
||||
from apps.search.models import MUserSearch
|
||||
|
||||
user_search = MUserSearch.get_user(user_id)
|
||||
user_search.index_subscriptions_for_search()
|
||||
user_search = MUserSearch.get_user(user_id)
|
||||
user_search.index_subscriptions_for_search()
|
||||
|
||||
class IndexSubscriptionsChunkForSearch(Task):
|
||||
@app.task()
|
||||
def IndexSubscriptionsChunkForSearch(feed_ids, user_id):
|
||||
logging.debug(" ---> Indexing: %s for %s" % (feed_ids, user_id))
|
||||
from apps.search.models import MUserSearch
|
||||
|
||||
ignore_result = False
|
||||
|
||||
def run(self, feed_ids, user_id):
|
||||
from apps.search.models import MUserSearch
|
||||
|
||||
user_search = MUserSearch.get_user(user_id)
|
||||
user_search.index_subscriptions_chunk_for_search(feed_ids)
|
||||
user_search = MUserSearch.get_user(user_id)
|
||||
user_search.index_subscriptions_chunk_for_search(feed_ids)
|
||||
|
||||
class IndexFeedsForSearch(Task):
|
||||
@app.task()
|
||||
def IndexFeedsForSearch(feed_ids, user_id):
|
||||
from apps.search.models import MUserSearch
|
||||
|
||||
def run(self, feed_ids, user_id):
|
||||
from apps.search.models import MUserSearch
|
||||
|
||||
MUserSearch.index_feeds_for_search(feed_ids, user_id)
|
||||
MUserSearch.index_feeds_for_search(feed_ids, user_id)
|
||||
|
|
|
@ -2304,6 +2304,13 @@ class MSharedStory(mongo.DynamicDocument):
|
|||
image_sources = [img.get('src') for img in soup.findAll('img') if img and img.get('src')]
|
||||
if len(image_sources) > 0:
|
||||
self.image_urls = image_sources
|
||||
max_length = MSharedStory.image_urls.field.max_length
|
||||
while len(''.join(self.image_urls)) > max_length:
|
||||
if len(self.image_urls) <= 1:
|
||||
self.image_urls[0] = self.image_urls[0][:max_length-1]
|
||||
break
|
||||
else:
|
||||
self.image_urls.pop()
|
||||
self.save()
|
||||
|
||||
def calculate_image_sizes(self, force=False):
|
||||
|
@ -2314,10 +2321,7 @@ class MSharedStory(mongo.DynamicDocument):
|
|||
return self.image_sizes
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'NewsBlur Image Fetcher - %s '
|
||||
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
|
||||
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
|
||||
'Safari/534.48.3)' % (
|
||||
'User-Agent': 'NewsBlur Image Fetcher - %s' % (
|
||||
settings.NEWSBLUR_URL
|
||||
),
|
||||
}
|
||||
|
@ -2328,7 +2332,7 @@ class MSharedStory(mongo.DynamicDocument):
|
|||
for image_source in self.image_urls[:10]:
|
||||
if any(ignore in image_source for ignore in IGNORE_IMAGE_SOURCES):
|
||||
continue
|
||||
req = requests.get(image_source, headers=headers, stream=True)
|
||||
req = requests.get(image_source, headers=headers, stream=True, timeout=10)
|
||||
try:
|
||||
datastream = BytesIO(req.content)
|
||||
width, height = ImageOps.image_size(datastream)
|
||||
|
@ -2713,7 +2717,7 @@ class MSocialServices(mongo.Document):
|
|||
os.remove(filename)
|
||||
else:
|
||||
api.update_status(status=message)
|
||||
except tweepy.TweepError as e:
|
||||
except (tweepy.TweepError, requests.adapters.ReadError) as e:
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
logging.user(user, "~FRTwitter error: ~SB%s" % e)
|
||||
return
|
||||
|
@ -2728,7 +2732,7 @@ class MSocialServices(mongo.Document):
|
|||
|
||||
url = shared_story.image_urls[0]
|
||||
image_filename = os.path.basename(urllib.parse.urlparse(url).path)
|
||||
req = requests.get(url, stream=True)
|
||||
req = requests.get(url, stream=True, timeout=10)
|
||||
filename = "/tmp/%s-%s" % (shared_story.story_hash, image_filename)
|
||||
|
||||
if req.status_code == 200:
|
||||
|
|
|
@ -1,92 +1,79 @@
|
|||
from bson.objectid import ObjectId
|
||||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices, MSocialSubscription
|
||||
from django.contrib.auth.models import User
|
||||
from utils import log as logging
|
||||
|
||||
|
||||
class PostToService(Task):
|
||||
|
||||
def run(self, shared_story_id, service):
|
||||
try:
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
shared_story.post_to_service(service)
|
||||
except MSharedStory.DoesNotExist:
|
||||
logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
|
||||
@app.task()
|
||||
def PostToService(shared_story_id, service):
|
||||
try:
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
shared_story.post_to_service(service)
|
||||
except MSharedStory.DoesNotExist:
|
||||
logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
|
||||
|
||||
class EmailNewFollower(Task):
|
||||
|
||||
def run(self, follower_user_id, followee_user_id):
|
||||
user_profile = MSocialProfile.get_user(followee_user_id)
|
||||
user_profile.send_email_for_new_follower(follower_user_id)
|
||||
@app.task()
|
||||
def EmailNewFollower(follower_user_id, followee_user_id):
|
||||
user_profile = MSocialProfile.get_user(followee_user_id)
|
||||
user_profile.send_email_for_new_follower(follower_user_id)
|
||||
|
||||
class EmailFollowRequest(Task):
|
||||
|
||||
def run(self, follower_user_id, followee_user_id):
|
||||
user_profile = MSocialProfile.get_user(followee_user_id)
|
||||
user_profile.send_email_for_follow_request(follower_user_id)
|
||||
@app.task()
|
||||
def EmailFollowRequest(follower_user_id, followee_user_id):
|
||||
user_profile = MSocialProfile.get_user(followee_user_id)
|
||||
user_profile.send_email_for_follow_request(follower_user_id)
|
||||
|
||||
class EmailFirstShare(Task):
|
||||
|
||||
def run(self, user_id):
|
||||
user = User.objects.get(pk=user_id)
|
||||
user.profile.send_first_share_to_blurblog_email()
|
||||
@app.task()
|
||||
def EmailFirstShare(user_id):
|
||||
user = User.objects.get(pk=user_id)
|
||||
user.profile.send_first_share_to_blurblog_email()
|
||||
|
||||
class EmailCommentReplies(Task):
|
||||
|
||||
def run(self, shared_story_id, reply_id):
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
shared_story.send_emails_for_new_reply(ObjectId(reply_id))
|
||||
@app.task()
|
||||
def EmailCommentReplies(shared_story_id, reply_id):
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
shared_story.send_emails_for_new_reply(ObjectId(reply_id))
|
||||
|
||||
class EmailStoryReshares(Task):
|
||||
|
||||
def run(self, shared_story_id):
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
shared_story.send_email_for_reshare()
|
||||
@app.task()
|
||||
def EmailStoryReshares(shared_story_id):
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
shared_story.send_email_for_reshare()
|
||||
|
||||
class SyncTwitterFriends(Task):
|
||||
|
||||
def run(self, user_id):
|
||||
social_services = MSocialServices.objects.get(user_id=user_id)
|
||||
social_services.sync_twitter_friends()
|
||||
@app.task()
|
||||
def SyncTwitterFriends(user_id):
|
||||
social_services = MSocialServices.objects.get(user_id=user_id)
|
||||
social_services.sync_twitter_friends()
|
||||
|
||||
class SyncFacebookFriends(Task):
|
||||
|
||||
def run(self, user_id):
|
||||
social_services = MSocialServices.objects.get(user_id=user_id)
|
||||
social_services.sync_facebook_friends()
|
||||
@app.task()
|
||||
def SyncFacebookFriends(user_id):
|
||||
social_services = MSocialServices.objects.get(user_id=user_id)
|
||||
social_services.sync_facebook_friends()
|
||||
|
||||
class SharePopularStories(Task):
|
||||
name = 'share-popular-stories'
|
||||
|
||||
def run(self, **kwargs):
|
||||
logging.debug(" ---> Sharing popular stories...")
|
||||
MSharedStory.share_popular_stories(interactive=False)
|
||||
@app.task(name="share-popular-stories")
|
||||
def SharePopularStories():
|
||||
logging.debug(" ---> Sharing popular stories...")
|
||||
MSharedStory.share_popular_stories(interactive=False)
|
||||
|
||||
class CleanSocialSpam(Task):
|
||||
name = 'clean-social-spam'
|
||||
|
||||
def run(self, **kwargs):
|
||||
logging.debug(" ---> Finding social spammers...")
|
||||
MSharedStory.count_potential_spammers(destroy=True)
|
||||
@app.task(name='clean-social-spam')
|
||||
def CleanSocialSpam():
|
||||
logging.debug(" ---> Finding social spammers...")
|
||||
MSharedStory.count_potential_spammers(destroy=True)
|
||||
|
||||
|
||||
class UpdateRecalcForSubscription(Task):
|
||||
@app.task()
|
||||
def UpdateRecalcForSubscription(subscription_user_id, shared_story_id):
|
||||
user = User.objects.get(pk=subscription_user_id)
|
||||
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
|
||||
try:
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
except MSharedStory.DoesNotExist:
|
||||
return
|
||||
|
||||
def run(self, subscription_user_id, shared_story_id):
|
||||
user = User.objects.get(pk=subscription_user_id)
|
||||
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
|
||||
try:
|
||||
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
|
||||
except MSharedStory.DoesNotExist:
|
||||
return
|
||||
|
||||
logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
|
||||
socialsubs.count(),
|
||||
user.username
|
||||
))
|
||||
for socialsub in socialsubs:
|
||||
socialsub.needs_unread_recalc = True
|
||||
socialsub.save()
|
||||
|
||||
shared_story.publish_update_to_subscribers()
|
||||
logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
|
||||
socialsubs.count(),
|
||||
user.username
|
||||
))
|
||||
for socialsub in socialsubs:
|
||||
socialsub.needs_unread_recalc = True
|
||||
socialsub.save()
|
||||
|
||||
shared_story.publish_update_to_subscribers()
|
||||
|
|
|
@ -507,7 +507,7 @@ def load_social_page(request, user_id, username=None, **kwargs):
|
|||
|
||||
return render(request, template, params)
|
||||
|
||||
@required_params('story_id', feed_id=int)
|
||||
@required_params('story_id', feed_id=int, method="GET")
|
||||
def story_public_comments(request):
|
||||
format = request.GET.get('format', 'json')
|
||||
relative_user_id = request.GET.get('user_id', None)
|
||||
|
@ -1175,7 +1175,7 @@ def ignore_follower(request):
|
|||
return {'code': code}
|
||||
|
||||
|
||||
@required_params('query')
|
||||
@required_params('query', method="GET")
|
||||
@json.json_view
|
||||
def find_friends(request):
|
||||
query = request.GET['query']
|
||||
|
@ -1372,7 +1372,7 @@ def shared_stories_rss_feed(request, user_id, username):
|
|||
))
|
||||
return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml')
|
||||
|
||||
@required_params('user_id')
|
||||
@required_params('user_id', method="GET")
|
||||
@json.json_view
|
||||
def social_feed_trainer(request):
|
||||
social_user_id = request.GET['user_id']
|
||||
|
|
|
@ -15,7 +15,7 @@ def faq(request):
|
|||
return render(request, 'static/faq.xhtml')
|
||||
|
||||
def api(request):
|
||||
filename = settings.TEMPLATE_DIRS[0] + '/static/api.yml'
|
||||
filename = settings.TEMPLATES[0]['DIRS'][0] + '/static/api.yml'
|
||||
api_yml_file = open(filename).read()
|
||||
data = yaml.load(api_yml_file)
|
||||
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
from celery import Task
|
||||
from newsblur.celeryapp import app
|
||||
from apps.statistics.models import MStatistics
|
||||
from apps.statistics.models import MFeedback
|
||||
# from utils import log as logging
|
||||
from utils import log as logging
|
||||
|
||||
|
||||
|
||||
class CollectStats(Task):
|
||||
name = 'collect-stats'
|
||||
|
||||
def run(self, **kwargs):
|
||||
# logging.debug(" ---> ~FBCollecting stats...")
|
||||
MStatistics.collect_statistics()
|
||||
@app.task(name='collect-stats')
|
||||
def CollectStats():
|
||||
logging.debug(" ---> ~FBCollecting stats...")
|
||||
MStatistics.collect_statistics()
|
||||
|
||||
|
||||
class CollectFeedback(Task):
|
||||
name = 'collect-feedback'
|
||||
|
||||
def run(self, **kwargs):
|
||||
# logging.debug(" ---> ~FBCollecting feedback...")
|
||||
MFeedback.collect_feedback()
|
||||
@app.task(name='collect-feedback')
|
||||
def CollectFeedback():
|
||||
logging.debug(" ---> ~FBCollecting feedback...")
|
||||
MFeedback.collect_feedback()
|
||||
|
|
|
@ -128,6 +128,13 @@
|
|||
|
||||
<activity
|
||||
android:name=".activity.InAppBrowser" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.Premium" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.MuteConfig"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.SearchForFeeds" android:launchMode="singleTop" >
|
||||
|
@ -174,7 +181,7 @@
|
|||
android:exported="false">
|
||||
</receiver>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.newsblur.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.4.10'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
|
@ -8,7 +9,8 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,33 +20,40 @@ repositories {
|
|||
url 'https://maven.google.com'
|
||||
}
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-core-utils:28.0.0'
|
||||
implementation 'com.android.support:support-fragment:28.0.0'
|
||||
implementation 'com.android.support:support-core-ui:28.0.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.android.support:recyclerview-v7:28.0.0'
|
||||
implementation 'com.android.billingclient:billing:3.0.1'
|
||||
implementation 'nl.dionsegijn:konfetti:1.2.2'
|
||||
implementation 'com.github.jinatonic.confetti:confetti:1.1.2'
|
||||
implementation 'com.google.android.play:core:1.8.3'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "com.newsblur"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 168
|
||||
versionName "10.1"
|
||||
targetSdkVersion 29
|
||||
versionCode 177
|
||||
versionName "10.1.1"
|
||||
}
|
||||
compileOptions.with {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_7
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
viewBinding.enabled = true
|
||||
android.buildFeatures.viewBinding = true
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
|
|
2
clients/android/NewsBlur/gradle.properties
Normal file
2
clients/android/NewsBlur/gradle.properties
Normal file
|
@ -0,0 +1,2 @@
|
|||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
20
clients/android/NewsBlur/release/output.json
Normal file
20
clients/android/NewsBlur/release/output.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 1,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.newsblur",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"properties": [],
|
||||
"versionCode": 171,
|
||||
"versionName": "171",
|
||||
"enabled": true,
|
||||
"outputFile": "NewsBlur-release.apk"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,8 +9,7 @@
|
|||
<item
|
||||
android:top="0.5dp"
|
||||
android:bottom="0.5dp">
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/dark_feed_background_selected_end"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
<item
|
||||
android:top="0.5dp"
|
||||
android:bottom="0.5dp">
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/feed_background_selected_end"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
|
9
clients/android/NewsBlur/res/drawable/ic_bookmark.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_bookmark.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M19,18l2,1V3c0,-1.1 -0.9,-2 -2,-2H8.99C7.89,1 7,1.9 7,3h10c1.1,0 2,0.9 2,2v13zM15,5H5c-1.1,0 -2,0.9 -2,2v16l7,-3 7,3V7c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_dining.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_dining.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M8.1,13.34l2.83,-2.83L3.91,3.5c-1.56,1.56 -1.56,4.09 0,5.66l4.19,4.18zM14.88,11.53c1.53,0.71 3.68,0.21 5.27,-1.38 1.91,-1.91 2.28,-4.65 0.81,-6.12 -1.46,-1.46 -4.2,-1.1 -6.12,0.81 -1.59,1.59 -2.09,3.74 -1.38,5.27L3.7,19.87l1.41,1.41L12,14.41l6.88,6.88 1.41,-1.41L13.41,13l1.47,-1.47z" />
|
||||
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_folder.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_folder.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M21,9A1,1 0,0 1,22 10A1,1 0,0 1,21 11H16.53L16.4,12.21L14.2,17.15C14,17.65 13.47,18 12.86,18H8.5C7.7,18 7,17.27 7,16.5V10C7,9.61 7.16,9.26 7.43,9L11.63,4.1L12.4,4.84C12.6,5.03 12.72,5.29 12.72,5.58L12.69,5.8L11,9H21M2,18V10H5V18H2Z" />
|
||||
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_lock.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_lock.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
|
||||
</vector>
|
12
clients/android/NewsBlur/res/drawable/ic_rss_feed.xml
Normal file
12
clients/android/NewsBlur/res/drawable/ic_rss_feed.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M6.18,17.82m-2.18,0a2.18,2.18 0,1 1,4.36 0a2.18,2.18 0,1 1,-4.36 0" />
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z" />
|
||||
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_search.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_search.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_sync.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_sync.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
|
||||
</vector>
|
9
clients/android/NewsBlur/res/drawable/ic_text.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_text.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A6A6A6"
|
||||
android:pathData="M14,17L4,17v2h10v-2zM20,9L4,9v2h16L20,9zM4,15h16v-2L4,13v2zM4,5v2h16L20,5L4,5z" />
|
||||
</vector>
|
BIN
clients/android/NewsBlur/res/drawable/mute_feed_off.webp
Normal file
BIN
clients/android/NewsBlur/res/drawable/mute_feed_off.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 828 B |
BIN
clients/android/NewsBlur/res/drawable/mute_feed_on.webp
Normal file
BIN
clients/android/NewsBlur/res/drawable/mute_feed_on.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 686 B |
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</LinearLayout>
|
|
@ -27,6 +27,13 @@
|
|||
android:layout_below="@id/itemlist_search_query"
|
||||
/>
|
||||
|
||||
<include layout="@layout/row_fleuron"
|
||||
android:id="@+id/footer_fleuron"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/itemlist_search_query"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemlist_sync_status"
|
||||
android:layout_width="fill_parent"
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
/>
|
||||
|
||||
<!-- The scrollable and pull-able feed list. -->
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -180,7 +180,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:tag="folderFeedListFragment" />
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<!-- top_bar_border -->
|
||||
<View
|
||||
|
|
60
clients/android/NewsBlur/res/layout/activity_mute_config.xml
Normal file
60
clients/android/NewsBlur/res/layout/activity_mute_config.xml
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?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"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container_sites_count"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_reset_sites"
|
||||
style="?linkText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:padding="4dp"
|
||||
android:text="@string/mute_config_reset_button"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_sites"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:text="@string/mute_config_sites"
|
||||
android:textColor="@color/positive"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ExpandableListView
|
||||
android:id="@+id/list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:groupIndicator="@null" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_sync_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@color/status_overlay_background"
|
||||
android:gravity="center"
|
||||
android:padding="2dp"
|
||||
android:textColor="@color/status_overlay_text"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
293
clients/android/NewsBlur/res/layout/activity_premium.xml
Normal file
293
clients/android/NewsBlur/res/layout/activity_premium.xml
Normal file
|
@ -0,0 +1,293 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_going_premium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/premium_title_going_premium"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:lineSpacingExtra="@dimen/extra_line_spacing"
|
||||
android:text="@string/premium_subtitle"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_policies"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:lineSpacingExtra="@dimen/extra_line_spacing"
|
||||
android:text="@string/premium_policies"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_hand_pointing_right" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_sub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="40dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_sub_title"
|
||||
style="?linkText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/premium_subscription_title"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_sub_price"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/premium_subscription_price"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/loading"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_sync" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_sync" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_folder" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_read_by_folder" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_search" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_search" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_bookmark" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_searchable_tags" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_lock" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_privacy_options" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_rss_feed" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_custom_rss" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_text" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_text_view" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_dining" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="40dp"
|
||||
android:text="@string/premium_shiloh" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_shiloh"
|
||||
android:layout_width="104dp"
|
||||
android:layout_height="104dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container_gone_premium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="120dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/premium_title_gone_premium"
|
||||
android:textSize="40sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subscription_renewal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="320dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:lineSpacingExtra="@dimen/extra_line_spacing"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<nl.dionsegijn.konfetti.KonfettiView
|
||||
android:id="@+id/konfetti"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -10,13 +10,13 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/activity_details_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/profile_details">
|
||||
|
||||
<android.support.v4.view.PagerTitleStrip
|
||||
<androidx.viewpager.widget.PagerTitleStrip
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
|
@ -24,6 +24,6 @@
|
|||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
</android.support.v4.view.ViewPager>
|
||||
</androidx.viewpager.widget.ViewPager>
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -78,11 +78,11 @@
|
|||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
</LinearLayout>
|
|
@ -8,7 +8,7 @@
|
|||
android:id="@+id/choose_folders_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?divider"
|
||||
style="?android:listDivider"
|
||||
android:dividerHeight="2dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
android:layout_height="6dp"
|
||||
/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/itemgridfragment_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
android:id="@+id/share_bar_underline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
style="?divider"
|
||||
style="?android:divider"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
android:layout_height="match_parent"
|
||||
>
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/reading_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -2,17 +2,41 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
>
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/fleuron"
|
||||
android:src="@drawable/fleuron"
|
||||
android:layout_height="32dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
/>
|
||||
android:src="@drawable/fleuron" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_subscribe"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subscription"
|
||||
style="?defaultText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/premium_subscribers_folder" />
|
||||
|
||||
<TextView
|
||||
style="?linkText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/premium_subscribers" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
android:id="@+id/check_box"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
|
@ -20,7 +21,6 @@
|
|||
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" />
|
||||
|
||||
|
@ -39,4 +39,13 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_toggle"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/description_row_feed_mute_toggle"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
|
@ -63,6 +63,9 @@
|
|||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:id="@+id/menu_statistics"
|
||||
android:title="@string/menu_statistics"
|
||||
android:showAsAction="never" />
|
||||
<item android:id="@+id/menu_delete_feed"
|
||||
android:title="@string/menu_delete_feed"
|
||||
android:showAsAction="never" />
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
android:title="@string/settings"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_mute_sites"
|
||||
android:title="@string/mute_sites"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_widget"
|
||||
android:title="@string/widget"
|
||||
android:showAsAction="never" />
|
||||
|
@ -43,6 +47,10 @@
|
|||
android:title="@string/menu_loginas"
|
||||
android:showAsAction="never"
|
||||
android:visible="false"/>
|
||||
|
||||
<item android:id="@+id/menu_premium_account"
|
||||
android:title="@string/menu_premium_account"
|
||||
android:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/menu_logout"
|
||||
android:title="@string/menu_logout"
|
||||
|
|
|
@ -54,6 +54,12 @@
|
|||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/menu_mute_all"
|
||||
android:title="@string/menu_mute_all" />
|
||||
<item
|
||||
android:id="@+id/menu_mute_none"
|
||||
android:title="@string/menu_mute_none" />
|
||||
<item
|
||||
android:id="@+id/menu_select_all"
|
||||
android:title="@string/menu_select_all" />
|
|
@ -23,7 +23,6 @@
|
|||
<attr name="commentsHeader" format="string" />
|
||||
<attr name="rowBorderTop" format="string" />
|
||||
<attr name="rowBorderBottom" format="string" />
|
||||
<attr name="divider" format="string" />
|
||||
<attr name="profileCount" format="string" />
|
||||
<attr name="profileActivityList" format="string" />
|
||||
<attr name="itemHeaderDivider" format="string" />
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
<resources>
|
||||
<dimen name="thumbnails_small_size">50dp</dimen>
|
||||
<dimen name="thumbnails_size">90dp</dimen>
|
||||
<dimen name="extra_line_spacing">4dp</dimen>
|
||||
</resources>
|
|
@ -16,8 +16,7 @@
|
|||
<string name="login_next">Next</string>
|
||||
|
||||
<string name="title_feed_search">Search for feeds</string>
|
||||
<string name="add_feed_message">Add \"%s\" to your feeds?</string>
|
||||
|
||||
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="orig_text_loading">Fetching story text…</string>
|
||||
|
||||
|
@ -29,6 +28,7 @@
|
|||
<string name="description_profile_picture">The user\'s profile picture</string>
|
||||
<string name="description_row_folder_icon">folder icon</string>
|
||||
<string name="description_row_feed_icon">feed icon</string>
|
||||
<string name="description_row_feed_mute_toggle">feed mute toggle</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>
|
||||
|
@ -135,6 +135,7 @@
|
|||
<string name="menu_send_story_full">Send story to…</string>
|
||||
<string name="menu_mark_feed_as_read">Mark feed as read</string>
|
||||
<string name="menu_delete_feed">Delete feed</string>
|
||||
<string name="menu_statistics">Statistics</string>
|
||||
<string name="menu_delete_saved_search">Delete saved search</string>
|
||||
<string name="menu_unfollow">Unfollow user</string>
|
||||
<string name="menu_choose_folders">Choose folders</string>
|
||||
|
@ -184,6 +185,8 @@
|
|||
<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_mute_all">Mute All</string>
|
||||
<string name="menu_mute_none">Mute None</string>
|
||||
<string name="menu_select_all">Select All</string>
|
||||
<string name="menu_select_none">Select None</string>
|
||||
<string name="menu_widget_background">Widget Background</string>
|
||||
|
@ -213,6 +216,7 @@
|
|||
<string name="menu_feedback_post">Create a feedback post</string>
|
||||
<string name="menu_feedback_email">Email a bug report</string>
|
||||
<string name="menu_theme_choose">Theme…</string>
|
||||
<string name="menu_premium_account">Premium Account</string>
|
||||
|
||||
<string name="description_add_new_folder_icon">Add new folder icon</string>
|
||||
|
||||
|
@ -255,10 +259,34 @@
|
|||
<string name="feed_stories_per_month">%d stories/month</string>
|
||||
|
||||
<string name="settings">Preferences</string>
|
||||
<string name="mute_sites">Mute Sites</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="title_widget_loading">Loading...</string>
|
||||
|
||||
<string name="premium_subscribers_folder">Reading by folder is only available to</string>
|
||||
<string name="premium_subscribers_search">Search is only available to</string>
|
||||
<string name="premium_subscribers">premium subscribers</string>
|
||||
<string name="premium_toolbar_title">NewsBlur Premium</string>
|
||||
<string name="premium_title_going_premium">Thank you so much for going premium!</string>
|
||||
<string name="premium_title_gone_premium">Thank you for going premium!</string>
|
||||
<string name="premium_subscription_renewal">Your premium subscription is set to\nrenew on %s</string>
|
||||
<string name="premium_subscription_expiration">Your premium subscription is set\nto expire on %s</string>
|
||||
<string name="premium_subscription_no_expiration">Your premium subscription is set\nto never expire. Whoa!</string>
|
||||
<string name="premium_subtitle">Upgrading to a NewsBlur premium subscription gives you all of these features. Payments will be charged to your Play Store account at confirmation of purchase. Subscription renew unless auto-renew is turned off at least 24 hours before the end of the current period. Cancel at any time from Account Settings in Play Store.</string>
|
||||
<string name="premium_policies"><![CDATA[See NewsBlur\'s <a href="https://newsblur.com/privacy">privacy policy</a> and <a href="https://newsblur.com/tos">terms of use</a> for details.]]></string>
|
||||
<string name="premium_subscription_title">NewsBlur Premium Subscription</string>
|
||||
<string name="premium_subscription_price">$35.99 per year ($3.00/month)</string>
|
||||
<string name="premium_subscription_details_error">Error retrieving subscription details</string>
|
||||
<string name="premium_sync">Sites updated up to 10x more often</string>
|
||||
<string name="premium_read_by_folder">River of News (reading by folder)</string>
|
||||
<string name="premium_search">Search sites and folders</string>
|
||||
<string name="premium_searchable_tags">Save stories with searchable tags</string>
|
||||
<string name="premium_privacy_options">Privacy options for your blurblog</string>
|
||||
<string name="premium_custom_rss">Custom RSS feeds for folders and saves stories</string>
|
||||
<string name="premium_text_view">Text view conveniently extracts the story</string>
|
||||
<string name="premium_shiloh">You feed Shiloh, my poor, hungry dog, for a month</string>
|
||||
|
||||
<string name="settings_cat_offline">Offline</string>
|
||||
<string name="settings_enable_offline">Download Stories</string>
|
||||
|
@ -268,6 +296,12 @@
|
|||
<string name="settings_keep_old_stories">Keep Stories after Reading</string>
|
||||
<string name="settings_keep_old_stories_sum">Disable to reduce storage usage</string>
|
||||
|
||||
<string name="mute_config_title">You can follow up to 64 sites with a free standard account</string>
|
||||
<string name="mute_config_message">Please mute %d sites or reset to most popular sites.</string>
|
||||
<string name="mute_config_reset_button">RESET TO POPULAR SITES</string>
|
||||
<string name="mute_config_upgrade">UPGRADE</string>
|
||||
<string name="mute_config_sites">%1$s/%2$s</string>
|
||||
|
||||
<string name="menu_network_select">Download Using</string>
|
||||
<string name="menu_network_select_sum">Restrict background data to chosen networks</string>
|
||||
<string name="menu_network_select_opt_any">Any Network</string>
|
||||
|
@ -364,7 +398,6 @@
|
|||
<string name="settings_reading">Reading</string>
|
||||
<string name="settings_immersive_enter_single_tap">Immersive Mode Via Single Tap</string>
|
||||
<string name="settings_show_content_preview">Show Content Preview Text</string>
|
||||
<string name="settings_show_thumbnails">Show Image Preview Thumbnails</string>
|
||||
<string name="settings_thumbnails_style">Image Preview Thumbnails</string>
|
||||
<string name="settings_notifications">Notifications</string>
|
||||
<string name="settings_enable_notifications">Enable Notifications</string>
|
||||
|
@ -414,6 +447,7 @@
|
|||
<string name="sync_status_recounts">Catching up reading actions...</string>
|
||||
<string name="sync_status_ffsync">On its way...</string>
|
||||
<string name="sync_status_cleanup">Cleaning up...</string>
|
||||
<string name="sync_status_starred">Sync saved stories actions…</string>
|
||||
<string name="sync_status_stories">Fetching fresh stories...</string>
|
||||
<string name="sync_status_unreads">Storing%sunread stories...</string>
|
||||
<string name="sync_status_text">Storing text for %s stories...</string>
|
||||
|
@ -461,12 +495,14 @@
|
|||
<string name="gest_action_markunread">Mark Story Unread</string>
|
||||
<string name="gest_action_save">Save Story</string>
|
||||
<string name="gest_action_unsave">Unsave Story</string>
|
||||
<string name="gest_action_statistics">Statistics</string>
|
||||
<string-array name="ltr_gesture_action_entries">
|
||||
<item>@string/gest_action_none</item>
|
||||
<item>@string/gest_action_markread</item>
|
||||
<item>@string/gest_action_markunread</item>
|
||||
<item>@string/gest_action_save</item>
|
||||
<item>@string/gest_action_unsave</item>
|
||||
<item>@string/gest_action_statistics</item>
|
||||
</string-array>
|
||||
<string-array name="ltr_gesture_action_values">
|
||||
<item>GEST_ACTION_NONE</item>
|
||||
|
@ -474,6 +510,7 @@
|
|||
<item>GEST_ACTION_MARKUNREAD</item>
|
||||
<item>GEST_ACTION_SAVE</item>
|
||||
<item>GEST_ACTION_UNSAVE</item>
|
||||
<item>GEST_ACTION_STATISTICS</item>
|
||||
</string-array>
|
||||
<string name="ltr_gesture_action_value">GEST_ACTION_MARKREAD</string>
|
||||
|
||||
|
@ -484,6 +521,7 @@
|
|||
<item>@string/gest_action_markunread</item>
|
||||
<item>@string/gest_action_save</item>
|
||||
<item>@string/gest_action_unsave</item>
|
||||
<item>@string/gest_action_statistics</item>
|
||||
</string-array>
|
||||
<string-array name="rtl_gesture_action_values">
|
||||
<item>GEST_ACTION_NONE</item>
|
||||
|
@ -491,6 +529,7 @@
|
|||
<item>GEST_ACTION_MARKUNREAD</item>
|
||||
<item>GEST_ACTION_SAVE</item>
|
||||
<item>GEST_ACTION_UNSAVE</item>
|
||||
<item>GEST_ACTION_STATISTICS</item>
|
||||
</string-array>
|
||||
<string name="rtl_gesture_action_value">GEST_ACTION_MARKUNREAD</string>
|
||||
|
||||
|
@ -556,7 +595,5 @@
|
|||
|
||||
<string name="story_notification_channel_id">story_notification_channel</string>
|
||||
<string name="story_notification_channel_name">New Stories</string>
|
||||
<string name="save_widget">Save Widget</string>
|
||||
<string name="select_feed">Select Feed</string>
|
||||
<string name="go_to_feed">Go to feed</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.UserDetails;
|
||||
|
@ -21,7 +21,7 @@ public class ActivityDetailsPagerAdapter extends FragmentPagerAdapter {
|
|||
private final Profile profile;
|
||||
|
||||
public ActivityDetailsPagerAdapter(FragmentManager fragmentManager, Profile profile) {
|
||||
super(fragmentManager);
|
||||
super(fragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
|
||||
this.profile = profile;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.newsblur.activity;
|
|||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.View;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
|
|
@ -2,8 +2,8 @@ package com.newsblur.activity;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
|
@ -46,6 +46,7 @@ public class AddSocial extends NbActivity {
|
|||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
switch (resultCode) {
|
||||
case AddTwitter.TWITTER_AUTHED:
|
||||
addSocialFragment.setTwitterAuthed();
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
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.util.WidgetBackground;
|
||||
import com.newsblur.widget.WidgetUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
abstract public class FeedChooser extends NbActivity {
|
||||
|
||||
protected FeedChooserAdapter adapter;
|
||||
protected ArrayList<Feed> feeds;
|
||||
protected ArrayList<Folder> folders;
|
||||
protected Map<String, Feed> feedMap = new HashMap<>();
|
||||
protected ArrayList<String> folderNames = new ArrayList<>();
|
||||
protected ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
|
||||
|
||||
abstract void bindLayout();
|
||||
|
||||
abstract void setupList();
|
||||
|
||||
abstract void processFeeds(Cursor cursor);
|
||||
|
||||
abstract void processData();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
bindLayout();
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setupList();
|
||||
loadFeeds();
|
||||
loadFolders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_feed_chooser, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
ListOrderFilter listOrderFilter = PrefsUtils.getFeedChooserListOrder(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.getFeedChooserFeedOrder(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.getFeedChooserFolderView(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);
|
||||
}
|
||||
|
||||
WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(this);
|
||||
if (widgetBackground == WidgetBackground.DEFAULT) {
|
||||
menu.findItem(R.id.menu_widget_background_default).setChecked(true);
|
||||
} else if (widgetBackground == WidgetBackground.TRANSPARENT) {
|
||||
menu.findItem(R.id.menu_widget_background_transparent).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_widget_background_default:
|
||||
setWidgetBackground(WidgetBackground.DEFAULT);
|
||||
return true;
|
||||
case R.id.menu_widget_background_transparent:
|
||||
setWidgetBackground(WidgetBackground.TRANSPARENT);
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setAdapterData() {
|
||||
adapter.setData(this.folderNames, this.folderChildren, this.feeds);
|
||||
}
|
||||
|
||||
private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
|
||||
PrefsUtils.setFeedChooserFeedOrder(this, feedOrderFilter);
|
||||
adapter.replaceFeedOrder(feedOrderFilter);
|
||||
}
|
||||
|
||||
private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
|
||||
PrefsUtils.setFeedChooserListOrder(this, listOrderFilter);
|
||||
adapter.replaceListOrder(listOrderFilter);
|
||||
}
|
||||
|
||||
private void replaceFolderView(FolderViewFilter folderViewFilter) {
|
||||
PrefsUtils.setFeedChooserFolderView(this, folderViewFilter);
|
||||
adapter.replaceFolderView(folderViewFilter);
|
||||
setAdapterData();
|
||||
}
|
||||
|
||||
private void setWidgetBackground(WidgetBackground widgetBackground) {
|
||||
PrefsUtils.setWidgetBackground(this, widgetBackground);
|
||||
WidgetUtils.updateWidget(this);
|
||||
}
|
||||
|
||||
private void loadFeeds() {
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
|
||||
loader.registerListener(loader.getId(), (loader1, cursor) -> processFeeds(cursor));
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
private void loadFolders() {
|
||||
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
|
||||
loader.registerListener(loader.getId(), (loader1, cursor) -> processFolders(cursor));
|
||||
loader.startLoading();
|
||||
}
|
||||
|
||||
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, (o1, o2) -> Folder.compareFolderNames(o1.flatName(), o2.flatName()));
|
||||
processData();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
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 FeedChooserAdapter extends BaseExpandableListAdapter {
|
||||
|
||||
protected final static int defaultTextSizeChild = 14;
|
||||
protected final static int defaultTextSizeGroup = 13;
|
||||
|
||||
protected Set<String> feedIds = new HashSet<>();
|
||||
protected ArrayList<String> folderNames = new ArrayList<>();
|
||||
protected ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
|
||||
|
||||
protected FolderViewFilter folderViewFilter;
|
||||
protected ListOrderFilter listOrderFilter;
|
||||
protected FeedOrderFilter feedOrderFilter;
|
||||
|
||||
protected float textSize;
|
||||
|
||||
FeedChooserAdapter(Context context) {
|
||||
folderViewFilter = PrefsUtils.getFeedChooserFolderView(context);
|
||||
listOrderFilter = PrefsUtils.getFeedChooserListOrder(context);
|
||||
feedOrderFilter = PrefsUtils.getFeedChooserFeedOrder(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);
|
||||
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", Locale.getDefault());
|
||||
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);
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildSelectable(int groupPosition, int childPosition) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return super.areAllItemsEnabled();
|
||||
}
|
||||
|
||||
protected 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();
|
||||
}
|
||||
|
||||
protected void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
|
||||
this.feedOrderFilter = feedOrderFilter;
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
protected void replaceListOrder(ListOrderFilter listOrderFilter) {
|
||||
this.listOrderFilter = listOrderFilter;
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
protected void replaceFolderView(FolderViewFilter folderViewFilter) {
|
||||
this.folderViewFilter = folderViewFilter;
|
||||
}
|
||||
|
||||
protected void notifyDataChanged() {
|
||||
for (ArrayList<Feed> feedList : this.folderChildren) {
|
||||
Collections.sort(feedList, getListComparator());
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
protected void setFeedIds(Set<String> feedIds) {
|
||||
this.feedIds.clear();
|
||||
this.feedIds.addAll(feedIds);
|
||||
}
|
||||
|
||||
protected void replaceFeedIds(Set<String> feedIds) {
|
||||
setFeedIds(feedIds);
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private Comparator<Feed> getListComparator() {
|
||||
return (o1, 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,10 +3,14 @@ package com.newsblur.activity;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.google.android.play.core.review.ReviewInfo;
|
||||
import com.google.android.play.core.review.ReviewManager;
|
||||
import com.google.android.play.core.review.ReviewManagerFactory;
|
||||
import com.google.android.play.core.tasks.Task;
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.fragment.DeleteFeedFragment;
|
||||
|
@ -14,6 +18,7 @@ import com.newsblur.fragment.FeedIntelTrainerFragment;
|
|||
import com.newsblur.fragment.RenameDialogFragment;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
public class FeedItemsList extends ItemsList {
|
||||
|
@ -22,6 +27,8 @@ public class FeedItemsList extends ItemsList {
|
|||
public static final String EXTRA_FOLDER_NAME = "folderName";
|
||||
private Feed feed;
|
||||
private String folderName;
|
||||
private ReviewManager reviewManager;
|
||||
private ReviewInfo reviewInfo;
|
||||
|
||||
public static void startActivity(Context context, FeedSet feedSet,
|
||||
Feed feed, String folderName) {
|
||||
|
@ -36,13 +43,28 @@ public class FeedItemsList extends ItemsList {
|
|||
protected void onCreate(Bundle bundle) {
|
||||
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
|
||||
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
|
||||
|
||||
|
||||
super.onCreate(bundle);
|
||||
|
||||
UIUtils.setCustomActionBar(this, feed.faviconUrl, feed.title);
|
||||
}
|
||||
checkInAppReview();
|
||||
}
|
||||
|
||||
public void deleteFeed() {
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// see checkInAppReview()
|
||||
if (reviewInfo != null) {
|
||||
Task<Void> flow = reviewManager.launchReviewFlow(this, reviewInfo);
|
||||
flow.addOnCompleteListener(task -> {
|
||||
PrefsUtils.setInAppReviewed(this);
|
||||
super.onBackPressed();
|
||||
});
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteFeed() {
|
||||
DialogFragment deleteFeedFragment = DeleteFeedFragment.newInstance(feed, folderName);
|
||||
deleteFeedFragment.show(getSupportFragmentManager(), "dialog");
|
||||
}
|
||||
|
@ -85,6 +107,10 @@ public class FeedItemsList extends ItemsList {
|
|||
// TODO: since this activity uses a feed object passed as an extra and doesn't query the DB,
|
||||
// the name change won't be reflected until the activity finishes.
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_statistics) {
|
||||
FeedUtils.openStatistics(this, feed.feedId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -122,4 +148,16 @@ public class FeedItemsList extends ItemsList {
|
|||
String getSaveSearchFeedId() {
|
||||
return "feed:" + feed.feedId;
|
||||
}
|
||||
|
||||
private void checkInAppReview() {
|
||||
if (!PrefsUtils.hasInAppReviewed(this)) {
|
||||
reviewManager = ReviewManagerFactory.create(this);
|
||||
Task<ReviewInfo> request = reviewManager.requestReviewFlow();
|
||||
request.addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
reviewInfo = task.getResult();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ package com.newsblur.activity;
|
|||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.View;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.newsblur.databinding.ActivityInAppBrowserBinding;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
|
@ -53,13 +54,4 @@ public class InAppBrowser extends FragmentActivity {
|
|||
|
||||
binding.webView.loadUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (binding.webView.canGoBack()) {
|
||||
binding.webView.goBack();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
|
@ -97,7 +97,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
if (activeSearchQuery != null) {
|
||||
binding.itemlistSearchQuery.setText(activeSearchQuery);
|
||||
binding.itemlistSearchQuery.setVisibility(View.VISIBLE);
|
||||
fs.setSearchQuery(activeSearchQuery);
|
||||
checkSearchQuery();
|
||||
}
|
||||
|
||||
binding.itemlistSearchQuery.setOnKeyListener(new OnKeyListener() {
|
||||
|
@ -191,6 +191,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
menu.findItem(R.id.menu_instafetch_feed).setVisible(false);
|
||||
menu.findItem(R.id.menu_intel).setVisible(false);
|
||||
menu.findItem(R.id.menu_rename_feed).setVisible(false);
|
||||
menu.findItem(R.id.menu_statistics).setVisible(false);
|
||||
}
|
||||
|
||||
if (!fs.isInfrequent()) {
|
||||
|
@ -349,11 +350,16 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
}
|
||||
|
||||
private void checkSearchQuery() {
|
||||
String oldQuery = fs.getSearchQuery();
|
||||
String q = binding.itemlistSearchQuery.getText().toString().trim();
|
||||
if (q.length() < 1) {
|
||||
updateFleuron(false);
|
||||
q = null;
|
||||
} else if (!PrefsUtils.getIsPremium(this)) {
|
||||
updateFleuron(true);
|
||||
return;
|
||||
}
|
||||
|
||||
String oldQuery = fs.getSearchQuery();
|
||||
fs.setSearchQuery(q);
|
||||
if (!TextUtils.equals(q, oldQuery)) {
|
||||
FeedUtils.prepareReadingSession(fs, true);
|
||||
|
@ -364,6 +370,26 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
|
|||
}
|
||||
}
|
||||
|
||||
private void updateFleuron(boolean requiresPremium) {
|
||||
FragmentTransaction transaction = getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
|
||||
|
||||
if (requiresPremium) {
|
||||
transaction.hide(itemSetFragment);
|
||||
binding.footerFleuron.textSubscription.setText(R.string.premium_subscribers_search);
|
||||
binding.footerFleuron.containerSubscribe.setVisibility(View.VISIBLE);
|
||||
binding.footerFleuron.getRoot().setVisibility(View.VISIBLE);
|
||||
binding.footerFleuron.containerSubscribe.setOnClickListener(view -> UIUtils.startPremiumActivity(this));
|
||||
} else {
|
||||
transaction.show(itemSetFragment);
|
||||
binding.footerFleuron.containerSubscribe.setVisibility(View.GONE);
|
||||
binding.footerFleuron.getRoot().setVisibility(View.GONE);
|
||||
binding.footerFleuron.containerSubscribe.setOnClickListener(null);
|
||||
}
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storyOrderChanged(StoryOrder newValue) {
|
||||
updateStoryOrderPreference(newValue);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.view.Window;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.view.Window;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
|
|
@ -5,9 +5,9 @@ import android.graphics.Bitmap;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
|
@ -373,6 +373,14 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
} else if (item.getItemId() == R.id.menu_theme_black) {
|
||||
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
|
||||
UIUtils.restartActivity(this);
|
||||
} else if (item.getItemId() == R.id.menu_premium_account) {
|
||||
Intent intent = new Intent(this, Premium.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_mute_sites) {
|
||||
Intent intent = new Intent(this, MuteConfig.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.databinding.ActivityMuteConfigBinding;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.domain.Folder;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class MuteConfig extends FeedChooser implements MuteConfigAdapter.FeedStateChangedListener {
|
||||
|
||||
private ActivityMuteConfigBinding binding;
|
||||
private boolean checkedInitFeedsLimit = false;
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
menu.findItem(R.id.menu_select_all).setVisible(false);
|
||||
menu.findItem(R.id.menu_select_none).setVisible(false);
|
||||
menu.findItem(R.id.menu_widget_background).setVisible(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_mute_all:
|
||||
setFeedsState(true);
|
||||
return true;
|
||||
case R.id.menu_mute_none:
|
||||
setFeedsState(false);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void bindLayout() {
|
||||
binding = ActivityMuteConfigBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
}
|
||||
|
||||
@Override
|
||||
void setupList() {
|
||||
adapter = new MuteConfigAdapter(this, this);
|
||||
binding.listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
void processFeeds(Cursor cursor) {
|
||||
ArrayList<Feed> feeds = new ArrayList<>();
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Feed feed = Feed.fromCursor(cursor);
|
||||
feeds.add(feed);
|
||||
feedMap.put(feed.feedId, feed);
|
||||
}
|
||||
this.feeds = feeds;
|
||||
processData();
|
||||
}
|
||||
|
||||
@Override
|
||||
void processData() {
|
||||
if (folders != null && feeds != null) {
|
||||
for (Folder folder : folders) {
|
||||
ArrayList<Feed> children = new ArrayList<>();
|
||||
for (String feedId : folder.feedIds) {
|
||||
Feed feed = feedMap.get(feedId);
|
||||
if (!children.contains(feed)) {
|
||||
children.add(feed);
|
||||
}
|
||||
}
|
||||
folderNames.add(folder.flatName());
|
||||
folderChildren.add(children);
|
||||
}
|
||||
|
||||
setAdapterData();
|
||||
syncActiveFeedCount();
|
||||
checkedInitFeedsLimit = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAdapterData() {
|
||||
Set<String> feedIds = new HashSet<>(this.feeds.size());
|
||||
for (Feed feed : this.feeds) {
|
||||
feedIds.add(feed.feedId);
|
||||
}
|
||||
adapter.setFeedIds(feedIds);
|
||||
|
||||
super.setAdapterData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleUpdate(int updateType) {
|
||||
super.handleUpdate(updateType);
|
||||
if ((updateType & UPDATE_STATUS) != 0) {
|
||||
String syncStatus = NBSyncService.getSyncStatusMessage(this, false);
|
||||
if (syncStatus != null) {
|
||||
binding.textSyncStatus.setText(syncStatus);
|
||||
binding.textSyncStatus.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.textSyncStatus.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFeedStateChanged() {
|
||||
syncActiveFeedCount();
|
||||
}
|
||||
|
||||
private void syncActiveFeedCount() {
|
||||
// free standard accounts can follow up to 64 sites
|
||||
boolean isPremium = PrefsUtils.getIsPremium(this);
|
||||
if (!isPremium && feeds != null) {
|
||||
int activeSites = 0;
|
||||
for (Feed feed : feeds) {
|
||||
if (feed.active) {
|
||||
activeSites++;
|
||||
}
|
||||
}
|
||||
int textColorRes = activeSites > AppConstants.FREE_ACCOUNT_SITE_LIMIT ? R.color.negative : R.color.positive;
|
||||
binding.textSites.setTextColor(ContextCompat.getColor(this, textColorRes));
|
||||
binding.textSites.setText(String.format(getString(R.string.mute_config_sites), activeSites, AppConstants.FREE_ACCOUNT_SITE_LIMIT));
|
||||
showSitesCount();
|
||||
|
||||
if (activeSites > AppConstants.FREE_ACCOUNT_SITE_LIMIT && !checkedInitFeedsLimit) {
|
||||
showAccountFeedsLimitDialog(activeSites - AppConstants.FREE_ACCOUNT_SITE_LIMIT);
|
||||
}
|
||||
} else {
|
||||
hideSitesCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void setFeedsState(boolean isMute) {
|
||||
for (Feed feed : feeds) {
|
||||
feed.active = !isMute;
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
if (isMute) FeedUtils.muteFeeds(this, adapter.feedIds);
|
||||
else FeedUtils.unmuteFeeds(this, adapter.feedIds);
|
||||
}
|
||||
|
||||
private void showAccountFeedsLimitDialog(int exceededLimitCount) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.mute_config_title)
|
||||
.setMessage(String.format(getString(R.string.mute_config_message), exceededLimitCount))
|
||||
.setNeutralButton(android.R.string.ok, null)
|
||||
.setPositiveButton(R.string.mute_config_upgrade, (dialogInterface, i) -> openUpgradeToPremium())
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showSitesCount() {
|
||||
ViewGroup.LayoutParams oldLayout = binding.listView.getLayoutParams();
|
||||
FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
|
||||
newLayout.topMargin = UIUtils.dp2px(this, 56);
|
||||
binding.listView.setLayoutParams(newLayout);
|
||||
binding.containerSitesCount.setVisibility(View.VISIBLE);
|
||||
binding.textResetSites.setOnClickListener(view -> resetToPopularFeeds());
|
||||
}
|
||||
|
||||
private void hideSitesCount() {
|
||||
ViewGroup.LayoutParams oldLayout = binding.listView.getLayoutParams();
|
||||
FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
|
||||
newLayout.topMargin = UIUtils.dp2px(this, 0);
|
||||
binding.listView.setLayoutParams(newLayout);
|
||||
binding.containerSitesCount.setVisibility(View.GONE);
|
||||
binding.textResetSites.setOnClickListener(null);
|
||||
}
|
||||
|
||||
// reset to most popular sites based on subscribers
|
||||
private void resetToPopularFeeds() {
|
||||
// sort descending by subscribers
|
||||
Collections.sort(feeds, (f1, f2) -> {
|
||||
if (TextUtils.isEmpty(f1.subscribers)) f1.subscribers = "0";
|
||||
if (TextUtils.isEmpty(f2.subscribers)) f2.subscribers = "0";
|
||||
return Integer.valueOf(f2.subscribers).compareTo(Integer.valueOf(f1.subscribers));
|
||||
});
|
||||
Set<String> activeFeedIds = new HashSet<>();
|
||||
Set<String> inactiveFeedIds = new HashSet<>();
|
||||
for (int index = 0; index < feeds.size(); index++) {
|
||||
Feed feed = feeds.get(index);
|
||||
if (index < AppConstants.FREE_ACCOUNT_SITE_LIMIT) {
|
||||
activeFeedIds.add(feed.feedId);
|
||||
} else {
|
||||
inactiveFeedIds.add(feed.feedId);
|
||||
}
|
||||
}
|
||||
FeedUtils.unmuteFeeds(this, activeFeedIds);
|
||||
FeedUtils.muteFeeds(this, inactiveFeedIds);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void openUpgradeToPremium() {
|
||||
Intent intent = new Intent(this, Premium.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class MuteConfigAdapter extends FeedChooserAdapter {
|
||||
|
||||
private FeedStateChangedListener listener;
|
||||
|
||||
MuteConfigAdapter(Context context, FeedStateChangedListener listener) {
|
||||
super(context);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
|
||||
View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
|
||||
|
||||
groupView.setOnClickListener(v -> {
|
||||
ArrayList<Feed> folderChild = MuteConfigAdapter.this.folderChildren.get(groupPosition);
|
||||
boolean allAreMute = true;
|
||||
for (Feed feed : folderChild) {
|
||||
if (feed.active) {
|
||||
allAreMute = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> feedIds = new HashSet<>(folderChild.size());
|
||||
for (Feed feed : folderChild) {
|
||||
// flip active flag
|
||||
feed.active = allAreMute;
|
||||
feedIds.add(feed.feedId);
|
||||
}
|
||||
|
||||
// if allAreMute initially, we need to unMute feeds
|
||||
if (allAreMute) FeedUtils.unmuteFeeds(groupView.getContext(), feedIds);
|
||||
else FeedUtils.muteFeeds(groupView.getContext(), feedIds);
|
||||
|
||||
listener.onFeedStateChanged();
|
||||
notifyDataChanged();
|
||||
});
|
||||
return groupView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
|
||||
View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
|
||||
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
|
||||
final CheckBox checkBox = childView.findViewById(R.id.check_box);
|
||||
final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
|
||||
checkBox.setVisibility(View.GONE);
|
||||
imgToggle.setVisibility(View.VISIBLE);
|
||||
|
||||
if (feed.active) imgToggle.setBackgroundResource(R.drawable.mute_feed_on);
|
||||
else imgToggle.setBackgroundResource(R.drawable.mute_feed_off);
|
||||
|
||||
childView.setOnClickListener(v -> {
|
||||
feed.active = !feed.active;
|
||||
Set<String> feedIds = new HashSet<>(1);
|
||||
feedIds.add(feed.feedId);
|
||||
if (feed.active) FeedUtils.unmuteFeeds(childView.getContext(), feedIds);
|
||||
else FeedUtils.muteFeeds(childView.getContext(), feedIds);
|
||||
|
||||
listener.onFeedStateChanged();
|
||||
notifyDataChanged();
|
||||
});
|
||||
return childView;
|
||||
}
|
||||
|
||||
interface FeedStateChangedListener {
|
||||
|
||||
void onFeedStateChanged();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.PrefConstants.ThemeValue;
|
||||
|
|
308
clients/android/NewsBlur/src/com/newsblur/activity/Premium.java
Normal file
308
clients/android/NewsBlur/src/com/newsblur/activity/Premium.java
Normal file
|
@ -0,0 +1,308 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.android.billingclient.api.BillingClientStateListener;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.databinding.ActivityPremiumBinding;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.network.domain.NewsBlurResponse;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.BetterLinkMovementMethod;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.Log;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nl.dionsegijn.konfetti.emitters.StreamEmitter;
|
||||
import nl.dionsegijn.konfetti.models.Shape;
|
||||
import nl.dionsegijn.konfetti.models.Size;
|
||||
|
||||
public class Premium extends NbActivity {
|
||||
|
||||
private ActivityPremiumBinding binding;
|
||||
private BillingClient billingClient;
|
||||
private SkuDetails subscriptionDetails;
|
||||
private Purchase purchasedSubscription;
|
||||
|
||||
private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult -> {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener OK");
|
||||
verifyUserSubscriptionStatus();
|
||||
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
// Billing API version is not supported for the type requested.
|
||||
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener BILLING_UNAVAILABLE");
|
||||
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
|
||||
// Network connection is down.
|
||||
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener SERVICE_UNAVAILABLE");
|
||||
} else {
|
||||
// Handle any other error codes.
|
||||
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener ERROR - message: " + billingResult.getDebugMessage());
|
||||
}
|
||||
};
|
||||
|
||||
private PurchasesUpdatedListener purchaseUpdateListener = (billingResult, purchases) -> {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
|
||||
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener OK");
|
||||
for (Purchase purchase : purchases) {
|
||||
handlePurchase(purchase);
|
||||
}
|
||||
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
|
||||
// Handle an error caused by a user cancelling the purchase flow.
|
||||
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener USER_CANCELLED");
|
||||
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
// Billing API version is not supported for the type requested.
|
||||
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener BILLING_UNAVAILABLE");
|
||||
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
|
||||
// Network connection is down.
|
||||
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener SERVICE_UNAVAILABLE");
|
||||
} else {
|
||||
// Handle any other error codes.
|
||||
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener ERROR - message: " + billingResult.getDebugMessage());
|
||||
}
|
||||
};
|
||||
|
||||
private BillingClientStateListener billingClientStateListener = new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
// The BillingClient is ready. You can query purchases here.
|
||||
Log.d(Premium.this.getLocalClassName(), "onBillingSetupFinished OK");
|
||||
retrievePlayStoreSubscriptions();
|
||||
verifyUserSubscriptionStatus();
|
||||
} else {
|
||||
showSubscriptionDetailsError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
Log.d(Premium.this.getLocalClassName(), "onBillingServiceDisconnected");
|
||||
// Try to restart the connection on the next request to
|
||||
// Google Play by calling the startConnection() method.
|
||||
showSubscriptionDetailsError();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
binding = ActivityPremiumBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setupUI();
|
||||
setupBillingClient();
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
UIUtils.setCustomActionBar(this, R.drawable.logo, getString(R.string.premium_toolbar_title));
|
||||
|
||||
// linkify before setting the string resource
|
||||
BetterLinkMovementMethod.linkify(Linkify.WEB_URLS, binding.textPolicies)
|
||||
.setOnLinkClickListener((textView, url) -> {
|
||||
UIUtils.handleUri(Premium.this, Uri.parse(url));
|
||||
return true;
|
||||
});
|
||||
binding.textPolicies.setText(UIUtils.fromHtml(getString(R.string.premium_policies)));
|
||||
binding.textSubTitle.setPaintFlags(binding.textSubTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||
FeedUtils.iconLoader.displayImage(AppConstants.SHILOH_PHOTO_URL, binding.imgShiloh, 0, false);
|
||||
}
|
||||
|
||||
private void setupBillingClient() {
|
||||
billingClient = BillingClient.newBuilder(this)
|
||||
.setListener(purchaseUpdateListener)
|
||||
.enablePendingPurchases()
|
||||
.build();
|
||||
|
||||
billingClient.startConnection(billingClientStateListener);
|
||||
}
|
||||
|
||||
private void verifyUserSubscriptionStatus() {
|
||||
boolean hasNewsBlurSubscription = PrefsUtils.getIsPremium(this);
|
||||
Purchase playStoreSubscription = null;
|
||||
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
|
||||
if (result.getPurchasesList() != null) {
|
||||
for (Purchase purchase : result.getPurchasesList()) {
|
||||
if (purchase.getSku().equals(AppConstants.PREMIUM_SKU)) {
|
||||
playStoreSubscription = purchase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewsBlurSubscription || playStoreSubscription != null) {
|
||||
binding.containerGoingPremium.setVisibility(View.GONE);
|
||||
binding.containerGonePremium.setVisibility(View.VISIBLE);
|
||||
long expirationTimeMs = PrefsUtils.getPremiumExpire(this);
|
||||
String renewalString = null;
|
||||
|
||||
if (expirationTimeMs == 0) {
|
||||
renewalString = getString(R.string.premium_subscription_no_expiration);
|
||||
} else if (expirationTimeMs > 0) {
|
||||
// date constructor expects ms
|
||||
Date expirationDate = new Date(expirationTimeMs * 1000);
|
||||
DateFormat dateFormat = new SimpleDateFormat("EEE, MMMM d, yyyy", Locale.getDefault());
|
||||
dateFormat.setTimeZone(TimeZone.getDefault());
|
||||
renewalString = getString(R.string.premium_subscription_renewal, dateFormat.format(expirationDate));
|
||||
|
||||
if (playStoreSubscription != null && !playStoreSubscription.isAutoRenewing()) {
|
||||
renewalString = getString(R.string.premium_subscription_expiration, dateFormat.format(expirationDate));
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(renewalString)) {
|
||||
binding.textSubscriptionRenewal.setText(renewalString);
|
||||
binding.textSubscriptionRenewal.setVisibility(View.VISIBLE);
|
||||
}
|
||||
showConfetti();
|
||||
}
|
||||
|
||||
if (!hasNewsBlurSubscription && playStoreSubscription != null) {
|
||||
purchasedSubscription = playStoreSubscription;
|
||||
notifyNewsBlurOfSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
private void retrievePlayStoreSubscriptions() {
|
||||
List<String> skuList = new ArrayList<>(1);
|
||||
// add sub SKUs from Play Store
|
||||
skuList.add(AppConstants.PREMIUM_SKU);
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
|
||||
billingClient.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) -> {
|
||||
Log.d(Premium.this.getLocalClassName(), "SkuDetailsResponse");
|
||||
processSkuDetailsList(skuDetailsList);
|
||||
});
|
||||
}
|
||||
|
||||
private void processSkuDetailsList(@Nullable List<SkuDetails> skuDetailsList) {
|
||||
if (skuDetailsList != null) {
|
||||
for (SkuDetails skuDetails : skuDetailsList) {
|
||||
if (skuDetails.getSku().equals(AppConstants.PREMIUM_SKU)) {
|
||||
Log.d(Premium.this.getLocalClassName(), "Sku detail: " + skuDetails.getTitle() + " | " + skuDetails.getDescription() + " | " + skuDetails.getPrice() + " | " + skuDetails.getSku());
|
||||
subscriptionDetails = skuDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subscriptionDetails != null) {
|
||||
showSubscriptionDetails();
|
||||
} else {
|
||||
showSubscriptionDetailsError();
|
||||
}
|
||||
}
|
||||
|
||||
private void showSubscriptionDetailsError() {
|
||||
binding.textLoading.setText(R.string.premium_subscription_details_error);
|
||||
binding.textLoading.setVisibility(View.VISIBLE);
|
||||
binding.containerSub.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void showSubscriptionDetails() {
|
||||
// handling dynamic currency and pricing for 1Y subscriptions
|
||||
String currencySymbol = subscriptionDetails.getPrice().substring(0, 1);
|
||||
String priceString = subscriptionDetails.getPrice().substring(1);
|
||||
double price = Double.parseDouble(priceString);
|
||||
StringBuilder pricingText = new StringBuilder();
|
||||
pricingText.append(subscriptionDetails.getPrice());
|
||||
pricingText.append(" per year (");
|
||||
pricingText.append(currencySymbol);
|
||||
pricingText.append(String.format(Locale.getDefault(), "%.2f", price / 12));
|
||||
pricingText.append("/month)");
|
||||
|
||||
binding.textSubTitle.setText(subscriptionDetails.getTitle());
|
||||
binding.textSubPrice.setText(pricingText);
|
||||
binding.textLoading.setVisibility(View.GONE);
|
||||
binding.containerSub.setVisibility(View.VISIBLE);
|
||||
binding.containerSub.setOnClickListener(view -> launchBillingFlow(subscriptionDetails));
|
||||
}
|
||||
|
||||
private void launchBillingFlow(@NonNull SkuDetails skuDetails) {
|
||||
Log.d(Premium.this.getLocalClassName(), "launchBillingFlow for sku: " + skuDetails.getSku());
|
||||
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(skuDetails)
|
||||
.build();
|
||||
billingClient.launchBillingFlow(this, billingFlowParams);
|
||||
}
|
||||
|
||||
private void handlePurchase(Purchase purchase) {
|
||||
Log.d(Premium.this.getLocalClassName(), "handlePurchase: " + purchase.getOrderId());
|
||||
purchasedSubscription = purchase;
|
||||
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()) {
|
||||
verifyUserSubscriptionStatus();
|
||||
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged()) {
|
||||
// need to acknowledge first time sub otherwise it will void
|
||||
Log.d(Premium.this.getLocalClassName(), "acknowledge purchase: " + purchase.getOrderId());
|
||||
AcknowledgePurchaseParams acknowledgePurchaseParams =
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.getPurchaseToken())
|
||||
.build();
|
||||
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void showConfetti() {
|
||||
binding.konfetti.build()
|
||||
.addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA, Color.BLUE, Color.CYAN, Color.RED)
|
||||
.setDirection(90)
|
||||
.setFadeOutEnabled(true)
|
||||
.setTimeToLive(1000L)
|
||||
.addShapes(Shape.Square.INSTANCE, Shape.Circle.INSTANCE)
|
||||
.addSizes(new Size(10, 5f))
|
||||
.setPosition(0, binding.konfetti.getWidth() + 0f , -50f, -20f)
|
||||
.streamFor(100, StreamEmitter.INDEFINITE);
|
||||
}
|
||||
|
||||
private void notifyNewsBlurOfSubscription() {
|
||||
if (purchasedSubscription != null) {
|
||||
APIManager apiManager = new APIManager(this);
|
||||
new AsyncTask<Void, Void, NewsBlurResponse>() {
|
||||
@Override
|
||||
protected NewsBlurResponse doInBackground(Void... voids) {
|
||||
return apiManager.saveReceipt(purchasedSubscription.getOrderId(), purchasedSubscription.getSku());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(NewsBlurResponse result) {
|
||||
super.onPostExecute(result);
|
||||
if (!result.isError()) {
|
||||
NBSyncService.forceFeedsFolders();
|
||||
triggerSync();
|
||||
}
|
||||
finish();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,9 @@ package com.newsblur.activity;
|
|||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.MenuItem;
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@ import android.content.res.Configuration;
|
|||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v4.view.ViewPager.OnPageChangeListener;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
|
@ -178,7 +178,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
transaction.commit();
|
||||
}
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -436,7 +436,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
private void updateCursor() {
|
||||
synchronized (STORIES_MUTEX) {
|
||||
try {
|
||||
getSupportLoaderManager().restartLoader(0, null, this);
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} catch (IllegalStateException ise) {
|
||||
; // our heavy use of async can race loader calls, which it will gripe about, but this
|
||||
// is only a refresh call, so dropping a refresh during creation is perfectly fine.
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.fragment.RegisterProgressFragment;
|
||||
|
|
|
@ -8,9 +8,10 @@ import java.util.Set;
|
|||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
@ -55,7 +56,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
resultsList.setEmptyView(emptyView);
|
||||
resultsList.setOnItemClickListener(this);
|
||||
resultsList.setItemsCanFocus(false);
|
||||
searchLoader = getSupportLoaderManager().initLoader(0, new Bundle(), this);
|
||||
searchLoader = LoaderManager.getInstance(this).initLoader(0, new Bundle(), this);
|
||||
|
||||
onSearchRequested();
|
||||
}
|
||||
|
@ -70,6 +71,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
@ -83,7 +85,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
|
|||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(SearchAsyncTaskLoader.SEARCH_TERM, query);
|
||||
searchLoader = getSupportLoaderManager().restartLoader(0, bundle, this);
|
||||
searchLoader = LoaderManager.getInstance(this).restartLoader(0, bundle, this);
|
||||
|
||||
searchLoader.forceLoad();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
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;
|
||||
|
@ -14,43 +10,18 @@ import com.newsblur.R;
|
|||
import com.newsblur.databinding.ActivityWidgetConfigBinding;
|
||||
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.util.WidgetBackground;
|
||||
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;
|
||||
|
||||
public class WidgetConfig extends NbActivity {
|
||||
public class WidgetConfig extends FeedChooser {
|
||||
|
||||
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<>();
|
||||
private ActivityWidgetConfigBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setupList();
|
||||
loadFeeds();
|
||||
loadFolders();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
@ -61,127 +32,46 @@ public class WidgetConfig extends NbActivity {
|
|||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_widget, menu);
|
||||
inflater.inflate(R.menu.menu_feed_chooser, 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);
|
||||
}
|
||||
|
||||
WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(this);
|
||||
if (widgetBackground == WidgetBackground.DEFAULT) {
|
||||
menu.findItem(R.id.menu_widget_background_default).setChecked(true);
|
||||
} else if (widgetBackground == WidgetBackground.TRANSPARENT) {
|
||||
menu.findItem(R.id.menu_widget_background_transparent).setChecked(true);
|
||||
}
|
||||
menu.findItem(R.id.menu_mute_all).setVisible(false);
|
||||
menu.findItem(R.id.menu_mute_none).setVisible(false);
|
||||
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;
|
||||
case R.id.menu_widget_background_default:
|
||||
setWidgetBackground(WidgetBackground.DEFAULT);
|
||||
return true;
|
||||
case R.id.menu_widget_background_transparent:
|
||||
setWidgetBackground(WidgetBackground.TRANSPARENT);
|
||||
replaceWidgetFeedIds(Collections.emptySet());
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupList() {
|
||||
@Override
|
||||
void bindLayout() {
|
||||
binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
}
|
||||
|
||||
@Override
|
||||
void setupList() {
|
||||
adapter = new WidgetConfigAdapter(this);
|
||||
binding.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) {
|
||||
@Override
|
||||
void processFeeds(Cursor cursor) {
|
||||
ArrayList<Feed> feeds = new ArrayList<>();
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Feed feed = Feed.fromCursor(cursor);
|
||||
|
@ -194,25 +84,25 @@ public class WidgetConfig extends NbActivity {
|
|||
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);
|
||||
@Override
|
||||
public void setAdapterData() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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();
|
||||
adapter.setFeedIds(feedIds);
|
||||
|
||||
super.setAdapterData();
|
||||
binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void processData() {
|
||||
@Override
|
||||
void processData() {
|
||||
if (folders != null && feeds != null) {
|
||||
for (Folder folder : folders) {
|
||||
ArrayList<Feed> activeFeeds = new ArrayList<>();
|
||||
|
@ -226,7 +116,6 @@ public class WidgetConfig extends NbActivity {
|
|||
folderChildren.add(activeFeeds);
|
||||
}
|
||||
|
||||
setSelectedFeeds();
|
||||
setAdapterData();
|
||||
}
|
||||
}
|
||||
|
@ -243,44 +132,4 @@ public class WidgetConfig extends NbActivity {
|
|||
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 setWidgetBackground(WidgetBackground widgetBackground) {
|
||||
PrefsUtils.setWidgetBackground(this, widgetBackground);
|
||||
WidgetUtils.updateWidget(this);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
|
@ -1,296 +1,72 @@
|
|||
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;
|
||||
public class WidgetConfigAdapter extends FeedChooserAdapter {
|
||||
|
||||
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;
|
||||
super(context);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
|
||||
|
||||
((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;
|
||||
}
|
||||
groupView.setOnClickListener(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();
|
||||
}
|
||||
for (Feed feed : folderChild) {
|
||||
if (allSelected) {
|
||||
feedIds.remove(feed.feedId);
|
||||
} else {
|
||||
feedIds.add(feed.feedId);
|
||||
}
|
||||
}
|
||||
setWidgetFeedIds(parent.getContext());
|
||||
notifyDataChanged();
|
||||
});
|
||||
return convertView;
|
||||
return groupView;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
|
||||
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));
|
||||
final CheckBox checkBox = childView.findViewById(R.id.check_box);
|
||||
final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
|
||||
checkBox.setVisibility(View.VISIBLE);
|
||||
imgToggle.setVisibility(View.GONE);
|
||||
|
||||
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());
|
||||
childView.setOnClickListener(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();
|
||||
return childView;
|
||||
}
|
||||
|
||||
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,9 +6,9 @@ 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 androidx.annotation.Nullable;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -284,6 +284,19 @@ public class BlurDatabaseHelper {
|
|||
return hashes;
|
||||
}
|
||||
|
||||
public Set<String> getStarredStoryHashes() {
|
||||
String q = "SELECT " + DatabaseConstants.STORY_HASH +
|
||||
" FROM " + DatabaseConstants.STORY_TABLE +
|
||||
" WHERE " + DatabaseConstants.STORY_STARRED + " = 1" ;
|
||||
Cursor c = dbRO.rawQuery(q, null);
|
||||
Set<String> hashes = new HashSet<>(c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
hashes.add(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_HASH)));
|
||||
}
|
||||
c.close();
|
||||
return hashes;
|
||||
}
|
||||
|
||||
public Set<String> getAllStoryImages() {
|
||||
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null);
|
||||
Set<String> urls = new HashSet<String>(c.getCount());
|
||||
|
@ -584,6 +597,22 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void markStoryHashesStarred(Collection<String> hashes, boolean isStarred) {
|
||||
synchronized (RW_MUTEX) {
|
||||
dbRW.beginTransaction();
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.STORY_STARRED, isStarred);
|
||||
for (String hash : hashes) {
|
||||
dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});
|
||||
}
|
||||
dbRW.setTransactionSuccessful();
|
||||
} finally {
|
||||
dbRW.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setFeedsActive(Set<String> feedIds, boolean active) {
|
||||
synchronized (RW_MUTEX) {
|
||||
dbRW.beginTransaction();
|
||||
|
|
|
@ -75,6 +75,8 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
public int totalSocialNeutCount = 0;
|
||||
/** Total positive unreads for all social feeds. */
|
||||
public int totalSocialPosiCount = 0;
|
||||
/** Total active feeds. */
|
||||
public int totalActiveFeedCount = 0;
|
||||
|
||||
/** Feeds, indexed by feed ID. */
|
||||
private Map<String,Feed> feeds = Collections.emptyMap();
|
||||
|
@ -557,6 +559,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
feedPosCounts = new HashMap<String,Integer>();
|
||||
totalNeutCount = 0;
|
||||
totalPosCount = 0;
|
||||
totalActiveFeedCount = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
Feed f = Feed.fromCursor(cursor);
|
||||
feeds.put(f.feedId, f);
|
||||
|
@ -570,6 +573,9 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
|||
feedNeutCounts.put(f.feedId, neut);
|
||||
totalNeutCount += neut;
|
||||
}
|
||||
if (f.active) {
|
||||
totalActiveFeedCount++;
|
||||
}
|
||||
}
|
||||
recountFeeds();
|
||||
notifyDataSetChanged();
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context;
|
|||
import android.database.Cursor;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.OperationCanceledException;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import com.newsblur.util.AppConstants;
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ package com.newsblur.database;
|
|||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
|
@ -178,7 +178,6 @@ public class ReadingAdapter extends PagerAdapter {
|
|||
}
|
||||
}
|
||||
fragment.setMenuVisibility(false);
|
||||
fragment.setUserVisibleHint(false);
|
||||
if (curTransaction == null) {
|
||||
curTransaction = fm.beginTransaction();
|
||||
}
|
||||
|
@ -208,11 +207,9 @@ public class ReadingAdapter extends PagerAdapter {
|
|||
if (fragment != lastActiveFragment) {
|
||||
if (lastActiveFragment != null) {
|
||||
lastActiveFragment.setMenuVisibility(false);
|
||||
lastActiveFragment.setUserVisibleHint(false);
|
||||
}
|
||||
if (fragment != null) {
|
||||
fragment.setMenuVisibility(true);
|
||||
fragment.setUserVisibleHint(true);
|
||||
}
|
||||
lastActiveFragment = fragment;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import android.database.Cursor;
|
|||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.util.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.GestureDetector;
|
||||
|
@ -126,7 +126,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
}
|
||||
|
||||
public int getStoryCount() {
|
||||
return stories.size();
|
||||
if (fs != null && UIUtils.needsPremiumAccess(context, fs)) {
|
||||
return Math.min(3, stories.size());
|
||||
} else {
|
||||
return stories.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -464,6 +468,9 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
case GEST_ACTION_UNSAVE:
|
||||
FeedUtils.setStorySaved(story, false, context, null);
|
||||
break;
|
||||
case GEST_ACTION_STATISTICS:
|
||||
FeedUtils.openStatistics(context, story.feedId);
|
||||
break;
|
||||
case GEST_ACTION_NONE:
|
||||
default:
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.newsblur.domain;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.Nullable;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
|
|
|
@ -6,12 +6,12 @@ import android.app.Dialog;
|
|||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
|
@ -5,7 +5,7 @@ import java.util.HashSet;
|
|||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.app.AlertDialog;
|
|||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.app.AlertDialog;
|
|||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
|
|
@ -15,7 +15,7 @@ import android.app.AlertDialog;
|
|||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
public class DeleteFeedFragment extends DialogFragment {
|
||||
private static final String FEED_TYPE = "feed_type";
|
||||
|
|
|
@ -4,9 +4,9 @@ import android.app.AlertDialog;
|
|||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.newsblur.R;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue