From ee554d0306a06903fa88be6c5af7954315685ed6 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Tue, 28 Nov 2023 20:26:29 +0100 Subject: AntennaPod Echo (#6780) --- ui/echo/README.md | 3 + ui/echo/build.gradle | 27 ++ ui/echo/src/main/AndroidManifest.xml | 19 + .../de/danoeh/antennapod/ui/echo/EchoActivity.java | 399 +++++++++++++++++++++ .../de/danoeh/antennapod/ui/echo/EchoProgress.java | 64 ++++ .../antennapod/ui/echo/screens/BaseScreen.java | 96 +++++ .../antennapod/ui/echo/screens/BubbleScreen.java | 35 ++ .../ui/echo/screens/FinalShareScreen.java | 89 +++++ .../ui/echo/screens/RotatingSquaresScreen.java | 37 ++ .../antennapod/ui/echo/screens/StripesScreen.java | 38 ++ .../antennapod/ui/echo/screens/WaveformScreen.java | 39 ++ ui/echo/src/main/res/drawable-nodpi/echo.png | Bin 0 -> 10930 bytes .../main/res/drawable-nodpi/logo_monochrome.png | Bin 0 -> 4451 bytes ui/echo/src/main/res/font/sarabun_regular.ttf | Bin 0 -> 83080 bytes ui/echo/src/main/res/font/sarabun_semi_bold.ttf | Bin 0 -> 82952 bytes ui/echo/src/main/res/layout/echo_activity.xml | 135 +++++++ ui/echo/src/main/res/values-de/echo-strings.xml | 46 +++ ui/echo/src/main/res/values-es/echo-strings.xml | 46 +++ ui/echo/src/main/res/values-fr/echo-strings.xml | 48 +++ ui/echo/src/main/res/values-it/echo-strings.xml | 46 +++ ui/echo/src/main/res/values/echo-strings.xml | 51 +++ ui/i18n/src/main/res/values/strings.xml | 6 + 22 files changed, 1224 insertions(+) create mode 100644 ui/echo/README.md create mode 100644 ui/echo/build.gradle create mode 100644 ui/echo/src/main/AndroidManifest.xml create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java create mode 100644 ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java create mode 100644 ui/echo/src/main/res/drawable-nodpi/echo.png create mode 100644 ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png create mode 100644 ui/echo/src/main/res/font/sarabun_regular.ttf create mode 100644 ui/echo/src/main/res/font/sarabun_semi_bold.ttf create mode 100644 ui/echo/src/main/res/layout/echo_activity.xml create mode 100644 ui/echo/src/main/res/values-de/echo-strings.xml create mode 100644 ui/echo/src/main/res/values-es/echo-strings.xml create mode 100644 ui/echo/src/main/res/values-fr/echo-strings.xml create mode 100644 ui/echo/src/main/res/values-it/echo-strings.xml create mode 100644 ui/echo/src/main/res/values/echo-strings.xml (limited to 'ui') diff --git a/ui/echo/README.md b/ui/echo/README.md new file mode 100644 index 000000000..93fa4904a --- /dev/null +++ b/ui/echo/README.md @@ -0,0 +1,3 @@ +# :ui:echo + +This module provides the "Echo" screen, a yearly rewind. diff --git a/ui/echo/build.gradle b/ui/echo/build.gradle new file mode 100644 index 000000000..de949d18b --- /dev/null +++ b/ui/echo/build.gradle @@ -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" +} diff --git a/ui/echo/src/main/AndroidManifest.xml b/ui/echo/src/main/AndroidManifest.xml new file mode 100644 index 000000000..86f938428 --- /dev/null +++ b/ui/echo/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java new file mode 100644 index 000000000..92adaed2e --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java @@ -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> 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 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 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))); + } +} diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java new file mode 100644 index 000000000..e089bfa09 --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java @@ -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) { + } +} diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java new file mode 100644 index 000000000..e8bc085cd --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java @@ -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 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; + } + } +} diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java new file mode 100644 index 000000000..bd79645dc --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java @@ -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(); + } + } +} diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java new file mode 100644 index 000000000..87b9cbd53 --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java @@ -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> favoritePods; + private final Typeface typefaceNormal; + private final Typeface typefaceBold; + + public FinalShareScreen(Context context, ArrayList> 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); + } +} diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java new file mode 100644 index 000000000..624b68f88 --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java @@ -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; + } + } +} \ No newline at end of file diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java new file mode 100644 index 000000000..60906776f --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java @@ -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; + } + } +} diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java new file mode 100644 index 000000000..d87f7fbb5 --- /dev/null +++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java @@ -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; + } + } +} diff --git a/ui/echo/src/main/res/drawable-nodpi/echo.png b/ui/echo/src/main/res/drawable-nodpi/echo.png new file mode 100644 index 000000000..8f1e3a854 Binary files /dev/null and b/ui/echo/src/main/res/drawable-nodpi/echo.png differ diff --git a/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png b/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png new file mode 100644 index 000000000..20408eb74 Binary files /dev/null and b/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png differ diff --git a/ui/echo/src/main/res/font/sarabun_regular.ttf b/ui/echo/src/main/res/font/sarabun_regular.ttf new file mode 100644 index 000000000..50fa7076d Binary files /dev/null and b/ui/echo/src/main/res/font/sarabun_regular.ttf differ diff --git a/ui/echo/src/main/res/font/sarabun_semi_bold.ttf b/ui/echo/src/main/res/font/sarabun_semi_bold.ttf new file mode 100644 index 000000000..7b760ce10 Binary files /dev/null and b/ui/echo/src/main/res/font/sarabun_semi_bold.ttf differ diff --git a/ui/echo/src/main/res/layout/echo_activity.xml b/ui/echo/src/main/res/layout/echo_activity.xml new file mode 100644 index 000000000..3c5590d3a --- /dev/null +++ b/ui/echo/src/main/res/layout/echo_activity.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/echo/src/main/res/values-de/echo-strings.xml b/ui/echo/src/main/res/values-de/echo-strings.xml new file mode 100644 index 000000000..2f0c3da0d --- /dev/null +++ b/ui/echo/src/main/res/values-de/echo-strings.xml @@ -0,0 +1,46 @@ + + + Jahresrückblick + Deine Lieblings-Podcasts und Statistiken aus dem letzten Jahr. Exklusiv auf deinem Telefon. + + Dein Podcast-Jahr + + privat auf deinem Telefon generiert + + Dieses Jahr hast du + + Stunden an Episoden von %1$d Podcast abgespielt + Stunden an Episoden von %1$d Podcasts abgespielt + + + Und du bist bereit, das neue Jahr frisch zu starten. In deiner Warteschlange liegen + Und du hast dieses Jahr noch eine ganze Menge vor dir. In deiner Warteschlange liegen + + Stunden aus %1$d Podcast-Episode + Stunden aus %1$d Podcast-Episoden + + Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt + Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt. Du kannst frisch ins Jahr starten, wenn du einige Episoden überspringst. + Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt. Moment, was? + + Wir haben uns angeschaut, wann Episoden veröffentlicht werden und wann du sie abspielst. Unsere Folgerung? + Du bist entspannt + Typischerweise spielst du eine Episode %1$s nach der Veröffentlichung ab. + Du bist ein Podcast-Junkie + + Wir haben uns auch gefragt: hörst du die Podcasts an, die du abonniert hast? + Wenn wir uns die Zahlen so anschauen, glauben wir, du hamsterst Podcasts + Zahlen lügen nicht, sagt man. Du hast dieses Jahr nur %1$d%% deiner %2$d aktiven Abonnements abgespielt, also liegen wir wahrscheinlich richtig. + Du hamsterst keine Podcasts + Du hast dieses Jahr %1$d%% deiner %2$d aktiven Abonnements abgespielt. Wie wäre es damit, mal wieder \"%3$s\" anzuhören? + Aufgeräumt! + Du hast dieses Jahr %1$d%% deiner %2$d aktiven Abonnements abgespielt. Wetten, du hältst auch deinen Schreibtisch sauber? + + Danke + 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. + 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! + Schauen wir uns jetzt noch deine Lieblings-Podcasts an… + + Meine Lieblings-Podcasts + Mein Podcast-Jahr %d. #AntennaPodEcho + diff --git a/ui/echo/src/main/res/values-es/echo-strings.xml b/ui/echo/src/main/res/values-es/echo-strings.xml new file mode 100644 index 000000000..44e81947b --- /dev/null +++ b/ui/echo/src/main/res/values-es/echo-strings.xml @@ -0,0 +1,46 @@ + + + Revisa el año + Tus mejores pódcasts y estadísticas del año pasado. Exclusivamente en tu teléfono. + + Tu año + en pódcasts + generado de forma privada en tu teléfono + + Este año has reproducido + + horas de episodios\nde %1$d pódcast + horas de episodios\nde %1$d pódcasts diferentes + + + Y estás listo para empezar el año de nuevo. Tienes + Y aún te queda bastante este año. Tienes + + hora esperando en tu cola\nde %1$d episodio + horas esperando en tu cola\nde %1$d episodios + + Eso es alrededor de %1$s cada día hasta que empiece %2$d + Eso son %1$s cada día hasta que empiece %2$d. Puedes empezar el año de cero si te saltas algunos episodios. + Eso son %1$s cada día hasta que empiece %2$d. Espera, ¿qué? + + Hemos analizado cuándo se publican los episodios y cuándo los completaste. ¿Nuestra conclusión? + Eres relajado + Normalmente, completaste un episodio %1$s después de su publicación. + Eres un adicto a los pódcasts + + También nos hemos preguntado: ¿Escuchas los pódcasts a los que estás suscrito? + Viendo los números, creemos que eres un acumulador + 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. + Mira. Aquí no hay acumulación. + Has reproducido episodios de %1$d%% de tus %2$d suscripciones activas este año. ¿Qué tal si vuelves a escuchar \"%3$s\"? + ¡Limpio! + Has reproducido episodios de %1$d%% de tus %2$d suscripciones activas este año. ¡Apostamos que también mantienes limpio tu escritorio! + + ¡Gracias + por estar con nosotros este año!\n\nReproduciste tu primer episodio con nosotros en %1$s. Ha sido un honor servirte desde entonces. + 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! + Ahora, echemos un vistazo a tus pódcasts favoritos… + + Mis pódcasts favoritos + Mi año %d en pódcasts. #AntennaPodEcho + diff --git a/ui/echo/src/main/res/values-fr/echo-strings.xml b/ui/echo/src/main/res/values-fr/echo-strings.xml new file mode 100644 index 000000000..a5be76ef1 --- /dev/null +++ b/ui/echo/src/main/res/values-fr/echo-strings.xml @@ -0,0 +1,48 @@ + + + Bilan de l\'année + Vos podcasts et statistiques de cette année. Exclusivement sur votre téléphone. + + Votre année + de podcasts + généré par votre téléphone pour respecter votre vie privée + + Cette année vous avez écouté + + heures d\'épisodes\nd\'%1$d seul podcast + heures d\'épisodes\nde %1$d podcasts différents + heures d\'épisodes\nde %1$d podcasts différents + + + Et vous êtes prêt à finir à l\'année avec seulement + Et vous avez de la réserve pour finir l\'année avec + + heures pour finir votre liste de lecture\nd\'%1$d seul épisode + heures finir votre liste de lecture\nde %1$d épisodes + heures finir votre liste de lecture\nde %1$d épisodes + + C\'est environ %1$s tous les jours avant que %2$d ne commence. + 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 ! + C\'est environ %1$s tous les jours avant que %2$d ne commence. Hein !? Ça semble tendu ! + + On a lancé quelques analyses entre le moment où un épisode est publié et son écoute. Notre conclusion ? + Vous êtes zen + En général, vous avez écouté un episode %1$s après sa publication. + Vous êtes accro aux podcasts + + On s\'est aussi demandé si vous écoutiez les podcasts auxquels vous êtes abonné ? + Au vu des chiffres on a détecté un petit syndrome de collectionnite ! + 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. + Rien à signaler ! Pas de collectionnite aigüe détectée ! + 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\" ? + Nickel ! + 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 ! + + Merci + 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. + 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 ! + Maintenant, jetons un coup d\'oeil à vos podcasts préférés… + + Mes podcasts préférés + Mon année %d en podcasts. #AntennaPodEcho + diff --git a/ui/echo/src/main/res/values-it/echo-strings.xml b/ui/echo/src/main/res/values-it/echo-strings.xml new file mode 100644 index 000000000..ecc8949ee --- /dev/null +++ b/ui/echo/src/main/res/values-it/echo-strings.xml @@ -0,0 +1,46 @@ + + + Passa in rassegna l\'anno + I tuoi podcast più ascoltati e statistiche sull\'anno appena trascorso. In esclusiva sul tuo telefono. + + Il tuo anno + in podcast + generato in privato sul tuo telefono + + Quest\'anno hai riprodotto + + ore di episodi\nda %1$d podcast + ore di episodi\nda %1$d podcast diversi + + + E sei pronto a cominciare da zero il nuovo anno. Hai + E ti resta ancora un bel po\' da fare quest\'anno. Hai + + ore provenienti da %1$d episodio\nin attesa nella tua coda + ore provenienti da %1$d episodi\nin attesa nella tua coda + + Cioè circa %1$s al giorno da qui al %2$d. + Cioè circa %1$s al giorno da qui al %2$d. Puoi cominciare da zero il nuovo anno se salti qualche episodio. + Cioè circa %1$s al giorno da qui al %2$d. Aspetta, come hai detto? + + Abbiamo analizzato quando gli episodi sono pubblicati e quando finisci di ascoltarli. La nostra conclusione? + Te la prendi comoda + In genere, finisci di ascoltare un episodio %1$s dopo la sua pubblicazione. + Sei podcast-dipendente + + Ci siamo chiesti anche: ascolti davvero i podcast a cui sei iscritto? + Dati alla mano, pensiamo che tu sia un accumulatore + 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. + Verificato. Non c\'è traccia di accumulo seriale. + Quest\'anno hai riprodotto episodi dal %1$d%% delle tue %2$d iscrizioni attive. Che ne dici di ascoltare di nuovo \"%3$s\"? + In ordine! + Quest\'anno hai riprodotto episodi dal %1$d%% delle tue %2$d iscrizioni attive. Scommettiamo che tieni in ordine anche la tua scrivania! + + Grazie + 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. + 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! + Ora diamo un\'occhiata ai tuoi podcast preferiti… + + I miei podcast preferiti + Il mio %d in podcast. #AntennaPodEcho + diff --git a/ui/echo/src/main/res/values/echo-strings.xml b/ui/echo/src/main/res/values/echo-strings.xml new file mode 100644 index 000000000..09a64f1e4 --- /dev/null +++ b/ui/echo/src/main/res/values/echo-strings.xml @@ -0,0 +1,51 @@ + + + Review the year + Your top podcasts and stats from the past year. Exclusively on your phone. + + Your year + in podcasts + generated privately on your phone + + This year you played + + hours of episodes\nfrom %1$d podcast + hours of episodes\nfrom %1$d different podcasts + + + And you\'re ready to make a clean start of the year. You have + And you still have quite a bit to go this year. You have + + hours waiting in your queue\nfrom %1$d episode + hours waiting in your queue\nfrom %1$d episodes + + That\'s about %1$s each day until %2$d starts. + That\'s about %1$s each day until %2$d starts. You can start the year clean if you skip a few episodes. + That\'s about %1$s each day until %2$d starts. Wait, what? + + We\'ve run some analysis on when episodes are released, and when you completed them. Our conclusion? + \uD83E\uDDD8 + You\'re easy going + Typically, you completed an episode %1$s after it was released. + \uD83C\uDFC3 + You\'re a podcast addict + + We\'ve also been wondering: do you listen to the podcasts that you\'re subscribed to? + Looking at the numbers, we think you\'re a hoarder + \uD83D\uDDC4\uFE0F + 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. + Check. No hoarding here. + \u2705 + You\'ve played episodes from %1$d%% of your %2$d active subscriptions this year. How about checking out \"%3$s\" again? + Clean! + \u2728 + You\'ve played episodes from %1$d%% of your %2$d active subscriptions this year. We bet you keep your desk clean, too! + + Thanks + 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. + 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! + Now, let\'s take a look at your favorite podcasts… + + My favorite podcasts + My year %d in podcasts. #AntennaPodEcho + diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index 493e8672c..74e979213 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -27,6 +27,8 @@ Years Notifications Recently played episodes + AntennaPod Echo + AntennaPod Echo %d \"%1$s\" not found @@ -620,6 +622,10 @@ 1 hour %d hours + + 1 day + %d days + Automatically activate the sleep timer when pressing play Automatically activate the sleep timer when pressing play between %s and %s Change time range -- cgit v1.2.3