Merge branch 'auto_sync_toots' into develop
This commit is contained in:
commit
dd7f3813d1
|
@ -1049,12 +1049,10 @@ public abstract class BaseMainActivity extends BaseActivity
|
||||||
ActivityCompat.requestPermissions(BaseMainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE);
|
ActivityCompat.requestPermissions(BaseMainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE);
|
||||||
} else {
|
} else {
|
||||||
Intent backupIntent = new Intent(BaseMainActivity.this, BackupStatusService.class);
|
Intent backupIntent = new Intent(BaseMainActivity.this, BackupStatusService.class);
|
||||||
backupIntent.putExtra("userId", userId);
|
|
||||||
startService(backupIntent);
|
startService(backupIntent);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
Intent backupIntent = new Intent(BaseMainActivity.this, BackupStatusService.class);
|
Intent backupIntent = new Intent(BaseMainActivity.this, BackupStatusService.class);
|
||||||
backupIntent.putExtra("userId", userId);
|
|
||||||
startService(backupIntent);
|
startService(backupIntent);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -41,6 +41,7 @@ import java.util.Locale;
|
||||||
|
|
||||||
import app.fedilab.android.helper.Helper;
|
import app.fedilab.android.helper.Helper;
|
||||||
import app.fedilab.android.jobs.ApplicationJob;
|
import app.fedilab.android.jobs.ApplicationJob;
|
||||||
|
import app.fedilab.android.jobs.BackupStatusesSyncJob;
|
||||||
import app.fedilab.android.jobs.NotificationsSyncJob;
|
import app.fedilab.android.jobs.NotificationsSyncJob;
|
||||||
import es.dmoral.toasty.Toasty;
|
import es.dmoral.toasty.Toasty;
|
||||||
import app.fedilab.android.BuildConfig;
|
import app.fedilab.android.BuildConfig;
|
||||||
|
@ -68,6 +69,7 @@ public class MainApplication extends MultiDexApplication {
|
||||||
//System.setProperty("java.net.preferIPv4Stack" , "true");
|
//System.setProperty("java.net.preferIPv4Stack" , "true");
|
||||||
JobManager.create(this).addJobCreator(new ApplicationJob());
|
JobManager.create(this).addJobCreator(new ApplicationJob());
|
||||||
NotificationsSyncJob.schedule(false);
|
NotificationsSyncJob.schedule(false);
|
||||||
|
BackupStatusesSyncJob.schedule(false);
|
||||||
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
|
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
|
||||||
StrictMode.setVmPolicy(builder.build());
|
StrictMode.setVmPolicy(builder.build());
|
||||||
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE);
|
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE);
|
||||||
|
|
|
@ -297,6 +297,20 @@ public class ContentSettingsFragment extends Fragment implements ScreenShotable
|
||||||
count4 = 0;
|
count4 = 0;
|
||||||
count5 = 0;
|
count5 = 0;
|
||||||
|
|
||||||
|
String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
|
||||||
|
String instance = sharedpreferences.getString(Helper.PREF_INSTANCE, Helper.getLiveInstance(context));
|
||||||
|
|
||||||
|
boolean auto_backup = sharedpreferences.getBoolean(Helper.SET_AUTO_BACKUP_STATUSES+userId+instance, false);
|
||||||
|
final CheckBox set_auto_backup = rootView.findViewById(R.id.set_auto_backup);
|
||||||
|
set_auto_backup.setChecked(auto_backup);
|
||||||
|
set_auto_backup.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
SharedPreferences.Editor editor = sharedpreferences.edit();
|
||||||
|
editor.putBoolean(Helper.SET_AUTO_BACKUP_STATUSES+userId+instance, set_auto_backup.isChecked());
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
TagsEditText set_featured_tags = rootView.findViewById(R.id.set_featured_tags);
|
TagsEditText set_featured_tags = rootView.findViewById(R.id.set_featured_tags);
|
||||||
|
@ -447,8 +461,6 @@ public class ContentSettingsFragment extends Fragment implements ScreenShotable
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
|
|
||||||
String instance = sharedpreferences.getString(Helper.PREF_INSTANCE, Helper.getLiveInstance(context));
|
|
||||||
|
|
||||||
boolean display_admin_menu = sharedpreferences.getBoolean(Helper.SET_DISPLAY_ADMIN_MENU + userId + instance, false);
|
boolean display_admin_menu = sharedpreferences.getBoolean(Helper.SET_DISPLAY_ADMIN_MENU + userId + instance, false);
|
||||||
|
|
||||||
|
|
|
@ -380,6 +380,7 @@ public class Helper {
|
||||||
public static final String SET_DISPLAY_ADMIN_MENU = "set_display_admin_menu";
|
public static final String SET_DISPLAY_ADMIN_MENU = "set_display_admin_menu";
|
||||||
public static final String SET_DISPLAY_ADMIN_STATUSES = "set_display_admin_statuses";
|
public static final String SET_DISPLAY_ADMIN_STATUSES = "set_display_admin_statuses";
|
||||||
public static final String SET_DISPLAY_FEDILAB_FEATURES_BUTTON = "set_display_fedilab_features_button";
|
public static final String SET_DISPLAY_FEDILAB_FEATURES_BUTTON = "set_display_fedilab_features_button";
|
||||||
|
public static final String SET_AUTO_BACKUP_STATUSES = "set_auto_backup_statuses";
|
||||||
|
|
||||||
public static final int S_NO = 0;
|
public static final int S_NO = 0;
|
||||||
static final int S_512KO = 1;
|
static final int S_512KO = 1;
|
||||||
|
@ -472,6 +473,7 @@ public class Helper {
|
||||||
public static final String SET_PROXY_PASSWORD = "set_proxy_password";
|
public static final String SET_PROXY_PASSWORD = "set_proxy_password";
|
||||||
//Refresh job
|
//Refresh job
|
||||||
public static final int MINUTES_BETWEEN_NOTIFICATIONS_REFRESH = 15;
|
public static final int MINUTES_BETWEEN_NOTIFICATIONS_REFRESH = 15;
|
||||||
|
public static final int MINUTES_BETWEEN_BACKUP = 60;
|
||||||
public static final int MINUTES_BETWEEN_HOME_TIMELINE = 30;
|
public static final int MINUTES_BETWEEN_HOME_TIMELINE = 30;
|
||||||
public static final int SPLIT_TOOT_SIZE = 500;
|
public static final int SPLIT_TOOT_SIZE = 500;
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ public class ApplicationJob implements JobCreator {
|
||||||
return new ScheduledTootsSyncJob();
|
return new ScheduledTootsSyncJob();
|
||||||
case ScheduledBoostsSyncJob.SCHEDULED_BOOST:
|
case ScheduledBoostsSyncJob.SCHEDULED_BOOST:
|
||||||
return new ScheduledBoostsSyncJob();
|
return new ScheduledBoostsSyncJob();
|
||||||
|
case BackupStatusesSyncJob.BACKUP_SYNC:
|
||||||
|
return new BackupStatusesSyncJob();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
package app.fedilab.android.jobs;
|
||||||
|
/* Copyright 2019 Thomas Schneider
|
||||||
|
*
|
||||||
|
* This file is a part of Fedilab
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.engine.GlideException;
|
||||||
|
import com.bumptech.glide.request.RequestListener;
|
||||||
|
import com.bumptech.glide.request.target.SimpleTarget;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
|
import com.evernote.android.job.Job;
|
||||||
|
import com.evernote.android.job.JobManager;
|
||||||
|
import com.evernote.android.job.JobRequest;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import app.fedilab.android.R;
|
||||||
|
import app.fedilab.android.activities.BaseMainActivity;
|
||||||
|
import app.fedilab.android.activities.MainActivity;
|
||||||
|
import app.fedilab.android.activities.OwnerStatusActivity;
|
||||||
|
import app.fedilab.android.client.API;
|
||||||
|
import app.fedilab.android.client.APIResponse;
|
||||||
|
import app.fedilab.android.client.Entities.Account;
|
||||||
|
import app.fedilab.android.client.Entities.Notification;
|
||||||
|
import app.fedilab.android.fragments.DisplayNotificationsFragment;
|
||||||
|
import app.fedilab.android.helper.Helper;
|
||||||
|
import app.fedilab.android.services.BackupStatusInDataBaseService;
|
||||||
|
import app.fedilab.android.services.BackupStatusService;
|
||||||
|
import app.fedilab.android.sqlite.AccountDAO;
|
||||||
|
import app.fedilab.android.sqlite.Sqlite;
|
||||||
|
|
||||||
|
import static app.fedilab.android.helper.Helper.INTENT_ACTION;
|
||||||
|
import static app.fedilab.android.helper.Helper.INTENT_TARGETED_ACCOUNT;
|
||||||
|
import static app.fedilab.android.helper.Helper.NOTIFICATION_INTENT;
|
||||||
|
import static app.fedilab.android.helper.Helper.PREF_INSTANCE;
|
||||||
|
import static app.fedilab.android.helper.Helper.PREF_KEY_ID;
|
||||||
|
import static app.fedilab.android.helper.Helper.canNotify;
|
||||||
|
import static app.fedilab.android.helper.Helper.notify_user;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Thomas on 06/01/2019.
|
||||||
|
* backup statuses
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class BackupStatusesSyncJob extends Job {
|
||||||
|
|
||||||
|
static final String BACKUP_SYNC = "job_backup";
|
||||||
|
static {
|
||||||
|
Helper.installProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Result onRunJob(@NonNull Params params) {
|
||||||
|
//Code refresh here
|
||||||
|
|
||||||
|
backupService();
|
||||||
|
return Result.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static int schedule(boolean updateCurrent) {
|
||||||
|
|
||||||
|
Set<JobRequest> jobRequests = JobManager.instance().getAllJobRequestsForTag(BACKUP_SYNC);
|
||||||
|
if (!jobRequests.isEmpty() && !updateCurrent) {
|
||||||
|
return jobRequests.iterator().next().getJobId();
|
||||||
|
}
|
||||||
|
|
||||||
|
int jobRequestschedule = -1;
|
||||||
|
try {
|
||||||
|
jobRequestschedule = new JobRequest.Builder(BackupStatusesSyncJob.BACKUP_SYNC)
|
||||||
|
.setPeriodic(TimeUnit.MINUTES.toMillis(Helper.MINUTES_BETWEEN_BACKUP), TimeUnit.MINUTES.toMillis(5))
|
||||||
|
.setUpdateCurrent(updateCurrent)
|
||||||
|
.setRequiredNetworkType(JobRequest.NetworkType.METERED)
|
||||||
|
.setRequirementsEnforced(false)
|
||||||
|
.build()
|
||||||
|
.schedule();
|
||||||
|
}catch (Exception ignored){}
|
||||||
|
|
||||||
|
return jobRequestschedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task in background starts here.
|
||||||
|
*/
|
||||||
|
private void backupService() {
|
||||||
|
SQLiteDatabase db = Sqlite.getInstance(getContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
|
||||||
|
final List<Account> accounts = new AccountDAO(getContext(), db).getAllAccount();
|
||||||
|
SharedPreferences sharedpreferences = getContext().getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE);
|
||||||
|
for(Account account: accounts) {
|
||||||
|
boolean autobackup = sharedpreferences.getBoolean(Helper.SET_AUTO_BACKUP_STATUSES + account.getId() + account.getInstance(), false);
|
||||||
|
if( autobackup) {
|
||||||
|
Intent backupIntent = new Intent(getContext(), BackupStatusInDataBaseService.class);
|
||||||
|
backupIntent.putExtra("userId", account.getId());
|
||||||
|
backupIntent.putExtra("instance", account.getInstance());
|
||||||
|
getContext().startService(backupIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -74,27 +74,42 @@ public class BackupStatusInDataBaseService extends IntentService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onHandleIntent(@Nullable Intent intent) {
|
protected void onHandleIntent(@Nullable Intent intent) {
|
||||||
|
boolean toastMessage = true;
|
||||||
|
String userId = null;
|
||||||
|
String instance = null;
|
||||||
|
if( intent != null){
|
||||||
|
userId = intent.getStringExtra("userId");
|
||||||
|
instance = intent.getStringExtra("instance");
|
||||||
|
toastMessage = false;
|
||||||
|
}
|
||||||
|
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
|
||||||
|
if( userId == null || instance == null) {
|
||||||
|
userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
|
||||||
|
instance = sharedpreferences.getString(Helper.PREF_INSTANCE, null);
|
||||||
|
}
|
||||||
|
boolean finalToastMessage = toastMessage;
|
||||||
if( instanceRunning == 0 ){
|
if( instanceRunning == 0 ){
|
||||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Toasty.info(getApplicationContext(), getString(R.string.data_export_start), Toast.LENGTH_LONG).show();
|
if(finalToastMessage) {
|
||||||
|
Toasty.info(getApplicationContext(), getString(R.string.data_export_start), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}else {
|
}else {
|
||||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Toasty.info(getApplicationContext(), getString(R.string.data_export_running), Toast.LENGTH_LONG).show();
|
if(finalToastMessage) {
|
||||||
|
Toasty.info(getApplicationContext(), getString(R.string.data_export_running), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
instanceRunning++;
|
instanceRunning++;
|
||||||
String message;
|
String message;
|
||||||
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
|
|
||||||
String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
|
|
||||||
String instance = sharedpreferences.getString(Helper.PREF_INSTANCE, null);
|
|
||||||
SQLiteDatabase db = Sqlite.getInstance(BackupStatusInDataBaseService.this, Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
|
SQLiteDatabase db = Sqlite.getInstance(BackupStatusInDataBaseService.this, Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
|
||||||
Account account = new AccountDAO(getApplicationContext(), db).getUniqAccount(userId, instance);
|
Account account = new AccountDAO(getApplicationContext(), db).getUniqAccount(userId, instance);
|
||||||
API api = new API(getApplicationContext(), account.getInstance(), account.getToken());
|
API api = new API(getApplicationContext(), account.getInstance(), account.getToken());
|
||||||
|
@ -126,8 +141,10 @@ public class BackupStatusInDataBaseService extends IntentService {
|
||||||
Intent mainActivity = new Intent(BackupStatusInDataBaseService.this, MainActivity.class);
|
Intent mainActivity = new Intent(BackupStatusInDataBaseService.this, MainActivity.class);
|
||||||
mainActivity.putExtra(Helper.INTENT_ACTION, Helper.BACKUP_INTENT);
|
mainActivity.putExtra(Helper.INTENT_ACTION, Helper.BACKUP_INTENT);
|
||||||
String title = getString(R.string.data_backup_toots, account.getAcct());
|
String title = getString(R.string.data_backup_toots, account.getAcct());
|
||||||
Helper.notify_user(getApplicationContext(),account, mainActivity, BitmapFactory.decodeResource(getResources(),
|
if(finalToastMessage) {
|
||||||
R.drawable.mastodonlogo), Helper.NotifType.BACKUP, title, message);
|
Helper.notify_user(getApplicationContext(), account, mainActivity, BitmapFactory.decodeResource(getResources(),
|
||||||
|
R.drawable.mastodonlogo), Helper.NotifType.BACKUP, title, message);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
message = getString(R.string.data_export_error, account.getAcct());
|
message = getString(R.string.data_export_error, account.getAcct());
|
||||||
|
@ -135,7 +152,9 @@ public class BackupStatusInDataBaseService extends IntentService {
|
||||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Toasty.error(getApplicationContext(), finalMessage, Toast.LENGTH_LONG).show();
|
if(finalToastMessage) {
|
||||||
|
Toasty.error(getApplicationContext(), finalMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -946,6 +946,41 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/set_backup"
|
||||||
|
android:layout_marginTop="@dimen/settings_checkbox_margin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/settings_checkbox_margin"
|
||||||
|
android:layout_marginBottom="@dimen/settings_checkbox_margin"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/set_auto_backup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:text="@string/set_auto_backup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<TextView
|
||||||
|
android:textColor="@color/mastodonC2"
|
||||||
|
android:text="@string/set_auto_backup_indication"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:text="@string/set_crash_reports"
|
android:text="@string/set_crash_reports"
|
||||||
android:layout_marginTop="@dimen/settings_checkbox_margin"
|
android:layout_marginTop="@dimen/settings_checkbox_margin"
|
||||||
|
|
|
@ -1143,6 +1143,9 @@
|
||||||
<string name="owner_charts">Charts</string>
|
<string name="owner_charts">Charts</string>
|
||||||
<string name="display_charts">Display charts</string>
|
<string name="display_charts">Display charts</string>
|
||||||
<string name="collecting_data_wait">The application collects your local data, please wait...</string>
|
<string name="collecting_data_wait">The application collects your local data, please wait...</string>
|
||||||
|
<string name="set_backup">Backup</string>
|
||||||
|
<string name="set_auto_backup">Auto backup statuses</string>
|
||||||
|
<string name="set_auto_backup_indication">This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts</string>
|
||||||
<plurals name="number_of_vote">
|
<plurals name="number_of_vote">
|
||||||
<item quantity="one">%d vote</item>
|
<item quantity="one">%d vote</item>
|
||||||
<item quantity="other">%d votes</item>
|
<item quantity="other">%d votes</item>
|
||||||
|
|
Loading…
Reference in New Issue