Merge branch 'develop'
This commit is contained in:
commit
624d57f1d6
@ -12,5 +12,5 @@ script:
|
||||
- ./gradlew clean assembleDebug assembleRelease testDebug
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
- yes | sdkmanager "build-tools;28.0.3"
|
||||
- yes | sdkmanager "platforms;android-29"
|
||||
- yes | sdkmanager "build-tools;29.0.2"
|
28
CHANGELOG.md
28
CHANGELOG.md
@ -1,5 +1,31 @@
|
||||
#v1.1.0
|
||||
|
||||
- OPML import/export for local account
|
||||
- Dark theme
|
||||
- Share or download item image
|
||||
- Open item in webview
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
#v1.0.2.2
|
||||
|
||||
Disable Proguard as it makes fail some functionalities.
|
||||
|
||||
#v1.0.2.1
|
||||
|
||||
Fix a crash related to Proguard Rules.
|
||||
|
||||
#v1.0.2
|
||||
|
||||
- Add swipe background to main list items
|
||||
- Add preference to parse a fixed number of items when adding a local feed
|
||||
- Change feed/folders way to interact
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
|
||||
|
||||
# 1.0 Initial release
|
||||
# v1.0.1
|
||||
|
||||
# v1.0 Initial release
|
||||
|
||||
- Local RSS parsing
|
||||
- RSS 2.0, ATOM and JSON formats support
|
||||
|
@ -4,19 +4,31 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "com.readrops.app"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 5
|
||||
versionName "1.0.2.2"
|
||||
targetSdkVersion 29
|
||||
versionCode 6
|
||||
versionName "1.1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = [
|
||||
"room.incremental": "true"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false // proguard makes some functionalities fail so It's disabled until I find the problem source
|
||||
shrinkResources false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
@ -46,10 +58,12 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.palette:palette:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
@ -66,14 +80,14 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
kapt 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.1.0'
|
||||
kapt 'androidx.room:room-compiler:2.1.0'
|
||||
implementation 'android.arch.persistence.room:rxjava2:1.1.1'
|
||||
implementation 'androidx.room:room-runtime:2.2.2'
|
||||
kapt 'androidx.room:room-compiler:2.2.2'
|
||||
implementation 'androidx.room:room-rxjava2:2.2.2'
|
||||
|
||||
implementation 'androidx.paging:paging-runtime:2.1.0'
|
||||
implementation 'androidx.paging:paging-common:2.1.0'
|
||||
|
||||
implementation 'joda-time:joda-time:2.10.3'
|
||||
implementation 'joda-time:joda-time:2.10.5'
|
||||
implementation 'org.jsoup:jsoup:1.12.1'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
@ -83,11 +97,7 @@ dependencies {
|
||||
implementation 'com.mikepenz:fastadapter:3.3.1'
|
||||
implementation 'com.mikepenz:fastadapter-commons:3.3.0'
|
||||
implementation 'com.mikepenz:materialdrawer:6.1.2'
|
||||
implementation "com.mikepenz:aboutlibraries:6.2.3"
|
||||
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
implementation "com.mikepenz:aboutlibraries:6.2.3"
|
||||
}
|
||||
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@ -23,3 +23,10 @@
|
||||
-dontwarn org.xmlpull.v1.XmlPullParser
|
||||
-dontwarn org.xmlpull.v1.XmlSerializer
|
||||
-keep class org.xmlpull.v1.* {*;}
|
||||
|
||||
-keep class org.simpleframework.xml.** { *; }
|
||||
|
||||
-keep class com.readrops.readropslibrary.services.freshrss.json.** { *; }
|
||||
-keep class com.readrops.readropslibrary.services.nextcloudnews.json.** { *; }
|
||||
|
||||
-keep class com.readrops.readropslibrary.localfeed.** { *; }
|
@ -5,9 +5,11 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".utils.ReadropsApp"
|
||||
android:name=".ReadropsApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_readrops"
|
||||
android:label="@string/app_name"
|
||||
@ -15,10 +17,26 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<provider
|
||||
android:authorities="${applicationId}"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name=".activities.WebViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<service android:name=".utils.feedscolors.FeedsColorsIntentService" />
|
||||
|
||||
<activity android:name=".activities.SettingsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
@ -28,32 +46,27 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.AccountTypeListActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.AddAccountActivity"
|
||||
android:label="@string/add_account" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.ManageFeedsFoldersActivity"
|
||||
android:label="@string/manage_feeds_folders"
|
||||
android:parentActivityName=".activities.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:label="@string/articles"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.ItemActivity"
|
||||
android:parentActivityName=".activities.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.AddFeedActivity"
|
||||
android:label="@string/add_feed_title"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
57
app/src/main/java/com/readrops/app/ReadropsApp.java
Normal file
57
app/src/main/java/com/readrops/app/ReadropsApp.java
Normal file
@ -0,0 +1,57 @@
|
||||
package com.readrops.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
|
||||
public class ReadropsApp extends Application {
|
||||
|
||||
public static final String FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel";
|
||||
public static final String OPML_EXPORT_CHANNEL_ID = "opmlExportChannel";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
RxJavaPlugins.setErrorHandler(e -> {
|
||||
});
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Stetho.initializeWithDefaults(this);
|
||||
}
|
||||
|
||||
createNotificationChannels();
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||
|
||||
if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
else
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
}
|
||||
|
||||
private void createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel feedsColorsChannel = new NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
|
||||
getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
feedsColorsChannel.setDescription(getString(R.string.get_feeds_colors));
|
||||
|
||||
NotificationChannel opmlExportChannel = new NotificationChannel(OPML_EXPORT_CHANNEL_ID,
|
||||
getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
opmlExportChannel.setDescription(getString(R.string.opml_export_description));
|
||||
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
|
||||
manager.createNotificationChannel(feedsColorsChannel);
|
||||
manager.createNotificationChannel(opmlExportChannel);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +1,47 @@
|
||||
package com.readrops.app.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.adapters.AccountTypeListAdapter;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.entities.account.AccountType;
|
||||
import com.readrops.app.databinding.ActivityAccountTypeListBinding;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.viewmodels.AccountViewModel;
|
||||
import com.readrops.app.adapters.AccountTypeListAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableCompletableObserver;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.fragments.settings.AccountSettingsFragment.OPEN_OPML_FILE_REQUEST;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
|
||||
|
||||
public class AccountTypeListActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = AccountTypeListActivity.class.getSimpleName();
|
||||
|
||||
private ActivityAccountTypeListBinding binding;
|
||||
private AccountTypeListAdapter adapter;
|
||||
private AccountViewModel viewModel;
|
||||
@ -47,24 +60,43 @@ public class AccountTypeListActivity extends AppCompatActivity {
|
||||
binding.accountTypeRecyclerview.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.accountTypeRecyclerview.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
|
||||
|
||||
fromMainActivity = getIntent().getBooleanExtra("fromMainActivity", false);
|
||||
fromMainActivity = getIntent().getBooleanExtra(FROM_MAIN_ACTIVITY, false);
|
||||
|
||||
if (fromMainActivity)
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
adapter = new AccountTypeListAdapter(accountType -> {
|
||||
if (!(accountType == AccountType.LOCAL)) {
|
||||
if (accountType != AccountType.LOCAL) {
|
||||
Intent intent = new Intent(getApplicationContext(), AddAccountActivity.class);
|
||||
|
||||
if (fromMainActivity)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
|
||||
|
||||
intent.putExtra("accountType", (Parcelable) accountType);
|
||||
intent.putExtra(ACCOUNT_TYPE, (Parcelable) accountType);
|
||||
|
||||
startActivity(intent);
|
||||
finish();
|
||||
} else
|
||||
createNewLocalAccount(accountType);
|
||||
} else {
|
||||
Account account = new Account(null, getString(AccountType.LOCAL.getName()), AccountType.LOCAL);
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
viewModel.insert(account)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<Long>() {
|
||||
@Override
|
||||
public void onSuccess(Long id) {
|
||||
account.setId(id.intValue());
|
||||
goToNextActivity(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -82,39 +114,6 @@ public class AccountTypeListActivity extends AppCompatActivity {
|
||||
return accountTypes;
|
||||
}
|
||||
|
||||
private void createNewLocalAccount(AccountType accountType) {
|
||||
Account account = new Account(null, getString(accountType.getName()), accountType);
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
viewModel.insert(account)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<Long>() {
|
||||
@Override
|
||||
public void onSuccess(Long id) {
|
||||
account.setId(id.intValue());
|
||||
|
||||
if (fromMainActivity) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
@ -125,4 +124,75 @@ public class AccountTypeListActivity extends AppCompatActivity {
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void openOPMLFile(View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("application/*");
|
||||
|
||||
startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getData();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(this)
|
||||
.title(R.string.opml_processing)
|
||||
.content(R.string.operation_takes_time)
|
||||
.progress(true, 100)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
parseOPMLFile(uri, dialog);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
|
||||
Account account = new Account(null, getString(AccountType.LOCAL.getName()), AccountType.LOCAL);
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
viewModel.insert(account)
|
||||
.flatMapCompletable(id -> {
|
||||
account.setId(id.intValue());
|
||||
viewModel.setAccount(account);
|
||||
|
||||
return viewModel.parseOPMLFile(uri);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
dialog.dismiss();
|
||||
goToNextActivity(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
|
||||
dialog.dismiss();
|
||||
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void goToNextActivity(Account account) {
|
||||
if (fromMainActivity) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,11 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class AddAccountActivity extends AppCompatActivity {
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
|
||||
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
|
||||
|
||||
public static final String EDIT_ACCOUNT = "EDIT_ACCOUNT";
|
||||
public class AddAccountActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityAddAccountBinding binding;
|
||||
private AccountViewModel viewModel;
|
||||
@ -45,7 +47,7 @@ public class AddAccountActivity extends AppCompatActivity {
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_add_account);
|
||||
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
|
||||
|
||||
accountType = getIntent().getParcelableExtra("accountType");
|
||||
accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE);
|
||||
|
||||
int flag = getIntent().getFlags();
|
||||
forwardResult = flag == Intent.FLAG_ACTIVITY_FORWARD_RESULT;
|
||||
@ -114,13 +116,13 @@ public class AddAccountActivity extends AppCompatActivity {
|
||||
|
||||
if (forwardResult) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
|
||||
} else {
|
||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
intent.putExtra(MainActivity.ACCOUNT_KEY, account);
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@ -250,7 +252,7 @@ public class AddAccountActivity extends AppCompatActivity {
|
||||
createAccount(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
|
||||
public class AddFeedActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
|
||||
private AccountArrayAdapter arrayAdapter;
|
||||
@ -311,7 +313,7 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
|
||||
public void finish() {
|
||||
if (!feedsToUpdate.isEmpty()) {
|
||||
Intent intent = new Intent();
|
||||
intent.putParcelableArrayListExtra("feedIds", feedsToUpdate);
|
||||
intent.putParcelableArrayListExtra(FEEDS, feedsToUpdate);
|
||||
|
||||
setResult(RESULT_OK, intent);
|
||||
}
|
||||
|
@ -1,22 +1,35 @@
|
||||
package com.readrops.app.activities;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
@ -26,11 +39,19 @@ import com.readrops.app.database.pojo.ItemWithFeed;
|
||||
import com.readrops.app.utils.DateUtils;
|
||||
import com.readrops.app.utils.GlideApp;
|
||||
import com.readrops.app.utils.ReadropsWebView;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.viewmodels.ItemViewModel;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR;
|
||||
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.WEB_URL;
|
||||
|
||||
public class ItemActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = ItemActivity.class.getSimpleName();
|
||||
|
||||
private ItemViewModel viewModel;
|
||||
private TextView date;
|
||||
private TextView title;
|
||||
@ -45,9 +66,6 @@ public class ItemActivity extends AppCompatActivity {
|
||||
private FloatingActionButton actionButton;
|
||||
private ReadropsWebView webView;
|
||||
|
||||
public static final String ITEM_ID = "itemId";
|
||||
public static final String IMAGE_URL = "imageUrl";
|
||||
|
||||
private ItemWithFeed itemWithFeed;
|
||||
|
||||
private boolean appBarCollapsed;
|
||||
@ -81,6 +99,8 @@ public class ItemActivity extends AppCompatActivity {
|
||||
readTimeLayout = findViewById(R.id.activity_item_readtime_layout);
|
||||
dateLayout = findViewById(R.id.activity_item_date_layout);
|
||||
|
||||
registerForContextMenu(webView);
|
||||
|
||||
if (imageUrl == null) {
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
toolbarLayout.setTitleEnabled(false);
|
||||
@ -111,9 +131,9 @@ public class ItemActivity extends AppCompatActivity {
|
||||
}
|
||||
}));
|
||||
|
||||
viewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()).create(ItemViewModel.class);
|
||||
viewModel = ViewModelProviders.of(this).get(ItemViewModel.class);
|
||||
viewModel.getItemById(itemId).observe(this, this::bindUI);
|
||||
actionButton.setOnClickListener(v -> openLink());
|
||||
actionButton.setOnClickListener(v -> openInNavigator());
|
||||
}
|
||||
|
||||
private void bindUI(ItemWithFeed itemWithFeed) {
|
||||
@ -188,11 +208,7 @@ public class ItemActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuItem item = menu.findItem(R.id.item_open);
|
||||
|
||||
if (appBarCollapsed)
|
||||
item.setVisible(true);
|
||||
else
|
||||
item.setVisible(false);
|
||||
item.setVisible(appBarCollapsed);
|
||||
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
@ -207,10 +223,16 @@ public class ItemActivity extends AppCompatActivity {
|
||||
shareArticle();
|
||||
return true;
|
||||
case R.id.item_open:
|
||||
openLink();
|
||||
int value = Integer.valueOf(SharedPreferencesManager.readString(this,
|
||||
SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN));
|
||||
if (value == 0)
|
||||
openInNavigator();
|
||||
else
|
||||
openInWebView();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -219,15 +241,86 @@ public class ItemActivity extends AppCompatActivity {
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private void openLink() {
|
||||
private void openInNavigator() {
|
||||
Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(itemWithFeed.getItem().getLink()));
|
||||
startActivity(urlIntent);
|
||||
}
|
||||
|
||||
private void openInWebView() {
|
||||
Intent intent = new Intent(this, WebViewActivity.class);
|
||||
intent.putExtra(WEB_URL, itemWithFeed.getItem().getLink());
|
||||
intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getColor() != 0 ? itemWithFeed.getColor() : itemWithFeed.getBgColor());
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void shareArticle() {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, itemWithFeed.getItem().getTitle() + " - " + itemWithFeed.getItem().getLink());
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)));
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_article)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
WebView.HitTestResult hitTestResult = webView.getHitTestResult();
|
||||
|
||||
if (hitTestResult.getType() == WebView.HitTestResult.IMAGE_TYPE ||
|
||||
hitTestResult.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.image_options)
|
||||
.items(R.array.image_options)
|
||||
.itemsCallback((dialog, itemView, position, text) -> {
|
||||
if (position == 0)
|
||||
shareImage(hitTestResult.getExtra());
|
||||
else
|
||||
downloadImage(hitTestResult.getExtra());
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadImage(String url) {
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
|
||||
.setTitle(getString(R.string.download_image))
|
||||
.setMimeType("image/png")
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "image.png");
|
||||
|
||||
request.allowScanningByMediaScanner();
|
||||
|
||||
DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
|
||||
downloadManager.enqueue(request);
|
||||
}
|
||||
|
||||
private void shareImage(String url) {
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.load(url)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
try {
|
||||
Uri uri = viewModel.saveImageInCache(resource);
|
||||
Intent intent = ShareCompat.IntentBuilder.from(ItemActivity.this)
|
||||
.setType("image/png")
|
||||
.setStream(uri)
|
||||
.setChooserTitle(R.string.share_image)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
// not useful
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.pojo.ItemWithFeed;
|
||||
import com.readrops.app.fragments.settings.AccountSettingsFragment;
|
||||
import com.readrops.app.utils.DrawerManager;
|
||||
import com.readrops.app.utils.GlideApp;
|
||||
import com.readrops.app.utils.ReadropsItemTouchCallback;
|
||||
@ -64,6 +63,14 @@ import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
|
||||
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
|
||||
import static com.readrops.app.utils.ReadropsKeys.SYNCING;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener,
|
||||
ReadropsItemTouchCallback.SwipeCallback, ActionMode.Callback {
|
||||
|
||||
@ -74,10 +81,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
public static final int ITEM_REQUEST = 3;
|
||||
public static final int ADD_ACCOUNT_REQUEST = 4;
|
||||
|
||||
public static final String ACCOUNT_KEY = "account";
|
||||
|
||||
private static final String SYNCING_KEY = "SYNCING";
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private MainItemListAdapter adapter;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
@ -158,14 +161,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
switch (id) {
|
||||
case DrawerManager.ADD_ACCOUNT_ID:
|
||||
Intent intent = new Intent(this, AccountTypeListActivity.class);
|
||||
intent.putExtra("fromMainActivity", true);
|
||||
intent.putExtra(FROM_MAIN_ACTIVITY, true);
|
||||
startActivityForResult(intent, ADD_ACCOUNT_REQUEST);
|
||||
break;
|
||||
case DrawerManager.ACCOUNT_SETTINGS_ID:
|
||||
Intent intent1 = new Intent(this, SettingsActivity.class);
|
||||
intent1.putExtra(SettingsActivity.SETTINGS_KEY,
|
||||
intent1.putExtra(SETTINGS,
|
||||
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
|
||||
intent1.putExtra(AccountSettingsFragment.ACCOUNT, viewModel.getCurrentAccount());
|
||||
intent1.putExtra(ACCOUNT, viewModel.getCurrentAccount());
|
||||
startActivity(intent1);
|
||||
break;
|
||||
default:
|
||||
@ -177,16 +180,16 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
}
|
||||
} else {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
intent.putExtra(SettingsActivity.SETTINGS_KEY,
|
||||
intent.putExtra(SETTINGS,
|
||||
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
|
||||
intent.putExtra(AccountSettingsFragment.ACCOUNT, viewModel.getCurrentAccount());
|
||||
intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
|
||||
startActivityForResult(intent, MANAGE_ACCOUNT_REQUEST);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
Account currentAccount = getIntent().getParcelableExtra(MainActivity.ACCOUNT_KEY);
|
||||
Account currentAccount = getIntent().getParcelableExtra(ACCOUNT);
|
||||
WeakReference<Account> accountWeakReference = new WeakReference<>(currentAccount);
|
||||
|
||||
viewModel.getAllAccounts().observe(this, accounts -> {
|
||||
@ -210,7 +213,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
refreshLayout.setRefreshing(true);
|
||||
onRefresh();
|
||||
accountWeakReference.clear();
|
||||
} else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING_KEY)) {
|
||||
} else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING)) {
|
||||
refreshLayout.setRefreshing(true);
|
||||
onRefresh();
|
||||
savedInstanceState.clear();
|
||||
@ -239,7 +242,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
break;
|
||||
case DrawerManager.SETTINGS_ID:
|
||||
Intent intent = new Intent(getApplication(), SettingsActivity.class);
|
||||
intent.putExtra(SettingsActivity.SETTINGS_KEY,
|
||||
intent.putExtra(SETTINGS,
|
||||
SettingsActivity.SettingsKey.SETTINGS.ordinal());
|
||||
startActivity(intent);
|
||||
break;
|
||||
@ -289,8 +292,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
if (actionMode == null) {
|
||||
Intent intent = new Intent(getApplicationContext(), ItemActivity.class);
|
||||
|
||||
intent.putExtra(ItemActivity.ITEM_ID, itemWithFeed.getItem().getId());
|
||||
intent.putExtra(ItemActivity.IMAGE_URL, itemWithFeed.getItem().getImageLink());
|
||||
intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId());
|
||||
intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink());
|
||||
startActivityForResult(intent, ITEM_REQUEST);
|
||||
|
||||
viewModel.setItemReadState(itemWithFeed, true)
|
||||
@ -312,7 +315,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(ItemWithFeed itemWithFeed, int position) {
|
||||
if (actionMode != null)
|
||||
if (actionMode != null || refreshLayout.isRefreshing())
|
||||
return;
|
||||
|
||||
selectedItemWithFeed = itemWithFeed;
|
||||
@ -501,7 +504,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == ADD_FEED_REQUEST && resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
ArrayList<Feed> feeds = data.getParcelableArrayListExtra("feedIds");
|
||||
ArrayList<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
|
||||
|
||||
if (feeds != null && feeds.size() > 0 && viewModel.isAccountLocal()) {
|
||||
refreshLayout.setRefreshing(true);
|
||||
@ -515,7 +518,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
|
||||
} else if (requestCode == ADD_ACCOUNT_REQUEST && resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
Account newAccount = data.getParcelableExtra(ACCOUNT_KEY);
|
||||
Account newAccount = data.getParcelableExtra(ACCOUNT);
|
||||
|
||||
if (newAccount != null) {
|
||||
viewModel.addAccount(newAccount);
|
||||
@ -667,6 +670,12 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
}
|
||||
|
||||
private void startAboutActivity() {
|
||||
Libs.ActivityStyle activityStyle;
|
||||
if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
|
||||
activityStyle = Libs.ActivityStyle.DARK;
|
||||
else
|
||||
activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR;
|
||||
|
||||
new LibsBuilder()
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
@ -675,7 +684,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
.withLicenseShown(true)
|
||||
.withLicenseDialog(false)
|
||||
.withActivityTitle(getString(R.string.about))
|
||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||
.withActivityStyle(activityStyle)
|
||||
.withFields(R.string.class.getFields())
|
||||
.start(this);
|
||||
}
|
||||
@ -691,7 +700,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
if (refreshLayout.isRefreshing())
|
||||
outState.putBoolean(SYNCING_KEY, true);
|
||||
outState.putBoolean(SYNCING, true);
|
||||
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.app.R;
|
||||
@ -28,12 +27,12 @@ import com.readrops.readropslibrary.utils.UnknownFormatException;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
public class ManageFeedsFoldersActivity extends AppCompatActivity {
|
||||
|
||||
public static final String ACCOUNT = "ACCOUNT";
|
||||
|
||||
private ActivityManageFeedsFoldersBinding binding;
|
||||
private FeedsFoldersPageAdapter pageAdapter;
|
||||
private ManageFeedsFoldersViewModel viewModel;
|
||||
|
||||
private Account account;
|
||||
@ -48,30 +47,13 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
|
||||
|
||||
account = getIntent().getParcelableExtra(ACCOUNT);
|
||||
|
||||
pageAdapter = new FeedsFoldersPageAdapter(getSupportFragmentManager());
|
||||
FeedsFoldersPageAdapter pageAdapter = new FeedsFoldersPageAdapter(getSupportFragmentManager());
|
||||
|
||||
binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter);
|
||||
binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager);
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(ManageFeedsFoldersViewModel.class);
|
||||
viewModel.setAccount(account);
|
||||
|
||||
binding.manageFeedsFoldersViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
binding.manageFeedsFoldersTablayout.getTabAt(position).select();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -92,8 +74,9 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
|
||||
case R.id.add_folder:
|
||||
addFolder();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,7 +115,7 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
|
||||
public class FeedsFoldersPageAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private FeedsFoldersPageAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -11,9 +11,10 @@ import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.fragments.settings.AccountSettingsFragment;
|
||||
import com.readrops.app.fragments.settings.SettingsFragment;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
|
||||
|
||||
public static final String SETTINGS_KEY = SettingsKey.class.getSimpleName().toUpperCase();
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@ -22,9 +23,9 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Account account = getIntent().getParcelableExtra(AccountSettingsFragment.ACCOUNT);
|
||||
Account account = getIntent().getParcelableExtra(ACCOUNT);
|
||||
|
||||
SettingsKey settingsKey = SettingsKey.values()[getIntent().getIntExtra(SETTINGS_KEY, -1)];
|
||||
SettingsKey settingsKey = SettingsKey.values()[getIntent().getIntExtra(SETTINGS, -1)];
|
||||
Fragment fragment = null;
|
||||
|
||||
switch (settingsKey) {
|
||||
|
123
app/src/main/java/com/readrops/app/activities/WebViewActivity.kt
Normal file
123
app/src/main/java/com/readrops/app/activities/WebViewActivity.kt
Normal file
@ -0,0 +1,123 @@
|
||||
package com.readrops.app.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.webkit.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.databinding.ActivityWebViewBinding
|
||||
import com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR
|
||||
import com.readrops.app.utils.ReadropsKeys.WEB_URL
|
||||
|
||||
class WebViewActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityWebViewBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_web_view)
|
||||
|
||||
setSupportActionBar(binding.activityWebViewToolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
title = ""
|
||||
val actionBarColor = intent.getIntExtra(ACTION_BAR_COLOR, ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
supportActionBar?.setBackgroundDrawable(ColorDrawable(actionBarColor))
|
||||
setWebViewSettings()
|
||||
|
||||
binding.activityWebViewSwipe.setOnRefreshListener { binding.webView.reload() }
|
||||
binding.activityWebViewProgress.indeterminateDrawable.setColorFilter(actionBarColor, PorterDuff.Mode.SRC_IN)
|
||||
binding.activityWebViewProgress.max = 100
|
||||
|
||||
val url: String = intent.getStringExtra(WEB_URL)
|
||||
binding.webView.loadUrl(url)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun setWebViewSettings() {
|
||||
val settings: WebSettings = binding.webView.settings
|
||||
|
||||
settings.javaScriptEnabled = true
|
||||
settings.setSupportZoom(true)
|
||||
|
||||
binding.webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
binding.webView.loadUrl(request?.url.toString())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
binding.activityWebViewSwipe.isRefreshing = false
|
||||
binding.activityWebViewProgress.progress = 0
|
||||
binding.activityWebViewProgress.visibility = View.VISIBLE
|
||||
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
}
|
||||
|
||||
binding.webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
||||
setTitle(title)
|
||||
supportActionBar?.subtitle = Uri.parse(view?.url).host
|
||||
|
||||
super.onReceivedTitle(view, title)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
binding.activityWebViewProgress.progress = newProgress
|
||||
if (newProgress == 100)
|
||||
binding.activityWebViewProgress.visibility = View.GONE
|
||||
|
||||
super.onProgressChanged(view, newProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.webView.canGoBack())
|
||||
binding.webView.goBack()
|
||||
else
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (binding.webView.canGoBack())
|
||||
binding.webView.goBack()
|
||||
else
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
R.id.web_view_refresh -> {
|
||||
binding.webView.reload()
|
||||
}
|
||||
R.id.web_view_share -> {
|
||||
shareLink()
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun shareLink() {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TEXT, binding.webView.url.toString())
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_url)))
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.webview_menu, menu)
|
||||
return true
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package com.readrops.app.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
@ -38,6 +37,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItemListAdapter.ItemViewHolder> implements ListPreloader.PreloadModelProvider<String> {
|
||||
|
||||
@ -63,18 +63,18 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ItemWithFeed itemWithFeed, @NonNull ItemWithFeed t1) {
|
||||
Item item = itemWithFeed.getItem();
|
||||
Item item1 = t1.getItem();
|
||||
Item oldItem = itemWithFeed.getItem();
|
||||
Item newItem = t1.getItem();
|
||||
|
||||
boolean folder = false;
|
||||
if (itemWithFeed.getFolder() != null && t1.getFolder() != null)
|
||||
folder = itemWithFeed.getFolder().getName().equals(t1.getFolder().getName());
|
||||
|
||||
return item.getTitle().equals(item1.getTitle()) &&
|
||||
return oldItem.getTitle().equals(newItem.getTitle()) &&
|
||||
itemWithFeed.getFeedName().equals(t1.getFeedName()) &&
|
||||
folder &&
|
||||
item.isRead() == item1.isRead() &&
|
||||
item.isReadItLater() == item1.isReadItLater() &&
|
||||
oldItem.isRead() == newItem.isRead() &&
|
||||
oldItem.isReadItLater() == newItem.isReadItLater() &&
|
||||
itemWithFeed.getColor() == t1.getColor() &&
|
||||
itemWithFeed.getBgColor() == t1.getBgColor();
|
||||
}
|
||||
@ -87,7 +87,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
|
||||
|
||||
private static final DrawableCrossFadeFactory FADE_FACTORY = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
|
||||
|
||||
private static final RequestOptions REQUEST_OPTIONS = new RequestOptions().transforms(new CenterCrop(), new RoundedCorners(16));
|
||||
private static final RequestOptions REQUEST_OPTIONS = new RequestOptions().transform(new CenterCrop(), new RoundedCorners(16));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@ -102,7 +102,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.size() > 0) {
|
||||
if (!payloads.isEmpty()) {
|
||||
ItemWithFeed itemWithFeed = (ItemWithFeed) payloads.get(0);
|
||||
|
||||
holder.bind(itemWithFeed);
|
||||
@ -171,7 +171,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
|
||||
}
|
||||
}
|
||||
|
||||
public LinkedHashSet<Integer> getSelection() {
|
||||
public Set<Integer> getSelection() {
|
||||
return selection;
|
||||
}
|
||||
|
||||
@ -250,7 +250,7 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
|
||||
public class ItemViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private ListItemBinding binding;
|
||||
View[] alphaViews;
|
||||
private View[] alphaViews;
|
||||
|
||||
ItemViewHolder(ListItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
@ -352,16 +352,17 @@ public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItem
|
||||
|
||||
private void setSelected(boolean selected) {
|
||||
Context context = itemView.getContext();
|
||||
TypedValue outValue = new TypedValue();
|
||||
|
||||
if (selected) {
|
||||
itemView.setBackground(new ColorDrawable(ContextCompat.getColor(context, R.color.selected_background)));
|
||||
context.getTheme().resolveAttribute(
|
||||
android.R.attr.colorControlHighlight, outValue, true);
|
||||
} else {
|
||||
TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(
|
||||
android.R.attr.selectableItemBackground, outValue, true);
|
||||
|
||||
itemView.setBackgroundResource(outValue.resourceId);
|
||||
}
|
||||
|
||||
itemView.setBackgroundResource(outValue.resourceId);
|
||||
}
|
||||
|
||||
public ImageView getItemImage() {
|
||||
|
@ -0,0 +1,76 @@
|
||||
package com.readrops.app.database;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.PositionalDataSource;
|
||||
|
||||
/**
|
||||
* Workaround class to avoid item recycler view scrolling down when updating data
|
||||
* This class is to keep until a new version of androidx paging is released with
|
||||
* bug https://issuetracker.google.com/issues/123834703 merged.
|
||||
* @param <T>
|
||||
*/
|
||||
public class RoomFactoryWrapper<T> extends DataSource.Factory<Integer, T> {
|
||||
final DataSource.Factory<Integer, T> m_wrappedFactory;
|
||||
|
||||
public RoomFactoryWrapper(@NonNull DataSource.Factory<Integer, T> wrappedFactory) {
|
||||
m_wrappedFactory = wrappedFactory;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource<Integer, T> create() {
|
||||
return new DataSourceWrapper<>((PositionalDataSource<T>) m_wrappedFactory.create());
|
||||
}
|
||||
|
||||
public static class DataSourceWrapper<T> extends PositionalDataSource<T> {
|
||||
final PositionalDataSource<T> m_wrappedSource;
|
||||
|
||||
DataSourceWrapper(PositionalDataSource<T> wrappedSource) {
|
||||
m_wrappedSource = wrappedSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
|
||||
m_wrappedSource.addInvalidatedCallback(onInvalidatedCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeInvalidatedCallback(
|
||||
@NonNull InvalidatedCallback onInvalidatedCallback) {
|
||||
m_wrappedSource.removeInvalidatedCallback(onInvalidatedCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate() {
|
||||
m_wrappedSource.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvalid() {
|
||||
return m_wrappedSource.isInvalid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInitial(@NonNull LoadInitialParams params,
|
||||
@NonNull LoadInitialCallback<T> callback) {
|
||||
// Workaround for paging bug: https://issuetracker.google.com/issues/123834703
|
||||
// edit initial load position to start 1/2 load ahead of requested position
|
||||
int newStartPos = params.placeholdersEnabled
|
||||
? params.requestedStartPosition
|
||||
: Math.max(0, params.requestedStartPosition - (params.requestedLoadSize / 2));
|
||||
m_wrappedSource.loadInitial(new LoadInitialParams(
|
||||
newStartPos,
|
||||
params.requestedLoadSize,
|
||||
params.pageSize,
|
||||
params.placeholdersEnabled
|
||||
), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadRange(@NonNull LoadRangeParams params,
|
||||
@NonNull LoadRangeCallback<T> callback) {
|
||||
m_wrappedSource.loadRange(params, callback);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,11 +7,17 @@ import androidx.room.Update;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public interface BaseDao<T> {
|
||||
|
||||
@Insert
|
||||
long insert(T entity); // can't turn return type to Single<Long> while some repositories can't use rxjava properly
|
||||
Single<Long> insert(T entity);
|
||||
|
||||
// only here for compatibility with LocalFeedRepository
|
||||
// which hasn't been written with rxjava usage in mind
|
||||
@Insert
|
||||
long compatInsert(T entity);
|
||||
|
||||
@Insert
|
||||
List<Long> insert(List<T> entities);
|
||||
|
@ -4,10 +4,11 @@ package com.readrops.app.database.dao;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.RoomWarnings;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.pojo.FeedWithFolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -19,7 +20,10 @@ import io.reactivex.Single;
|
||||
public abstract class FeedDao implements BaseDao<Feed> {
|
||||
|
||||
@Query("Select * from Feed Where account_id = :accountId order by name ASC")
|
||||
public abstract List<Feed> getAllFeeds(int accountId);
|
||||
public abstract List<Feed> getFeeds(int accountId);
|
||||
|
||||
@Query("Select * from Feed Order By name ASC")
|
||||
public abstract LiveData<List<Feed>> getAllFeeds();
|
||||
|
||||
@Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end")
|
||||
public abstract boolean feedExists(String feedUrl, int accountId);
|
||||
@ -54,6 +58,7 @@ public abstract class FeedDao implements BaseDao<Feed> {
|
||||
@Query("Update Feed set text_color = :textColor, background_color = :bgColor Where id = :feedId")
|
||||
public abstract void updateColors(int feedId, int textColor, int bgColor);
|
||||
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
@Query("Select Feed.name as feed_name, Feed.id as feed_id, Folder.name as folder_name, Folder.id as folder_id, Folder.remoteId as folder_remoteId," +
|
||||
"Feed.description as feed_description, Feed.icon_url as feed_icon_url, Feed.url as feed_url, Feed.folder_id as feed_folder_id" +
|
||||
", Feed.siteUrl as feed_siteUrl, Feed.remoteId as feed_remoteId from Feed Left Join Folder on Feed.folder_id = Folder.id Where Feed.account_id = :accountId Order by Feed.name")
|
||||
|
@ -39,6 +39,9 @@ public abstract class FolderDao implements BaseDao<Folder> {
|
||||
@Query("Delete From Folder Where remoteId in (:ids)")
|
||||
abstract void deleteByIds(List<String> ids);
|
||||
|
||||
@Query("Select * From Folder Where name = :name And account_id = :accountId")
|
||||
public abstract Folder getFolderByName(String name, int accountId);
|
||||
|
||||
/**
|
||||
* Insert, update and delete folders
|
||||
*
|
||||
|
@ -6,6 +6,7 @@ import androidx.paging.DataSource;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.RawQuery;
|
||||
import androidx.room.RoomWarnings;
|
||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
@ -51,6 +52,7 @@ public abstract class ItemDao implements BaseDao<Item> {
|
||||
@Query("Select count(*) From Item Where feed_id = :feedId And read = 0")
|
||||
public abstract int getUnreadCount(int feedId);
|
||||
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
@Query("Select title, Item.description, content, link, pub_date, image_link, author, read, text_color, " +
|
||||
"background_color, read_time, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " +
|
||||
"Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id")
|
||||
@ -64,4 +66,7 @@ public abstract class ItemDao implements BaseDao<Item> {
|
||||
|
||||
@Query("Update Item set read_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)")
|
||||
public abstract void resetReadChanges(int accountId);
|
||||
|
||||
@Query("Update Item set read = :read Where remoteId = :remoteId")
|
||||
public abstract void setReadState(String remoteId, boolean read);
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.activities.ManageFeedsFoldersActivity;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
@ -29,7 +28,7 @@ import java.util.TreeMap;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.activities.ManageFeedsFoldersActivity.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
public class EditFeedDialogFragment extends DialogFragment implements AdapterView.OnItemSelectedListener {
|
||||
|
||||
@ -63,7 +62,7 @@ public class EditFeedDialogFragment extends DialogFragment implements AdapterVie
|
||||
viewModel = ViewModelProviders.of(getActivity()).get(ManageFeedsFoldersViewModel.class);
|
||||
|
||||
feedWithFolder = getArguments().getParcelable("feedWithFolder");
|
||||
account = getArguments().getParcelable(ManageFeedsFoldersActivity.ACCOUNT);
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
viewModel.setAccount(account);
|
||||
|
||||
|
@ -9,10 +9,10 @@ import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.activities.ManageFeedsFoldersActivity.ACCOUNT
|
||||
import com.readrops.app.database.entities.account.Account
|
||||
import com.readrops.app.database.pojo.FeedWithFolder
|
||||
import com.readrops.app.databinding.FeedOptionsLayoutBinding
|
||||
import com.readrops.app.utils.ReadropsKeys.ACCOUNT
|
||||
|
||||
class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
@ -21,7 +21,7 @@ class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
|
||||
private lateinit var binding: FeedOptionsLayoutBinding
|
||||
|
||||
companion object {
|
||||
val FEED_KEY = "FEED_KEY"
|
||||
const val FEED_KEY = "FEED_KEY"
|
||||
|
||||
fun newInstance(feedWithFolder: FeedWithFolder, account: Account): FeedOptionsDialogFragment {
|
||||
val bundle = Bundle()
|
||||
|
@ -20,7 +20,6 @@ import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.pojo.FeedWithFolder;
|
||||
import com.readrops.app.databinding.FragmentFeedsBinding;
|
||||
import com.readrops.app.fragments.settings.AccountSettingsFragment;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
|
||||
@ -29,7 +28,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableCompletableObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.activities.ManageFeedsFoldersActivity.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
|
||||
public class FeedsFragment extends Fragment {
|
||||
@ -48,7 +47,7 @@ public class FeedsFragment extends Fragment {
|
||||
FeedsFragment fragment = new FeedsFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putParcelable(AccountSettingsFragment.ACCOUNT, account);
|
||||
args.putParcelable(ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
|
@ -16,7 +16,7 @@ class FolderOptionsDialogFragment : BottomSheetDialogFragment() {
|
||||
private lateinit var foldersOptionsLayoutBinding: FolderOptionsLayoutBinding
|
||||
|
||||
companion object {
|
||||
val FOLDER_KEY = "FOLDER_KEY"
|
||||
const val FOLDER_KEY = "FOLDER_KEY"
|
||||
|
||||
fun newInstance(folder: Folder): FolderOptionsDialogFragment {
|
||||
val args = Bundle()
|
||||
|
@ -16,7 +16,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.activities.ManageFeedsFoldersActivity;
|
||||
import com.readrops.app.adapters.FoldersAdapter;
|
||||
import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
@ -30,6 +29,8 @@ import com.readrops.readropslibrary.utils.UnknownFormatException;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
public class FoldersFragment extends Fragment {
|
||||
|
||||
private FoldersAdapter adapter;
|
||||
@ -46,7 +47,7 @@ public class FoldersFragment extends Fragment {
|
||||
FoldersFragment fragment = new FoldersFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ManageFeedsFoldersActivity.ACCOUNT, account);
|
||||
args.putParcelable(ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
@ -56,7 +57,7 @@ public class FoldersFragment extends Fragment {
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
account = getArguments().getParcelable(ManageFeedsFoldersActivity.ACCOUNT);
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
if (account.getLogin() == null)
|
||||
account.setLogin(SharedPreferencesManager.readString(getContext(), account.getLoginKey()));
|
||||
|
@ -1,11 +1,22 @@
|
||||
package com.readrops.app.fragments.settings;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.preference.Preference;
|
||||
@ -13,21 +24,39 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.ReadropsApp;
|
||||
import com.readrops.app.activities.AddAccountActivity;
|
||||
import com.readrops.app.activities.ManageFeedsFoldersActivity;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.entities.account.AccountType;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.utils.matchers.OPMLMatcher;
|
||||
import com.readrops.app.viewmodels.AccountViewModel;
|
||||
import com.readrops.readropslibrary.opml.OPMLParser;
|
||||
import com.readrops.readropslibrary.opml.model.OPML;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableCompletableObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
|
||||
|
||||
/**
|
||||
* A simple {@link Fragment} subclass.
|
||||
*/
|
||||
public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
public static final String ACCOUNT = "ACCOUNT";
|
||||
private static final String TAG = AccountSettingsFragment.class.getSimpleName();
|
||||
|
||||
public static final int OPEN_OPML_FILE_REQUEST = 1;
|
||||
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
|
||||
|
||||
private Account account;
|
||||
private AccountViewModel viewModel;
|
||||
@ -50,9 +79,18 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.acount_preferences);
|
||||
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
Preference feedsFoldersPref = findPreference("feeds_folders_key");
|
||||
Preference credentialsPref = findPreference("credentials_key");
|
||||
Preference deleteAccountPref = findPreference("delete_account_key");
|
||||
Preference opmlPref = findPreference("opml_import_export");
|
||||
|
||||
if (account.is(AccountType.LOCAL))
|
||||
credentialsPref.setVisible(false);
|
||||
|
||||
if (!account.is(AccountType.LOCAL))
|
||||
opmlPref.setVisible(false);
|
||||
|
||||
feedsFoldersPref.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(getContext(), ManageFeedsFoldersActivity.class);
|
||||
@ -65,7 +103,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
credentialsPref.setOnPreferenceClickListener(preference -> {
|
||||
if (!account.isLocal()) {
|
||||
Intent intent = new Intent(getContext(), AddAccountActivity.class);
|
||||
intent.putExtra(AddAccountActivity.EDIT_ACCOUNT, account);
|
||||
intent.putExtra(EDIT_ACCOUNT, account);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@ -76,6 +114,23 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
deleteAccount();
|
||||
return true;
|
||||
});
|
||||
|
||||
opmlPref.setOnPreferenceClickListener(preference -> {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.items(R.array.opml_import_export)
|
||||
.itemsCallback(((dialog, itemView, position, text) -> {
|
||||
if (position == 0) {
|
||||
openOPMLFile();
|
||||
} else {
|
||||
if (isExternalStoragePermissionGranted())
|
||||
exportAsOPMLFile();
|
||||
else
|
||||
requestExternalStoragePermission();
|
||||
}
|
||||
}))
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -83,7 +138,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
viewModel.setAccount(account);
|
||||
}
|
||||
|
||||
private void deleteAccount() {
|
||||
@ -102,9 +157,162 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Utils.showSnackbar(getView(), e.getMessage());
|
||||
}
|
||||
})))
|
||||
.show();
|
||||
}
|
||||
|
||||
// region opml import
|
||||
|
||||
private void openOPMLFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("application/*");
|
||||
|
||||
startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getData();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.opml_processing)
|
||||
.content(R.string.operation_takes_time)
|
||||
.progress(true, 100)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
parseOPMLFile(uri, dialog);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
|
||||
viewModel.parseOPMLFile(uri)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
dialog.dismiss();
|
||||
|
||||
displayErrorMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayErrorMessage() {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.processing_file_failed)
|
||||
.neutralText(R.string.cancel)
|
||||
.iconRes(R.drawable.ic_error)
|
||||
.show();
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region opml export
|
||||
|
||||
private void exportAsOPMLFile() {
|
||||
try {
|
||||
String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
|
||||
File file = new File(filePath, "subscriptions.opml");
|
||||
|
||||
final OutputStream outputStream = new FileOutputStream(file);
|
||||
|
||||
viewModel.getFoldersWithFeeds()
|
||||
.flatMapCompletable(folderListMap -> {
|
||||
OPML opml = OPMLMatcher.INSTANCE.foldersAndFeedsToOPML(folderListMap, getContext());
|
||||
|
||||
return OPMLParser.write(opml, outputStream);
|
||||
}).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doAfterTerminate(() -> {
|
||||
try {
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Utils.showSnackbar(getView(), e.getMessage());
|
||||
}
|
||||
})
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
displayNotification(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Utils.showSnackbar(getView(), e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Utils.showSnackbar(getView(), e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void displayNotification(File file) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(file.getAbsolutePath()));
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.opml_export))
|
||||
.setContentText(file.getName())
|
||||
.setSmallIcon(R.drawable.ic_readrops)
|
||||
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setDeleteIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setAutoCancel(true)
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat manager = NotificationManagerCompat.from(getContext());
|
||||
manager.notify(2, notification);
|
||||
}
|
||||
|
||||
private boolean isExternalStoragePermissionGranted() {
|
||||
return ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestExternalStoragePermission() {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST) {
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
if (shouldShowRequestPermissionRationale(permissions[0])) {
|
||||
Utils.showSnackBarWithAction(getView(), getString(R.string.external_storage_opml_export),
|
||||
getString(R.string.try_again), v -> requestExternalStoragePermission());
|
||||
} else {
|
||||
Utils.showSnackBarWithAction(getView(), getString(R.string.external_storage_opml_export),
|
||||
getString(R.string.permissions), v -> {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", getContext().getPackageName(), null));
|
||||
getContext().startActivity(intent);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
exportAsOPMLFile();
|
||||
}
|
||||
}
|
||||
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
@ -1,15 +1,58 @@
|
||||
package com.readrops.app.fragments.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.database.Database;
|
||||
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
Preference feedsColorsPreference = findPreference("reload_feeds_colors");
|
||||
Preference themePreference = findPreference("dark_theme");
|
||||
|
||||
AtomicBoolean serviceStarted = new AtomicBoolean(false);
|
||||
feedsColorsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Database database = Database.getInstance(getContext());
|
||||
|
||||
database.feedDao().getAllFeeds().observe(getActivity(), feeds -> {
|
||||
if (!serviceStarted.get()) {
|
||||
Intent intent = new Intent(getContext(), FeedsColorsIntentService.class);
|
||||
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
|
||||
|
||||
getContext().startService(intent);
|
||||
serviceStarted.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
boolean darkTheme = Boolean.parseBoolean(newValue.toString());
|
||||
|
||||
if (darkTheme) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
package com.readrops.app.repositories;
|
||||
|
||||
import android.app.Application;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Patterns;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.palette.graphics.Palette;
|
||||
|
||||
import com.readrops.app.database.Database;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
@ -15,17 +13,20 @@ import com.readrops.app.database.entities.Item;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.entities.account.AccountType;
|
||||
import com.readrops.app.utils.FeedInsertionResult;
|
||||
import com.readrops.app.utils.HtmlParser;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.utils.feedscolors.FeedColorsKt;
|
||||
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
|
||||
public abstract class ARepository<T> {
|
||||
|
||||
@ -39,14 +40,48 @@ public abstract class ARepository<T> {
|
||||
this.application = application;
|
||||
this.database = Database.getInstance(application);
|
||||
this.account = account;
|
||||
|
||||
api = createAPI();
|
||||
}
|
||||
|
||||
protected abstract T createAPI();
|
||||
|
||||
public abstract Single<Boolean> login(Account account, boolean insert);
|
||||
|
||||
public abstract Observable<Feed> sync(List<Feed> feeds);
|
||||
|
||||
public abstract Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results);
|
||||
|
||||
public Completable insertOPMLFoldersAndFeeds(Map<Folder, List<Feed>> foldersAndFeeds) {
|
||||
List<Completable> completableList = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<Folder, List<Feed>> entry : foldersAndFeeds.entrySet()) {
|
||||
Folder folder = entry.getKey();
|
||||
folder.setAccountId(account.getId());
|
||||
|
||||
Completable completable = Single.<Integer>create(emitter -> {
|
||||
Folder dbFolder = database.folderDao().getFolderByName(folder.getName(), account.getId());
|
||||
|
||||
if (dbFolder != null)
|
||||
emitter.onSuccess(dbFolder.getId());
|
||||
else
|
||||
emitter.onSuccess((int) database.folderDao().compatInsert(folder));
|
||||
}).flatMap(folderId -> {
|
||||
List<Feed> feeds = entry.getValue();
|
||||
for (Feed feed : feeds) {
|
||||
feed.setFolderId(folderId);
|
||||
}
|
||||
|
||||
List<ParsingResult> parsingResults = ParsingResult.toParsingResults(feeds);
|
||||
return addFeeds(parsingResults);
|
||||
}).flatMapCompletable(feedInsertionResults -> Completable.complete());
|
||||
|
||||
completableList.add(completable);
|
||||
}
|
||||
|
||||
return Completable.concat(completableList);
|
||||
}
|
||||
|
||||
public Completable updateFeed(Feed feed) {
|
||||
return Completable.create(emitter -> {
|
||||
database.feedDao().updateFeedFields(feed.getId(), feed.getName(), feed.getUrl(), feed.getFolderId());
|
||||
@ -58,11 +93,8 @@ public abstract class ARepository<T> {
|
||||
return database.feedDao().delete(feed);
|
||||
}
|
||||
|
||||
public Completable addFolder(Folder folder) {
|
||||
return Completable.create(emitter -> {
|
||||
database.folderDao().insert(folder);
|
||||
emitter.onComplete();
|
||||
});
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return database.folderDao().insert(folder);
|
||||
}
|
||||
|
||||
public Completable updateFolder(Folder folder) {
|
||||
@ -75,10 +107,9 @@ public abstract class ARepository<T> {
|
||||
|
||||
public Completable setItemReadState(Item item, boolean read) {
|
||||
return database.itemDao().setReadState(item.getId(), read ? 1 : 0, !item.isReadChanged() ? 1 : 0);
|
||||
|
||||
}
|
||||
|
||||
public Completable setAllItemsReadState(Boolean read) {
|
||||
public Completable setAllItemsReadState(boolean read) {
|
||||
return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId());
|
||||
}
|
||||
|
||||
@ -90,48 +121,46 @@ public abstract class ARepository<T> {
|
||||
return database.feedDao().getFeedCount(accountId);
|
||||
}
|
||||
|
||||
protected void setFaviconUtils(List<Feed> feeds) {
|
||||
Observable.<Feed>create(emitter -> {
|
||||
for (Feed feed : feeds) {
|
||||
setFavIconUtils(feed);
|
||||
emitter.onNext(feed);
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return Single.create(emitter -> {
|
||||
List<Folder> folders = database.folderDao().getFolders(account.getId());
|
||||
Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Folder::compareTo);
|
||||
|
||||
for (Folder folder : folders) {
|
||||
List<Feed> feeds = database.feedDao().getFeedsByFolder(folder.getId());
|
||||
|
||||
for (Feed feed : feeds) {
|
||||
int unreadCount = database.itemDao().getUnreadCount(feed.getId());
|
||||
feed.setUnreadCount(unreadCount);
|
||||
}
|
||||
|
||||
foldersWithFeeds.put(folder, feeds);
|
||||
}
|
||||
}).subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.doOnNext(feed1 -> database.feedDao().updateColors(feed1.getId(),
|
||||
feed1.getTextColor(), feed1.getBackgroundColor()))
|
||||
.subscribe();
|
||||
|
||||
Folder noFolder = new Folder("no folder");
|
||||
|
||||
List<Feed> feedsWithoutFolder = database.feedDao().getFeedsWithoutFolder(account.getId());
|
||||
for (Feed feed : feedsWithoutFolder) {
|
||||
feed.setUnreadCount(database.itemDao().getUnreadCount(feed.getId()));
|
||||
}
|
||||
|
||||
foldersWithFeeds.put(noFolder, feedsWithoutFolder);
|
||||
|
||||
emitter.onSuccess(foldersWithFeeds);
|
||||
});
|
||||
}
|
||||
|
||||
protected void setFavIconUtils(Feed feed) throws IOException {
|
||||
String favUrl;
|
||||
|
||||
if (feed.getIconUrl() != null)
|
||||
favUrl = feed.getIconUrl();
|
||||
else
|
||||
favUrl = HtmlParser.getFaviconLink(feed.getSiteUrl());
|
||||
|
||||
if (favUrl != null && Patterns.WEB_URL.matcher(favUrl).matches()) {
|
||||
feed.setIconUrl(favUrl);
|
||||
setFeedColors(favUrl, feed);
|
||||
}
|
||||
protected void setFeedColors(Feed feed) {
|
||||
FeedColorsKt.setFeedColors(feed);
|
||||
database.feedDao().updateColors(feed.getId(),
|
||||
feed.getTextColor(), feed.getBackgroundColor());
|
||||
}
|
||||
|
||||
protected void setFeedColors(String favUrl, Feed feed) {
|
||||
Bitmap favicon = Utils.getImageFromUrl(favUrl);
|
||||
protected void setFeedsColors(List<Feed> feeds) {
|
||||
Intent intent = new Intent(application, FeedsColorsIntentService.class);
|
||||
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
|
||||
|
||||
if (favicon != null) {
|
||||
Palette palette = Palette.from(favicon).generate();
|
||||
|
||||
if (palette.getDominantSwatch() != null) {
|
||||
feed.setTextColor(palette.getDominantSwatch().getRgb());
|
||||
}
|
||||
|
||||
if (palette.getMutedSwatch() != null) {
|
||||
feed.setBackgroundColor(palette.getMutedSwatch().getRgb());
|
||||
}
|
||||
|
||||
}
|
||||
application.startService(intent);
|
||||
}
|
||||
|
||||
public static ARepository repositoryFactory(Account account, AccountType accountType, Application application) throws Exception {
|
||||
|
@ -2,6 +2,7 @@ package com.readrops.app.repositories;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
import android.util.TimingLogger;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -11,8 +12,8 @@ import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.Item;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.utils.FeedInsertionResult;
|
||||
import com.readrops.app.utils.FeedMatcher;
|
||||
import com.readrops.app.utils.ItemMatcher;
|
||||
import com.readrops.app.utils.matchers.FeedMatcher;
|
||||
import com.readrops.app.utils.matchers.ItemMatcher;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.readropslibrary.services.SyncType;
|
||||
@ -35,12 +36,17 @@ import io.reactivex.Single;
|
||||
public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
|
||||
private static final String TAG = FreshRSSRepository.class.getSimpleName();
|
||||
|
||||
|
||||
public FreshRSSRepository(@NonNull Application application, @Nullable Account account) {
|
||||
super(application, account);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FreshRSSAPI createAPI() {
|
||||
if (account != null)
|
||||
api = new FreshRSSAPI(account.toCredentials());
|
||||
return new FreshRSSAPI(account.toCredentials());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -65,8 +71,14 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
.flatMap(userInfo -> {
|
||||
account.setDisplayedName(userInfo.getUserName());
|
||||
|
||||
if (insert)
|
||||
account.setId((int) database.accountDao().insert(account));
|
||||
if (insert) {
|
||||
return database.accountDao().insert(account)
|
||||
.flatMap(id -> {
|
||||
account.setId(id.intValue());
|
||||
|
||||
return Single.just(true);
|
||||
});
|
||||
}
|
||||
|
||||
return Single.just(true);
|
||||
});
|
||||
@ -83,6 +95,8 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
} else
|
||||
syncType = SyncType.INITIAL_SYNC;
|
||||
|
||||
TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer");
|
||||
|
||||
return Single.<FreshRSSSyncData>create(emitter -> {
|
||||
syncData.setReadItemsIds(database.itemDao().getReadChanges(account.getId()));
|
||||
syncData.setUnreadItemsIds(database.itemDao().getUnreadChanges(account.getId()));
|
||||
@ -90,14 +104,21 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
emitter.onSuccess(syncData);
|
||||
}).flatMap(syncData1 -> api.sync(syncType, syncData1, account.getWriteToken()))
|
||||
.flatMapObservable(syncResult -> {
|
||||
insertFolders(syncResult.getFolders(), account);
|
||||
insertFeeds(syncResult.getFeeds(), account);
|
||||
insertItems(syncResult.getItems(), account, syncType == SyncType.INITIAL_SYNC);
|
||||
logger.addSplit("server queries");
|
||||
|
||||
insertFolders(syncResult.getFolders());
|
||||
logger.addSplit("folders insertion");
|
||||
insertFeeds(syncResult.getFeeds());
|
||||
logger.addSplit("feeds insertion");
|
||||
|
||||
insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC);
|
||||
logger.addSplit("items insertion");
|
||||
|
||||
account.setLastModified(syncResult.getLastUpdated());
|
||||
database.accountDao().updateLastModified(account.getId(), syncResult.getLastUpdated());
|
||||
|
||||
database.itemDao().resetReadChanges(account.getId());
|
||||
logger.dumpToLog();
|
||||
|
||||
return Observable.empty();
|
||||
});
|
||||
@ -149,8 +170,9 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable addFolder(Folder folder) {
|
||||
return api.createFolder(account.getWriteToken(), folder.getName());
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return api.createFolder(account.getWriteToken(), folder.getName())
|
||||
.andThen(super.addFolder(folder));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -169,7 +191,7 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
.andThen(super.deleteFolder(folder));
|
||||
}
|
||||
|
||||
private List<Feed> insertFeeds(List<FreshRSSFeed> freshRSSFeeds, Account account) {
|
||||
private void insertFeeds(List<FreshRSSFeed> freshRSSFeeds) {
|
||||
List<Feed> feeds = new ArrayList<>();
|
||||
|
||||
for (FreshRSSFeed freshRSSFeed : freshRSSFeeds) {
|
||||
@ -178,16 +200,13 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
|
||||
List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(feeds, account);
|
||||
|
||||
List<Feed> insertedFeeds = new ArrayList<>();
|
||||
if (!insertedFeedsIds.isEmpty()) {
|
||||
insertedFeeds.addAll(database.feedDao().selectFromIdList(insertedFeedsIds));
|
||||
setFaviconUtils(insertedFeeds);
|
||||
setFeedsColors(database.feedDao().selectFromIdList(insertedFeedsIds));
|
||||
}
|
||||
|
||||
return insertedFeeds;
|
||||
}
|
||||
|
||||
private void insertFolders(List<FreshRSSFolder> freshRSSFolders, Account account) {
|
||||
private void insertFolders(List<FreshRSSFolder> freshRSSFolders) {
|
||||
List<Folder> folders = new ArrayList<>();
|
||||
|
||||
for (FreshRSSFolder freshRSSFolder : freshRSSFolders) {
|
||||
@ -205,15 +224,15 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
|
||||
database.folderDao().foldersUpsert(folders, account);
|
||||
}
|
||||
|
||||
private void insertItems(List<FreshRSSItem> items, Account account, boolean initialSync) {
|
||||
private void insertItems(List<FreshRSSItem> items, boolean initialSync) {
|
||||
List<Item> newItems = new ArrayList<>();
|
||||
|
||||
for (FreshRSSItem freshRSSItem : items) {
|
||||
int feedId = database.feedDao().getFeedIdByRemoteId(String.valueOf(freshRSSItem.getOrigin().getStreamId()), account.getId());
|
||||
|
||||
if (!initialSync && feedId > 0) {
|
||||
if (database.itemDao().remoteItemExists(freshRSSItem.getId(), feedId))
|
||||
break;
|
||||
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(freshRSSItem.getId(), feedId)) {
|
||||
database.itemDao().setReadState(freshRSSItem.getId(), freshRSSItem.isRead());
|
||||
break;
|
||||
}
|
||||
|
||||
Item item = ItemMatcher.freshRSSItemtoItem(freshRSSItem, feedId);
|
||||
|
@ -10,9 +10,9 @@ import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.Item;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.utils.FeedInsertionResult;
|
||||
import com.readrops.app.utils.FeedMatcher;
|
||||
import com.readrops.app.utils.matchers.FeedMatcher;
|
||||
import com.readrops.app.utils.HtmlParser;
|
||||
import com.readrops.app.utils.ItemMatcher;
|
||||
import com.readrops.app.utils.matchers.ItemMatcher;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
@ -46,6 +46,11 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
super(application, account);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void createAPI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<Boolean> login(Account account, boolean insert) {
|
||||
return null;
|
||||
@ -57,7 +62,7 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
List<Feed> feedList;
|
||||
|
||||
if (feeds == null || feeds.size() == 0)
|
||||
feedList = database.feedDao().getAllFeeds(account.getId());
|
||||
feedList = database.feedDao().getFeeds(account.getId());
|
||||
else
|
||||
feedList = new ArrayList<>(feeds);
|
||||
|
||||
@ -117,7 +122,7 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
RSSQueryResult queryResult = rssNet.queryUrl(parsingResult.getUrl(), new HashMap<>());
|
||||
|
||||
if (queryResult != null && queryResult.getException() == null) {
|
||||
Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType());
|
||||
Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType(), parsingResult);
|
||||
if (feed != null) {
|
||||
insertionResult.setFeed(feed);
|
||||
insertionResult.setParsingResult(parsingResult);
|
||||
@ -128,8 +133,6 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
insertionResult.setInsertionError(getErrorFromException(queryResult.getException()));
|
||||
|
||||
insertionResults.add(insertionResult);
|
||||
} else {
|
||||
// error 304
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (e instanceof IOException)
|
||||
@ -147,8 +150,8 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
}
|
||||
|
||||
private void insertNewItems(AFeed feed, RSSQuery.RSSType type) throws ParseException {
|
||||
Feed dbFeed = null;
|
||||
List<Item> items = null;
|
||||
Feed dbFeed;
|
||||
List<Item> items;
|
||||
|
||||
switch (type) {
|
||||
case RSS_2:
|
||||
@ -163,6 +166,8 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
dbFeed = database.feedDao().getFeedByUrl(((JSONFeed) feed).getFeedUrl(), account.getId());
|
||||
items = ItemMatcher.itemsFromJSON(((JSONFeed) feed).getItems(), dbFeed);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown RSS type");
|
||||
}
|
||||
|
||||
database.feedDao().updateHeaders(dbFeed.getEtag(), dbFeed.getLastModified(), dbFeed.getId());
|
||||
@ -175,8 +180,8 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
insertItems(items, dbFeed);
|
||||
}
|
||||
|
||||
private Feed insertFeed(AFeed feed, RSSQuery.RSSType type) throws IOException {
|
||||
Feed dbFeed = null;
|
||||
private Feed insertFeed(AFeed feed, RSSQuery.RSSType type, ParsingResult parsingResult) {
|
||||
Feed dbFeed;
|
||||
switch (type) {
|
||||
case RSS_2:
|
||||
dbFeed = FeedMatcher.feedFromRSS((RSSFeed) feed);
|
||||
@ -187,19 +192,23 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
case RSS_JSON:
|
||||
dbFeed = FeedMatcher.feedFromJSON((JSONFeed) feed);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown RSS type");
|
||||
}
|
||||
|
||||
dbFeed.setFolderId(parsingResult.getFolderId());
|
||||
|
||||
if (database.feedDao().feedExists(dbFeed.getUrl(), account.getId()))
|
||||
return null; // feed already inserted
|
||||
|
||||
setFavIconUtils(dbFeed);
|
||||
setFeedColors(dbFeed);
|
||||
dbFeed.setAccountId(account.getId());
|
||||
|
||||
// we need empty headers to query the feed just after, without any 304 result
|
||||
dbFeed.setEtag(null);
|
||||
dbFeed.setLastModified(null);
|
||||
|
||||
dbFeed.setId((int) (database.feedDao().insert(dbFeed)));
|
||||
dbFeed.setId((int) (database.feedDao().compatInsert(dbFeed)));
|
||||
return dbFeed;
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,8 @@ import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.Item;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.utils.FeedInsertionResult;
|
||||
import com.readrops.app.utils.FeedMatcher;
|
||||
import com.readrops.app.utils.ItemMatcher;
|
||||
import com.readrops.app.utils.matchers.FeedMatcher;
|
||||
import com.readrops.app.utils.matchers.ItemMatcher;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.readropslibrary.services.SyncType;
|
||||
@ -46,14 +46,19 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
|
||||
public NextNewsRepository(@NonNull Application application, @Nullable Account account) {
|
||||
super(application, account);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NextNewsAPI createAPI() {
|
||||
if (account != null)
|
||||
api = new NextNewsAPI(account.toCredentials());
|
||||
return new NextNewsAPI(account.toCredentials());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<Boolean> login(Account account, boolean insert) {
|
||||
return Single.create(emitter -> {
|
||||
return Single.<NextNewsUser>create(emitter -> {
|
||||
if (api == null)
|
||||
api = new NextNewsAPI(account.toCredentials());
|
||||
else
|
||||
@ -61,15 +66,23 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
|
||||
NextNewsUser user = api.login();
|
||||
|
||||
emitter.onSuccess(user);
|
||||
}).flatMap(user -> {
|
||||
if (user != null) {
|
||||
account.setDisplayedName(user.getDisplayName());
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
if (insert)
|
||||
account.setId((int) database.accountDao().insert(account));
|
||||
emitter.onSuccess(true);
|
||||
if (insert) {
|
||||
return database.accountDao().insert(account)
|
||||
.flatMap(id -> {
|
||||
account.setId(id.intValue());
|
||||
return Single.just(true);
|
||||
});
|
||||
}
|
||||
|
||||
return Single.just(true);
|
||||
} else
|
||||
emitter.onSuccess(false);
|
||||
return Single.just(false);
|
||||
});
|
||||
}
|
||||
|
||||
@ -201,8 +214,8 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable addFolder(Folder folder) {
|
||||
return Completable.create(emitter -> {
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return Single.<Folder>create(emitter -> {
|
||||
try {
|
||||
int folderRemoteId = folder.getRemoteId() == null ? 0 : Integer.parseInt(folder.getRemoteId());
|
||||
NextNewsFolders folders = api.createFolder(new NextNewsFolder(folderRemoteId, folder.getName()));
|
||||
@ -212,15 +225,13 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
|
||||
folder.setName(nextNewsFolder.getName());
|
||||
folder.setRemoteId(String.valueOf(nextNewsFolder.getId()));
|
||||
database.folderDao().insert(folder);
|
||||
emitter.onSuccess(folder);
|
||||
} else
|
||||
emitter.onError(new Exception("Unknown error"));
|
||||
} catch (Exception e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
|
||||
emitter.onComplete();
|
||||
});
|
||||
}).flatMap(folder1 -> database.folderDao().insert(folder));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -269,7 +280,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
List<Feed> insertedFeeds = new ArrayList<>();
|
||||
if (!insertedFeedsIds.isEmpty()) {
|
||||
insertedFeeds.addAll(database.feedDao().selectFromIdList(insertedFeedsIds));
|
||||
setFaviconUtils(insertedFeeds);
|
||||
setFeedsColors(insertedFeeds);
|
||||
}
|
||||
|
||||
return insertedFeeds;
|
||||
@ -295,9 +306,9 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
for (NextNewsItem nextNewsItem : items) {
|
||||
int feedId = database.feedDao().getFeedIdByRemoteId(String.valueOf(nextNewsItem.getFeedId()), account.getId());
|
||||
|
||||
if (!initialSync && feedId > 0) {
|
||||
if (database.itemDao().remoteItemExists(String.valueOf(nextNewsItem.getId()), feedId))
|
||||
break;
|
||||
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(nextNewsItem.getId()), feedId)) {
|
||||
database.itemDao().setReadState(String.valueOf(nextNewsItem.getId()), !nextNewsItem.isUnread());
|
||||
break;
|
||||
}
|
||||
|
||||
Item item = ItemMatcher.nextNewsItemToItem(nextNewsItem, feedId);
|
||||
@ -306,7 +317,9 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
|
||||
newItems.add(item);
|
||||
}
|
||||
|
||||
Collections.sort(newItems, Item::compareTo);
|
||||
database.itemDao().insert(newItems);
|
||||
if (!newItems.isEmpty()) {
|
||||
Collections.sort(newItems, Item::compareTo);
|
||||
database.itemDao().insert(newItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,11 @@ import androidx.annotation.NonNull;
|
||||
import com.mikepenz.fastadapter.FastAdapter;
|
||||
import com.mikepenz.fastadapter.items.AbstractItem;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.ParsingResultViewHolder> {
|
||||
@ -22,6 +24,8 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
|
||||
|
||||
private boolean checked;
|
||||
|
||||
private Integer folderId;
|
||||
|
||||
public ParsingResult(String url, String label) {
|
||||
this.url = url;
|
||||
this.label = label;
|
||||
@ -39,6 +43,18 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
|
||||
return label;
|
||||
}
|
||||
|
||||
public static List<ParsingResult> toParsingResults(List<Feed> feeds) {
|
||||
List<ParsingResult> parsingResults = new ArrayList<>();
|
||||
|
||||
for (Feed feed : feeds) {
|
||||
ParsingResult parsingResult = new ParsingResult(feed.getUrl(), null);
|
||||
parsingResult.setFolderId(feed.getFolderId());
|
||||
parsingResults.add(parsingResult);
|
||||
}
|
||||
|
||||
return parsingResults;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
@ -51,6 +67,14 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
|
||||
return checked;
|
||||
}
|
||||
|
||||
public Integer getFolderId() {
|
||||
return folderId;
|
||||
}
|
||||
|
||||
public void setFolderId(Integer folderId) {
|
||||
this.folderId = folderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelectable() {
|
||||
return true;
|
||||
@ -116,7 +140,18 @@ public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.Par
|
||||
public void unbindView(@NotNull ParsingResult item) {
|
||||
// not useful
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null)
|
||||
return false;
|
||||
else if (!(o instanceof ParsingResult))
|
||||
return false;
|
||||
else {
|
||||
ParsingResult parsingResult = (ParsingResult) o;
|
||||
|
||||
return parsingResult.getUrl().equals(this.getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
package com.readrops.app.utils;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
|
||||
public class ReadropsApp extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
RxJavaPlugins.setErrorHandler(e -> { });
|
||||
Stetho.initializeWithDefaults(this);
|
||||
}
|
||||
}
|
22
app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt
Normal file
22
app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package com.readrops.app.utils
|
||||
|
||||
object ReadropsKeys {
|
||||
|
||||
const val ACCOUNT = "ACCOUNT_KEY"
|
||||
const val ACCOUNT_TYPE = "ACCOUNT_TYPE_KEY"
|
||||
const val EDIT_ACCOUNT = "EDIT_ACCOUNT"
|
||||
|
||||
const val FROM_MAIN_ACTIVITY = "FROM_MAIN_ACTIVITY_KEY"
|
||||
|
||||
const val ITEM_ID = "ITEM_ID_KEY"
|
||||
const val IMAGE_URL = "IMAGE_URL_KEY"
|
||||
|
||||
const val SYNCING = "SYNCING_KEY"
|
||||
|
||||
const val SETTINGS = "SETTINGS_KEY"
|
||||
|
||||
const val WEB_URL = "WEB_URL_KEY"
|
||||
const val ACTION_BAR_COLOR = "ACTION_BAR_COLOR_KEY"
|
||||
|
||||
const val FEEDS = "FEEDS"
|
||||
}
|
@ -2,31 +2,58 @@ package com.readrops.app.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Base64;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.database.pojo.ItemWithFeed;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.parser.Parser;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
public class ReadropsWebView extends WebView {
|
||||
|
||||
private ItemWithFeed itemWithFeed;
|
||||
|
||||
@ColorInt
|
||||
private int textColor;
|
||||
@ColorInt
|
||||
private int backgroundColor;
|
||||
|
||||
public ReadropsWebView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
getColors(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setItem(ItemWithFeed itemWithFeed) {
|
||||
this.itemWithFeed = itemWithFeed;
|
||||
loadData(getText(), "text/html; charset=utf-8", "UTF-8");
|
||||
|
||||
String text = getText();
|
||||
String base64Content = null;
|
||||
|
||||
if (text != null)
|
||||
base64Content = Base64.encodeToString(text.getBytes(), Base64.NO_PADDING);
|
||||
|
||||
loadData(base64Content, "text/html; charset=utf-8", "base64");
|
||||
}
|
||||
|
||||
private void getColors(Context context, AttributeSet attrs) {
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ReadropsWebView);
|
||||
textColor = typedArray.getColor(R.styleable.ReadropsWebView_textColor, 0);
|
||||
backgroundColor = typedArray.getColor(R.styleable.ReadropsWebView_backgroundColor, 0);
|
||||
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@ -40,12 +67,13 @@ public class ReadropsWebView extends WebView {
|
||||
}
|
||||
|
||||
setVerticalScrollBarEnabled(false);
|
||||
setBackgroundColor(getResources().getColor(R.color.colorBackground));
|
||||
setBackgroundColor(backgroundColor);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getText() {
|
||||
if (itemWithFeed.getItem().getText() != null) {
|
||||
Document document = Jsoup.parse(itemWithFeed.getItem().getText(), itemWithFeed.getWebsiteUrl());
|
||||
Document document = Jsoup.parse(Parser.unescapeEntities(itemWithFeed.getItem().getText(), false), itemWithFeed.getWebsiteUrl());
|
||||
|
||||
formatDocument(document);
|
||||
|
||||
@ -53,6 +81,8 @@ public class ReadropsWebView extends WebView {
|
||||
return getContext().getString(R.string.webview_html_template,
|
||||
Utils.getCssColor(itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() :
|
||||
color),
|
||||
Utils.getCssColor(this.textColor),
|
||||
Utils.getCssColor(backgroundColor),
|
||||
document.body().html());
|
||||
|
||||
} else
|
||||
|
@ -50,15 +50,17 @@ public final class SharedPreferencesManager {
|
||||
|
||||
public enum SharedPrefKey {
|
||||
SHOW_READ_ARTICLES("show_read_articles", false),
|
||||
ITEMS_TO_PARSE_MAX_NB("items_to_parse_max_nb", "20");
|
||||
ITEMS_TO_PARSE_MAX_NB("items_to_parse_max_nb", "20"),
|
||||
OPEN_ITEMS_IN("open_items_in", "0"),
|
||||
DARK_THEME("dark_theme", "false");
|
||||
|
||||
@NonNull
|
||||
private String key;
|
||||
@NonNull
|
||||
private Object defaultValue;
|
||||
|
||||
public Boolean getBooleanDefaultValue() {
|
||||
return (Boolean) defaultValue;
|
||||
public boolean getBooleanDefaultValue() {
|
||||
return Boolean.valueOf(defaultValue.toString());
|
||||
}
|
||||
|
||||
public String getStringDefaultValue() {
|
||||
@ -66,7 +68,7 @@ public final class SharedPreferencesManager {
|
||||
}
|
||||
|
||||
public int getIntDefaultValue() {
|
||||
return (int) defaultValue;
|
||||
return Integer.parseInt(defaultValue.toString());
|
||||
}
|
||||
|
||||
SharedPrefKey(@NonNull String key, @NonNull Object defaultValue) {
|
||||
|
@ -15,6 +15,8 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -95,4 +97,13 @@ public final class Utils {
|
||||
Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove html tags and trim the text
|
||||
* @param text string to clean
|
||||
* @return cleaned text
|
||||
*/
|
||||
public static String cleanText(String text) {
|
||||
return Jsoup.parse(text).text().trim();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
package com.readrops.app.utils.feedscolors
|
||||
|
||||
import androidx.palette.graphics.Palette
|
||||
import com.readrops.app.database.entities.Feed
|
||||
import com.readrops.app.utils.HtmlParser
|
||||
import com.readrops.app.utils.Utils
|
||||
|
||||
fun setFeedColors(feed: Feed) {
|
||||
getFaviconLink(feed)
|
||||
|
||||
if (feed.iconUrl != null) {
|
||||
val bitmap = Utils.getImageFromUrl(feed.iconUrl) ?: return
|
||||
val palette = Palette.from(bitmap).generate()
|
||||
|
||||
val dominantSwatch = palette.dominantSwatch
|
||||
if (dominantSwatch != null)
|
||||
feed.textColor = dominantSwatch.rgb
|
||||
|
||||
val mutedSwatch = palette.mutedSwatch
|
||||
if (mutedSwatch != null)
|
||||
feed.backgroundColor = mutedSwatch.rgb
|
||||
}
|
||||
}
|
||||
|
||||
fun getFaviconLink(feed: Feed) {
|
||||
feed.iconUrl = if (feed.iconUrl != null)
|
||||
feed.iconUrl
|
||||
else
|
||||
HtmlParser.getFaviconLink(feed.siteUrl)
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.readrops.app.utils.feedscolors
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.ReadropsApp
|
||||
import com.readrops.app.database.Database
|
||||
import com.readrops.app.database.entities.Feed
|
||||
import com.readrops.app.utils.ReadropsKeys.FEEDS
|
||||
|
||||
class FeedsColorsIntentService : IntentService("FeedsColorsIntentService") {
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
val feeds: List<Feed> = intent!!.getParcelableArrayListExtra(FEEDS)!!
|
||||
val database = Database.getInstance(this)
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(this, ReadropsApp.FEEDS_COLORS_CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.get_feeds_colors))
|
||||
.setProgress(feeds.size, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_notif)
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
|
||||
var feedsNb = 0
|
||||
feeds.forEach {
|
||||
notificationBuilder.setContentText(it.name)
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||
setFeedColors(it)
|
||||
|
||||
database.feedDao().updateColors(it.id, it.textColor, it.backgroundColor)
|
||||
notificationBuilder.setProgress(feeds.size, ++feedsNb, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package com.readrops.app.utils;
|
||||
package com.readrops.app.utils.matchers;
|
||||
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
|
||||
import com.readrops.readropslibrary.localfeed.json.JSONFeed;
|
||||
import com.readrops.readropslibrary.localfeed.rss.RSSChannel;
|
@ -1,7 +1,9 @@
|
||||
package com.readrops.app.utils;
|
||||
package com.readrops.app.utils.matchers;
|
||||
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.Item;
|
||||
import com.readrops.app.utils.DateUtils;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.readropslibrary.localfeed.atom.ATOMEntry;
|
||||
import com.readrops.readropslibrary.localfeed.json.JSONItem;
|
||||
import com.readrops.readropslibrary.localfeed.rss.RSSEnclosure;
|
||||
@ -13,7 +15,6 @@ import com.readrops.readropslibrary.utils.ParseException;
|
||||
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -58,6 +59,7 @@ public final class ItemMatcher {
|
||||
newItem.setLink(item.getAlternate().get(0).getHref());
|
||||
newItem.setFeedId(feedId);
|
||||
newItem.setRemoteId(item.getId());
|
||||
newItem.setRead(item.isRead());
|
||||
|
||||
return newItem;
|
||||
}
|
||||
@ -72,7 +74,7 @@ public final class ItemMatcher {
|
||||
newItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
|
||||
newItem.setDescription(item.getDescription());
|
||||
newItem.setGuid(item.getGuid());
|
||||
newItem.setTitle(Jsoup.parse(item.getTitle()).text().trim());
|
||||
newItem.setTitle(Utils.cleanText(item.getTitle()));
|
||||
|
||||
try {
|
||||
newItem.setPubDate(DateUtils.stringToLocalDateTime(item.getDate()));
|
||||
@ -117,13 +119,14 @@ public final class ItemMatcher {
|
||||
dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
|
||||
dbItem.setDescription(item.getSummary());
|
||||
dbItem.setGuid(item.getId());
|
||||
dbItem.setTitle(Jsoup.parse(item.getTitle()).text().trim());
|
||||
dbItem.setTitle(Utils.cleanText(item.getTitle()));
|
||||
|
||||
try {
|
||||
dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getUpdated()));
|
||||
} catch (Exception e) {
|
||||
throw new ParseException();
|
||||
}
|
||||
|
||||
dbItem.setLink(item.getUrl());
|
||||
|
||||
dbItem.setFeedId(feed.getId());
|
||||
@ -146,7 +149,7 @@ public final class ItemMatcher {
|
||||
dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
|
||||
dbItem.setDescription(item.getSummary());
|
||||
dbItem.setGuid(item.getId());
|
||||
dbItem.setTitle(Jsoup.parse(item.getTitle()).text().trim());
|
||||
dbItem.setTitle(Utils.cleanText(item.getTitle()));
|
||||
|
||||
try {
|
||||
dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getPubDate()));
|
@ -0,0 +1,59 @@
|
||||
package com.readrops.app.utils.matchers
|
||||
|
||||
import android.content.Context
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.database.entities.Feed
|
||||
import com.readrops.app.database.entities.Folder
|
||||
import com.readrops.readropslibrary.opml.model.Body
|
||||
import com.readrops.readropslibrary.opml.model.Head
|
||||
import com.readrops.readropslibrary.opml.model.OPML
|
||||
import com.readrops.readropslibrary.opml.model.Outline
|
||||
|
||||
object OPMLMatcher {
|
||||
|
||||
fun opmltoFoldersAndFeeds(opml: OPML): Map<Folder, List<Feed>> {
|
||||
val foldersAndFeeds: MutableMap<Folder, List<Feed>> = HashMap()
|
||||
val body = opml.body!!
|
||||
|
||||
body.outlines?.forEach { outline ->
|
||||
val folder = Folder(outline.title)
|
||||
|
||||
val feeds = arrayListOf<Feed>()
|
||||
outline.outlines?.forEach { feedOutline ->
|
||||
val feed = Feed().apply {
|
||||
name = feedOutline.title
|
||||
url = feedOutline.xmlUrl
|
||||
siteUrl = feedOutline.htmlUrl
|
||||
}
|
||||
|
||||
feeds.add(feed)
|
||||
}
|
||||
|
||||
foldersAndFeeds[folder] = feeds
|
||||
}
|
||||
|
||||
return foldersAndFeeds
|
||||
}
|
||||
|
||||
fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder, List<Feed>>, context: Context): OPML {
|
||||
val outlines = arrayListOf<Outline>()
|
||||
for (folderAndFeeds in foldersAndFeeds) {
|
||||
val outline = Outline(folderAndFeeds.key.name)
|
||||
|
||||
val feedOutlines = arrayListOf<Outline>()
|
||||
folderAndFeeds.value.forEach { feed ->
|
||||
val feedOutline = Outline(feed.name, feed.url, feed.siteUrl)
|
||||
|
||||
feedOutlines.add(feedOutline)
|
||||
}
|
||||
|
||||
outline.outlines = feedOutlines
|
||||
outlines.add(outline)
|
||||
}
|
||||
|
||||
val head = Head(context.getString(R.string.subscriptions))
|
||||
val body = Body(outlines)
|
||||
|
||||
return OPML("2.0", head, body)
|
||||
}
|
||||
}
|
@ -1,20 +1,31 @@
|
||||
package com.readrops.app.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
|
||||
import com.readrops.app.database.Database;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.entities.account.AccountType;
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.app.utils.matchers.OPMLMatcher;
|
||||
import com.readrops.readropslibrary.opml.OPMLParser;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public class AccountViewModel extends AndroidViewModel {
|
||||
|
||||
private static final String TAG = AccountViewModel.class.getSimpleName();
|
||||
|
||||
private ARepository repository;
|
||||
private Database database;
|
||||
|
||||
@ -28,15 +39,20 @@ public class AccountViewModel extends AndroidViewModel {
|
||||
repository = ARepository.repositoryFactory(null, accountType, getApplication());
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
try {
|
||||
repository = ARepository.repositoryFactory(account, getApplication());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Single<Boolean> login(Account account, boolean insert) {
|
||||
return repository.login(account, insert);
|
||||
}
|
||||
|
||||
public Single<Long> insert(Account account) {
|
||||
return Single.create(emitter -> {
|
||||
long id = database.accountDao().insert(account);
|
||||
emitter.onSuccess(id);
|
||||
});
|
||||
return database.accountDao().insert(account);
|
||||
}
|
||||
|
||||
public Completable update(Account account) {
|
||||
@ -50,4 +66,17 @@ public class AccountViewModel extends AndroidViewModel {
|
||||
public Single<Integer> getAccountCount() {
|
||||
return database.accountDao().getAccountCount();
|
||||
}
|
||||
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return repository.getFoldersWithFeeds();
|
||||
}
|
||||
|
||||
public Completable parseOPMLFile(Uri uri) {
|
||||
return OPMLParser.read(uri, getApplication())
|
||||
.flatMapCompletable(opml -> {
|
||||
Map<Folder, List<Feed>> foldersAndFeeds = OPMLMatcher.INSTANCE.opmltoFoldersAndFeeds(opml);
|
||||
|
||||
return repository.insertOPMLFoldersAndFeeds(foldersAndFeeds);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,23 @@
|
||||
package com.readrops.app.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.readrops.app.database.Database;
|
||||
import com.readrops.app.database.dao.ItemDao;
|
||||
import com.readrops.app.database.pojo.ItemWithFeed;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class ItemViewModel extends AndroidViewModel {
|
||||
|
||||
private ItemDao itemDao;
|
||||
@ -23,4 +32,19 @@ public class ItemViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
|
||||
public Uri saveImageInCache(Bitmap bitmap) throws IOException {
|
||||
File imagesFolder = new File(getApplication().getCacheDir().getAbsolutePath(), "images");
|
||||
|
||||
if (!imagesFolder.exists())
|
||||
imagesFolder.mkdirs();
|
||||
|
||||
File image = new File(imagesFolder, "shared_image.png");
|
||||
OutputStream stream = new FileOutputStream(image);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
|
||||
|
||||
stream.flush();
|
||||
stream.close();
|
||||
|
||||
return FileProvider.getUriForFile(getApplication(), getApplication().getPackageName(), image);
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,10 @@ import androidx.paging.PagedList;
|
||||
import com.readrops.app.activities.MainActivity;
|
||||
import com.readrops.app.database.Database;
|
||||
import com.readrops.app.database.ItemsListQueryBuilder;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.RoomFactoryWrapper;
|
||||
import com.readrops.app.database.entities.Feed;
|
||||
import com.readrops.app.database.entities.Folder;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
import com.readrops.app.database.pojo.ItemWithFeed;
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
|
||||
@ -22,7 +23,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
@ -68,7 +68,7 @@ public class MainViewModel extends AndroidViewModel {
|
||||
if (lastFetch != null)
|
||||
itemsWithFeed.removeSource(lastFetch);
|
||||
|
||||
lastFetch = new LivePagedListBuilder<>(db.itemDao().selectAll(queryBuilder.getQuery()),
|
||||
lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(db.itemDao().selectAll(queryBuilder.getQuery())),
|
||||
new PagedList.Config.Builder()
|
||||
.setPageSize(100)
|
||||
.setPrefetchDistance(150)
|
||||
@ -124,32 +124,7 @@ public class MainViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return Single.create(emitter -> {
|
||||
List<Folder> folders = db.folderDao().getFolders(currentAccount.getId());
|
||||
Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Folder::compareTo);
|
||||
|
||||
for (Folder folder : folders) {
|
||||
List<Feed> feeds = db.feedDao().getFeedsByFolder(folder.getId());
|
||||
|
||||
for (Feed feed : feeds) {
|
||||
int unreadCount = db.itemDao().getUnreadCount(feed.getId());
|
||||
feed.setUnreadCount(unreadCount);
|
||||
}
|
||||
|
||||
foldersWithFeeds.put(folder, feeds);
|
||||
}
|
||||
|
||||
Folder noFolder = new Folder("no folder");
|
||||
|
||||
List<Feed> feedsWithoutFolder = db.feedDao().getFeedsWithoutFolder(currentAccount.getId());
|
||||
for (Feed feed : feedsWithoutFolder) {
|
||||
feed.setUnreadCount(db.itemDao().getUnreadCount(feed.getId()));
|
||||
}
|
||||
|
||||
foldersWithFeeds.put(noFolder, feedsWithoutFolder);
|
||||
|
||||
emitter.onSuccess(foldersWithFeeds);
|
||||
});
|
||||
return repository.getFoldersWithFeeds();
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
@ -17,6 +17,7 @@ import com.readrops.app.repositories.ARepository;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public class ManageFeedsFoldersViewModel extends AndroidViewModel {
|
||||
|
||||
@ -69,7 +70,7 @@ public class ManageFeedsFoldersViewModel extends AndroidViewModel {
|
||||
return db.folderDao().getFoldersWithFeedCount(account.getId());
|
||||
}
|
||||
|
||||
public Completable addFolder(Folder folder) {
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return repository.addFolder(folder);
|
||||
}
|
||||
|
||||
|
6
app/src/main/res/color/generic_button_color_selector.xml
Normal file
6
app/src/main/res/color/generic_button_color_selector.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:color="@android:color/darker_gray" android:state_enabled="false" />
|
||||
<item android:color="@android:color/white" />
|
||||
</selector>
|
5
app/src/main/res/drawable/ic_error.xml
Normal file
5
app/src/main/res/drawable/ic_error.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_import_export.xml
Normal file
5
app/src/main/res/drawable/ic_import_export.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
|
||||
</vector>
|
20
app/src/main/res/drawable/ic_notif.xml
Normal file
20
app/src/main/res/drawable/ic_notif.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M119.105,183.425C107.127,195.403 86.992,194.681 74.134,181.823C61.275,168.964 60.557,148.832 72.535,136.854C84.513,124.877 104.645,125.595 117.5,138.451C130.364,151.315 131.083,171.447 119.105,183.425" />
|
||||
<path
|
||||
android:fillColor="#727272"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M113.728,159.622C110.739,162.611 104.392,161.119 99.558,156.29C94.718,151.454 93.23,145.105 96.223,142.113C99.215,139.121 105.559,140.61 110.396,145.448C115.227,150.281 116.718,156.628 113.728,159.622" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M16.301,127.979a111.96,111.829 89.999,1 0,223.658 0a111.96,111.829 89.999,1 0,-223.658 0z"
|
||||
android:strokeWidth="29.11"
|
||||
android:strokeColor="#000000" />
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
@ -49,6 +49,30 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/account_type_list_choose"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/account_type_item" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_type_or"
|
||||
style="@style/TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/or"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/account_type_recyclerview" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/account_type_opml_import"
|
||||
style="@style/GenericButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/opml_import"
|
||||
android:onClick="openOPMLFile"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/account_type_or" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
@ -127,6 +127,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="true"
|
||||
android:text="@string/validate"
|
||||
android:textColor="@color/generic_button_color_selector"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.6"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -156,7 +156,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/activity_item_details_layout"
|
||||
android:layout_marginTop="15dp" />
|
||||
android:layout_marginTop="15dp"
|
||||
app:backgroundColor="?android:attr/colorBackground"
|
||||
app:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
@ -18,7 +18,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
android:theme="@style/ToolbarTheme"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight.ActionBar" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/sync_progress_layout"
|
||||
@ -88,10 +89,10 @@
|
||||
android:id="@+id/empty_list_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:image="@drawable/ic_rss_feed_grey"
|
||||
app:text="@string/no_item"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
app:image="@drawable/ic_rss_feed_grey"
|
||||
app:text="@string/no_item" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/add_feed_fab"
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/manage_feeds_folders_toolbar"
|
||||
style="@style/ToolbarTheme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
@ -26,7 +27,7 @@
|
||||
android:layout_height="match_parent"
|
||||
app:tabIndicator="@drawable/tab_indicator"
|
||||
app:tabIndicatorColor="@android:color/white"
|
||||
app:tabIndicatorHeight="4dp"/>
|
||||
app:tabIndicatorHeight="4dp" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
60
app/src/main/res/layout/activity_web_view.xml
Normal file
60
app/src/main/res/layout/activity_web_view.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="com.readrops.app.activities.WebViewActivity">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/activity_web_view_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/activity_web_view_swipe"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/activity_web_view_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:indeterminate="false" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
@ -2,31 +2,26 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<FrameLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
<ImageView
|
||||
android:id="@+id/empty_list_image"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
tools:src="@drawable/ic_rss_feed_grey" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/empty_list_image"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
tools:src="@drawable/ic_rss_feed_grey" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_list_text_v"
|
||||
style="@style/TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/no_feed" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
<TextView
|
||||
android:id="@+id/empty_list_text_v"
|
||||
style="@style/TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/no_feed" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
@ -10,7 +10,7 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/item_share"
|
||||
android:title="@string/share"
|
||||
android:title="@string/share_article"
|
||||
android:icon="@drawable/ic_share_white"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
|
17
app/src/main/res/menu/webview_menu.xml
Normal file
17
app/src/main/res/menu/webview_menu.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/web_view_share"
|
||||
android:icon="@drawable/ic_share_white"
|
||||
android:title="@string/share_article"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/web_view_refresh"
|
||||
android:icon="@drawable/ic_refresh"
|
||||
android:title="@string/actualize"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
@ -20,7 +20,7 @@
|
||||
<string name="read_time">%1$s min</string>
|
||||
<string name="read_time_lower_than_1">Moins d\'une minute</string>
|
||||
<string name="read_time_one_minute">1 min</string>
|
||||
<string name="share">Partager l\'article</string>
|
||||
<string name="share_article">Partager l\'article</string>
|
||||
<string name="open_url">Ouvrir le lien</string>
|
||||
<string name="add_folder">Ajouter un dossier</string>
|
||||
<string name="feed_folder">Dossier du flux</string>
|
||||
@ -84,5 +84,31 @@
|
||||
<string name="no_item">Aucun item</string>
|
||||
<string name="no_feed_found">Aucun flux trouvé</string>
|
||||
<string name="feed_insertion_error">Erreur pour le flux %1$s</string>
|
||||
<string name="get_feeds_colors">Récupération des couleurs des flux</string>
|
||||
<string name="feeds_colors">Couleurs des flux</string>
|
||||
<string name="global">Général</string>
|
||||
<string name="reload_feeds_colors">Recharger les couleurs des flux</string>
|
||||
<string name="open_items_in">Ouvrir les articles avec</string>
|
||||
<string name="webview">Vue web</string>
|
||||
<string name="external_navigator">Navigateur externe</string>
|
||||
<string name="actualize">Actualiser</string>
|
||||
<string name="share_url">Partager le lien</string>
|
||||
<string name="opml_import_export">Import/Export OPML</string>
|
||||
<string name="opml_processing">Traitement du fichier OPML</string>
|
||||
<string name="operation_takes_time">Cette opération peut prendre un certain temps car il faut interroger chaque flux.</string>
|
||||
<string name="processing_file_failed">Une erreur s\'est produite lors du traitement du fichier</string>
|
||||
<string name="opml_import">Import OPML</string>
|
||||
<string name="opml_export">Export OPML</string>
|
||||
<string name="external_storage_opml_export">L\'export des soubscriptions nécessite l\'accès au stockage</string>
|
||||
<string name="try_again">Réessayer</string>
|
||||
<string name="permissions">Permissions</string>
|
||||
<string name="or">Ou</string>
|
||||
<string name="image_options">Options de l\'image</string>
|
||||
<string name="download_image">Télécharger l\'image</string>
|
||||
<string name="share_image">Partager l\'image</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
<string name="opml_export_description">Export des flux et dossiers</string>
|
||||
|
||||
</resources>
|
19
app/src/main/res/values-night/styles.xml
Normal file
19
app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="MaterialDrawerTheme.ActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
|
||||
<item name="actionModeBackground">@color/colorPrimary</item>
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
|
||||
<item name="bottomSheetDialogTheme">@style/Theme.Design.BottomSheetDialog</item>
|
||||
<item name="buttonStyle">@style/GenericButton</item>
|
||||
</style>
|
||||
|
||||
<style name="GenericButton" parent="Widget.AppCompat.Button.Colored">
|
||||
<item name="android:textColor">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -5,6 +5,16 @@
|
||||
<item>@string/filter_oldest</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="opml_import_export">
|
||||
<item>@string/opml_import</item>
|
||||
<item>@string/opml_export</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="image_options">
|
||||
<item>@string/share_image</item>
|
||||
<item>@string/download_image</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="items_per_feed_numbers_values">
|
||||
<item>20</item>
|
||||
<item>50</item>
|
||||
@ -19,4 +29,23 @@
|
||||
<item>@string/unlimited</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="open_items_in">
|
||||
<item>@string/external_navigator</item>
|
||||
<item>@string/webview</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="open_item_in_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="themes">
|
||||
<item>@string/light</item>
|
||||
<item>@string/dark</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="themes_values">
|
||||
<item>false</item>
|
||||
<item>true</item>
|
||||
</string-array>
|
||||
</resources>
|
@ -6,4 +6,9 @@
|
||||
<attr name="text" format="string" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ReadropsWebView">
|
||||
<attr name="textColor" format="color" />
|
||||
<attr name="backgroundColor" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
@ -6,6 +6,4 @@
|
||||
<color name="colorControlNormal">#d7d7d7</color>
|
||||
<color name="colorBackground">#fafafa</color>
|
||||
<color name="textColorPrimary">#000000</color>
|
||||
|
||||
<color name="selected_background">#E0E0E0</color>
|
||||
</resources>
|
||||
|
@ -25,6 +25,8 @@
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
color: %2$s;
|
||||
background-color: %3$s;
|
||||
}
|
||||
|
||||
h1, p, div {
|
||||
@ -44,7 +46,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
%2$s
|
||||
%4$s
|
||||
</body>
|
||||
</html>]]></string>
|
||||
</resources>
|
@ -22,7 +22,7 @@
|
||||
<string name="read_time_lower_than_1">Less than a minute</string>
|
||||
<string name="read_time_one_minute">1 min</string>
|
||||
<string name="interpoint" translatable="false">·</string>
|
||||
<string name="share">Share Article</string>
|
||||
<string name="share_article">Share Article</string>
|
||||
<string name="open_url">Open url</string>
|
||||
<string name="add_folder">Add folder</string>
|
||||
<string name="feed_folder">Feed folder</string>
|
||||
@ -92,4 +92,31 @@
|
||||
<string name="no_item">No item</string>
|
||||
<string name="no_feed_found">No feed found</string>
|
||||
<string name="feed_insertion_error">Error for feed %1$s</string>
|
||||
<string name="get_feeds_colors">Get feeds colors</string>
|
||||
<string name="feeds_colors">Feeds Colors</string>
|
||||
<string name="global">Global</string>
|
||||
<string name="reload_feeds_colors">Reload feeds colors</string>
|
||||
<string name="open_items_in">Open items in</string>
|
||||
<string name="webview">Webview</string>
|
||||
<string name="external_navigator">External navigator</string>
|
||||
<string name="actualize">Actualize</string>
|
||||
<string name="share_url">Share url</string>
|
||||
<string name="opml_import_export">OPML Import/Export</string>
|
||||
<string name="opml_processing">Processing OPML file</string>
|
||||
<string name="operation_takes_time">This operation can take a significant time as each feed needs to be queried.</string>
|
||||
<string name="processing_file_failed">An error occurred during the file processing</string>
|
||||
<string name="opml_import">OPML import</string>
|
||||
<string name="opml_export">OPML export</string>
|
||||
<string name="subscriptions" translatable="false">Subscriptions</string>
|
||||
<string name="external_storage_opml_export">Subscriptions export needs external storage permission</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="permissions">Permissions</string>
|
||||
<string name="or">Or</string>
|
||||
<string name="image_options">Image Options</string>
|
||||
<string name="download_image">Download image</string>
|
||||
<string name="share_image">Share image</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="opml_export_description">Export feeds and folders</string>
|
||||
</resources>
|
||||
|
@ -1,8 +1,6 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<style name="AppTheme" parent="MaterialDrawerTheme.Light.ActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
@ -10,7 +8,10 @@
|
||||
<item name="colorControlNormal">@color/colorControlNormal</item>
|
||||
<item name="android:textColorPrimary">@color/textColorPrimary</item>
|
||||
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
<item name="actionModeBackground">@color/colorPrimary</item>
|
||||
<item name="actionBarTheme">@style/ToolbarTheme</item>
|
||||
|
||||
<item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="AppTheme">
|
||||
@ -23,15 +24,18 @@
|
||||
<item name="android:windowBackground">@drawable/splash_background</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Design.CollapsingToolbar.Expanded.Custom" parent="TextAppearance.Design.CollapsingToolbar.Expanded">
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:color">@color/colorBackground</item>
|
||||
<item name="android:layout_margin">14dp</item>
|
||||
<style name="ToolbarTheme" parent="ThemeOverlay.AppCompat.ActionBar">
|
||||
<item name="android:textColorPrimary">@android:color/white</item>
|
||||
<item name="colorControlNormal">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="GenericButton" parent="Base.Widget.AppCompat.Button.Colored">
|
||||
<style name="GenericButton" parent="Widget.AppCompat.Button.Colored">
|
||||
<item name="android:colorBackground">@color/colorPrimary</item>
|
||||
<item name="android:textColorPrimary">@color/colorControlNormal</item>
|
||||
</style>
|
||||
|
||||
<style name="DrawerArrowStyle" parent="MaterialDrawer.DrawerArrowStyle">
|
||||
<item name="color">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -11,6 +11,11 @@
|
||||
android:key="credentials_key"
|
||||
android:title="@string/credentials" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_import_export"
|
||||
android:key="opml_import_export"
|
||||
android:title="@string/opml_import_export" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_delete_grey"
|
||||
android:key="delete_account_key"
|
||||
|
8
app/src/main/res/xml/file_paths.xml
Normal file
8
app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<paths>
|
||||
<cache-path
|
||||
name="shared_images"
|
||||
path="images/" />
|
||||
</paths>
|
||||
</resources>
|
@ -10,4 +10,24 @@
|
||||
android:title="@string/number_items_to_parse" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/global">
|
||||
<Preference
|
||||
android:key="reload_feeds_colors"
|
||||
android:title="@string/reload_feeds_colors" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/open_items_in"
|
||||
android:entryValues="@array/open_item_in_values"
|
||||
android:key="open_items_in"
|
||||
android:title="@string/open_items_in" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="false"
|
||||
android:entries="@array/themes"
|
||||
android:entryValues="@array/themes_values"
|
||||
android:key="dark_theme"
|
||||
android:title="@string/theme" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
36
app/src/test/java/com/readrops/app/HtmlParserTest.java
Normal file
36
app/src/test/java/com/readrops/app/HtmlParserTest.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.readrops.app;
|
||||
|
||||
import com.readrops.app.utils.HtmlParser;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
||||
public class HtmlParserTest {
|
||||
|
||||
@Test
|
||||
public void getFeedLinkTest() throws Exception {
|
||||
String url = "https://github.com/readrops/Readrops";
|
||||
|
||||
ParsingResult parsingResult = new ParsingResult("https://github.com/readrops/Readrops/commits/develop.atom", "Recent Commits to Readrops:develop");
|
||||
List<ParsingResult> parsingResultList = new ArrayList<>();
|
||||
parsingResultList.add(parsingResult);
|
||||
|
||||
List<ParsingResult> parsingResultList1 = HtmlParser.getFeedLink(url);
|
||||
|
||||
Assert.assertEquals(parsingResultList, parsingResultList1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFaviconLinkTest() throws IOException {
|
||||
String url = "https://github.com/readrops/Readrops";
|
||||
|
||||
assertEquals("https://github.com/fluidicon.png", HtmlParser.getFaviconLink(url));
|
||||
}
|
||||
}
|
17
app/src/test/java/com/readrops/app/UtilsTest.java
Normal file
17
app/src/test/java/com/readrops/app/UtilsTest.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.readrops.app;
|
||||
|
||||
import com.readrops.app.utils.Utils;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
||||
public class UtilsTest {
|
||||
|
||||
@Test
|
||||
public void cleanTextTest() {
|
||||
String text = " <p>This is a text<br/>to</p> clean ";
|
||||
|
||||
assertEquals("This is a text to clean", Utils.cleanText(text));
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.61'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
5
fastlane/metadata/android/en-US/changelogs/6.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/6.txt
Normal file
@ -0,0 +1,5 @@
|
||||
- OPML import/export for local account
|
||||
- Dark theme
|
||||
- Share or download item image
|
||||
- Open item in webview
|
||||
- Minor bug fixes and improvements
|
@ -3,19 +3,22 @@ apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
targetSdkVersion 29
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
@ -29,7 +32,10 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
@ -45,6 +51,5 @@ dependencies {
|
||||
implementation 'org.jsoup:jsoup:1.12.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package com.readrops.readropslibrary.opml
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.readrops.readropslibrary.opml.model.OPML
|
||||
import com.readrops.readropslibrary.utils.LibUtils
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import org.simpleframework.xml.Serializer
|
||||
import org.simpleframework.xml.core.Persister
|
||||
import java.io.OutputStream
|
||||
|
||||
class OPMLParser {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun read(uri: Uri, context: Context): Single<OPML> {
|
||||
return Single.create { emitter ->
|
||||
val fileString = LibUtils.fileToString(uri, context)
|
||||
val serializer: Serializer = Persister()
|
||||
|
||||
val opml: OPML = serializer.read(OPML::class.java, fileString)
|
||||
|
||||
emitter.onSuccess(opml)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun write(opml: OPML, outputStream: OutputStream): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val serializer: Serializer = Persister()
|
||||
serializer.write(opml, outputStream)
|
||||
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.readrops.readropslibrary.opml.model
|
||||
|
||||
import org.simpleframework.xml.ElementList
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "body", strict = false)
|
||||
data class Body(@field:ElementList(inline = true, required = true) var outlines: List<Outline>?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXMl
|
||||
*/
|
||||
constructor() : this(null)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.readrops.readropslibrary.opml.model
|
||||
|
||||
import org.simpleframework.xml.Element
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "head", strict = false)
|
||||
data class Head(@field:Element(required = false) var title: String?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXML
|
||||
*/
|
||||
constructor() : this(null)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.readrops.readropslibrary.opml.model
|
||||
|
||||
import org.simpleframework.xml.Attribute
|
||||
import org.simpleframework.xml.Element
|
||||
import org.simpleframework.xml.Order
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Order(elements = ["head", "body"])
|
||||
@Root(name = "opml", strict = false)
|
||||
data class OPML(@field:Attribute(required = true) var version: String?,
|
||||
@field:Element(required = true) var head: Head?,
|
||||
@field:Element(required = true) var body: Body?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXML
|
||||
*/
|
||||
constructor() : this(null, null, null)
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.readrops.readropslibrary.opml.model
|
||||
|
||||
import org.simpleframework.xml.Attribute
|
||||
import org.simpleframework.xml.ElementList
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "outline", strict = false)
|
||||
data class Outline(@field:Attribute(required = false) var title: String?,
|
||||
@field:Attribute(required = false) var text: String?,
|
||||
@field:Attribute(required = false) var type: String?,
|
||||
@field:Attribute(required = false) var xmlUrl: String?,
|
||||
@field:Attribute(required = false) var htmlUrl: String?,
|
||||
@field:ElementList(inline = true, required = false) var outlines: List<Outline>?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXML
|
||||
*/
|
||||
constructor() : this(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
|
||||
constructor(title: String) : this(title, null, null, null, null, null)
|
||||
|
||||
constructor(title: String, xmlUrl: String, htmlUrl: String) : this(title, title, "rss", xmlUrl, htmlUrl, null)
|
||||
}
|
@ -15,6 +15,8 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
||||
*/
|
||||
public abstract class API<T> {
|
||||
|
||||
protected static final int MAX_ITEMS = 5000;
|
||||
|
||||
protected T api;
|
||||
|
||||
public API(Credentials credentials, @NonNull Class<T> clazz, @NonNull String endPoint) {
|
||||
|
@ -22,7 +22,9 @@ import okhttp3.RequestBody;
|
||||
|
||||
public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
|
||||
private static final String GOOGLE_READ = "user/-/state/com.google/read";
|
||||
public static final String GOOGLE_READ = "user/-/state/com.google/read";
|
||||
|
||||
private static final String FEED_PREFIX = "feed/";
|
||||
|
||||
public FreshRSSAPI(Credentials credentials) {
|
||||
super(credentials, FreshRSSService.class, FreshRSSService.END_POINT);
|
||||
@ -93,9 +95,9 @@ public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
|
||||
switch (syncType) {
|
||||
case INITIAL_SYNC:
|
||||
return getItems(GOOGLE_READ, 10000, null);
|
||||
return getItems(GOOGLE_READ, MAX_ITEMS, null);
|
||||
case CLASSIC_SYNC:
|
||||
return getItems(GOOGLE_READ, 10000, syncData.getLastModified());
|
||||
return getItems(GOOGLE_READ, MAX_ITEMS, syncData.getLastModified());
|
||||
}
|
||||
|
||||
return Single.error(new Exception("Unknown sync type"));
|
||||
@ -134,7 +136,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
* @param lastModified fetch only items created after this timestamp
|
||||
* @return the items
|
||||
*/
|
||||
public Single<FreshRSSItems> getItems(@NonNull String excludeTarget, @NonNull Integer max, @Nullable Long lastModified) {
|
||||
public Single<FreshRSSItems> getItems(@Nullable String excludeTarget, int max, @Nullable Long lastModified) {
|
||||
return api.getItems(excludeTarget, max, lastModified);
|
||||
}
|
||||
|
||||
@ -147,7 +149,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
* @param token token for modifications
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable markItemsReadUnread(@NonNull Boolean read, @NonNull List<String> itemIds, @NonNull String token) {
|
||||
public Completable markItemsReadUnread(boolean read, @NonNull List<String> itemIds, @NonNull String token) {
|
||||
if (read)
|
||||
return api.setItemsReadState(token, GOOGLE_READ, null, itemIds);
|
||||
else
|
||||
@ -162,7 +164,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable createFeed(@NonNull String token, @NonNull String feedUrl) {
|
||||
return api.createOrDeleteFeed(token, "feed/" + feedUrl, "subscribe");
|
||||
return api.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,7 +175,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable deleteFeed(@NonNull String token, @NonNull String feedUrl) {
|
||||
return api.createOrDeleteFeed(token, "feed/" + feedUrl, "unsubscribe");
|
||||
return api.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,7 +188,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable updateFeed(@NonNull String token, @NonNull String feedUrl, @NonNull String title, @NonNull String folderId) {
|
||||
return api.updateFeed(token, "feed/" + feedUrl, title, folderId, "edit");
|
||||
return api.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,7 +35,7 @@ public interface FreshRSSService {
|
||||
Single<FreshRSSFeeds> getFeeds();
|
||||
|
||||
@GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
|
||||
Single<FreshRSSItems> getItems(@Query("xt") String excludeTarget, @Query("n") Integer max, @Query("ot") Long lastModified);
|
||||
Single<FreshRSSItems> getItems(@Query("xt") String excludeTarget, @Query("n") int max, @Query("ot") Long lastModified);
|
||||
|
||||
@GET("reader/api/0/tag/list?output=json")
|
||||
Single<FreshRSSFolders> getFolders();
|
||||
|
@ -1,6 +1,8 @@
|
||||
|
||||
package com.readrops.readropslibrary.services.freshrss.json;
|
||||
|
||||
import com.readrops.readropslibrary.services.freshrss.FreshRSSAPI;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FreshRSSItem {
|
||||
@ -105,4 +107,8 @@ public class FreshRSSItem {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return categories.contains(FreshRSSAPI.GOOGLE_READ);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ public class NextNewsAPI extends API<NextNewsService> {
|
||||
private void initialSync(NextNewsSyncResult syncResult) throws IOException {
|
||||
getFeedsAndFolders(syncResult);
|
||||
|
||||
Response<NextNewsItems> itemsResponse = api.getItems(3, false, -1).execute();
|
||||
Response<NextNewsItems> itemsResponse = api.getItems(3, false, MAX_ITEMS).execute();
|
||||
NextNewsItems itemList = itemsResponse.body();
|
||||
|
||||
if (!itemsResponse.isSuccessful())
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.readrops.readropslibrary.utils;
|
||||
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
import com.readrops.readropslibrary.BuildConfig;
|
||||
import com.readrops.readropslibrary.services.Credentials;
|
||||
|
||||
@ -11,6 +10,7 @@ import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
|
||||
public class HttpManager {
|
||||
|
||||
@ -29,14 +29,15 @@ public class HttpManager {
|
||||
|
||||
private void buildOkHttp() {
|
||||
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
|
||||
.callTimeout(30, TimeUnit.SECONDS)
|
||||
.callTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.HOURS);
|
||||
|
||||
httpBuilder.addInterceptor(new AuthInterceptor());
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StethoInterceptor loggingInterceptor = new StethoInterceptor();
|
||||
httpBuilder.addNetworkInterceptor(loggingInterceptor);
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.level(HttpLoggingInterceptor.Level.BASIC);
|
||||
httpBuilder.addInterceptor(interceptor);
|
||||
}
|
||||
|
||||
okHttpClient = httpBuilder.build();
|
||||
|
@ -1,5 +1,9 @@
|
||||
package com.readrops.readropslibrary.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Scanner;
|
||||
|
||||
@ -28,6 +32,10 @@ public final class LibUtils {
|
||||
return scanner.hasNext() ? scanner.next() : "";
|
||||
}
|
||||
|
||||
public static String fileToString(Uri uri, Context context) throws FileNotFoundException {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
|
||||
return inputStreamToString(inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user