mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-02 23:56:12 +00:00
getUserHistory support
This commit is contained in:
parent
0af4bad906
commit
90dca2537a
18 changed files with 210 additions and 14 deletions
|
@ -251,6 +251,9 @@ class PlatformVideo extends PlatformContent {
|
|||
this.duration = obj.duration ?? -1; //Long
|
||||
this.viewCount = obj.viewCount ?? -1; //Long
|
||||
|
||||
this.playbackTime = obj.playbackTime ?? -1;
|
||||
this.playbackDate = obj.playbackDate ?? undefined;
|
||||
|
||||
this.isLive = obj.isLive ?? false; //Boolean
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
}
|
||||
|
|
|
@ -182,6 +182,10 @@ interface IPlatformClient {
|
|||
* Retrieves the subscriptions of the currently logged in user
|
||||
*/
|
||||
fun getUserSubscriptions(): Array<String>;
|
||||
/**
|
||||
* Retrieves the history of the currently logged in user
|
||||
*/
|
||||
fun getUserHistory(): IPager<IPlatformContent>;
|
||||
|
||||
|
||||
fun isClaimTypeSupported(claimType: Int): Boolean;
|
||||
|
|
|
@ -20,7 +20,8 @@ data class PlatformClientCapabilities(
|
|||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false,
|
||||
val hasGetChannelPlaylists: Boolean = false,
|
||||
val hasGetContentRecommendations: Boolean = false
|
||||
val hasGetContentRecommendations: Boolean = false,
|
||||
val hasGetUserHistory: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.video
|
|||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
/**
|
||||
* A search result representing a video (overview data)
|
||||
|
@ -12,6 +13,9 @@ interface IPlatformVideo : IPlatformContent {
|
|||
val duration: Long;
|
||||
val viewCount: Long;
|
||||
|
||||
val playbackTime: Long;
|
||||
val playbackDate: OffsetDateTime?;
|
||||
|
||||
val isLive : Boolean;
|
||||
|
||||
val isShort: Boolean;
|
||||
|
|
|
@ -6,8 +6,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.polycentric.core.combineHashCodes
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
@ -33,6 +31,10 @@ open class SerializedPlatformVideo(
|
|||
|
||||
override val isLive: Boolean = false;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
override fun toJson() : String {
|
||||
return Json.encodeToString(this);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
|||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
|
@ -43,6 +42,10 @@ open class SerializedPlatformVideoDetails(
|
|||
) : IPlatformVideo, IPlatformVideoDetails {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
override val isLive: Boolean get() = false;
|
||||
|
||||
override val dash: IDashManifestSource? get() = null;
|
||||
|
|
|
@ -272,7 +272,8 @@ open class JSClient : IPlatformClient {
|
|||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
|
||||
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -712,6 +713,13 @@ open class JSClient : IPlatformClient {
|
|||
.toTypedArray();
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
|
||||
override fun getUserHistory(): IPager<IPlatformContent> {
|
||||
ensureEnabled();
|
||||
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
||||
}
|
||||
|
||||
fun validate() {
|
||||
try {
|
||||
plugin.start();
|
||||
|
|
|
@ -5,10 +5,16 @@ import com.futo.platformplayer.constructs.Event0
|
|||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
@ -110,7 +116,28 @@ class SourcePluginDescriptor {
|
|||
var enableShorts: Boolean? = null;
|
||||
}
|
||||
|
||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
|
||||
@FormField(R.string.sync, "group", R.string.sync_desc, 3)
|
||||
var sync = Sync();
|
||||
@Serializable
|
||||
class Sync {
|
||||
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1)
|
||||
var enableHistorySync: Boolean? = null;
|
||||
|
||||
@FormField(R.string.sync_history, FieldForm.BUTTON, R.string.sync_history_desc, 2)
|
||||
@FormFieldButton()
|
||||
fun syncHistoryNow() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val clients = StatePlatform.instance.getEnabledClients();
|
||||
for (client in clients) {
|
||||
if (client is JSClient) {//) && client.descriptor.appSettings.sync.enableHistorySync == true) {
|
||||
StateHistory.instance.syncRemoteHistory(client);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
||||
var rateLimit = RateLimit();
|
||||
@Serializable
|
||||
class RateLimit {
|
||||
|
|
|
@ -8,6 +8,10 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
|
@ -17,6 +21,10 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||
final override val duration: Long;
|
||||
final override val viewCount: Long;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
final override val isLive: Boolean;
|
||||
final override val isShort: Boolean;
|
||||
|
||||
|
@ -29,5 +37,11 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||
playbackTime = _content.getOrDefault<Long>(config, "playbackTime", contextName, -1)?.toLong() ?: -1;
|
||||
val playbackDateInt = _content.getOrDefault<Int>(config, "playbackDate", contextName, null)?.toLong();
|
||||
if(playbackDateInt == null || playbackDateInt == 0.toLong())
|
||||
playbackDate = null;
|
||||
else
|
||||
playbackDate = OffsetDateTime.of(LocalDateTime.ofEpochSecond(playbackDateInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
|
@ -19,7 +18,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
|
@ -53,6 +52,10 @@ class LocalVideoDetails: IPlatformVideoDetails {
|
|||
override val isLive: Boolean = false;
|
||||
override val isShort: Boolean = false;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
constructor(file: File) {
|
||||
id = PlatformID("Local", file.path, "LOCAL")
|
||||
name = file.name;
|
||||
|
|
|
@ -47,10 +47,10 @@ class DeveloperEndpoints(private val context: Context) {
|
|||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||
|
||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject> {
|
||||
val remotes = mutableListOf<V8RemoteObject>();
|
||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject?> {
|
||||
val remotes = mutableListOf<V8RemoteObject?>();
|
||||
for(obj in objs)
|
||||
remotes.add(createRemoteObject(obj)!!);
|
||||
remotes.add(createRemoteObject(obj));
|
||||
return remotes;
|
||||
}
|
||||
private inline fun <reified T> createRemoteObject(obj: T): V8RemoteObject? {
|
||||
|
|
|
@ -73,6 +73,10 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||
|
||||
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
//TODO: Offline subtitles
|
||||
override val subtitles: List<ISubtitleSource> = listOf();
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ class V8RemoteObject {
|
|||
}
|
||||
|
||||
|
||||
fun List<V8RemoteObject>.serialize() : String {
|
||||
fun List<V8RemoteObject?>.serialize() : String {
|
||||
return _gson.toJson(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.views.pills.WidePillButton
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
@ -152,6 +153,9 @@ class TutorialFragment : MainFragment() {
|
|||
override val viewCount: Long = -1
|
||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||
override val isShort: Boolean = false;
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
|
|
@ -636,6 +636,20 @@ class StateApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val enabledPlugins = StatePlatform.instance.getEnabledClients();
|
||||
for(plugin in enabledPlugins) {
|
||||
try {
|
||||
if(plugin is JSClient) {
|
||||
if(plugin.descriptor.appSettings.sync.enableHistorySync == true)
|
||||
StateHistory.instance.syncRemoteHistory(plugin);
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to update remote history for ${plugin.name}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
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.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StatePlaylists.Companion
|
||||
import com.futo.platformplayer.states.StateApp.Companion
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||
|
@ -19,7 +22,6 @@ import kotlinx.coroutines.launch
|
|||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import kotlin.math.min
|
||||
|
||||
class StateHistory {
|
||||
//Legacy
|
||||
|
@ -31,6 +33,8 @@ class StateHistory {
|
|||
})
|
||||
.load();
|
||||
|
||||
private val _remoteHistoryDatesStore = FragmentedStorage.get<StringDateMapStorage>("remoteHistoryDates");
|
||||
|
||||
private val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
|
||||
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
|
||||
.withIndex({ it.url }, historyIndex, false, true)
|
||||
|
@ -186,8 +190,95 @@ class StateHistory {
|
|||
val toDelete = _historyDBStore.getAllIndexes().filter { minutesToDelete == -1L || (now - it.datetime) < minutesToDelete * 60 };
|
||||
for(item in toDelete)
|
||||
_historyDBStore.delete(item);
|
||||
_remoteHistoryDatesStore.map = HashMap<String, Long>();
|
||||
_remoteHistoryDatesStore.save();
|
||||
}
|
||||
|
||||
fun syncRemoteHistory(plugin: JSClient) {
|
||||
if (plugin.capabilities.hasGetUserHistory &&
|
||||
plugin.isLoggedIn) {
|
||||
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
|
||||
|
||||
val hist = StatePlatform.instance.getUserHistory(plugin.id);
|
||||
|
||||
syncRemoteHistory(plugin.id, hist, 100, 3);
|
||||
}
|
||||
}
|
||||
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int) {
|
||||
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
|
||||
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
|
||||
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
|
||||
var exceededDate = false;
|
||||
try {
|
||||
val toSync = mutableListOf<IPlatformVideo>();
|
||||
var pageCount = 0;
|
||||
var videoCount = 0;
|
||||
var isFirst = true;
|
||||
var oldestPlayback = OffsetDateTime.MAX;
|
||||
var newestPlayback = OffsetDateTime.MIN;
|
||||
do {
|
||||
if (!isFirst) videos.nextPage();
|
||||
val newVideos = videos.getResults();
|
||||
|
||||
var foundVideos = false;
|
||||
var toSyncAddedCount = 0;
|
||||
for(video in newVideos) {
|
||||
if(video is IPlatformVideo && video.playbackDate != null) {
|
||||
|
||||
if(video.playbackDate!! < lastDate) {
|
||||
exceededDate = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(video.playbackTime > 0) {
|
||||
toSync.add(video);
|
||||
toSyncAddedCount++;
|
||||
foundVideos = true;
|
||||
oldestPlayback = video.playbackDate!!;
|
||||
if(newestPlayback == OffsetDateTime.MIN)
|
||||
newestPlayback = video.playbackDate!!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageCount++;
|
||||
videoCount += newVideos.size;
|
||||
isFirst = false;
|
||||
|
||||
if(!foundVideos)
|
||||
{
|
||||
Logger.i(TAG, "Found no more videos in remote history");
|
||||
break;
|
||||
}
|
||||
}
|
||||
while(videos.hasMorePages() && videoCount <= maxVideosCount && pageCount <= maxPageCount && !exceededDate);
|
||||
|
||||
var updated = 0;
|
||||
if(oldestPlayback < OffsetDateTime.MAX) {
|
||||
for(video in toSync){
|
||||
val hist = getHistoryByVideo(video, true, video.playbackDate);
|
||||
if(hist != null && hist.position < video.playbackTime) {
|
||||
Logger.i(TAG, "Updated history for video [${video.name}] from remote history");
|
||||
updateHistoryPosition(video, hist, true, video.playbackTime, video.playbackDate, false);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
if(updated > 0) {
|
||||
_remoteHistoryDatesStore.setAndSave(pluginId, newestPlayback);
|
||||
|
||||
try {
|
||||
val client = StatePlatform.instance.getClient(pluginId);
|
||||
UIDialogs.appToast("Updated ${updated} history from ${client.name}")
|
||||
}
|
||||
catch(ex: Throwable){}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
|
||||
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "StateHistory";
|
||||
|
|
|
@ -1036,6 +1036,16 @@ class StatePlatform {
|
|||
return client.getLiveChatWindow(url);
|
||||
}
|
||||
|
||||
//Account
|
||||
fun getUserHistory(id: String): IPager<IPlatformContent> {
|
||||
val client = getClient(id);
|
||||
if(client is JSClient && client.isLoggedIn) {
|
||||
return client.fromPool(_pagerClientPool).getUserHistory()
|
||||
}
|
||||
return EmptyPager<IPlatformContent>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun injectDevPlugin(source: SourcePluginConfig, script: String): String? {
|
||||
var devId: String? = null;
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
<string name="general">General</string>
|
||||
<string name="channel">Channel</string>
|
||||
<string name="home">Home</string>
|
||||
<string name="sync_history">Sync Remote History</string>
|
||||
<string name="sync_history_desc">Synchronize account history from this platform on startup</string>
|
||||
<string name="progress_bar">Progress Bar</string>
|
||||
<string name="advanced_settings">Advanced Settings</string>
|
||||
<string name="advanced_settings_description">If advanced settings should be shown, this exposes additional settings to finetune your experience.</string>
|
||||
|
@ -592,6 +594,8 @@
|
|||
<string name="various_tests_against_a_custom_source">Various tests against a custom source</string>
|
||||
<string name="writes_to_disk_till_no_space_is_left">Writes to disk till no space is left</string>
|
||||
<string name="visibility">Visibility</string>
|
||||
<string name="sync">Sync</string>
|
||||
<string name="sync_desc">Synchronization of platform data</string>
|
||||
<string name="check_for_updates_setting">Check for updates</string>
|
||||
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
||||
<string name="automatic_update_setting">Automatic Update</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue