diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2fd04972..a659758a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,8 @@
+
+
+
();
- var menu: SlideUpMenuOverlay? = null;
val originalNotif = subscription.doNotifications;
val originalLive = subscription.doFetchLive;
@@ -62,54 +62,69 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts;
- items.addAll(listOf(
- SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
- subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
- }, false),
- SlideUpMenuGroup(container.context, "Fetch Settings",
- "Depending on the platform you might not need to enable a type for it to be available.",
- -1, listOf()),
- SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
- subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
- }, false),
- SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for finished streams", "fetchStreams", {
- subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchLive;
- }, false),
- SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
- subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchLive;
- }, false),
- SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
- subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchLive;
- }, false)));
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
+ val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
+ val capabilities = plugin.getChannelCapabilities();
- menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
+ withContext(Dispatchers.Main) {
- if(subscription.doNotifications)
- menu.selectOption(null, "notifications", true, true);
- if(subscription.doFetchLive)
- menu.selectOption(null, "fetchLive", true, true);
- if(subscription.doFetchStreams)
- menu.selectOption(null, "fetchStreams", true, true);
- if(subscription.doFetchVideos)
- menu.selectOption(null, "fetchVideos", true, true);
- if(subscription.doFetchPosts)
- menu.selectOption(null, "fetchPosts", true, true);
+ var menu: SlideUpMenuOverlay? = null;
- menu.onOK.subscribe {
- subscription.save();
- menu.hide(true);
- };
- menu.onCancel.subscribe {
- subscription.doNotifications = originalNotif;
- subscription.doFetchLive = originalLive;
- subscription.doFetchStreams = originalStream;
- subscription.doFetchVideos = originalVideo;
- subscription.doFetchPosts = originalPosts;
- };
- menu.setOk("Save");
+ items.addAll(listOf(
+ SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
+ subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
+ }, false),
+ SlideUpMenuGroup(container.context, "Fetch Settings",
+ "Depending on the platform you might not need to enable a type for it to be available.",
+ -1, listOf()),
+ if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
+ subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
+ }, false) else null,
+ if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
+ subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
+ }, false) else null,
+ if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
+ SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
+ subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
+ }, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
+ SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
+ subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
+ }, false) else null,
+ if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
+ subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
+ }, false) else null).filterNotNull());
- menu.show();
+ menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
+
+ if(subscription.doNotifications)
+ menu.selectOption(null, "notifications", true, true);
+ if(subscription.doFetchLive)
+ menu.selectOption(null, "fetchLive", true, true);
+ if(subscription.doFetchStreams)
+ menu.selectOption(null, "fetchStreams", true, true);
+ if(subscription.doFetchVideos)
+ menu.selectOption(null, "fetchVideos", true, true);
+ if(subscription.doFetchPosts)
+ menu.selectOption(null, "fetchPosts", true, true);
+
+ menu.onOK.subscribe {
+ subscription.save();
+ menu.hide(true);
+ };
+ menu.onCancel.subscribe {
+ subscription.doNotifications = originalNotif;
+ subscription.doFetchLive = originalLive;
+ subscription.doFetchStreams = originalStream;
+ subscription.doFetchVideos = originalVideo;
+ subscription.doFetchPosts = originalPosts;
+ };
+
+ menu.setOk("Save");
+
+ menu.show();
+ }
+ }
}
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
diff --git a/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt b/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt
index e2566c40..fbebe2b2 100644
--- a/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt
+++ b/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt
@@ -6,33 +6,25 @@ import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
-import android.media.MediaSession2Service.MediaNotification
-import androidx.concurrent.futures.CallbackToFutureAdapter
-import androidx.concurrent.futures.ResolvableFuture
import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
-import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
+import com.futo.platformplayer.Settings
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
-import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp
-import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StateNotifications
import com.futo.platformplayer.states.StateSubscriptions
-import com.futo.platformplayer.views.adapters.viewholders.TabViewHolder
-import com.google.common.util.concurrent.ListenableFuture
+import com.futo.platformplayer.toHumanNowDiffString
+import com.futo.platformplayer.toHumanNowDiffStringMinDay
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.time.OffsetDateTime
@@ -54,8 +46,10 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
this.setSound(null, null);
};
notificationManager.createNotificationChannel(notificationChannel);
+ val contentChannel = StateNotifications.instance.contentNotifChannel
+ notificationManager.createNotificationChannel(contentChannel);
try {
- doSubscriptionUpdating(notificationManager, notificationChannel);
+ doSubscriptionUpdating(notificationManager, notificationChannel, contentChannel);
}
catch(ex: Throwable) {
exception = ex;
@@ -77,13 +71,13 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
}
- suspend fun doSubscriptionUpdating(manager: NotificationManager, notificationChannel: NotificationChannel) {
- val notif = NotificationCompat.Builder(appContext, notificationChannel.id)
+ suspend fun doSubscriptionUpdating(manager: NotificationManager, backgroundChannel: NotificationChannel, contentChannel: NotificationChannel) {
+ val notif = NotificationCompat.Builder(appContext, backgroundChannel.id)
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
.setContentTitle("Grayjay")
.setContentText("Updating subscriptions...")
.setSilent(true)
- .setChannelId(notificationChannel.id)
+ .setChannelId(backgroundChannel.id)
.setProgress(1, 0, true);
manager.notify(12, notif.build());
@@ -94,6 +88,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
val newItems = mutableListOf();
val now = OffsetDateTime.now();
+ val threeDays = now.minusDays(4);
val contentNotifs = mutableListOf>();
withContext(Dispatchers.IO) {
val results = StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total ->
@@ -111,8 +106,14 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
synchronized(newSubChanges) {
if(!newSubChanges.contains(sub)) {
newSubChanges.add(sub);
- if(sub.doNotifications && content.datetime?.let { it < now } == true)
- contentNotifs.add(Pair(sub, content));
+ if(sub.doNotifications) {
+ if(content.datetime != null) {
+ if(content.datetime!! <= now.plusMinutes(StateNotifications.instance.plannedWarningMinutesEarly) && content.datetime!! > threeDays)
+ contentNotifs.add(Pair(sub, content));
+ else if(content.datetime!! > now.plusMinutes(StateNotifications.instance.plannedWarningMinutesEarly) && Settings.instance.notifications.plannedContentNotification)
+ StateNotifications.instance.scheduleContentNotification(applicationContext, content);
+ }
+ }
}
newItems.add(content);
}
@@ -135,22 +136,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
val items = contentNotifs.take(5).toList()
for(i in items.indices) {
val contentNotif = items.get(i);
- val thumbnail = if(contentNotif.second is IPlatformVideo) (contentNotif.second as IPlatformVideo).thumbnails.getHQThumbnail()
- else null;
- if(thumbnail != null)
- Glide.with(appContext).asBitmap()
- .load(thumbnail)
- .into(object: CustomTarget() {
- override fun onResourceReady(resource: Bitmap, transition: Transition?) {
- notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, resource);
- }
- override fun onLoadCleared(placeholder: Drawable?) {}
- override fun onLoadFailed(errorDrawable: Drawable?) {
- notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, null);
- }
- })
- else
- notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, null);
+ StateNotifications.instance.notifyNewContentWithThumbnail(appContext, manager, contentChannel, 13 + i, contentNotif.second);
}
}
catch(ex: Throwable) {
@@ -165,20 +151,4 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
.setSilent(true)
.setChannelId(notificationChannel.id).build());*/
}
-
- fun notifyNewContent(manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, sub: Subscription, content: IPlatformContent, thumbnail: Bitmap? = null) {
- val notifBuilder = NotificationCompat.Builder(appContext, notificationChannel.id)
- .setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
- .setContentTitle("New by [${sub.channel.name}]")
- .setContentText("${content.name}")
- .setSilent(true)
- .setContentIntent(PendingIntent.getActivity(this.appContext, 0, MainActivity.getVideoIntent(this.appContext, content.url),
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
- .setChannelId(notificationChannel.id);
- if(thumbnail != null) {
- //notifBuilder.setLargeIcon(thumbnail);
- notifBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(thumbnail).bigLargeIcon(null as Bitmap?));
- }
- manager.notify(id, notifBuilder.build());
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
index 5a5faa6b..87614cc0 100644
--- a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
+++ b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
@@ -58,6 +58,14 @@ class ChannelContentCache {
uncacheContent(content);
}
}
+ fun clearToday() {
+ val yesterday = OffsetDateTime.now().minusDays(1);
+ synchronized(_channelContents) {
+ for(channel in _channelContents)
+ for(content in channel.value.getItems().filter { it.datetime?.isAfter(yesterday) == true })
+ uncacheContent(content);
+ }
+ }
fun getChannelCachePager(channelUrl: String): PlatformContentPager {
val validID = channelUrl.toSafeFileName();
diff --git a/app/src/main/java/com/futo/platformplayer/receivers/PlannedNotificationReceiver.kt b/app/src/main/java/com/futo/platformplayer/receivers/PlannedNotificationReceiver.kt
new file mode 100644
index 00000000..e1805c23
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/receivers/PlannedNotificationReceiver.kt
@@ -0,0 +1,48 @@
+package com.futo.platformplayer.receivers
+
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.futo.platformplayer.Settings
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StateNotifications
+
+
+class PlannedNotificationReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ try {
+ Logger.i(TAG, "Planned Notification received");
+ if(!Settings.instance.notifications.plannedContentNotification)
+ return;
+ if(StateApp.instance.contextOrNull == null)
+ StateApp.instance.initializeFiles();
+
+ val notifs = StateNotifications.instance.getScheduledNotifications(60 * 15, true);
+ if(!notifs.isEmpty() && context != null) {
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
+ val channel = StateNotifications.instance.contentNotifChannel;
+ notificationManager.createNotificationChannel(channel);
+ var i = 0;
+ for (notif in notifs) {
+ StateNotifications.instance.notifyNewContentWithThumbnail(context, notificationManager, channel, 110 + i, notif);
+ i++;
+ }
+ }
+ }
+ catch(ex: Throwable) {
+ Logger.e(TAG, "Failed PlannedNotificationReceiver.onReceive", ex);
+ }
+ }
+
+ companion object {
+ private val TAG = "PlannedNotificationReceiver"
+
+ fun getIntent(context: Context): PendingIntent {
+ return PendingIntent.getBroadcast(context, 110, Intent(context, PlannedNotificationReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt b/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt
new file mode 100644
index 00000000..1ce15a20
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt
@@ -0,0 +1,147 @@
+package com.futo.platformplayer.states
+
+import android.app.AlarmManager
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
+import com.futo.platformplayer.activities.MainActivity
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
+import com.futo.platformplayer.api.media.models.video.IPlatformVideo
+import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
+import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.receivers.PlannedNotificationReceiver
+import com.futo.platformplayer.serializers.PlatformContentSerializer
+import com.futo.platformplayer.stores.FragmentedStorage
+import com.futo.platformplayer.toHumanNowDiffString
+import com.futo.platformplayer.toHumanNowDiffStringMinDay
+import java.time.OffsetDateTime
+
+class StateNotifications {
+ private val _alarmManagerLock = Object();
+ private var _alarmManager: AlarmManager? = null;
+ val plannedWarningMinutesEarly: Long = 10;
+
+ val contentNotifChannel = NotificationChannel("contentChannel", "Content Notifications",
+ NotificationManager.IMPORTANCE_HIGH).apply {
+ this.enableVibration(false);
+ this.setSound(null, null);
+ };
+
+ private val _plannedContent = FragmentedStorage.storeJson("planned_content_notifs", PlatformContentSerializer())
+ .load();
+
+ private fun getAlarmManager(context: Context): AlarmManager {
+ synchronized(_alarmManagerLock) {
+ if(_alarmManager == null)
+ _alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
+ return _alarmManager!!;
+ }
+ }
+
+ fun scheduleContentNotification(context: Context, content: IPlatformContent) {
+ try {
+ var existing = _plannedContent.findItem { it.url == content.url };
+ if(existing != null) {
+ _plannedContent.delete(existing);
+ existing = null;
+ }
+ if(existing == null && content.datetime != null) {
+ val item = SerializedPlatformContent.fromContent(content);
+ _plannedContent.saveAsync(item);
+
+ val manager = getAlarmManager(context);
+ val notifyDateTime = content.datetime!!.minusMinutes(plannedWarningMinutesEarly);
+ if(Build.VERSION.SDK_INT >= 31 && !manager.canScheduleExactAlarms()) {
+ Logger.i(TAG, "Scheduling in-exact notification for [${content.name}] at ${notifyDateTime.toHumanNowDiffString()}")
+ manager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, notifyDateTime.toEpochSecond().times(1000), PlannedNotificationReceiver.getIntent(context));
+ }
+ else {
+ Logger.i(TAG, "Scheduling exact notification for [${content.name}] at ${notifyDateTime.toHumanNowDiffString()}")
+ manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, notifyDateTime.toEpochSecond().times(1000), PlannedNotificationReceiver.getIntent(context))
+ }
+ }
+ }
+ catch(ex: Throwable) {
+ Logger.e(TAG, "scheduleContentNotification failed for [${content.name}]", ex);
+ }
+ }
+ fun removeChannelPlannedContent(channelUrl: String) {
+ val toDeletes = _plannedContent.findItems { it.author.url == channelUrl };
+ for(toDelete in toDeletes)
+ _plannedContent.delete(toDelete);
+ }
+
+ fun getScheduledNotifications(secondsFuture: Long, deleteReturned: Boolean = false): List {
+ val minDate = OffsetDateTime.now().plusSeconds(secondsFuture);
+ val toNotify = _plannedContent.findItems { it.datetime?.let { it.isBefore(minDate) } == true }
+
+ if(deleteReturned) {
+ for(toDelete in toNotify)
+ _plannedContent.delete(toDelete);
+ }
+ return toNotify;
+ }
+
+ fun notifyNewContentWithThumbnail(context: Context, manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, content: IPlatformContent) {
+ val thumbnail = if(content is IPlatformVideo) (content as IPlatformVideo).thumbnails.getHQThumbnail()
+ else null;
+ if(thumbnail != null)
+ Glide.with(context).asBitmap()
+ .load(thumbnail)
+ .into(object: CustomTarget() {
+ override fun onResourceReady(resource: Bitmap, transition: Transition?) {
+ notifyNewContent(context, manager, notificationChannel, id, content, resource);
+ }
+ override fun onLoadCleared(placeholder: Drawable?) {}
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ notifyNewContent(context, manager, notificationChannel, id, content, null);
+ }
+ })
+ else
+ notifyNewContent(context, manager, notificationChannel, id, content, null);
+ }
+
+ fun notifyNewContent(context: Context, manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, content: IPlatformContent, thumbnail: Bitmap? = null) {
+ val notifBuilder = NotificationCompat.Builder(context, notificationChannel.id)
+ .setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
+ .setContentTitle("New by [${content.author.name}]")
+ .setContentText("${content.name}")
+ .setSubText(content.datetime?.toHumanNowDiffStringMinDay())
+ .setSilent(true)
+ .setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.getVideoIntent(context, content.url),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
+ .setChannelId(notificationChannel.id);
+ if(thumbnail != null) {
+ //notifBuilder.setLargeIcon(thumbnail);
+ notifBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(thumbnail).bigLargeIcon(null as Bitmap?));
+ }
+ manager.notify(id, notifBuilder.build());
+ }
+
+
+ companion object {
+ val TAG = "StateNotifications";
+ private var _instance : StateNotifications? = null;
+ val instance : StateNotifications
+ get(){
+ if(_instance == null)
+ _instance = StateNotifications();
+ return _instance!!;
+ };
+
+ fun finish() {
+ _instance?.let {
+ _instance = null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/stores/FragmentedStorage.kt b/app/src/main/java/com/futo/platformplayer/stores/FragmentedStorage.kt
index a996a9c8..6426e948 100644
--- a/app/src/main/java/com/futo/platformplayer/stores/FragmentedStorage.kt
+++ b/app/src/main/java/com/futo/platformplayer/stores/FragmentedStorage.kt
@@ -31,7 +31,7 @@ class FragmentedStorage {
fun initialize(filesDir: File) {
_filesDir = filesDir;
}
-
+ inline fun storeJson(name: String, serializer: KSerializer? = null): ManagedStore = store(name, JsonStoreSerializer.create(serializer), null, null);
inline fun storeJson(parentDir: File, name: String, serializer: KSerializer? = null): ManagedStore = store(name, JsonStoreSerializer.create(serializer), null, parentDir);
inline fun storeJson(name: String, prettyName: String? = null, parentDir: File? = null): ManagedStore = store(name, JsonStoreSerializer.create(), prettyName, parentDir);
inline fun store(name: String, serializer: StoreSerializer, prettyName: String? = null, parentDir: File? = null): ManagedStore {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 069318e6..c6514a13 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -267,6 +267,9 @@
A list of user-reported and self-reported issues
Also removes any data related plugin like login or settings
Announcement
+ Notifications
+ Planned Content Notifications
+ Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.
Attempt to utilize byte ranges
Auto Update
Auto-Rotate