diff --git a/app/build.gradle b/app/build.gradle index adc58b9e..bf773b7b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,7 @@ android { } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } @@ -74,17 +75,20 @@ android { } dependencies { - // Core + // Compatibility for new methods in old Api + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" + implementation "androidx.core:core-ktx:1.12.0" + implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.browser:browser:1.8.0" - implementation "androidx.core:core-ktx:1.12.0" + implementation "androidx.webkit:webkit:1.10.0" implementation "androidx.fragment:fragment-ktx:1.6.2" - implementation "androidx.work:work-runtime-ktx:2.9.0" + implementation "androidx.preference:preference-ktx:1.2.1" + + // Kotlin shit implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" - implementation "androidx.preference:preference-ktx:1.2.1" - implementation "androidx.webkit:webkit:1.10.0" // Glide ext.glide_version = "4.16.0" diff --git a/app/schemas/com.mrboomdev.awery.data.db.AweryDB/2.json b/app/schemas/com.mrboomdev.awery.data.db.AweryDB/2.json index 249ba94f..e4b4081a 100644 --- a/app/schemas/com.mrboomdev.awery.data.db.AweryDB/2.json +++ b/app/schemas/com.mrboomdev.awery.data.db.AweryDB/2.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "6aaacfffb3e51c35cb67ef20833d4b33", + "identityHash": "8f75f6096ab783134a8453d60fc1ff8e", "entities": [ { "tableName": "media", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`global_id` TEXT NOT NULL, `titles` TEXT, `lists` TEXT, `trackers` TEXT, `title` TEXT, `banner` TEXT, `description` TEXT, `color` TEXT, `url` TEXT, `country` TEXT, `releaseDate` TEXT, `duration` TEXT, `type` TEXT, `id` INTEGER NOT NULL, `episodes_count` TEXT, `average_score` TEXT, `tags` TEXT, `genres` TEXT, `status` TEXT, `poster_extra_large` TEXT, `poster_large` TEXT, `poster_medium` TEXT, PRIMARY KEY(`global_id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`global_id` TEXT NOT NULL, `titles` TEXT, `lists` TEXT, `trackers` TEXT, `title` TEXT, `banner` TEXT, `description` TEXT, `color` TEXT, `url` TEXT, `country` TEXT, `releaseDate` TEXT, `duration` TEXT, `type` TEXT, `id` INTEGER NOT NULL, `episodes_count` TEXT, `average_score` TEXT, `tags` TEXT, `genres` TEXT, `status` TEXT, `poster_extra_large` TEXT, `poster_large` TEXT, `poster_medium` TEXT, `last_source` TEXT, `last_episode` REAL NOT NULL DEFAULT -1, `last_episode_progress` REAL NOT NULL DEFAULT -1, PRIMARY KEY(`global_id`))", "fields": [ { "fieldPath": "globalId", @@ -139,6 +139,26 @@ "columnName": "poster_medium", "affinity": "TEXT", "notNull": false + }, + { + "fieldPath": "lastSource", + "columnName": "last_source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEpisode", + "columnName": "last_episode", + "affinity": "REAL", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "lastEpisodeProgress", + "columnName": "last_episode_progress", + "affinity": "REAL", + "notNull": true, + "defaultValue": "-1" } ], "primaryKey": { @@ -180,7 +200,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6aaacfffb3e51c35cb67ef20833d4b33')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8f75f6096ab783134a8453d60fc1ff8e')" ] } } \ No newline at end of file diff --git a/app/src/main/assets/repos.json b/app/src/main/assets/repos.json index c6fa4e45..bf884af5 100644 --- a/app/src/main/assets/repos.json +++ b/app/src/main/assets/repos.json @@ -2,6 +2,6 @@ { "type": "AweryJS", "title": "Awery - Official Repository", - "url": "https://github.com/MrBoomDeveloper/AweryRepo" + "url": "https://repo.mrboomdev.ru/cdn/repo.json" } ] \ No newline at end of file diff --git a/app/src/main/assets/settings.json b/app/src/main/assets/settings.json index 8d27633c..f9b5a141 100644 --- a/app/src/main/assets/settings.json +++ b/app/src/main/assets/settings.json @@ -61,6 +61,18 @@ { "key": "60", "title": "60s" }, { "key": "120", "title": "120s" } ] + }, { + "key": "big_seek", "type": "select", + "title": "Quick Rewind ", "icon": "outline_fast_forward_24", + "items": [ + { "key": "0", "title": "Disabled" }, + { "key": "30", "title": "30s" }, + { "key": "60", "title": "1m" }, + { "key": "120", "title": "2m" }, + { "key": "180", "title": "3m" }, + { "key": "240", "title": "4m" }, + { "key": "300", "title": "5m" } + ] }, { "key": "gestures", "type": "select", "string_value": "TODO: MAKE THIS VALUE AS DEFAULT", "title": "Gestures mode", diff --git a/app/src/main/assets/tags.json b/app/src/main/assets/tags.json index 762d6c89..dff2006c 100644 --- a/app/src/main/assets/tags.json +++ b/app/src/main/assets/tags.json @@ -1,11 +1,145 @@ { + "action": { + "title": "Action", + "variants": [ "action", "экшен" ], + "is_adult": false + }, + + "comedy": { + "title": "Comedy", + "variants": [ "comedy", "комедия" ], + "is_adult": false + }, + + "drama": { + "title": "Drama", + "variants": [ "drama", "драма" ], + "is_adult": false + }, + + "fantasy": { + "title": "Fantasy", + "variants": [ "fantasy", "фэнтези" ], + "is_adult": false + }, + + "horror": { + "title": "Horror", + "variants": [ "horror", "хоррор" ], + "is_adult": false + }, + + "mystery": { + "title": "Mystery", + "variants": [ "mystery", "мистика" ], + "is_adult": false + }, + + "romance": { + "title": "Romance", + "variants": [ "romance", "романтика" ], + "is_adult": false + }, + + "sci-fi": { + "title": "Sci-Fi", + "variants": [ "sci-fi", "фантастика" ], + "is_adult": false + }, + + "thriller": { + "title": "Thriller", + "variants": [ "thriller", "триллер" ], + "is_adult": false + }, + + "western": { + "title": "Western", + "variants": [ "western", "вестерн" ], + "is_adult": false + }, + + "adventure": { + "title": "Adventure", + "variants": [ "adventure", "приключения", "приключение" ], + "is_adult": false + }, + + "biography": { + "title": "Biography", + "variants": [ "biography", "биография" ], + "is_adult": false + }, + + "crime": { + "title": "Crime", + "variants": [ "crime", "криминал" ], + "is_adult": false + }, + + "documentary": { + "title": "Documentary", + "variants": [ "documentary", "документальное" ], + "is_adult": false + }, + + "history": { + "title": "History", + "variants": [ "history", "история" ], + "is_adult": false + }, + + "music": { + "title": "Music", + "variants": [ "music", "музыка" ], + "is_adult": false + }, + + "reality": { + "title": "Reality", + "variants": [ "reality", "реалити" ], + "is_adult": false + }, + + "war": { + "title": "War", + "variants": [ "war", "война" ], + "is_adult": false + }, + + "animation": { + "title": "Animation", + "variants": [ "animation", "anime", "анимация", "аниме" ], + "is_adult": false + }, + + "anime": { + "title": "Anime", + "variants": [ "anime", "аниме" ], + "is_adult": false + }, + + "cgi": { + "title": "CGI", + "variants": [ "cgi" ], + "is_adult": false + }, + "gay": { "title": "Gay", - "variants": [ "yaoi", "gay", "boys love" ] + "variants": [ "yaoi", "gay", "boys love", "сёнен-ай" ], + "is_adult": false }, "lesbian": { "title": "Lesbian", - "variants": [ "lesbian", "yuri" ] + "variants": [ "lesbian", "yuri", "сёдзе-ай" ], + "is_adult": false + }, + + "lgbt": { + "title": "LGBT", + "variants": [ "lgbt", "lesbian", "gay", "bisexual", "transgender", "lgbtq", "лгбт" ], + "is_adult": false } } \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/AweryApp.java b/app/src/main/java/com/mrboomdev/awery/AweryApp.java index 4396b6f6..5a8c6dc5 100644 --- a/app/src/main/java/com/mrboomdev/awery/AweryApp.java +++ b/app/src/main/java/com/mrboomdev/awery/AweryApp.java @@ -67,8 +67,9 @@ public class AweryApp extends App implements Application.ActivityLifecycleCallba private static final Map, ActivityInfo> activities = new HashMap<>(); private static final Handler handler = new Handler(Looper.getMainLooper()); private static final List disposables = new ArrayList<>(); - private static final String TAG = "AweryApp"; public static final boolean USE_KT_APP_INIT = true; + private static final String TAG = "AweryApp"; + private static Thread mainThread; private static AweryApp app; private static AweryDB db; @@ -94,12 +95,7 @@ public static Context getContext() { } public static void toast(Activity activity, String text, int duration) { - if(activity == null) { - Toast.makeText(app, text, duration).show(); - return; - } - - activity.runOnUiThread(() -> Toast.makeText(activity, text, duration).show()); + runOnUiThread(() -> Toast.makeText(activity, text, duration).show()); } public static void toast(String text, int duration) { @@ -151,13 +147,8 @@ public static boolean isTv() { } public static void runOnUiThread(Runnable runnable) { - var activity = getAnyActivity(); - - if(activity != null) { - activity.runOnUiThread(runnable); - } else { - postRunnable(runnable); - } + if(Thread.currentThread() != mainThread) handler.post(runnable); + else runnable.run(); } @Nullable @@ -179,6 +170,8 @@ public void onCreate() { setupCrashHandler(); app = this; + mainThread = Thread.currentThread(); + super.onCreate(); if(AwerySettings.getInstance().getBoolean(AwerySettings.VERBOSE_NETWORK)) { diff --git a/app/src/main/java/com/mrboomdev/awery/data/db/AweryDB.java b/app/src/main/java/com/mrboomdev/awery/data/db/AweryDB.java index a1fb1f00..672afa02 100644 --- a/app/src/main/java/com/mrboomdev/awery/data/db/AweryDB.java +++ b/app/src/main/java/com/mrboomdev/awery/data/db/AweryDB.java @@ -1,5 +1,6 @@ package com.mrboomdev.awery.data.db; +import androidx.room.AutoMigration; import androidx.room.Database; import androidx.room.RoomDatabase; @@ -7,11 +8,11 @@ import com.mrboomdev.awery.data.db.dao.CatalogMediaDao; @Database( - version = 1, + version = 2, - /*autoMigrations = { + autoMigrations = { @AutoMigration(from = 1, to = 2) - },*/ + }, entities = { DBCatalogMedia.class, diff --git a/app/src/main/java/com/mrboomdev/awery/data/db/DBCatalogMedia.java b/app/src/main/java/com/mrboomdev/awery/data/db/DBCatalogMedia.java index b3d85db3..9ede53ba 100644 --- a/app/src/main/java/com/mrboomdev/awery/data/db/DBCatalogMedia.java +++ b/app/src/main/java/com/mrboomdev/awery/data/db/DBCatalogMedia.java @@ -45,6 +45,12 @@ public class DBCatalogMedia { public String largePoster; @ColumnInfo(name = "poster_medium") public String mediumPoster; + @ColumnInfo(name = "last_source") + public String lastSource; + @ColumnInfo(name = "last_episode", defaultValue = "-1") + public float lastEpisode; + @ColumnInfo(name = "last_episode_progress", defaultValue = "-1") + public float lastEpisodeProgress; public DBCatalogMedia(@NonNull String globalId) { this.globalId = globalId; @@ -62,6 +68,10 @@ public static DBCatalogMedia fromCatalogMedia(@NonNull CatalogMedia media) { dbMedia.id = media.id; dbMedia.country = media.country; + dbMedia.lastEpisode = media.lastEpisode; + dbMedia.lastSource = media.lastSource; + dbMedia.lastEpisodeProgress = media.lastEpisodeProgress; + if(media.averageScore != null) { dbMedia.averageScore = Float.toString(media.averageScore); } @@ -123,6 +133,10 @@ public CatalogMedia toCatalogMedia() { media.color = color; media.country = country; + media.lastSource = lastSource; + media.lastEpisode = lastEpisode; + media.lastEpisodeProgress = lastEpisodeProgress; + if(averageScore != null) { media.averageScore = Float.parseFloat(averageScore); } diff --git a/app/src/main/java/com/mrboomdev/awery/data/settings/AwerySettings.java b/app/src/main/java/com/mrboomdev/awery/data/settings/AwerySettings.java index 645e08f7..e05bd5c4 100644 --- a/app/src/main/java/com/mrboomdev/awery/data/settings/AwerySettings.java +++ b/app/src/main/java/com/mrboomdev/awery/data/settings/AwerySettings.java @@ -12,6 +12,10 @@ import java.util.HashSet; import java.util.Set; +/** + * An utility class for working with shared preferences, which contains constant key names for quick access. + * @author MrBoomDev + */ public class AwerySettings { public static final String APP_SETTINGS = "Awery"; public static final String APP_SECRETS = "Secrets"; @@ -19,6 +23,7 @@ public class AwerySettings { private SharedPreferences.Editor editor; public static final String PLAYER_GESTURES = "settings_player_gestures"; + public static final String PLAYER_BIG_SEEK = "settings_player_big_seek"; public static final String DOUBLE_TAP_SEEK = "settings_player_double_tab_seek"; public static final String DEFAULT_HOME_TAB = "settings_ui_default_tab"; public static final String ADULT_CONTENT = "settings_content_adult_content"; @@ -39,21 +44,50 @@ public AwerySettings(SharedPreferences prefs) { this.prefs = prefs; } - public boolean getBoolean(String name, boolean defaultValue) { - return prefs.getBoolean(name, defaultValue); + /** + * @see #getBoolean(String) + * @return the value of the specified key, or the default value if the key does not exist + * @author MrBoomDev + */ + public boolean getBoolean(String key, boolean defaultValue) { + if(!prefs.contains(key)) { + checkEditorExistence().putBoolean(key, defaultValue); + saveSync(); + return defaultValue; + } + + return prefs.getBoolean(key, defaultValue); } - public boolean getBoolean(String name) { - return getBoolean(name, false); + /** + * @return whether the specified key exists + * @author MrBoomDev + */ + public boolean contains(String key) { + return prefs.contains(key); + } + + /** + * @see #getBoolean(String, boolean) + * @return the value of the specified key or false if the key does not exist + * @author MrBoomDev + */ + public boolean getBoolean(String key) { + return getBoolean(key, false); } public AwerySettings setBoolean(String key, boolean value) { - checkEditorExistence(); - editor.putBoolean(key, value); + checkEditorExistence().putBoolean(key, value); return this; } public int getInt(String key, int defaultValue) { + if(!prefs.contains(key)) { + checkEditorExistence().putInt(key, defaultValue); + saveSync(); + return defaultValue; + } + return prefs.getInt(key, defaultValue); } @@ -62,27 +96,37 @@ public int getInt(String key) { } public AwerySettings setInt(String key, int value) { - checkEditorExistence(); - editor.putInt(key, value); + checkEditorExistence().putInt(key, value); return this; } public String getString(String key, String defaultValue) { + if(!prefs.contains(key)) { + checkEditorExistence().putString(key, defaultValue); + saveSync(); + return defaultValue; + } + return prefs.getString(key, defaultValue); } public String getString(String key) { - return getString(key, ""); + return getString(key, null); } public AwerySettings setString(String key, String value) { - checkEditorExistence(); - editor.putString(key, value); + checkEditorExistence().putString(key, value); return this; } - public Set getStringSet(String name, Set defaultValue) { - return new HashSet<>(prefs.getStringSet(name, defaultValue)); + public Set getStringSet(String key, Set defaultValue) { + if(!prefs.contains(key)) { + checkEditorExistence().putStringSet(key, defaultValue); + saveSync(); + return defaultValue; + } + + return new HashSet<>(prefs.getStringSet(key, defaultValue)); } public Set getStringSet(String name) { @@ -90,67 +134,100 @@ public Set getStringSet(String name) { } public AwerySettings setStringSet(String key, Set value) { - checkEditorExistence(); - editor.putStringSet(key, value); + checkEditorExistence().putStringSet(key, value); return this; } - private void checkEditorExistence() { + private SharedPreferences.Editor checkEditorExistence() { if(editor == null) { editor = prefs.edit(); } + + return editor; } + /** + * Saves the changes to the shared preferences asynchronously. + * @return this instance for chaining methods + * @see #saveSync() + * @author MrBoomDev + */ public AwerySettings saveAsync() { if(editor == null) { return this; } editor.apply(); - save(); + editor = null; return this; } + /** + * Saves the changes to the shared preferences synchronously. + * @return this instance for chaining methods + * @see #saveAsync() + * @author MrBoomDev + */ public AwerySettings saveSync() { if(editor == null) { return this; } editor.commit(); - save(); - return this; - } - - private void save() { editor = null; + return this; } + /** + * @param context Application context + * @param name the name of file + * @return the singleton instance of {@link AwerySettings} + * @author MrBoomDev + * @see #getInstance() + * @see #getInstance(Context) + * @see #getInstance(String) + */ @NonNull public static AwerySettings getInstance(@NonNull Context context, String name) { return new AwerySettings(context.getSharedPreferences(name, 0)); } + /** + * @param context Application context + * @return the singleton instance of {@link AwerySettings} + * @author MrBoomDev + * @see #getInstance() + * @see #getInstance(String) + * @see #getInstance(Context, String) + */ @NonNull public static AwerySettings getInstance(@NonNull Context context) { return getInstance(context, APP_SETTINGS); } + /** + * @param name the name of file + * @return the singleton instance of {@link AwerySettings} + * @see #getInstance() + * @see #getInstance(Context) + * @see #getInstance(Context, String) + * @author MrBoomDev + */ @NonNull public static AwerySettings getInstance(String name) { - return getInstance(AweryApp.getAnyContext(), name); + return getInstance(AweryApp.getContext(), name); } + /** + * @return the singleton instance of {@link AwerySettings} + * @see #getInstance(String) + * @see #getInstance(Context) + * @see #getInstance(Context, String) + * @author MrBoomDev + */ @NonNull @Contract(" -> new") public static AwerySettings getInstance() { return getInstance(APP_SETTINGS); } - - public static SharedPreferences getPreferences(String fileName) { - return AweryApp.getAnyContext().getSharedPreferences(fileName, 0); - } - - public static SharedPreferences getPreferences() { - return getPreferences(APP_SETTINGS); - } } \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogEpisode.java b/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogEpisode.java index 50db5629..d67ec1dd 100644 --- a/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogEpisode.java +++ b/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogEpisode.java @@ -9,7 +9,7 @@ import java.util.List; -public class CatalogEpisode implements Parcelable { +public class CatalogEpisode implements Parcelable, Comparable { private final String title, banner, description, url; private final float number; private final long releaseDate; @@ -104,4 +104,13 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeFloat(number); dest.writeLong(releaseDate); } + + @Override + public int compareTo(@NonNull CatalogEpisode o) { + if(o.getNumber() != getNumber()) { + return Float.compare(o.getNumber(), getNumber()); + } + + return o.getTitle().compareToIgnoreCase(getTitle()); + } } \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogMedia.java b/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogMedia.java index 27033e53..9131807a 100644 --- a/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogMedia.java +++ b/app/src/main/java/com/mrboomdev/awery/extensions/support/template/CatalogMedia.java @@ -45,6 +45,9 @@ public class CatalogMedia { public Drawable cachedBanner; @Json(ignore = true) public long visualId; + public String lastSource; + public float lastEpisode = -1; + public float lastEpisodeProgress = -1; /** * @param globalId The unique id of the media in the following format: @@ -70,6 +73,13 @@ public String getBestBanner() { return getBestPoster(); } + public void merge(@NonNull CatalogMedia media) { + if(media.lastEpisode != -1) lastEpisode = media.lastEpisode; + if(media.lastEpisodeProgress != -1) lastEpisodeProgress = media.lastEpisodeProgress; + if(media.lists != null) lists = media.lists; + if(media.lastSource != null) lastSource = media.lastSource; + } + public void clearBookmarks() { if(lists == null) return; lists.clear(); diff --git a/app/src/main/java/com/mrboomdev/awery/ui/ThemeManager.java b/app/src/main/java/com/mrboomdev/awery/ui/ThemeManager.java index b595a90b..b90d5296 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/ThemeManager.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/ThemeManager.java @@ -22,15 +22,25 @@ public static boolean isAmoled(Context context) { public static boolean isMaterialYou(Context context) { var prefs = AwerySettings.getInstance(context); - return prefs.getBoolean(AwerySettings.THEME_USE_MATERIAL_YOU, DynamicColors.isDynamicColorAvailable()); + + if(!prefs.contains(AwerySettings.THEME_USE_MATERIAL_YOU)) { + boolean isMaterialYouSupported = DynamicColors.isDynamicColorAvailable(); + + prefs.setBoolean(AwerySettings.THEME_USE_MATERIAL_YOU, isMaterialYouSupported); + prefs.saveAsync(); + + return isMaterialYouSupported; + } + + return prefs.getBoolean(AwerySettings.THEME_USE_MATERIAL_YOU); } public static void apply(Activity activity, Bitmap bitmap) { var prefs = AwerySettings.getInstance(activity); boolean useOLED = prefs.getBoolean(AwerySettings.THEME_USE_OLDED); - boolean useMaterialYou = prefs.getBoolean(AwerySettings.THEME_USE_MATERIAL_YOU, DynamicColors.isDynamicColorAvailable()); boolean useColorsFromPoster = prefs.getBoolean(AwerySettings.THEME_USE_COLORS_FROM_MEDIA); + boolean useMaterialYou = isMaterialYou(activity); if(useMaterialYou || (useColorsFromPoster && bitmap != null)) { applyMaterialYou(activity, bitmap, useOLED); diff --git a/app/src/main/java/com/mrboomdev/awery/ui/activity/MediaActivity.java b/app/src/main/java/com/mrboomdev/awery/ui/activity/MediaActivity.java index d61c9548..9b264547 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/activity/MediaActivity.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/activity/MediaActivity.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.os.Bundle; +import android.util.Log; import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; @@ -16,6 +17,8 @@ import com.google.android.material.navigation.NavigationBarView; import com.google.android.material.navigationrail.NavigationRailView; import com.mrboomdev.awery.AweryApp; +import com.mrboomdev.awery.R; +import com.mrboomdev.awery.databinding.MediaDetailsActivityBinding; import com.mrboomdev.awery.extensions.support.template.CatalogMedia; import com.mrboomdev.awery.ui.ThemeManager; import com.mrboomdev.awery.ui.fragments.MediaCommentsFragment; @@ -28,10 +31,8 @@ import java.io.IOException; import java.util.Objects; -import com.mrboomdev.awery.R; -import com.mrboomdev.awery.databinding.MediaDetailsActivityBinding; - public class MediaActivity extends AppCompatActivity { + private static final String TAG = "MediaActivity"; private MediaDetailsActivityBinding binding; private CatalogMedia media; @@ -42,21 +43,56 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { EdgeToEdge.enable(this); super.onCreate(savedInstanceState); + binding = MediaDetailsActivityBinding.inflate(getLayoutInflater()); + binding.pager.setUserInputEnabled(false); + binding.pager.setPageTransformer(new FadeTransformer()); + + var navigation = (NavigationBarView) binding.navigation; + + ViewUtil.setOnApplyUiInsetsListener(navigation, insets -> { + if(navigation instanceof NavigationRailView) { + navigation.setPadding(insets.left, insets.top, 0, 0); + } else { + ViewUtil.setBottomPadding(navigation, insets.bottom, false); + } + }); + + if(binding.navigation instanceof NavigationRailView rail) { + var style = com.google.android.material.R.attr.floatingActionButtonSmallSecondaryStyle; + var header = new FloatingActionButton(this, null, style); + header.setImageResource(R.drawable.ic_round_arrow_back_ios_new_24); + header.setOnClickListener(v -> finish()); + rail.addHeaderView(header); + } + try { var adapter = CatalogMedia.getJsonAdapter(); var json = getIntent().getStringExtra("media"); - media = adapter.fromJson(Objects.requireNonNull(json)); + + var media = adapter.fromJson(Objects.requireNonNull(json)); + if(media == null) throw new NullPointerException("Media is null!"); + + new Thread(() -> { + var db = AweryApp.getDatabase().getMediaDao(); + var dbMedia = db.get(media.globalId); + + if(dbMedia != null) { + media.merge(dbMedia.toCatalogMedia()); + } + + runOnUiThread(() -> setMedia(media)); + }).start(); } catch(IOException e) { AweryApp.toast(this, "Failed to load media!", 1); - e.printStackTrace(); + Log.e(TAG, "Failed to load media!", e); finish(); - return; } + } - binding = MediaDetailsActivityBinding.inflate(getLayoutInflater()); + @SuppressLint("NonConstantResourceId") + public void setMedia(CatalogMedia media) { + this.media = media; binding.pager.setAdapter(new PagerAdapter(getSupportFragmentManager(), getLifecycle())); - binding.pager.setUserInputEnabled(false); - binding.pager.setPageTransformer(new FadeTransformer()); var navigation = (NavigationBarView) binding.navigation; @@ -71,22 +107,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { return true; }); - ViewUtil.setOnApplyUiInsetsListener(navigation, insets -> { - if(navigation instanceof NavigationRailView) { - navigation.setPadding(insets.left, insets.top, 0, 0); - } else { - ViewUtil.setBottomPadding(navigation, insets.bottom, false); - } - }); - - if(binding.navigation instanceof NavigationRailView rail) { - var style = com.google.android.material.R.attr.floatingActionButtonSmallSecondaryStyle; - var header = new FloatingActionButton(this, null, style); - header.setImageResource(R.drawable.ic_round_arrow_back_ios_new_24); - header.setOnClickListener(v -> finish()); - rail.addHeaderView(header); - } - launchAction(Objects.requireNonNull(getIntent().getStringExtra("action"))); setContentView(binding.getRoot()); } diff --git a/app/src/main/java/com/mrboomdev/awery/ui/activity/player/PlayerActivity.java b/app/src/main/java/com/mrboomdev/awery/ui/activity/player/PlayerActivity.java index 2a2013a8..95293448 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/activity/player/PlayerActivity.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/activity/player/PlayerActivity.java @@ -75,7 +75,7 @@ public class PlayerActivity extends AppCompatActivity implements Player.Listener protected CatalogVideo video; private CatalogSubtitle subtitle; protected ExoPlayer player; - protected int doubleTapSeek; + protected int doubleTapSeek, bigSeek; protected GesturesMode gesturesMode; private MediaItem videoItem; @@ -232,10 +232,17 @@ public void run() { setupButton(binding.exit, this::finish); setupButton(binding.settings, controller::openSettingsDialog); - setupButton(binding.quickSkip, () -> { - player.seekTo(player.getCurrentPosition() + 60_000); - controller.updateTimers(); - }); + if(bigSeek > 0) { + var time = StringUtil.formatDate(bigSeek * 1000L); + binding.quickSkip.setText("Skip " + time); + + setupButton(binding.quickSkip, () -> { + player.seekTo(player.getCurrentPosition() + bigSeek * 1000L); + controller.updateTimers(); + }); + } else { + binding.quickSkip.setVisibility(View.GONE); + } setupButton(binding.pause, () -> { if(isVideoBuffering) return; @@ -267,6 +274,10 @@ private void loadSettings() { gesturesMode = StringUtil.parseEnum( prefs.getString(AwerySettings.PLAYER_GESTURES), GesturesMode.VOLUME_BRIGHTNESS); + + bigSeek = StringUtil.parseInteger( + prefs.getString(AwerySettings.PLAYER_BIG_SEEK, + "60"), 60); } @Override diff --git a/app/src/main/java/com/mrboomdev/awery/ui/activity/settings/SettingsAdapter.java b/app/src/main/java/com/mrboomdev/awery/ui/activity/settings/SettingsAdapter.java index 7a5b5016..5bd15617 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/activity/settings/SettingsAdapter.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/activity/settings/SettingsAdapter.java @@ -158,9 +158,9 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return; } - var sorted = new ArrayList<>(items).stream().sorted((a, b) -> - a.getTitle().compareToIgnoreCase(b.getTitle())) - .collect(Collectors.toList()); + var sorted = new ArrayList<>(items).stream() + .sorted((a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle())) + .toList(); AweryApp.runOnUiThread(() -> { createRadioButtons(radioGroup, sorted, selectedItem); @@ -178,7 +178,7 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { settingItem.getKey(), settingItem.getTitle(parent.getContext()), settingItem.getKey().equals(selected))) - .collect(Collectors.toList()), selectedItem); + .toList(), selectedItem); contentView.addView(radioGroup); } else { diff --git a/app/src/main/java/com/mrboomdev/awery/ui/adapter/MediaPlayEpisodesAdapter.java b/app/src/main/java/com/mrboomdev/awery/ui/adapter/MediaPlayEpisodesAdapter.java index d2aff5ac..7ef82ffe 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/adapter/MediaPlayEpisodesAdapter.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/adapter/MediaPlayEpisodesAdapter.java @@ -9,25 +9,31 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import com.mrboomdev.awery.AweryApp; +import com.mrboomdev.awery.data.db.DBCatalogMedia; import com.mrboomdev.awery.extensions.support.template.CatalogEpisode; import com.mrboomdev.awery.databinding.ItemListEpisodeBinding; +import com.mrboomdev.awery.extensions.support.template.CatalogMedia; import com.mrboomdev.awery.util.UniqueIdGenerator; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; +import java.util.Collections; public class MediaPlayEpisodesAdapter extends RecyclerView.Adapter { private final UniqueIdGenerator idGenerator = new UniqueIdGenerator(); private OnEpisodeSelectedListener onEpisodeSelectedListener; private ArrayList items = new ArrayList<>(); + private CatalogMedia media; public MediaPlayEpisodesAdapter() { setHasStableIds(true); } @SuppressLint("NotifyDataSetChanged") - public void setItems(@NonNull Collection items) { + public void setItems(CatalogMedia media, @NonNull Collection items) { + this.media = media; idGenerator.clear(); for(var item : items) { @@ -35,6 +41,8 @@ public void setItems(@NonNull Collection items) { } this.items = new ArrayList<>(items); + Collections.sort(this.items); + notifyDataSetChanged(); } @@ -59,8 +67,21 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { var holder = new ViewHolder(binding); binding.container.setOnClickListener(v -> { + var item = holder.getItem(); + + if(media.lastEpisode < item.getNumber()) { + media.lastEpisode = item.getNumber(); + notifyItemRangeChanged(0, items.size()); + + new Thread(() -> { + var db = AweryApp.getDatabase().getMediaDao(); + var dbMedia = DBCatalogMedia.fromCatalogMedia(media); + db.insert(dbMedia); + }).start(); + } + if(onEpisodeSelectedListener == null) return; - onEpisodeSelectedListener.onEpisodeSelected(holder.getItem(), items); + onEpisodeSelectedListener.onEpisodeSelected(item, items); }); return holder; @@ -76,7 +97,7 @@ public int getItemCount() { return items.size(); } - public static class ViewHolder extends RecyclerView.ViewHolder { + public class ViewHolder extends RecyclerView.ViewHolder { private final ItemListEpisodeBinding binding; private CatalogEpisode item; @@ -94,6 +115,7 @@ public void bind(@NonNull CatalogEpisode item) { this.item = item; binding.title.setText(item.getTitle()); + binding.container.setAlpha((media.lastEpisode >= item.getNumber()) ? .5f : 1); if(item.getReleaseDate() > 0) { var calendar = Calendar.getInstance(); diff --git a/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaInfoFragment.java b/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaInfoFragment.java index eaccd3a6..baddf52d 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaInfoFragment.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaInfoFragment.java @@ -4,6 +4,7 @@ import android.content.res.Configuration; import android.os.Bundle; import android.text.Html; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,6 +29,7 @@ import java.util.Calendar; public class MediaInfoFragment extends Fragment { + private static final String TAG = "MediaInfoFragment"; private MediaDetailsOverviewLayoutBinding binding; private CatalogMedia media; @@ -64,7 +66,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { throw new IOException("Failed to restore media!"); } } catch(IOException e) { - e.printStackTrace(); + Log.e(TAG, "Failed to restore media!", e); AweryApp.toast("Failed to restore media!", 1); } } diff --git a/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java b/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java index 94f40c63..10a99211 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java @@ -23,6 +23,9 @@ import com.mrboomdev.awery.AweryApp; import com.mrboomdev.awery.R; +import com.mrboomdev.awery.databinding.ItemListDropdownBinding; +import com.mrboomdev.awery.databinding.LayoutLoadingBinding; +import com.mrboomdev.awery.databinding.LayoutWatchVariantsBinding; import com.mrboomdev.awery.extensions.Extension; import com.mrboomdev.awery.extensions.ExtensionProvider; import com.mrboomdev.awery.extensions.ExtensionProviderChild; @@ -31,9 +34,6 @@ import com.mrboomdev.awery.extensions.support.template.CatalogEpisode; import com.mrboomdev.awery.extensions.support.template.CatalogFilter; import com.mrboomdev.awery.extensions.support.template.CatalogMedia; -import com.mrboomdev.awery.databinding.ItemListDropdownBinding; -import com.mrboomdev.awery.databinding.LayoutLoadingBinding; -import com.mrboomdev.awery.databinding.LayoutWatchVariantsBinding; import com.mrboomdev.awery.ui.activity.player.PlayerActivity; import com.mrboomdev.awery.ui.adapter.MediaPlayEpisodesAdapter; import com.mrboomdev.awery.util.CachedValue; @@ -276,7 +276,7 @@ private void loadEpisodesFromSource(@NonNull ExtensionProvider source) { AweryApp.runOnUiThread(() -> { try { - episodesAdapter.setItems(Collections.emptyList()); + episodesAdapter.setItems(media, Collections.emptyList()); } catch(IllegalStateException e) { Log.e(TAG, "Lets hope that the episodes adapter was just created ._."); } @@ -314,7 +314,7 @@ public void onSuccess(List episodes) { activity.runOnUiThread(() -> { placeholderAdapter.setEnabled(false); - episodesAdapter.setItems(episodes); + episodesAdapter.setItems(media, episodes); }); } diff --git a/app/src/main/java/com/mrboomdev/awery/util/MediaUtils.java b/app/src/main/java/com/mrboomdev/awery/util/MediaUtils.java index 793241ab..6bb92206 100644 --- a/app/src/main/java/com/mrboomdev/awery/util/MediaUtils.java +++ b/app/src/main/java/com/mrboomdev/awery/util/MediaUtils.java @@ -3,6 +3,7 @@ import android.app.Dialog; import android.content.Context; import android.content.Intent; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -13,13 +14,13 @@ import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.mrboomdev.awery.AweryApp; -import com.mrboomdev.awery.extensions.support.template.CatalogList; -import com.mrboomdev.awery.extensions.support.template.CatalogMedia; -import com.mrboomdev.awery.extensions.support.template.CatalogTag; import com.mrboomdev.awery.data.db.DBCatalogMedia; import com.mrboomdev.awery.data.settings.AwerySettings; import com.mrboomdev.awery.databinding.PopupMediaActionsBinding; import com.mrboomdev.awery.databinding.PopupMediaBookmarkBinding; +import com.mrboomdev.awery.extensions.support.template.CatalogList; +import com.mrboomdev.awery.extensions.support.template.CatalogMedia; +import com.mrboomdev.awery.extensions.support.template.CatalogTag; import com.mrboomdev.awery.ui.activity.MediaActivity; import com.mrboomdev.awery.ui.fragments.LibraryFragment; import com.mrboomdev.awery.util.ui.dialog.DialogUtil; @@ -29,7 +30,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; public class MediaUtils { public static final String ACTION_INFO = "info"; @@ -49,9 +49,7 @@ public static void launchMediaActivity(Context context, CatalogMedia media) { } public static Collection filterMedia(@NonNull Collection items) { - return items.stream() - .filter(item -> !isMediaFiltered(item)) - .collect(Collectors.toList()); + return items.stream().filter(item -> !isMediaFiltered(item)).toList(); } @Contract(pure = true) @@ -221,7 +219,7 @@ public static void openMediaBookmarkMenu(Context context, CatalogMedia media) { new Thread(() -> { var lists = AweryApp.getDatabase().getListDao().getAll().stream() .filter(item -> !AweryApp.HIDDEN_LISTS.contains(item.getId())) - .collect(Collectors.toList()); + .toList(); var current = AweryApp.getDatabase().getMediaDao().get(media.globalId); var mediaDao = AweryApp.getDatabase().getMediaDao(); @@ -241,7 +239,7 @@ public static void openMediaBookmarkMenu(Context context, CatalogMedia media) { checkbox.setText(item.getTitle()); binding.lists.addView(checkbox); - if(current != null && current.lists.contains(item.getId())) { + if(current != null && current.lists != null && current.lists.contains(item.getId())) { checked.put(item.getId(), true); checkbox.setChecked(true); } @@ -283,7 +281,7 @@ public static void openMediaBookmarkMenu(Context context, CatalogMedia media) { LibraryFragment.notifyDataChanged(); } catch(Exception e) { AweryApp.toast("Failed to save!"); - e.printStackTrace(); + Log.e("MediaUtils", "Failed to save bookmark", e); } }).start(); }); diff --git a/app/src/main/java/com/mrboomdev/awery/util/StringUtil.java b/app/src/main/java/com/mrboomdev/awery/util/StringUtil.java index c948b1f5..7be39c39 100644 --- a/app/src/main/java/com/mrboomdev/awery/util/StringUtil.java +++ b/app/src/main/java/com/mrboomdev/awery/util/StringUtil.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Unmodifiable; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -71,6 +72,30 @@ public static String formatTimestamp(long value) { (int) value / 60, (int) value % 60); } + @NonNull + public static String formatDate(long value) { + if(value < 0) { + return "0s"; + } + + value /= 1000; + + var seconds = (int) value % 60; + var minutes = (int) value / 60; + + if(minutes >= 60) { + return String.format(Locale.ENGLISH, "%dh %02d:%02d", + minutes / 60, minutes % 60, seconds); + } + + if(minutes >= 1) { + return String.format(Locale.ENGLISH, "%dm %02ds", + minutes, seconds); + } + + return String.format(Locale.ENGLISH, "%ds", seconds); + } + /** * Parses string to enum. If string is null or enum class is null, returns null. * If string is not a valid enum, returns null. @@ -124,6 +149,7 @@ public static String listToUniqueString(@NonNull Iterable iterable) { */ @NonNull public static @Unmodifiable List uniqueStringToList(@NonNull String uniqueString) { + if(uniqueString.equals(";;;")) return Collections.emptyList(); return List.of(uniqueString.substring(3, uniqueString.length() - 3).split(";;;")); } diff --git a/app/src/main/res/layout/screen_player.xml b/app/src/main/res/layout/screen_player.xml index c711cb9a..8ab1caea 100644 --- a/app/src/main/res/layout/screen_player.xml +++ b/app/src/main/res/layout/screen_player.xml @@ -268,7 +268,7 @@ android:id="@+id/quick_skip" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Skip 1:00" + tools:text="Skip 1:00" app:backgroundTint="#fff" android:textColor="#000" app:icon="@drawable/outline_fast_forward_24"