mirror of https://github.com/readrops/Readrops.git
Add initial support for feeds and folders export in OPML file
This commit is contained in:
parent
1daf6e0733
commit
198e9ae299
|
@ -6,6 +6,8 @@
|
|||
<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.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".utils.ReadropsApp"
|
||||
|
@ -16,7 +18,8 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
||||
<activity
|
||||
android:name=".activities.WebViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
package com.readrops.app.fragments.settings;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.preference.Preference;
|
||||
|
@ -18,8 +24,15 @@ 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.OPMLMatcher;
|
||||
import com.readrops.app.viewmodels.AccountViewModel;
|
||||
import com.readrops.readropslibrary.opml.OpmlParser;
|
||||
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;
|
||||
|
@ -34,7 +47,10 @@ import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
|
|||
*/
|
||||
public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
public static final int OPEN_OPML_FILE_REQUEST = 1;
|
||||
private static final String TAG = AccountSettingsFragment.class.getSimpleName();
|
||||
|
||||
private static final int OPEN_OPML_FILE_REQUEST = 1;
|
||||
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
|
||||
|
||||
private Account account;
|
||||
private AccountViewModel viewModel;
|
||||
|
@ -94,7 +110,19 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
|||
});
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
@ -137,6 +165,55 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
|||
startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
Log.d(TAG, "onComplete: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.d(TAG, "onError: ");
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isExternalStoragePermissionGranted() {
|
||||
return ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestExternalStoragePermission() {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
|
@ -155,8 +232,23 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
|||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@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]))
|
||||
|
||||
} else {
|
||||
exportAsOPMLFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
|
||||
OpmlParser.parse(uri, getContext())
|
||||
OPMLParser.parse(uri, getContext())
|
||||
.flatMapCompletable(opml -> viewModel.insertOPMLFoldersAndFeeds(opml))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
|
@ -118,6 +119,35 @@ public abstract class ARepository<T> {
|
|||
return database.feedDao().getFeedCount(accountId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 setFeedColors(Feed feed) {
|
||||
FeedColorsKt.setFeedColors(feed);
|
||||
database.feedDao().updateColors(feed.getId(),
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
package com.readrops.app.utils;
|
||||
|
||||
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.readropslibrary.localfeed.atom.ATOMFeed;
|
||||
import com.readrops.readropslibrary.localfeed.json.JSONFeed;
|
||||
import com.readrops.readropslibrary.localfeed.rss.RSSChannel;
|
||||
import com.readrops.readropslibrary.localfeed.rss.RSSFeed;
|
||||
import com.readrops.readropslibrary.opml.model.Body;
|
||||
import com.readrops.readropslibrary.opml.model.Opml;
|
||||
import com.readrops.readropslibrary.opml.model.Outline;
|
||||
import com.readrops.readropslibrary.services.freshrss.json.FreshRSSFeed;
|
||||
import com.readrops.readropslibrary.services.nextcloudnews.json.NextNewsFeed;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class FeedMatcher {
|
||||
|
||||
public static Feed nextNewsFeedToFeed(NextNewsFeed feed, Account account) {
|
||||
|
@ -109,27 +100,4 @@ public final class FeedMatcher {
|
|||
|
||||
return feed;
|
||||
}
|
||||
|
||||
public static Map<Folder, List<Feed>> feedsAndFoldersFromOPML(Opml opml) {
|
||||
Map<Folder, List<Feed>> foldersAndFeeds = new HashMap<>();
|
||||
Body body = opml.getBody();
|
||||
|
||||
for (Outline outline : body.getOutlines()) {
|
||||
Folder folder = new Folder(outline.getTitle());
|
||||
|
||||
List<Feed> feeds = new ArrayList<>();
|
||||
for (Outline feedOutline : outline.getOutlines()) {
|
||||
Feed feed = new Feed();
|
||||
feed.setName(feedOutline.getTitle());
|
||||
feed.setUrl(feedOutline.getXmlUrl());
|
||||
feed.setSiteUrl(feedOutline.getHtmlUrl());
|
||||
|
||||
feeds.add(feed);
|
||||
}
|
||||
|
||||
foldersAndFeeds.put(folder, feeds);
|
||||
}
|
||||
|
||||
return foldersAndFeeds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package com.readrops.app.utils
|
||||
|
||||
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().let {
|
||||
it.name = feedOutline.title
|
||||
it.url = feedOutline.xmlUrl
|
||||
it.siteUrl = feedOutline.htmlUrl
|
||||
|
||||
it
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -12,8 +12,8 @@ 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.FeedMatcher;
|
||||
import com.readrops.readropslibrary.opml.model.Opml;
|
||||
import com.readrops.app.utils.OPMLMatcher;
|
||||
import com.readrops.readropslibrary.opml.model.OPML;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -66,9 +66,13 @@ public class AccountViewModel extends AndroidViewModel {
|
|||
return database.accountDao().getAccountCount();
|
||||
}
|
||||
|
||||
public Completable insertOPMLFoldersAndFeeds(Opml opml) {
|
||||
Map<Folder, List<Feed>> foldersAndFeeds = FeedMatcher.feedsAndFoldersFromOPML(opml);
|
||||
public Completable insertOPMLFoldersAndFeeds(OPML opml) {
|
||||
Map<Folder, List<Feed>> foldersAndFeeds = OPMLMatcher.INSTANCE.opmltoFoldersAndFeeds(opml);
|
||||
|
||||
return repository.insertOPMLFoldersAndFeeds(foldersAndFeeds);
|
||||
}
|
||||
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return repository.getFoldersWithFeeds();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ import com.readrops.app.activities.MainActivity;
|
|||
import com.readrops.app.database.Database;
|
||||
import com.readrops.app.database.ItemsListQueryBuilder;
|
||||
import com.readrops.app.database.RoomFactoryWrapper;
|
||||
import com.readrops.app.database.entities.account.Account;
|
||||
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;
|
||||
|
||||
|
@ -23,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;
|
||||
|
@ -125,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
|
||||
|
|
|
@ -97,5 +97,7 @@
|
|||
<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="import_opml">Importer un fichier OPML</string>
|
||||
<string name="export_feeds_folders_opml">Exporter les flux et les dossiers dans un fichier OPML</string>
|
||||
|
||||
</resources>
|
|
@ -5,6 +5,11 @@
|
|||
<item>@string/filter_oldest</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="opml_import_export">
|
||||
<item>@string/import_opml</item>
|
||||
<item>@string/export_feeds_folders_opml</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="items_per_feed_numbers_values">
|
||||
<item>20</item>
|
||||
<item>50</item>
|
||||
|
|
|
@ -105,4 +105,7 @@
|
|||
<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="import_opml">Import OPML file</string>
|
||||
<string name="export_feeds_folders_opml">Export feeds and folders to an OPML file</string>
|
||||
<string name="subscriptions" translatable="false">Subscriptions</string>
|
||||
</resources>
|
||||
|
|
|
@ -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 parse(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
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.Single
|
||||
import org.simpleframework.xml.Serializer
|
||||
import org.simpleframework.xml.core.Persister
|
||||
|
||||
class OpmlParser {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parse(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -5,7 +5,7 @@ import org.simpleframework.xml.Element
|
|||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "opml", strict = false)
|
||||
data class Opml(@field:Attribute(required = true) var version: String?,
|
||||
data class OPML(@field:Attribute(required = true) var version: String?,
|
||||
@field:Element(required = true) var head: Head?,
|
||||
@field:Element(required = true) var body: Body?) {
|
||||
|
|
@ -20,7 +20,7 @@ data class Outline(@field:Attribute(required = false) var title: String?,
|
|||
null,
|
||||
null)
|
||||
|
||||
fun hasSubElements(): Boolean {
|
||||
return !outlines.isNullOrEmpty()
|
||||
}
|
||||
constructor(title: String) : this(title, null, null, null, null, null)
|
||||
|
||||
constructor(title: String, xmlUrl: String, htmlUrl: String) : this(title, null, null, xmlUrl, htmlUrl, null)
|
||||
}
|
Loading…
Reference in New Issue