diff options
33 files changed, 1480 insertions, 8 deletions
diff --git a/app/build.gradle b/app/build.gradle index b26dc2fb0..64bc442a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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') diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java index 21b36bc49..fc925aa03 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java @@ -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); diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java new file mode 100644 index 000000000..7261c6be4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java @@ -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); + } +} diff --git a/app/src/main/res/layout/home_section_echo.xml b/app/src/main/res/layout/home_section_echo.xml new file mode 100644 index 000000000..f5fdaa46f --- /dev/null +++ b/app/src/main/res/layout/home_section_echo.xml @@ -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> diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index d83557b0c..084b3a7ad 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -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 diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java index 4014937bd..d9c4a5098 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -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 = ""; + 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 = context.getResources().getQuantityString(R.plurals.time_hours_quantified, h, h); - result += hours + " "; + 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; } diff --git a/core/src/main/res/drawable/bg_blue_gradient.xml b/core/src/main/res/drawable/bg_blue_gradient.xml new file mode 100644 index 000000000..8ae045b6d --- /dev/null +++ b/core/src/main/res/drawable/bg_blue_gradient.xml @@ -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> diff --git a/core/src/main/res/drawable/ic_arrow_right_white.xml b/core/src/main/res/drawable/ic_arrow_right_white.xml new file mode 100644 index 000000000..4f33bed08 --- /dev/null +++ b/core/src/main/res/drawable/ic_arrow_right_white.xml @@ -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> diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index 553da121a..ad835faf4 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -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> diff --git a/settings.gradle b/settings.gradle index 3111771e8..1e97f7fe2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java index 21f12e223..96d80c209 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java @@ -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); 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 @@ +<?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> 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<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))); + } +} 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<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; + } + } +} 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<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); + } +} 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 Binary files differnew file mode 100644 index 000000000..8f1e3a854 --- /dev/null +++ b/ui/echo/src/main/res/drawable-nodpi/echo.png diff --git a/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png b/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png Binary files differnew file mode 100644 index 000000000..20408eb74 --- /dev/null +++ b/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png diff --git a/ui/echo/src/main/res/font/sarabun_regular.ttf b/ui/echo/src/main/res/font/sarabun_regular.ttf Binary files differnew file mode 100644 index 000000000..50fa7076d --- /dev/null +++ b/ui/echo/src/main/res/font/sarabun_regular.ttf diff --git a/ui/echo/src/main/res/font/sarabun_semi_bold.ttf b/ui/echo/src/main/res/font/sarabun_semi_bold.ttf Binary files differnew file mode 100644 index 000000000..7b760ce10 --- /dev/null +++ b/ui/echo/src/main/res/font/sarabun_semi_bold.ttf 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 @@ +<?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> 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 @@ +<?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> 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 @@ +<?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> 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 @@ +<?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> 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 @@ +<?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> 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 @@ +<?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> 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 @@ <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> |