AntennaPod Echo (#6780)

This commit is contained in:
ByteHamster 2023-11-28 20:26:29 +01:00 committed by GitHub
parent 637230e382
commit ee554d0306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1481 additions and 9 deletions

View File

@ -89,6 +89,7 @@ dependencies {
implementation project(':storage:preferences')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
implementation project(':ui:echo')
implementation project(':ui:glide')
implementation project(':ui:i18n')
implementation project(':ui:statistics')

View File

@ -21,12 +21,14 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import de.danoeh.antennapod.ui.home.sections.EchoSection;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import de.danoeh.antennapod.R;
@ -60,6 +62,7 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis
public static final String PREF_NAME = "PrefHomeFragment";
public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString";
public static final String PREF_DISABLE_NOTIFICATION_PERMISSION_NAG = "DisableNotificationPermissionNag";
public static final String PREF_HIDE_ECHO = "HideEcho";
private static final String KEY_UP_ARROW = "up_arrow";
private boolean displayUpArrow;
@ -94,13 +97,19 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis
private void populateSectionList() {
viewBinding.homeContainer.removeAllViews();
SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
if (!prefs.getBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, false)) {
addSection(new AllowNotificationsSection());
}
}
if (Calendar.getInstance().get(Calendar.MONTH) == Calendar.DECEMBER
&& Calendar.getInstance().get(Calendar.YEAR) == 2023
&& Calendar.getInstance().get(Calendar.DAY_OF_MONTH) >= 10
&& prefs.getInt(PREF_HIDE_ECHO, 0) != 2023) {
addSection(new EchoSection());
}
List<String> hiddenSections = getHiddenSections(getContext());
String[] sectionTags = getResources().getStringArray(R.array.home_section_tags);

View File

@ -0,0 +1,77 @@
package de.danoeh.antennapod.ui.home.sections;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.StatisticsItem;
import de.danoeh.antennapod.databinding.HomeSectionEchoBinding;
import de.danoeh.antennapod.ui.echo.EchoActivity;
import de.danoeh.antennapod.ui.home.HomeFragment;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Calendar;
public class EchoSection extends Fragment {
private HomeSectionEchoBinding viewBinding;
private Disposable disposable;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewBinding = HomeSectionEchoBinding.inflate(inflater);
viewBinding.titleLabel.setText(getString(R.string.antennapod_echo_year, 2023));
viewBinding.echoButton.setOnClickListener(v -> startActivity(new Intent(getContext(), EchoActivity.class)));
viewBinding.closeButton.setOnClickListener(v -> {
getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE)
.edit().putInt(HomeFragment.PREF_HIDE_ECHO, 2023).apply();
((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null);
});
updateVisibility();
return viewBinding.getRoot();
}
private long jan1() {
Calendar date = Calendar.getInstance();
date.set(Calendar.HOUR_OF_DAY, 0);
date.set(Calendar.MINUTE, 0);
date.set(Calendar.SECOND, 0);
date.set(Calendar.MILLISECOND, 0);
date.set(Calendar.DAY_OF_MONTH, 1);
date.set(Calendar.MONTH, 0);
date.set(Calendar.YEAR, 2023);
return date.getTimeInMillis();
}
private void updateVisibility() {
if (disposable != null) {
disposable.dispose();
}
disposable = Observable.fromCallable(
() -> {
DBReader.StatisticsResult statisticsResult = DBReader.getStatistics(false, jan1(), Long.MAX_VALUE);
long totalTime = 0;
for (StatisticsItem feedTime : statisticsResult.feedTime) {
totalTime += feedTime.timePlayed;
}
return totalTime;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(totalTime -> viewBinding.getRoot()
.setVisibility((totalTime >= 3600 * 10) ? View.VISIBLE : View.GONE),
Throwable::printStackTrace);
}
}

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:layout_marginVertical="8dp"
android:accessibilityHeading="true"
android:layout_weight="1"
android:text="@string/echo_home_header" />
<ImageView
android:id="@+id/closeButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/close_label"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_cancel" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:cardCornerRadius="8dp"
app:cardElevation="0dp">
<LinearLayout
android:id="@+id/echoButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_blue_gradient"
android:orientation="vertical"
android:padding="16dp"
android:foreground="?attr/selectableItemBackground">
<TextView
android:id="@+id/titleLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textColor="#fff"
android:text="@string/antennapod_echo_year"
android:textFontWeight="500"
style="@style/TextAppearance.Material3.TitleLarge" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#fff"
android:layout_weight="1"
android:text="@string/echo_home_subtitle"
style="@style/TextAppearance.Material3.BodyMedium" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom"
android:textColor="#fff"
android:importantForAccessibility="no"
android:src="@drawable/ic_arrow_right_white" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@ -806,6 +806,17 @@ public final class DBReader {
return result;
}
public static long getTimeBetweenReleaseAndPlayback(long timeFilterFrom, long timeFilterTo) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try (Cursor cursor = adapter.getTimeBetweenReleaseAndPlayback(timeFilterFrom, timeFilterTo)) {
cursor.moveToFirst();
long result = Long.parseLong(cursor.getString(0));
adapter.close();
return result;
}
}
/**
* Returns data necessary for displaying the navigation drawer. This includes
* the list of subscriptions, the number of items in the queue and the number of unread

View File

@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.util;
import android.content.Context;
import android.content.res.Resources;
import java.util.Locale;
import de.danoeh.antennapod.core.R;
@ -82,17 +83,31 @@ public final class Converter {
* Converts milliseconds to a localized string containing hours and minutes.
*/
public static String getDurationStringLocalized(Context context, long duration) {
int h = (int) (duration / HOURS_MIL);
int rest = (int) (duration - h * HOURS_MIL);
int m = rest / MINUTES_MIL;
return getDurationStringLocalized(context.getResources(), duration);
}
public static String getDurationStringLocalized(Resources resources, long duration) {
String result = "";
if (h > 0) {
String hours = context.getResources().getQuantityString(R.plurals.time_hours_quantified, h, h);
result += hours + " ";
int h = (int) (duration / HOURS_MIL);
int d = h / 24;
if (d > 0) {
String days = resources.getQuantityString(R.plurals.time_days_quantified, d, d);
result += days.replace(" ", "\u00A0") + " ";
h -= d * 24;
}
int rest = (int) (duration - (d * 24 + h) * HOURS_MIL);
int m = rest / MINUTES_MIL;
if (h > 0) {
String hours = resources.getQuantityString(R.plurals.time_hours_quantified, h, h);
result += hours.replace(" ", "\u00A0");
if (d == 0) {
result += " ";
}
}
if (d == 0) {
String minutes = resources.getQuantityString(R.plurals.time_minutes_quantified, m, m);
result += minutes.replace(" ", "\u00A0");
}
String minutes = context.getResources().getQuantityString(R.plurals.time_minutes_quantified, m, m);
result += minutes;
return result;
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:angle="90"
android:endColor="@color/gradient_025"
android:startColor="@color/gradient_075"
android:type="linear" />
<corners
android:radius="0dp"/>
</shape>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
</vector>

View File

@ -23,4 +23,9 @@
<color name="accent_light">#0078C2</color>
<color name="accent_dark">#3D8BFF</color>
<color name="gradient_000">#364ff3</color>
<color name="gradient_025">#2E6FF6</color>
<color name="gradient_075">#1EB0FC</color>
<color name="gradient_100">#16d0ff</color>
</resources>

View File

@ -21,6 +21,7 @@ include ':storage:preferences'
include ':ui:app-start-intent'
include ':ui:common'
include ':ui:echo'
include ':ui:glide'
include ':ui:i18n'
include ':ui:png-icons'

View File

@ -1237,6 +1237,21 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
public final Cursor getTimeBetweenReleaseAndPlayback(long timeFilterFrom, long timeFilterTo) {
final String from = " FROM " + TABLE_NAME_FEED_ITEMS
+ JOIN_FEED_ITEM_AND_MEDIA
+ " WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME + ">=" + timeFilterFrom
+ " AND " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ">=" + timeFilterFrom
+ " AND " + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME + "<" + timeFilterTo;
final String query = "SELECT " + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME
+ " - " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " AS diff"
+ from
+ " ORDER BY diff ASC"
+ " LIMIT 1"
+ " OFFSET (SELECT count(*)/2 " + from + ")";
return db.rawQuery(query, null);
}
public int getQueueSize() {
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
Cursor c = db.rawQuery(query, null);

3
ui/echo/README.md Normal file
View File

@ -0,0 +1,3 @@
# :ui:echo
This module provides the "Echo" screen, a yearly rewind.

27
ui/echo/build.gradle Normal file
View File

@ -0,0 +1,27 @@
plugins {
id("com.android.library")
}
apply from: "../../common.gradle"
apply from: "../../playFlavor.gradle"
android {
namespace "de.danoeh.antennapod.ui.echo"
lint {
disable "AppBundleLocaleChanges"
}
}
dependencies {
implementation project(":core")
implementation project(":model")
implementation project(":storage:preferences")
implementation project(':ui:glide')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "com.google.android.material:material:$googleMaterialVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "com.github.bumptech.glide:glide:$glideVersion"
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true">
<activity
android:label="@string/antennapod_echo"
android:name=".EchoActivity"
android:exported="false"
android:theme="@style/Theme.AntennaPod.Dark.NoTitle">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,399 @@
package de.danoeh.antennapod.ui.echo;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import androidx.core.view.WindowCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.StatisticsItem;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.echo.databinding.EchoActivityBinding;
import de.danoeh.antennapod.ui.echo.screens.BubbleScreen;
import de.danoeh.antennapod.ui.echo.screens.FinalShareScreen;
import de.danoeh.antennapod.ui.echo.screens.RotatingSquaresScreen;
import de.danoeh.antennapod.ui.echo.screens.StripesScreen;
import de.danoeh.antennapod.ui.echo.screens.WaveformScreen;
import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class EchoActivity extends AppCompatActivity {
private static final String TAG = "EchoActivity";
private static final int NUM_SCREENS = 7;
private static final int SHARE_SIZE = 1000;
private EchoActivityBinding viewBinding;
private int currentScreen = -1;
private boolean progressPaused = false;
private float progress = 0;
private Drawable currentDrawable;
private EchoProgress echoProgress;
private Disposable redrawTimer;
private long timeTouchDown;
private long timeLastFrame;
private Disposable disposable;
private long totalTime = 0;
private int totalActivePodcasts = 0;
private int playedPodcasts = 0;
private int playedActivePodcasts = 0;
private String randomUnplayedActivePodcast = "";
private int queueNumEpisodes = 0;
private long queueSecondsLeft = 0;
private long timeBetweenReleaseAndPlay = 0;
private long oldestDate = 0;
private final ArrayList<Pair<String, Drawable>> favoritePods = new ArrayList<>();
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
super.onCreate(savedInstanceState);
viewBinding = EchoActivityBinding.inflate(getLayoutInflater());
viewBinding.closeButton.setOnClickListener(v -> finish());
viewBinding.shareButton.setOnClickListener(v -> share());
viewBinding.echoImage.setOnTouchListener((v, event) -> {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
progressPaused = true;
timeTouchDown = System.currentTimeMillis();
} else if (event.getAction() == KeyEvent.ACTION_UP) {
progressPaused = false;
if (timeTouchDown + 500 > System.currentTimeMillis()) {
int newScreen;
if (event.getX() < 0.5f * viewBinding.echoImage.getMeasuredWidth()) {
newScreen = Math.max(currentScreen - 1, 0);
} else {
newScreen = Math.min(currentScreen + 1, NUM_SCREENS - 1);
if (currentScreen == NUM_SCREENS - 1) {
finish();
}
}
progress = newScreen;
echoProgress.setProgress(progress);
loadScreen(newScreen, false);
}
}
return true;
});
echoProgress = new EchoProgress(NUM_SCREENS);
viewBinding.echoProgressImage.setImageDrawable(echoProgress);
setContentView(viewBinding.getRoot());
loadScreen(0, false);
loadStatistics();
}
private void share() {
try {
Bitmap bitmap = Bitmap.createBitmap(SHARE_SIZE, SHARE_SIZE, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
currentDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
currentDrawable.draw(canvas);
viewBinding.echoImage.setImageDrawable(null);
viewBinding.echoImage.setImageDrawable(currentDrawable);
File file = new File(UserPreferences.getDataFolder(null), "AntennaPodEcho.png");
FileOutputStream stream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
stream.close();
Uri fileUri = FileProvider.getUriForFile(this, getString(R.string.provider_authority), file);
new ShareCompat.IntentBuilder(this)
.setType("image/png")
.addStream(fileUri)
.setText(getString(R.string.echo_share, 2023))
.setChooserTitle(R.string.share_file_label)
.startChooser();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onStart() {
super.onStart();
redrawTimer = Flowable.timer(20, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.io())
.repeat()
.subscribe(i -> {
if (progressPaused) {
return;
}
viewBinding.echoImage.postInvalidate();
if (progress >= NUM_SCREENS - 0.001f) {
return;
}
long timePassed = System.currentTimeMillis() - timeLastFrame;
timeLastFrame = System.currentTimeMillis();
if (timePassed > 500) {
timePassed = 0;
}
progress = Math.min(NUM_SCREENS - 0.001f, progress + timePassed / 10000.0f);
echoProgress.setProgress(progress);
viewBinding.echoProgressImage.postInvalidate();
loadScreen((int) progress, false);
});
}
@Override
protected void onStop() {
super.onStop();
redrawTimer.dispose();
if (disposable != null) {
disposable.dispose();
}
}
private void loadScreen(int screen, boolean force) {
if (screen == currentScreen && !force) {
return;
}
currentScreen = screen;
runOnUiThread(() -> {
viewBinding.echoLogo.setVisibility(currentScreen == 0 ? View.VISIBLE : View.GONE);
viewBinding.shareButton.setVisibility(currentScreen == 6 ? View.VISIBLE : View.GONE);
switch (currentScreen) {
case 0:
viewBinding.aboveLabel.setText(R.string.echo_intro_your_year);
viewBinding.largeLabel.setText(String.format(getEchoLanguage(), "%d", 2023));
viewBinding.belowLabel.setText(R.string.echo_intro_in_podcasts);
viewBinding.smallLabel.setText(R.string.echo_intro_locally);
currentDrawable = new BubbleScreen(this);
break;
case 1:
viewBinding.aboveLabel.setText(R.string.echo_hours_this_year);
viewBinding.largeLabel.setText(String.format(getEchoLanguage(), "%d", totalTime / 3600));
viewBinding.belowLabel.setText(getResources()
.getQuantityString(R.plurals.echo_hours_podcasts, playedPodcasts, playedPodcasts));
viewBinding.smallLabel.setText("");
currentDrawable = new WaveformScreen(this);
break;
case 2:
viewBinding.largeLabel.setText(String.format(getEchoLanguage(), "%d", queueSecondsLeft / 3600));
viewBinding.belowLabel.setText(getResources().getQuantityString(
R.plurals.echo_queue_hours_waiting, queueNumEpisodes, queueNumEpisodes));
int daysUntil2024 = Math.max(356 - Calendar.getInstance().get(Calendar.DAY_OF_YEAR) + 1, 1);
long secondsPerDay = queueSecondsLeft / daysUntil2024;
String timePerDay = Converter.getDurationStringLocalized(
getLocalizedResources(this, getEchoLanguage()), secondsPerDay * 1000);
double hoursPerDay = (double) (secondsPerDay / 3600);
if (hoursPerDay < 1.5) {
viewBinding.aboveLabel.setText(R.string.echo_queue_title_clean);
viewBinding.smallLabel.setText(getString(R.string.echo_queue_hours_clean, timePerDay, 2024));
} else if (hoursPerDay <= 24) {
viewBinding.aboveLabel.setText(R.string.echo_queue_title_many);
viewBinding.smallLabel.setText(getString(R.string.echo_queue_hours_normal, timePerDay, 2024));
} else {
viewBinding.aboveLabel.setText(R.string.echo_queue_title_many);
viewBinding.smallLabel.setText(getString(R.string.echo_queue_hours_much, timePerDay, 2024));
}
currentDrawable = new StripesScreen(this);
break;
case 3:
viewBinding.aboveLabel.setText(R.string.echo_listened_after_title);
if (timeBetweenReleaseAndPlay <= 1000L * 3600 * 24 * 2.5) {
viewBinding.largeLabel.setText(R.string.echo_listened_after_emoji_run);
viewBinding.belowLabel.setText(R.string.echo_listened_after_comment_addict);
} else {
viewBinding.largeLabel.setText(R.string.echo_listened_after_emoji_yoga);
viewBinding.belowLabel.setText(R.string.echo_listened_after_comment_easy);
}
viewBinding.smallLabel.setText(getString(R.string.echo_listened_after_time,
Converter.getDurationStringLocalized(
getLocalizedResources(this, getEchoLanguage()), timeBetweenReleaseAndPlay)));
currentDrawable = new RotatingSquaresScreen(this);
break;
case 4:
viewBinding.aboveLabel.setText(R.string.echo_hoarder_title);
int percentagePlayed = (int) (100.0 * playedActivePodcasts / totalActivePodcasts);
if (percentagePlayed < 25) {
viewBinding.largeLabel.setText(R.string.echo_hoarder_emoji_cabinet);
viewBinding.belowLabel.setText(R.string.echo_hoarder_subtitle_hoarder);
viewBinding.smallLabel.setText(getString(R.string.echo_hoarder_comment_hoarder,
percentagePlayed, totalActivePodcasts));
} else if (percentagePlayed < 75) {
viewBinding.largeLabel.setText(R.string.echo_hoarder_emoji_check);
viewBinding.belowLabel.setText(R.string.echo_hoarder_subtitle_medium);
viewBinding.smallLabel.setText(getString(R.string.echo_hoarder_comment_medium,
percentagePlayed, totalActivePodcasts, randomUnplayedActivePodcast));
} else {
viewBinding.largeLabel.setText(R.string.echo_hoarder_emoji_clean);
viewBinding.belowLabel.setText(R.string.echo_hoarder_subtitle_clean);
viewBinding.smallLabel.setText(getString(R.string.echo_hoarder_comment_clean,
percentagePlayed, totalActivePodcasts));
}
currentDrawable = new StripesScreen(this);
break;
case 5:
viewBinding.aboveLabel.setText("");
viewBinding.largeLabel.setText(R.string.echo_thanks_large);
if (oldestDate < jan1()) {
SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM yyyy", getEchoLanguage());
String dateFrom = dateFormat.format(new Date(oldestDate));
viewBinding.belowLabel.setText(getString(R.string.echo_thanks_we_are_glad_old, dateFrom));
} else {
viewBinding.belowLabel.setText(R.string.echo_thanks_we_are_glad_new);
}
viewBinding.smallLabel.setText(R.string.echo_thanks_now_favorite);
currentDrawable = new RotatingSquaresScreen(this);
break;
case 6:
viewBinding.aboveLabel.setText("");
viewBinding.largeLabel.setText("");
viewBinding.belowLabel.setText("");
viewBinding.smallLabel.setText("");
currentDrawable = new FinalShareScreen(this, favoritePods);
break;
default: // Keep
}
viewBinding.echoImage.setImageDrawable(currentDrawable);
});
}
private Locale getEchoLanguage() {
boolean hasTranslation = !getString(R.string.echo_listened_after_title)
.equals(getLocalizedResources(this, Locale.US).getString(R.string.echo_listened_after_title));
if (hasTranslation) {
return Locale.getDefault();
} else {
return Locale.US;
}
}
@NonNull
private Resources getLocalizedResources(Context context, Locale desiredLocale) {
Configuration conf = context.getResources().getConfiguration();
conf = new Configuration(conf);
conf.setLocale(desiredLocale);
Context localizedContext = context.createConfigurationContext(conf);
return localizedContext.getResources();
}
private long jan1() {
Calendar date = Calendar.getInstance();
date.set(Calendar.HOUR_OF_DAY, 0);
date.set(Calendar.MINUTE, 0);
date.set(Calendar.SECOND, 0);
date.set(Calendar.MILLISECOND, 0);
date.set(Calendar.DAY_OF_MONTH, 1);
date.set(Calendar.MONTH, 0);
date.set(Calendar.YEAR, 2023);
return date.getTimeInMillis();
}
private void loadStatistics() {
if (disposable != null) {
disposable.dispose();
}
long timeFilterFrom = jan1();
long timeFilterTo = Long.MAX_VALUE;
disposable = Observable.fromCallable(
() -> {
DBReader.StatisticsResult statisticsData = DBReader.getStatistics(
false, timeFilterFrom, timeFilterTo);
Collections.sort(statisticsData.feedTime, (item1, item2) ->
Long.compare(item2.timePlayed, item1.timePlayed));
favoritePods.clear();
for (int i = 0; i < 5 && i < statisticsData.feedTime.size(); i++) {
BitmapDrawable cover = new BitmapDrawable(getResources(), (Bitmap) null);
try {
final int size = SHARE_SIZE / 3;
final int radius = (i == 0) ? (size / 16) : (size / 8);
cover = new BitmapDrawable(getResources(), Glide.with(this)
.asBitmap()
.load(statisticsData.feedTime.get(i).feed.getImageUrl())
.apply(new RequestOptions()
.fitCenter()
.transform(new RoundedCorners(radius)))
.submit(size, size)
.get(1, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
favoritePods.add(new Pair<>(statisticsData.feedTime.get(i).feed.getTitle(), cover));
}
totalActivePodcasts = 0;
playedActivePodcasts = 0;
playedPodcasts = 0;
totalTime = 0;
ArrayList<String> unplayedActive = new ArrayList<>();
for (StatisticsItem item : statisticsData.feedTime) {
totalTime += item.timePlayed;
if (item.timePlayed > 0) {
playedPodcasts++;
}
if (item.feed.getPreferences().getKeepUpdated()) {
totalActivePodcasts++;
if (item.timePlayed > 0) {
playedActivePodcasts++;
} else {
unplayedActive.add(item.feed.getTitle());
}
}
}
if (!unplayedActive.isEmpty()) {
randomUnplayedActivePodcast = unplayedActive.get((int) (Math.random() * unplayedActive.size()));
}
List<FeedItem> queue = DBReader.getQueue();
queueNumEpisodes = queue.size();
queueSecondsLeft = 0;
for (FeedItem item : queue) {
float playbackSpeed = 1;
if (UserPreferences.timeRespectsSpeed()) {
playbackSpeed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(item.getMedia());
}
if (item.getMedia() != null) {
long itemTimeLeft = item.getMedia().getDuration() - item.getMedia().getPosition();
queueSecondsLeft += itemTimeLeft / playbackSpeed;
}
}
queueSecondsLeft /= 1000;
timeBetweenReleaseAndPlay = DBReader.getTimeBetweenReleaseAndPlayback(timeFilterFrom, timeFilterTo);
oldestDate = statisticsData.oldestDate;
return statisticsData;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> loadScreen(currentScreen, true),
error -> Log.e(TAG, Log.getStackTraceString(error)));
}
}

View File

@ -0,0 +1,64 @@
package de.danoeh.antennapod.ui.echo;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
public class EchoProgress extends Drawable {
private final Paint paint;
private final int numScreens;
private float progress = 0;
public EchoProgress(int numScreens) {
this.numScreens = numScreens;
paint = new Paint();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setColor(0xffffffff);
}
public void setProgress(float progress) {
this.progress = progress;
}
@Override
public void draw(@NonNull Canvas canvas) {
paint.setStrokeWidth(0.5f * getBounds().height());
float y = 0.5f * getBounds().height();
float sectionWidth = 1.0f * getBounds().width() / numScreens;
float sectionPadding = 0.03f * sectionWidth;
for (int i = 0; i < numScreens; i++) {
if (i + 1 < progress) {
paint.setAlpha(255);
} else {
paint.setAlpha(100);
}
canvas.drawLine(i * sectionWidth + sectionPadding, y, (i + 1) * sectionWidth - sectionPadding, y, paint);
if (Math.floor(1.0 * i) == Math.floor(progress)) {
paint.setAlpha(255);
canvas.drawLine(i * sectionWidth + sectionPadding, y, i * sectionWidth + sectionPadding
+ (progress - i) * (sectionWidth - 2 * sectionPadding), y, paint);
}
}
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
}

View File

@ -0,0 +1,96 @@
package de.danoeh.antennapod.ui.echo.screens;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import de.danoeh.antennapod.ui.echo.R;
public abstract class BaseScreen extends Drawable {
private final Paint paintBackground;
protected final Paint paintParticles;
protected final ArrayList<Particle> particles = new ArrayList<>();
private final int colorBackgroundFrom;
private final int colorBackgroundTo;
private long lastFrame = 0;
public BaseScreen(Context context) {
colorBackgroundFrom = ContextCompat.getColor(context, R.color.gradient_000);
colorBackgroundTo = ContextCompat.getColor(context, R.color.gradient_100);
paintBackground = new Paint();
paintParticles = new Paint();
paintParticles.setColor(0xffffffff);
paintParticles.setFlags(Paint.ANTI_ALIAS_FLAG);
paintParticles.setStyle(Paint.Style.FILL);
paintParticles.setAlpha(25);
}
@Override
public void draw(@NonNull Canvas canvas) {
float width = getBounds().width();
float height = getBounds().height();
paintBackground.setShader(new LinearGradient(0, 0, 0, height,
colorBackgroundFrom, colorBackgroundTo, Shader.TileMode.CLAMP));
canvas.drawRect(0, 0, width, height, paintBackground);
long timeSinceLastFrame = System.currentTimeMillis() - lastFrame;
lastFrame = System.currentTimeMillis();
if (timeSinceLastFrame > 500) {
timeSinceLastFrame = 0;
}
final float innerBoxSize = (Math.abs(width - height) < 0.001f) // Square share version
? (0.9f * width) : (0.9f * Math.min(width, 0.7f * height));
final float innerBoxX = (width - innerBoxSize) / 2;
final float innerBoxY = (height - innerBoxSize) / 2;
for (Particle p : particles) {
drawParticle(canvas, p, width, height, innerBoxX, innerBoxY, innerBoxSize);
particleTick(p, timeSinceLastFrame);
}
drawInner(canvas, innerBoxX, innerBoxY, innerBoxSize);
}
protected void drawInner(Canvas canvas, float innerBoxX, float innerBoxY, float innerBoxSize) {
}
protected abstract void particleTick(Particle p, long timeSinceLastFrame);
protected abstract void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
float innerBoxX, float innerBoxY, float innerBoxSize);
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
protected static class Particle {
double positionX;
double positionY;
double positionZ;
double speed;
public Particle(double positionX, double positionY, double positionZ, double speed) {
this.positionX = positionX;
this.positionY = positionY;
this.positionZ = positionZ;
this.speed = speed;
}
}
}

View File

@ -0,0 +1,35 @@
package de.danoeh.antennapod.ui.echo.screens;
import android.content.Context;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
public class BubbleScreen extends BaseScreen {
protected static final double PARTICLE_SPEED = 0.00002;
protected static final int NUM_PARTICLES = 15;
public BubbleScreen(Context context) {
super(context);
for (int i = 0; i < NUM_PARTICLES; i++) {
particles.add(new Particle(Math.random(), 2.0 * Math.random() - 0.5, // Could already be off-screen
0, PARTICLE_SPEED + 2 * PARTICLE_SPEED * Math.random()));
}
}
@Override
protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
float innerBoxX, float innerBoxY, float innerBoxSize) {
canvas.drawCircle((float) (width * p.positionX), (float) (p.positionY * height),
innerBoxSize / 5, paintParticles);
}
@Override
protected void particleTick(Particle p, long timeSinceLastFrame) {
p.positionY -= p.speed * timeSinceLastFrame;
if (p.positionY < -0.5) {
p.positionX = Math.random();
p.positionY = 1.5f;
p.speed = PARTICLE_SPEED + 2 * PARTICLE_SPEED * Math.random();
}
}
}

View File

@ -0,0 +1,89 @@
package de.danoeh.antennapod.ui.echo.screens;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.Pair;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.res.ResourcesCompat;
import de.danoeh.antennapod.ui.echo.R;
import java.util.ArrayList;
public class FinalShareScreen extends BubbleScreen {
private static final float[][] COVER_POSITIONS = new float[][]{ new float[] {0.0f, 0.0f},
new float[] {0.4f, 0.0f}, new float[] {0.4f, 0.2f}, new float[] {0.6f, 0.2f}, new float[] {0.8f, 0.2f}};
private final Paint paintTextMain;
private final Paint paintCoverBorder;
private final String heading;
private final Drawable logo;
private final ArrayList<Pair<String, Drawable>> favoritePods;
private final Typeface typefaceNormal;
private final Typeface typefaceBold;
public FinalShareScreen(Context context, ArrayList<Pair<String, Drawable>> favoritePods) {
super(context);
this.heading = context.getString(R.string.echo_share_heading);
this.logo = AppCompatResources.getDrawable(context, R.drawable.echo);
this.favoritePods = favoritePods;
typefaceNormal = ResourcesCompat.getFont(context, R.font.sarabun_regular);
typefaceBold = ResourcesCompat.getFont(context, R.font.sarabun_semi_bold);
paintTextMain = new Paint();
paintTextMain.setColor(0xffffffff);
paintTextMain.setFlags(Paint.ANTI_ALIAS_FLAG);
paintTextMain.setStyle(Paint.Style.FILL);
paintCoverBorder = new Paint();
paintCoverBorder.setColor(0xffffffff);
paintCoverBorder.setFlags(Paint.ANTI_ALIAS_FLAG);
paintCoverBorder.setStyle(Paint.Style.FILL);
paintCoverBorder.setAlpha(70);
}
protected void drawInner(Canvas canvas, float innerBoxX, float innerBoxY, float innerBoxSize) {
paintTextMain.setTextAlign(Paint.Align.CENTER);
paintTextMain.setTypeface(typefaceBold);
float headingSize = innerBoxSize / 14;
paintTextMain.setTextSize(headingSize);
canvas.drawText(heading, innerBoxX + 0.5f * innerBoxSize, innerBoxY + headingSize, paintTextMain);
paintTextMain.setTextSize(0.12f * innerBoxSize);
canvas.drawText("2023", innerBoxX + 0.8f * innerBoxSize, innerBoxY + 0.25f * innerBoxSize, paintTextMain);
paintTextMain.setTextAlign(Paint.Align.LEFT);
float fontSizePods = innerBoxSize / 18; // First one only
float textY = innerBoxY + 0.62f * innerBoxSize;
for (int i = 0; i < favoritePods.size(); i++) {
float coverSize = (i == 0) ? (0.4f * innerBoxSize) : (0.2f * innerBoxSize);
float coverX = COVER_POSITIONS[i][0];
float coverY = COVER_POSITIONS[i][1];
RectF logo1Pos = new RectF(innerBoxX + coverX * innerBoxSize,
innerBoxY + (coverY + 0.12f) * innerBoxSize,
innerBoxX + coverX * innerBoxSize + coverSize,
innerBoxY + (coverY + 0.12f) * innerBoxSize + coverSize);
logo1Pos.inset((int) (0.01f * innerBoxSize), (int) (0.01f * innerBoxSize));
float radius = (i == 0) ? (coverSize / 16) : (coverSize / 8);
canvas.drawRoundRect(logo1Pos, radius, radius, paintCoverBorder);
logo1Pos.inset((int) (0.003f * innerBoxSize), (int) (0.003f * innerBoxSize));
Rect pos = new Rect();
logo1Pos.round(pos);
favoritePods.get(i).second.setBounds(pos);
favoritePods.get(i).second.draw(canvas);
paintTextMain.setTextSize(fontSizePods);
canvas.drawText((i + 1) + ".", innerBoxX, textY, paintTextMain);
canvas.drawText(favoritePods.get(i).first, innerBoxX + 0.055f * innerBoxSize, textY, paintTextMain);
fontSizePods = innerBoxSize / 24; // Starting with second text is smaller
textY += 1.3f * fontSizePods;
paintTextMain.setTypeface(typefaceNormal);
}
double ratio = (1.0 * logo.getIntrinsicHeight()) / logo.getIntrinsicWidth();
logo.setBounds((int) (innerBoxX + 0.1 * innerBoxSize),
(int) (innerBoxY + innerBoxSize - 0.8 * innerBoxSize * ratio),
(int) (innerBoxX + 0.9 * innerBoxSize),
(int) (innerBoxY + innerBoxSize));
logo.draw(canvas);
}
}

View File

@ -0,0 +1,37 @@
package de.danoeh.antennapod.ui.echo.screens;
import android.content.Context;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
public class RotatingSquaresScreen extends BaseScreen {
public RotatingSquaresScreen(Context context) {
super(context);
for (int i = 0; i < 16; i++) {
particles.add(new Particle(
0.3 * (float) (i % 4) + 0.05 + 0.1 * Math.random() - 0.05,
0.2 * (float) (i / 4) + 0.20 + 0.1 * Math.random() - 0.05,
Math.random(), 0.00001 * (2 * Math.random() + 2)));
}
}
@Override
protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
float innerBoxX, float innerBoxY, float innerBoxSize) {
float x = (float) (p.positionX * width);
float y = (float) (p.positionY * height);
float size = innerBoxSize / 6;
canvas.save();
canvas.rotate((float) (360 * p.positionZ), x, y);
canvas.drawRect(x - size, y - size, x + size, y + size, paintParticles);
canvas.restore();
}
@Override
protected void particleTick(Particle p, long timeSinceLastFrame) {
p.positionZ += p.speed * timeSinceLastFrame;
if (p.positionZ > 1) {
p.positionZ -= 1;
}
}
}

View File

@ -0,0 +1,38 @@
package de.danoeh.antennapod.ui.echo.screens;
import android.content.Context;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
public class StripesScreen extends BaseScreen {
protected static final int NUM_PARTICLES = 15;
public StripesScreen(Context context) {
super(context);
for (int i = 0; i < NUM_PARTICLES; i++) {
particles.add(new Particle(2f * i / NUM_PARTICLES - 1f, 0, 0, 0));
}
}
@Override
public void draw(@NonNull Canvas canvas) {
paintParticles.setStrokeWidth(0.05f * getBounds().width());
super.draw(canvas);
}
@Override
protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
float innerBoxX, float innerBoxY, float innerBoxSize) {
float strokeWidth = 0.05f * width;
float x = (float) (width * p.positionX);
canvas.drawLine(x, -strokeWidth, x + width, height + strokeWidth, paintParticles);
}
@Override
protected void particleTick(Particle p, long timeSinceLastFrame) {
p.positionX += 0.00005 * timeSinceLastFrame;
if (p.positionX > 1f) {
p.positionX -= 2f;
}
}
}

View File

@ -0,0 +1,39 @@
package de.danoeh.antennapod.ui.echo.screens;
import android.content.Context;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
public class WaveformScreen extends BaseScreen {
protected static final int NUM_PARTICLES = 40;
public WaveformScreen(Context context) {
super(context);
for (int i = 0; i < NUM_PARTICLES; i++) {
particles.add(new Particle(1.1f + 1.1f * i / NUM_PARTICLES - 0.05f, 0, 0, 0));
}
}
@Override
protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
float innerBoxX, float innerBoxY, float innerBoxSize) {
float x = (float) (width * p.positionX);
canvas.drawRect(x, height, x + (1.1f * width) / NUM_PARTICLES,
(float) (0.95f * height - 0.3f * p.positionY * height), paintParticles);
}
@Override
protected void particleTick(Particle p, long timeSinceLastFrame) {
p.positionX += 0.0001 * timeSinceLastFrame;
if (p.positionY <= 0.2 || p.positionY >= 1) {
p.speed = -p.speed;
p.positionY -= p.speed * timeSinceLastFrame;
}
p.positionY -= p.speed * timeSinceLastFrame;
if (p.positionX > 1.05f) {
p.positionX -= 1.1;
p.positionY = 0.2 + 0.8 * Math.random();
p.speed = 0.0008 * Math.random() - 0.0004;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/echoImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/echoProgressImage"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
<ImageView
android:id="@+id/closeButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="16dp"
android:src="@drawable/ic_close_white"
android:contentDescription="@string/close_label"
android:layout_alignParentEnd="true"
android:layout_below="@id/echoProgressImage" />
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="16dp"
android:src="@drawable/logo_monochrome"
android:importantForAccessibility="no"
android:layout_alignParentStart="true"
android:layout_below="@id/echoProgressImage" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="32dp"
android:orientation="vertical">
<TextView
android:id="@+id/aboveLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:fontFamily="@font/sarabun_regular"
app:fontFamily="@font/sarabun_regular"
tools:text="text above"
style="@style/TextAppearance.Material3.TitleLarge" />
<TextView
android:id="@+id/largeLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:layout_marginVertical="8dp"
android:fontFamily="@font/sarabun_semi_bold"
app:fontFamily="@font/sarabun_semi_bold"
tools:text="large"
style="@style/TextAppearance.Material3.DisplayLarge"
tools:targetApi="p" />
<TextView
android:id="@+id/belowLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:fontFamily="@font/sarabun_regular"
app:fontFamily="@font/sarabun_regular"
tools:text="text below"
style="@style/TextAppearance.Material3.TitleLarge" />
<TextView
android:id="@+id/smallLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:textSize="16sp"
android:layout_marginTop="32dp"
android:fontFamily="@font/sarabun_regular"
app:fontFamily="@font/sarabun_regular"
tools:text="small" />
</LinearLayout>
<ImageView
android:id="@+id/echoLogo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="32dp"
android:src="@drawable/echo"
android:importantForAccessibility="no"
android:layout_alignParentBottom="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/shareButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_margin="32dp"
android:text="@string/share_label"
android:drawableLeft="@drawable/ic_share"
android:textColor="#fff"
android:contentDescription="@string/share_label"
style="@style/Widget.Material3.Button.OutlinedButton"
app:strokeColor="#fff"
tools:ignore="RtlHardcoded" />
</RelativeLayout>
</RelativeLayout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="echo_home_header">Jahresrückblick</string>
<string name="echo_home_subtitle">Deine Lieblings-Podcasts und Statistiken aus dem letzten Jahr. Exklusiv auf deinem Telefon.</string>
<string name="echo_intro_your_year">Dein Podcast-Jahr</string>
<string name="echo_intro_in_podcasts"> </string>
<string name="echo_intro_locally">privat auf deinem Telefon generiert</string>
<string name="echo_hours_this_year">Dieses Jahr hast du</string>
<plurals name="echo_hours_podcasts">
<item quantity="one">Stunden an Episoden von %1$d Podcast abgespielt</item>
<item quantity="other">Stunden an Episoden von %1$d Podcasts abgespielt</item>
</plurals>
<string name="echo_queue_title_clean">Und du bist bereit, das neue Jahr frisch zu starten. In deiner Warteschlange liegen</string>
<string name="echo_queue_title_many">Und du hast dieses Jahr noch eine ganze Menge vor dir. In deiner Warteschlange liegen</string>
<plurals name="echo_queue_hours_waiting">
<item quantity="one">Stunden aus %1$d Podcast-Episode</item>
<item quantity="other">Stunden aus %1$d Podcast-Episoden</item>
</plurals>
<string name="echo_queue_hours_clean">Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt</string>
<string name="echo_queue_hours_normal">Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt. Du kannst frisch ins Jahr starten, wenn du einige Episoden überspringst.</string>
<string name="echo_queue_hours_much">Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt. Moment, was?</string>
<string name="echo_listened_after_title">Wir haben uns angeschaut, wann Episoden veröffentlicht werden und wann du sie abspielst. Unsere Folgerung?</string>
<string name="echo_listened_after_comment_easy">Du bist entspannt</string>
<string name="echo_listened_after_time">Typischerweise spielst du eine Episode %1$s nach der Veröffentlichung ab.</string>
<string name="echo_listened_after_comment_addict">Du bist ein Podcast-Junkie</string>
<string name="echo_hoarder_title">Wir haben uns auch gefragt: hörst du die Podcasts an, die du abonniert hast?</string>
<string name="echo_hoarder_subtitle_hoarder">Wenn wir uns die Zahlen so anschauen, glauben wir, du hamsterst Podcasts</string>
<string name="echo_hoarder_comment_hoarder">Zahlen lügen nicht, sagt man. Du hast dieses Jahr nur %1$d%% deiner %2$d aktiven Abonnements abgespielt, also liegen wir wahrscheinlich richtig.</string>
<string name="echo_hoarder_subtitle_medium">Du hamsterst keine Podcasts</string>
<string name="echo_hoarder_comment_medium">Du hast dieses Jahr %1$d%% deiner %2$d aktiven Abonnements abgespielt. Wie wäre es damit, mal wieder \"%3$s\" anzuhören?</string>
<string name="echo_hoarder_subtitle_clean">Aufgeräumt!</string>
<string name="echo_hoarder_comment_clean">Du hast dieses Jahr %1$d%% deiner %2$d aktiven Abonnements abgespielt. Wetten, du hältst auch deinen Schreibtisch sauber?</string>
<string name="echo_thanks_large">Danke</string>
<string name="echo_thanks_we_are_glad_old">dass du dieses Jahr wieder dabei warst!\n\nDu hast deine erste Episode im %1$s abgespielt. Es ist uns eine Ehre, seitdem für dich da zu sein.</string>
<string name="echo_thanks_we_are_glad_new">dass du dich dieses Jahr für uns entschieden hast!\n\nEgal ob du von einer anderen App gekommen bist oder mit AntennaPod in Podcasts eingestiegen bist: Wir sind froh, dass du da bist!</string>
<string name="echo_thanks_now_favorite">Schauen wir uns jetzt noch deine Lieblings-Podcasts an…</string>
<string name="echo_share_heading">Meine Lieblings-Podcasts</string>
<string name="echo_share">Mein Podcast-Jahr %d. #AntennaPodEcho</string>
</resources>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation,PluralsCandidate">
<string name="echo_home_header">Revisa el año</string>
<string name="echo_home_subtitle">Tus mejores pódcasts y estadísticas del año pasado. Exclusivamente en tu teléfono.</string>
<string name="echo_intro_your_year">Tu año</string>
<string name="echo_intro_in_podcasts">en pódcasts</string>
<string name="echo_intro_locally">generado de forma privada en tu teléfono</string>
<string name="echo_hours_this_year">Este año has reproducido</string>
<plurals name="echo_hours_podcasts">
<item quantity="one">horas de episodios\nde %1$d pódcast</item>
<item quantity="other">horas de episodios\nde %1$d pódcasts diferentes</item>
</plurals>
<string name="echo_queue_title_clean">Y estás listo para empezar el año de nuevo. Tienes</string>
<string name="echo_queue_title_many">Y aún te queda bastante este año. Tienes</string>
<plurals name="echo_queue_hours_waiting">
<item quantity="one">hora esperando en tu cola\nde %1$d episodio</item>
<item quantity="other">horas esperando en tu cola\nde %1$d episodios</item>
</plurals>
<string name="echo_queue_hours_clean">Eso es alrededor de %1$s cada día hasta que empiece %2$d</string>
<string name="echo_queue_hours_normal">Eso son %1$s cada día hasta que empiece %2$d. Puedes empezar el año de cero si te saltas algunos episodios.</string>
<string name="echo_queue_hours_much">Eso son %1$s cada día hasta que empiece %2$d. Espera, ¿qué?</string>
<string name="echo_listened_after_title">Hemos analizado cuándo se publican los episodios y cuándo los completaste. ¿Nuestra conclusión?</string>
<string name="echo_listened_after_comment_easy">Eres relajado</string>
<string name="echo_listened_after_time">Normalmente, completaste un episodio %1$s después de su publicación.</string>
<string name="echo_listened_after_comment_addict">Eres un adicto a los pódcasts</string>
<string name="echo_hoarder_title">También nos hemos preguntado: ¿Escuchas los pódcasts a los que estás suscrito?</string>
<string name="echo_hoarder_subtitle_hoarder">Viendo los números, creemos que eres un acumulador</string>
<string name="echo_hoarder_comment_hoarder">Dicen que los números no mienten. Y con solo %1$d%% de tus %2$d suscripciones activas reproducidas este año, probablemente tengamos razón.</string>
<string name="echo_hoarder_subtitle_medium">Mira. Aquí no hay acumulación.</string>
<string name="echo_hoarder_comment_medium">Has reproducido episodios de %1$d%% de tus %2$d suscripciones activas este año. ¿Qué tal si vuelves a escuchar \"%3$s\"?</string>
<string name="echo_hoarder_subtitle_clean">¡Limpio!</string>
<string name="echo_hoarder_comment_clean">Has reproducido episodios de %1$d%% de tus %2$d suscripciones activas este año. ¡Apostamos que también mantienes limpio tu escritorio!</string>
<string name="echo_thanks_large">¡Gracias</string>
<string name="echo_thanks_we_are_glad_old">por estar con nosotros este año!\n\nReproduciste tu primer episodio con nosotros en %1$s. Ha sido un honor servirte desde entonces.</string>
<string name="echo_thanks_we_are_glad_new">por unirte a nosotros este año!\n\nWTanto si vienes de otra aplicación como si has empezado tu aventura con los pódcasts con nosotros, ¡Estamos encantados de tenerte!</string>
<string name="echo_thanks_now_favorite">Ahora, echemos un vistazo a tus pódcasts favoritos…</string>
<string name="echo_share_heading">Mis pódcasts favoritos</string>
<string name="echo_share">Mi año %d en pódcasts. #AntennaPodEcho</string>
</resources>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="echo_home_header">Bilan de l\'année</string>
<string name="echo_home_subtitle">Vos podcasts et statistiques de cette année. Exclusivement sur votre téléphone.</string>
<string name="echo_intro_your_year">Votre année</string>
<string name="echo_intro_in_podcasts">de podcasts</string>
<string name="echo_intro_locally">généré par votre téléphone pour respecter votre vie privée</string>
<string name="echo_hours_this_year">Cette année vous avez écouté</string>
<plurals name="echo_hours_podcasts">
<item quantity="one">heures d\'épisodes\nd\'%1$d seul podcast</item>
<item quantity="many">heures d\'épisodes\nde %1$d podcasts différents</item>
<item quantity="other">heures d\'épisodes\nde %1$d podcasts différents</item>
</plurals>
<string name="echo_queue_title_clean">Et vous êtes prêt à finir à l\'année avec seulement</string>
<string name="echo_queue_title_many">Et vous avez de la réserve pour finir l\'année avec</string>
<plurals name="echo_queue_hours_waiting">
<item quantity="one">heures pour finir votre liste de lecture\nd\'%1$d seul épisode</item>
<item quantity="many">heures finir votre liste de lecture\nde %1$d épisodes</item>
<item quantity="other">heures finir votre liste de lecture\nde %1$d épisodes</item>
</plurals>
<string name="echo_queue_hours_clean">C\'est environ %1$s tous les jours avant que %2$d ne commence.</string>
<string name="echo_queue_hours_normal">C\'est environ %1$s tous les jours avant que %2$d ne commence. Si vous voulez y arriver des épisodes vont devoir être sautés !</string>
<string name="echo_queue_hours_much">C\'est environ %1$s tous les jours avant que %2$d ne commence. Hein !? Ça semble tendu !</string>
<string name="echo_listened_after_title">On a lancé quelques analyses entre le moment où un épisode est publié et son écoute. Notre conclusion ?</string>
<string name="echo_listened_after_comment_easy">Vous êtes zen</string>
<string name="echo_listened_after_time">En général, vous avez écouté un episode %1$s après sa publication.</string>
<string name="echo_listened_after_comment_addict">Vous êtes accro aux podcasts</string>
<string name="echo_hoarder_title">On s\'est aussi demandé si vous écoutiez les podcasts auxquels vous êtes abonné ?</string>
<string name="echo_hoarder_subtitle_hoarder">Au vu des chiffres on a détecté un petit syndrome de collectionnite !</string>
<string name="echo_hoarder_comment_hoarder">Et il parait que les chiffres ne mentent pas. Avec seulement %1$d%% de vos %2$d abonnements actifs ayant été lu cette année, nous avons probablement raison.</string>
<string name="echo_hoarder_subtitle_medium">Rien à signaler ! Pas de collectionnite aigüe détectée !</string>
<string name="echo_hoarder_comment_medium">Vous avez lu %1$d%% de vos %2$d abonnements actifs cette année. Pourquoi ne pas allez rejeter un coup d\'oeil à \"%3$s\" ?</string>
<string name="echo_hoarder_subtitle_clean">Nickel !</string>
<string name="echo_hoarder_comment_clean">Vous avez lu %1$d%% de vos %2$d abonnements actifs cette année. On parie que vous ête du genre à avoir un bureau propre !</string>
<string name="echo_thanks_large">Merci</string>
<string name="echo_thanks_we_are_glad_old">d\'avoir partagé cette année avec nous !\n\nVotre premier épisode avec nous a été lu en %1$s et on est fier de continuer à vous être utile.</string>
<string name="echo_thanks_we_are_glad_new">de nous avoir rejoint cette année !\n\nPeu importe si vous avez changé d\'application ou commencé à écouter les podcasts avec nous : on est content de vous avoir !</string>
<string name="echo_thanks_now_favorite">Maintenant, jetons un coup d\'oeil à vos podcasts préférés…</string>
<string name="echo_share_heading">Mes podcasts préférés</string>
<string name="echo_share">Mon année %d en podcasts. #AntennaPodEcho</string>
</resources>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="echo_home_header">Passa in rassegna l\'anno</string>
<string name="echo_home_subtitle">I tuoi podcast più ascoltati e statistiche sull\'anno appena trascorso. In esclusiva sul tuo telefono.</string>
<string name="echo_intro_your_year">Il tuo anno</string>
<string name="echo_intro_in_podcasts">in podcast</string>
<string name="echo_intro_locally">generato in privato sul tuo telefono</string>
<string name="echo_hours_this_year">Quest\'anno hai riprodotto</string>
<plurals name="echo_hours_podcasts">
<item quantity="one">ore di episodi\nda %1$d podcast</item>
<item quantity="other">ore di episodi\nda %1$d podcast diversi</item>
</plurals>
<string name="echo_queue_title_clean">E sei pronto a cominciare da zero il nuovo anno. Hai</string>
<string name="echo_queue_title_many">E ti resta ancora un bel po\' da fare quest\'anno. Hai</string>
<plurals name="echo_queue_hours_waiting">
<item quantity="one">ore provenienti da %1$d episodio\nin attesa nella tua coda</item>
<item quantity="other">ore provenienti da %1$d episodi\nin attesa nella tua coda</item>
</plurals>
<string name="echo_queue_hours_clean">Cioè circa %1$s al giorno da qui al %2$d.</string>
<string name="echo_queue_hours_normal">Cioè circa %1$s al giorno da qui al %2$d. Puoi cominciare da zero il nuovo anno se salti qualche episodio.</string>
<string name="echo_queue_hours_much">Cioè circa %1$s al giorno da qui al %2$d. Aspetta, come hai detto?</string>
<string name="echo_listened_after_title">Abbiamo analizzato quando gli episodi sono pubblicati e quando finisci di ascoltarli. La nostra conclusione?</string>
<string name="echo_listened_after_comment_easy">Te la prendi comoda</string>
<string name="echo_listened_after_time">In genere, finisci di ascoltare un episodio %1$s dopo la sua pubblicazione.</string>
<string name="echo_listened_after_comment_addict">Sei podcast-dipendente</string>
<string name="echo_hoarder_title">Ci siamo chiesti anche: ascolti davvero i podcast a cui sei iscritto?</string>
<string name="echo_hoarder_subtitle_hoarder">Dati alla mano, pensiamo che tu sia un accumulatore</string>
<string name="echo_hoarder_comment_hoarder">Si dice che i numeri non mentano. E visto che quest\'anno hai riprodotto solo il %1$d%% delle tue %2$d iscrizioni attive, probabilmente abbiamo ragione.</string>
<string name="echo_hoarder_subtitle_medium">Verificato. Non c\'è traccia di accumulo seriale.</string>
<string name="echo_hoarder_comment_medium">Quest\'anno hai riprodotto episodi dal %1$d%% delle tue %2$d iscrizioni attive. Che ne dici di ascoltare di nuovo \"%3$s\"?</string>
<string name="echo_hoarder_subtitle_clean">In ordine!</string>
<string name="echo_hoarder_comment_clean">Quest\'anno hai riprodotto episodi dal %1$d%% delle tue %2$d iscrizioni attive. Scommettiamo che tieni in ordine anche la tua scrivania!</string>
<string name="echo_thanks_large">Grazie</string>
<string name="echo_thanks_we_are_glad_old">per essere rimasto con noi quest\'anno!\n\nHai riprodotto il tuo primo episodio con noi in %1$s. Da allora, siamo onorati di essere al tuo servizio.</string>
<string name="echo_thanks_we_are_glad_new">per esserti unito a noi quest\'anno!\n\nChe tu sia arrivato qui da un\'altra app o che tu abbia iniziato la tua avventura coi podcast con noi, siamo felici di averti qui!</string>
<string name="echo_thanks_now_favorite">Ora diamo un\'occhiata ai tuoi podcast preferiti…</string>
<string name="echo_share_heading">I miei podcast preferiti</string>
<string name="echo_share">Il mio %d in podcast. #AntennaPodEcho</string>
</resources>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation,PluralsCandidate">
<string name="echo_home_header">Review the year</string>
<string name="echo_home_subtitle">Your top podcasts and stats from the past year. Exclusively on your phone.</string>
<string name="echo_intro_your_year">Your year</string>
<string name="echo_intro_in_podcasts">in podcasts</string>
<string name="echo_intro_locally">generated privately on your phone</string>
<string name="echo_hours_this_year">This year you played</string>
<plurals name="echo_hours_podcasts">
<item quantity="one">hours of episodes\nfrom %1$d podcast</item>
<item quantity="other">hours of episodes\nfrom %1$d different podcasts</item>
</plurals>
<string name="echo_queue_title_clean">And you\'re ready to make a clean start of the year. You have</string>
<string name="echo_queue_title_many">And you still have quite a bit to go this year. You have</string>
<plurals name="echo_queue_hours_waiting">
<item quantity="one">hours waiting in your queue\nfrom %1$d episode</item>
<item quantity="other">hours waiting in your queue\nfrom %1$d episodes</item>
</plurals>
<string name="echo_queue_hours_clean">That\'s about %1$s each day until %2$d starts.</string>
<string name="echo_queue_hours_normal">That\'s about %1$s each day until %2$d starts. You can start the year clean if you skip a few episodes.</string>
<string name="echo_queue_hours_much">That\'s about %1$s each day until %2$d starts. Wait, what?</string>
<string name="echo_listened_after_title">We\'ve run some analysis on when episodes are released, and when you completed them. Our conclusion?</string>
<string name="echo_listened_after_emoji_yoga" translatable="false">\uD83E\uDDD8</string>
<string name="echo_listened_after_comment_easy">You\'re easy going</string>
<string name="echo_listened_after_time">Typically, you completed an episode %1$s after it was released.</string>
<string name="echo_listened_after_emoji_run" translatable="false">\uD83C\uDFC3</string>
<string name="echo_listened_after_comment_addict">You\'re a podcast addict</string>
<string name="echo_hoarder_title">We\'ve also been wondering: do you listen to the podcasts that you\'re subscribed to?</string>
<string name="echo_hoarder_subtitle_hoarder">Looking at the numbers, we think you\'re a hoarder</string>
<string name="echo_hoarder_emoji_cabinet" translatable="false">\uD83D\uDDC4\uFE0F</string>
<string name="echo_hoarder_comment_hoarder">Numbers don\'t lie, they say. And with only %1$d%% of your %2$d active subscriptions having been played this year, we\'re probably right.</string>
<string name="echo_hoarder_subtitle_medium">Check. No hoarding here.</string>
<string name="echo_hoarder_emoji_check" translatable="false">\u2705</string>
<string name="echo_hoarder_comment_medium">You\'ve played episodes from %1$d%% of your %2$d active subscriptions this year. How about checking out \"%3$s\" again?</string>
<string name="echo_hoarder_subtitle_clean">Clean!</string>
<string name="echo_hoarder_emoji_clean" translatable="false">\u2728</string>
<string name="echo_hoarder_comment_clean">You\'ve played episodes from %1$d%% of your %2$d active subscriptions this year. We bet you keep your desk clean, too!</string>
<string name="echo_thanks_large">Thanks</string>
<string name="echo_thanks_we_are_glad_old">for sticking with us this year!\n\nYou played your first episode with us in %1$s. It\'s been our honor to serve you since.</string>
<string name="echo_thanks_we_are_glad_new">for joining us this year!\n\nWhether you\'ve moved over from another app, or started your podcast adventure with us: we\'re glad to have you!</string>
<string name="echo_thanks_now_favorite">Now, let\'s take a look at your favorite podcasts…</string>
<string name="echo_share_heading">My favorite podcasts</string>
<string name="echo_share">My year %d in podcasts. #AntennaPodEcho</string>
</resources>

View File

@ -27,6 +27,8 @@
<string name="years_statistics_label">Years</string>
<string name="notification_pref_fragment">Notifications</string>
<string name="recently_played_episodes">Recently played episodes</string>
<string name="antennapod_echo" translatable="false">AntennaPod Echo</string>
<string name="antennapod_echo_year" translatable="false">AntennaPod Echo %d</string>
<!-- Google Assistant -->
<string name="app_action_not_found">\"%1$s\" not found</string>
@ -620,6 +622,10 @@
<item quantity="one">1 hour</item>
<item quantity="other">%d hours</item>
</plurals>
<plurals name="time_days_quantified">
<item quantity="one">1 day</item>
<item quantity="other">%d days</item>
</plurals>
<string name="auto_enable_label">Automatically activate the sleep timer when pressing play</string>
<string name="auto_enable_label_with_times">Automatically activate the sleep timer when pressing play between %s and %s</string>
<string name="auto_enable_change_times">Change time range</string>