screen saver POC
This commit is contained in:
parent
a0eb5caa78
commit
70def37196
27 changed files with 537 additions and 161 deletions
|
@ -1,2 +1,2 @@
|
|||
<dream xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsActivity="com.example.app/.ScreenSaverSettingsActivity" />
|
||||
android:settingsActivity="deckers.thibault.aves.debug/deckers.thibault.aves.ScreenSaverSettingsActivity" />
|
|
@ -141,6 +141,11 @@ This change eventually prevents building the app with Flutter v3.0.2.
|
|||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ScreenSaverSettingsActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/NormalTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".WallpaperActivity"
|
||||
android:exported="true"
|
||||
|
@ -165,6 +170,22 @@ This change eventually prevents building the app with Flutter v3.0.2.
|
|||
android:description="@string/analysis_service_description"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".ScreenSaverService"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_DREAM_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.dreams.DreamService" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.dream"
|
||||
android:resource="@xml/screen_saver" />
|
||||
</service>
|
||||
|
||||
<!-- file provider to share files having a file:// URI -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
|
|
@ -28,7 +28,7 @@ import io.flutter.plugin.common.MethodChannel
|
|||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
open class MainActivity : FlutterActivity() {
|
||||
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
||||
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
|
||||
private lateinit var intentStreamHandler: IntentStreamHandler
|
||||
|
@ -203,7 +203,7 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_MAIN -> {
|
||||
intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page ->
|
||||
|
@ -338,6 +338,8 @@ class MainActivity : FlutterActivity() {
|
|||
const val INTENT_DATA_KEY_QUERY = "query"
|
||||
|
||||
const val INTENT_ACTION_PICK = "pick"
|
||||
const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
|
||||
const val INTENT_ACTION_SCREEN_SAVER_SETTINGS = "screen_saver_settings"
|
||||
const val INTENT_ACTION_SEARCH = "search"
|
||||
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
|
||||
const val INTENT_ACTION_VIEW = "view"
|
||||
|
|
|
@ -1,4 +1,198 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
class ScreenSaverService {
|
||||
import android.service.dreams.DreamService
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
import deckers.thibault.aves.channel.calls.AccessibilityHandler
|
||||
import deckers.thibault.aves.channel.calls.DeviceHandler
|
||||
import deckers.thibault.aves.channel.calls.MediaFileHandler
|
||||
import deckers.thibault.aves.channel.calls.MediaStoreHandler
|
||||
import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler
|
||||
import deckers.thibault.aves.channel.calls.window.WindowHandler
|
||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.Log
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.FlutterSurfaceView
|
||||
import io.flutter.embedding.android.FlutterView
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
|
||||
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister
|
||||
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
// for FlutterView-level integration, cf https://docs.flutter.dev/development/add-to-app/android/add-flutter-view
|
||||
class ScreenSaverService : DreamService() {
|
||||
private var flutterEngine: FlutterEngine? = null
|
||||
private var flutterView: FlutterView? = null
|
||||
private var activePreDrawListener: ViewTreeObserver.OnPreDrawListener? = null
|
||||
private var isFlutterUiDisplayed = false
|
||||
private var isFirstFrameRendered = false
|
||||
private var isAttached = false
|
||||
|
||||
private val flutterUiDisplayListener: FlutterUiDisplayListener = object : FlutterUiDisplayListener {
|
||||
override fun onFlutterUiDisplayed() {
|
||||
isFlutterUiDisplayed = true
|
||||
isFirstFrameRendered = true
|
||||
}
|
||||
|
||||
override fun onFlutterUiNoLongerDisplayed() {
|
||||
isFlutterUiDisplayed = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
onAttach()
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
|
||||
// - need Service
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ServiceWindowHandler(this))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
|
||||
|
||||
// intent handling
|
||||
// detail fetch: dart -> platform
|
||||
MethodChannel(messenger, WallpaperActivity.VIEWER_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
result.success(intentDataMap)
|
||||
intentDataMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dream setup
|
||||
isInteractive = false
|
||||
isFullscreen = true
|
||||
setContentView(createFlutterView())
|
||||
}
|
||||
|
||||
override fun onDreamingStarted() {
|
||||
super.onDreamingStarted()
|
||||
onStart()
|
||||
}
|
||||
|
||||
override fun onDreamingStopped() {
|
||||
onDestroyView()
|
||||
super.onDreamingStopped()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
release()
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
// from `FlutterActivityAndFragmentDelegate`
|
||||
|
||||
private fun createFlutterView(): View {
|
||||
Log.d(LOG_TAG, "Creating FlutterView.")
|
||||
val flutterSurfaceView = FlutterSurfaceView(this)
|
||||
val pFlutterView = FlutterView(this, flutterSurfaceView)
|
||||
flutterView = pFlutterView
|
||||
|
||||
// Add listener to be notified when Flutter renders its first frame.
|
||||
pFlutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener)
|
||||
Log.d(LOG_TAG, "Attaching FlutterEngine to FlutterView.")
|
||||
pFlutterView.attachToFlutterEngine(flutterEngine!!)
|
||||
pFlutterView.id = FlutterActivity.FLUTTER_VIEW_ID
|
||||
delayFirstAndroidViewDraw(pFlutterView)
|
||||
return pFlutterView
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
flutterEngine = null
|
||||
flutterView = null
|
||||
}
|
||||
|
||||
private fun onAttach() {
|
||||
if (flutterEngine == null) {
|
||||
Log.d(LOG_TAG, "Setting up FlutterEngine.")
|
||||
flutterEngine = FlutterEngine(
|
||||
this,
|
||||
null,
|
||||
false,
|
||||
)
|
||||
}
|
||||
GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine!!)
|
||||
isAttached = true
|
||||
}
|
||||
|
||||
private fun onStart() {
|
||||
Log.d(LOG_TAG, "onStart()")
|
||||
doInitialFlutterViewRun()
|
||||
flutterView!!.visibility = View.VISIBLE
|
||||
flutterEngine!!.lifecycleChannel.appIsResumed()
|
||||
}
|
||||
|
||||
private fun onDestroyView() {
|
||||
Log.v(LOG_TAG, "onDestroyView()")
|
||||
flutterView ?: return
|
||||
|
||||
val pFlutterView = flutterView!!
|
||||
if (activePreDrawListener != null) {
|
||||
pFlutterView.viewTreeObserver.removeOnPreDrawListener(activePreDrawListener)
|
||||
activePreDrawListener = null
|
||||
}
|
||||
pFlutterView.detachFromFlutterEngine()
|
||||
pFlutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener)
|
||||
|
||||
flutterEngine!!.lifecycleChannel.appIsInactive()
|
||||
}
|
||||
|
||||
private fun delayFirstAndroidViewDraw(flutterView: FlutterView) {
|
||||
if (activePreDrawListener != null) {
|
||||
flutterView.viewTreeObserver.removeOnPreDrawListener(activePreDrawListener)
|
||||
}
|
||||
activePreDrawListener = object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
if (isFlutterUiDisplayed && activePreDrawListener != null) {
|
||||
flutterView.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
activePreDrawListener = null
|
||||
}
|
||||
return isFlutterUiDisplayed
|
||||
}
|
||||
}
|
||||
flutterView.viewTreeObserver.addOnPreDrawListener(activePreDrawListener)
|
||||
}
|
||||
|
||||
private fun doInitialFlutterViewRun() {
|
||||
val pFlutterEngine = flutterEngine!!
|
||||
if (pFlutterEngine.dartExecutor.isExecutingDart) {
|
||||
// No warning is logged because this situation will happen on every config
|
||||
// change if the developer does not choose to retain the Fragment instance.
|
||||
// So this is expected behavior in many cases.
|
||||
return
|
||||
}
|
||||
|
||||
pFlutterEngine.navigationChannel.setInitialRoute(DEFAULT_INITIAL_ROUTE)
|
||||
val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath()
|
||||
val entrypoint = DartEntrypoint(appBundlePathOverride, DEFAULT_DART_ENTRYPOINT)
|
||||
pFlutterEngine.dartExecutor.executeDartEntrypoint(entrypoint)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ScreenSaverService>()
|
||||
private val intentDataMap: MutableMap<String, Any?> = hashMapOf(
|
||||
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SCREEN_SAVER,
|
||||
)
|
||||
|
||||
// from `FlutterActivityLaunchConfigs`
|
||||
const val DEFAULT_DART_ENTRYPOINT = "main"
|
||||
const val DEFAULT_INITIAL_ROUTE = "/"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
class ScreenSaverSettingsActivity {
|
||||
import android.content.Intent
|
||||
|
||||
class ScreenSaverSettingsActivity : MainActivity() {
|
||||
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<dream xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsActivity="deckers.thibault.aves/deckers.thibault.aves.ScreenSaverSettingsActivity" />
|
|
@ -1,2 +1,2 @@
|
|||
<dream xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsActivity="com.example.app/.ScreenSaverSettingsActivity" />
|
||||
android:settingsActivity="deckers.thibault.aves.profile/deckers.thibault.aves.ScreenSaverSettingsActivity" />
|
2
android/app/src/profile/res/xml/screen_saverasd.xml
Normal file
2
android/app/src/profile/res/xml/screen_saverasd.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<dream xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsActivity="deckers.thibault.aves.profile/deckers.thibault.aves.ScreenSaverSettingsActivity" />
|
|
@ -4,6 +4,7 @@ enum AppMode {
|
|||
pickMultipleMediaExternal,
|
||||
pickMediaInternal,
|
||||
pickFilterInternal,
|
||||
screenSaver,
|
||||
setWallpaper,
|
||||
slideshow,
|
||||
view,
|
||||
|
|
|
@ -765,6 +765,8 @@
|
|||
"settingsUnitSystemTile": "Units",
|
||||
"settingsUnitSystemTitle": "Units",
|
||||
|
||||
"settingsScreenSaverTitle": "Screen Saver",
|
||||
|
||||
"statsPageTitle": "Stats",
|
||||
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
||||
"@statsWithGps": {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
|
@ -16,17 +17,19 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode {
|
|||
}
|
||||
}
|
||||
|
||||
void apply() {
|
||||
Future<void> apply() async {
|
||||
if (!await windowService.isActivity()) return;
|
||||
|
||||
debugPrint('Apply display refresh rate: $name');
|
||||
switch (this) {
|
||||
case DisplayRefreshRateMode.auto:
|
||||
FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
||||
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
||||
break;
|
||||
case DisplayRefreshRateMode.highest:
|
||||
FlutterDisplayMode.setHighRefreshRate();
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
break;
|
||||
case DisplayRefreshRateMode.lowest:
|
||||
FlutterDisplayMode.setLowRefreshRate();
|
||||
await FlutterDisplayMode.setLowRefreshRate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,6 +138,11 @@ class Settings extends ChangeNotifier {
|
|||
// file picker
|
||||
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
|
||||
|
||||
// screen saver
|
||||
static const screenSaverTransitionKey = 'screen_saver_transition';
|
||||
static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback';
|
||||
static const screenSaverIntervalKey = 'screen_saver_interval';
|
||||
|
||||
// slideshow
|
||||
static const slideshowRepeatKey = 'slideshow_loop';
|
||||
static const slideshowShuffleKey = 'slideshow_shuffle';
|
||||
|
@ -583,6 +588,20 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue);
|
||||
|
||||
// screen saver
|
||||
|
||||
ViewerTransition get screenSaverTransition => getEnumOrDefault(screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
|
||||
|
||||
set screenSaverTransition(ViewerTransition newValue) => setAndNotify(screenSaverTransitionKey, newValue.toString());
|
||||
|
||||
SlideshowVideoPlayback get screenSaverVideoPlayback => getEnumOrDefault(screenSaverVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values);
|
||||
|
||||
set screenSaverVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(screenSaverVideoPlaybackKey, newValue.toString());
|
||||
|
||||
SlideshowInterval get screenSaverInterval => getEnumOrDefault(screenSaverIntervalKey, SettingsDefaults.slideshowInterval, SlideshowInterval.values);
|
||||
|
||||
set screenSaverInterval(SlideshowInterval newValue) => setAndNotify(screenSaverIntervalKey, newValue.toString());
|
||||
|
||||
// slideshow
|
||||
|
||||
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
|
||||
|
@ -792,6 +811,9 @@ class Settings extends ChangeNotifier {
|
|||
case unitSystemKey:
|
||||
case accessibilityAnimationsKey:
|
||||
case timeToTakeActionKey:
|
||||
case screenSaverTransitionKey:
|
||||
case screenSaverVideoPlaybackKey:
|
||||
case screenSaverIntervalKey:
|
||||
case slideshowTransitionKey:
|
||||
case slideshowVideoPlaybackKey:
|
||||
case slideshowIntervalKey:
|
||||
|
|
|
@ -370,6 +370,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
AnalysisController? analysisController,
|
||||
String? directory,
|
||||
bool loadTopEntriesFirst = false,
|
||||
bool canAnalyze = true,
|
||||
});
|
||||
|
||||
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController});
|
||||
|
|
|
@ -24,6 +24,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
AnalysisController? analysisController,
|
||||
String? directory,
|
||||
bool loadTopEntriesFirst = false,
|
||||
bool canAnalyze = true,
|
||||
}) async {
|
||||
if (_initState == SourceInitializationState.none) {
|
||||
await _loadEssentials();
|
||||
|
@ -35,6 +36,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
analysisController: analysisController,
|
||||
directory: directory,
|
||||
loadTopEntriesFirst: loadTopEntriesFirst,
|
||||
canAnalyze: canAnalyze,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -63,6 +65,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
AnalysisController? analysisController,
|
||||
String? directory,
|
||||
required bool loadTopEntriesFirst,
|
||||
required bool canAnalyze,
|
||||
}) async {
|
||||
debugPrint('$runtimeType refresh start');
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
@ -182,7 +185,11 @@ class MediaStoreSource extends CollectionSource {
|
|||
if (analysisIds != null) {
|
||||
analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
|
||||
}
|
||||
await analyze(analysisController, entries: analysisEntries);
|
||||
if (canAnalyze) {
|
||||
await analyze(analysisController, entries: analysisEntries);
|
||||
} else {
|
||||
stateNotifier.value = SourceState.ready;
|
||||
}
|
||||
|
||||
// the home page may not reflect the current derived filters
|
||||
// as the initial addition of entries is silent,
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
abstract class WindowService {
|
||||
Future<bool> isActivity();
|
||||
|
||||
Future<void> keepScreenOn(bool on);
|
||||
|
||||
Future<bool> isRotationLocked();
|
||||
|
@ -17,6 +19,17 @@ abstract class WindowService {
|
|||
class PlatformWindowService implements WindowService {
|
||||
static const platform = MethodChannel('deckers.thibault/aves/window');
|
||||
|
||||
@override
|
||||
Future<bool> isActivity() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('isActivity');
|
||||
if (result != null) return result as bool;
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> keepScreenOn(bool on) async {
|
||||
try {
|
||||
|
|
|
@ -230,6 +230,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
break;
|
||||
case AppMode.pickMediaInternal:
|
||||
case AppMode.pickFilterInternal:
|
||||
case AppMode.screenSaver:
|
||||
case AppMode.setWallpaper:
|
||||
case AppMode.slideshow:
|
||||
case AppMode.view:
|
||||
|
|
|
@ -54,6 +54,7 @@ class InteractiveTile extends StatelessWidget {
|
|||
Navigator.pop(context, entry);
|
||||
break;
|
||||
case AppMode.pickFilterInternal:
|
||||
case AppMode.screenSaver:
|
||||
case AppMode.setWallpaper:
|
||||
case AppMode.slideshow:
|
||||
case AppMode.view:
|
||||
|
|
|
@ -63,6 +63,7 @@ class _InteractiveFilterTileState<T extends CollectionFilter> extends State<Inte
|
|||
Navigator.pop<T>(context, filter);
|
||||
break;
|
||||
case AppMode.pickMediaInternal:
|
||||
case AppMode.screenSaver:
|
||||
case AppMode.setWallpaper:
|
||||
case AppMode.slideshow:
|
||||
case AppMode.view:
|
||||
|
|
|
@ -21,7 +21,9 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/settings/screen_saver_settings_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/screen_saver_page.dart';
|
||||
import 'package:aves/widgets/wallpaper_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -49,6 +51,8 @@ class _HomePageState extends State<HomePage> {
|
|||
Set<String>? _shortcutFilters;
|
||||
|
||||
static const actionPick = 'pick';
|
||||
static const actionScreenSaver = 'screen_saver';
|
||||
static const actionScreenSaverSettings = 'screen_saver_settings';
|
||||
static const actionSearch = 'search';
|
||||
static const actionSetWallpaper = 'set_wallpaper';
|
||||
static const actionView = 'view';
|
||||
|
@ -71,20 +75,21 @@ class _HomePageState extends State<HomePage> {
|
|||
|
||||
Future<void> _setup() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
// do not check whether permission was granted,
|
||||
// as some app stores hide in some countries
|
||||
// apps that force quit on permission denial
|
||||
await [
|
||||
Permission.storage,
|
||||
// to access media with unredacted metadata with scoped storage (Android 10+)
|
||||
Permission.accessMediaLocation,
|
||||
].request();
|
||||
if (await windowService.isActivity()) {
|
||||
// do not check whether permission was granted, because some app stores
|
||||
// hide in some countries apps that force quit on permission denial
|
||||
await [
|
||||
Permission.storage,
|
||||
// to access media with unredacted metadata with scoped storage (Android 10+)
|
||||
Permission.accessMediaLocation,
|
||||
].request();
|
||||
}
|
||||
|
||||
var appMode = AppMode.main;
|
||||
final intentData = widget.intentData ?? await ViewerService.getIntentData();
|
||||
final intentAction = intentData['action'];
|
||||
|
||||
if (intentAction != actionSetWallpaper) {
|
||||
if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction)) {
|
||||
await androidFileUtils.init();
|
||||
if (settings.isInstalledAppAccessAllowed) {
|
||||
unawaited(androidFileUtils.initAppNames());
|
||||
|
@ -111,6 +116,13 @@ class _HomePageState extends State<HomePage> {
|
|||
debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple');
|
||||
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
||||
break;
|
||||
case actionScreenSaver:
|
||||
appMode = AppMode.screenSaver;
|
||||
_shortcutRouteName = ScreenSaverPage.routeName;
|
||||
break;
|
||||
case actionScreenSaverSettings:
|
||||
_shortcutRouteName = ScreenSaverSettingsPage.routeName;
|
||||
break;
|
||||
case actionSearch:
|
||||
_shortcutRouteName = CollectionSearchDelegate.pageRouteName;
|
||||
_shortcutSearchQuery = intentData['query'];
|
||||
|
@ -147,6 +159,12 @@ class _HomePageState extends State<HomePage> {
|
|||
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection,
|
||||
);
|
||||
break;
|
||||
case AppMode.screenSaver:
|
||||
final source = context.read<CollectionSource>();
|
||||
await source.init(
|
||||
canAnalyze: false,
|
||||
);
|
||||
break;
|
||||
case AppMode.view:
|
||||
if (_isViewerSourceable(_viewerEntry)) {
|
||||
final directory = _viewerEntry?.directory;
|
||||
|
@ -271,8 +289,20 @@ class _HomePageState extends State<HomePage> {
|
|||
switch (routeName) {
|
||||
case AlbumListPage.routeName:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: const RouteSettings(name: AlbumListPage.routeName),
|
||||
builder: (_) => const AlbumListPage(),
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: (context) => const AlbumListPage(),
|
||||
);
|
||||
case ScreenSaverPage.routeName:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: (context) => ScreenSaverPage(
|
||||
source: source,
|
||||
),
|
||||
);
|
||||
case ScreenSaverSettingsPage.routeName:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: (context) => const ScreenSaverSettingsPage(),
|
||||
);
|
||||
case CollectionSearchDelegate.pageRouteName:
|
||||
return SearchPageRoute(
|
||||
|
@ -286,7 +316,7 @@ class _HomePageState extends State<HomePage> {
|
|||
case CollectionPage.routeName:
|
||||
default:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: filters,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/slideshow_interval.dart';
|
||||
import 'package:aves/model/settings/enums/slideshow_video_playback.dart';
|
||||
import 'package:aves/model/settings/enums/viewer_transition.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScreenSaverSettingsPage extends StatelessWidget {
|
||||
static const routeName = '/settings/screen_saver';
|
||||
|
||||
const ScreenSaverSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsScreenSaverTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
SettingsSelectionListTile<ViewerTransition>(
|
||||
values: ViewerTransition.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => s.screenSaverTransition,
|
||||
onSelection: (v) => settings.screenSaverTransition = v,
|
||||
tileTitle: context.l10n.settingsSlideshowTransitionTile,
|
||||
dialogTitle: context.l10n.settingsSlideshowTransitionTitle,
|
||||
),
|
||||
SettingsSelectionListTile<SlideshowInterval>(
|
||||
values: SlideshowInterval.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => s.screenSaverInterval,
|
||||
onSelection: (v) => settings.screenSaverInterval = v,
|
||||
tileTitle: context.l10n.settingsSlideshowIntervalTile,
|
||||
dialogTitle: context.l10n.settingsSlideshowIntervalTitle,
|
||||
),
|
||||
SettingsSelectionListTile<SlideshowVideoPlayback>(
|
||||
values: SlideshowVideoPlayback.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => s.screenSaverVideoPlayback,
|
||||
onSelection: (v) => settings.screenSaverVideoPlayback = v,
|
||||
tileTitle: context.l10n.settingsSlideshowVideoPlaybackTile,
|
||||
dialogTitle: context.l10n.settingsSlideshowVideoPlaybackTitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -116,7 +116,8 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
|||
_buildImagePage(),
|
||||
];
|
||||
|
||||
if (context.read<ValueNotifier<AppMode>>().value != AppMode.slideshow) {
|
||||
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||
if (!{AppMode.screenSaver, AppMode.slideshow}.contains(appMode)) {
|
||||
final infoPage = NotificationListener<ShowImageNotification>(
|
||||
onNotification: (notification) {
|
||||
widget.onImagePageRequested();
|
||||
|
|
|
@ -276,14 +276,20 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
}
|
||||
|
||||
List<Widget> _buildOverlays() {
|
||||
if (context.read<ValueNotifier<AppMode>>().value == AppMode.slideshow) {
|
||||
return [_buildSlideshowBottomOverlay()];
|
||||
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||
switch (appMode) {
|
||||
case AppMode.screenSaver:
|
||||
return [];
|
||||
case AppMode.slideshow:
|
||||
return [
|
||||
_buildSlideshowBottomOverlay(),
|
||||
];
|
||||
default:
|
||||
return [
|
||||
_buildViewerTopOverlay(),
|
||||
_buildViewerBottomOverlay(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
_buildViewerTopOverlay(),
|
||||
_buildViewerBottomOverlay(),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildSlideshowBottomOverlay() {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/actions/slideshow_actions.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/slideshow_interval.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
|
@ -15,128 +13,110 @@ import 'package:aves/widgets/viewer/controller.dart';
|
|||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SlideshowPage extends StatefulWidget {
|
||||
static const routeName = '/collection/slideshow';
|
||||
class ScreenSaverPage extends StatefulWidget {
|
||||
static const routeName = '/screen_saver';
|
||||
|
||||
final CollectionLens collection;
|
||||
final CollectionSource source;
|
||||
|
||||
const SlideshowPage({
|
||||
const ScreenSaverPage({
|
||||
super.key,
|
||||
required this.collection,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlideshowPage> createState() => _SlideshowPageState();
|
||||
State<ScreenSaverPage> createState() => _ScreenSaverPageState();
|
||||
}
|
||||
|
||||
class _SlideshowPageState extends State<SlideshowPage> {
|
||||
late final CollectionLens _slideshowCollection;
|
||||
class _ScreenSaverPageState extends State<ScreenSaverPage> {
|
||||
late final ViewerController _viewerController;
|
||||
CollectionLens? _slideshowCollection;
|
||||
|
||||
CollectionSource get source => widget.source;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final originalCollection = widget.collection;
|
||||
var entries = originalCollection.sortedEntries;
|
||||
if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) {
|
||||
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
|
||||
}
|
||||
if (settings.slideshowShuffle) {
|
||||
entries.shuffle();
|
||||
}
|
||||
_slideshowCollection = CollectionLens(
|
||||
source: originalCollection.source,
|
||||
listenToSource: false,
|
||||
fixedSort: true,
|
||||
fixedSelection: entries,
|
||||
);
|
||||
_viewerController = ViewerController(
|
||||
transition: settings.slideshowTransition,
|
||||
repeat: settings.slideshowRepeat,
|
||||
transition: settings.screenSaverTransition,
|
||||
repeat: true,
|
||||
autopilot: true,
|
||||
autopilotInterval: settings.slideshowInterval.getDuration(),
|
||||
autopilotInterval: settings.screenSaverInterval.getDuration(),
|
||||
);
|
||||
source.stateNotifier.addListener(_onSourceStateChanged);
|
||||
_initSlideshowCollection();
|
||||
}
|
||||
|
||||
void _onSourceStateChanged() {
|
||||
if (_slideshowCollection == null) {
|
||||
_initSlideshowCollection();
|
||||
if (_slideshowCollection != null) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
source.stateNotifier.removeListener(_onSourceStateChanged);
|
||||
_viewerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entries = _slideshowCollection.sortedEntries;
|
||||
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||
value: ValueNotifier(AppMode.slideshow),
|
||||
child: MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: entries.isEmpty
|
||||
? EmptyContent(
|
||||
icon: AIcons.image,
|
||||
text: context.l10n.collectionEmptyImages,
|
||||
alignment: Alignment.center,
|
||||
)
|
||||
: ViewStateConductorProvider(
|
||||
child: VideoConductorProvider(
|
||||
child: MultiPageConductorProvider(
|
||||
child: NotificationListener<SlideshowActionNotification>(
|
||||
onNotification: (notification) {
|
||||
_onActionSelected(notification.action);
|
||||
return true;
|
||||
},
|
||||
child: EntryViewerStack(
|
||||
collection: _slideshowCollection,
|
||||
initialEntry: entries.first,
|
||||
viewerController: _viewerController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget child;
|
||||
|
||||
void _onActionSelected(SlideshowAction action) {
|
||||
switch (action) {
|
||||
case SlideshowAction.resume:
|
||||
_viewerController.autopilot = true;
|
||||
break;
|
||||
case SlideshowAction.showInCollection:
|
||||
_showInCollection();
|
||||
break;
|
||||
final collection = _slideshowCollection;
|
||||
if (collection == null) {
|
||||
child = const SizedBox();
|
||||
} else {
|
||||
final entries = collection.sortedEntries;
|
||||
if (entries.isEmpty) {
|
||||
child = EmptyContent(
|
||||
icon: AIcons.image,
|
||||
text: context.l10n.collectionEmptyImages,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
} else {
|
||||
child = ViewStateConductorProvider(
|
||||
child: VideoConductorProvider(
|
||||
child: MultiPageConductorProvider(
|
||||
child: EntryViewerStack(
|
||||
collection: collection,
|
||||
initialEntry: entries.first,
|
||||
viewerController: _viewerController,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showInCollection() {
|
||||
final entry = _viewerController.entryNotifier.value;
|
||||
if (entry == null) return;
|
||||
void _initSlideshowCollection() {
|
||||
if (source.stateNotifier.value != SourceState.ready || _slideshowCollection != null) return;
|
||||
|
||||
final source = _slideshowCollection.source;
|
||||
final album = entry.directory;
|
||||
final uri = entry.uri;
|
||||
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: album != null ? {AlbumFilter(album, source.getAlbumDisplayName(context, album))} : null,
|
||||
highlightTest: (entry) => entry.uri == uri,
|
||||
),
|
||||
),
|
||||
(route) => false,
|
||||
final originalCollection = CollectionLens(
|
||||
source: source,
|
||||
// TODO TLAD [screensaver] custom filters
|
||||
);
|
||||
var entries = originalCollection.sortedEntries;
|
||||
if (settings.screenSaverVideoPlayback == SlideshowVideoPlayback.skip) {
|
||||
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
|
||||
}
|
||||
entries.shuffle();
|
||||
_slideshowCollection = CollectionLens(
|
||||
source: originalCollection.source,
|
||||
listenToSource: false,
|
||||
fixedSort: true,
|
||||
fixedSelection: entries,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SlideshowActionNotification extends Notification {
|
||||
final SlideshowAction action;
|
||||
|
||||
SlideshowActionNotification(this.action);
|
||||
}
|
||||
|
|
|
@ -32,32 +32,19 @@ class SlideshowPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SlideshowPageState extends State<SlideshowPage> {
|
||||
late final CollectionLens _slideshowCollection;
|
||||
late final ViewerController _viewerController;
|
||||
late final CollectionLens _slideshowCollection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final originalCollection = widget.collection;
|
||||
var entries = originalCollection.sortedEntries;
|
||||
if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) {
|
||||
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
|
||||
}
|
||||
if (settings.slideshowShuffle) {
|
||||
entries.shuffle();
|
||||
}
|
||||
_slideshowCollection = CollectionLens(
|
||||
source: originalCollection.source,
|
||||
listenToSource: false,
|
||||
fixedSort: true,
|
||||
fixedSelection: entries,
|
||||
);
|
||||
_viewerController = ViewerController(
|
||||
transition: settings.slideshowTransition,
|
||||
repeat: settings.slideshowRepeat,
|
||||
autopilot: true,
|
||||
autopilotInterval: settings.slideshowInterval.getDuration(),
|
||||
);
|
||||
_initSlideshowCollection();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -101,6 +88,23 @@ class _SlideshowPageState extends State<SlideshowPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _initSlideshowCollection() {
|
||||
final originalCollection = widget.collection;
|
||||
var entries = originalCollection.sortedEntries;
|
||||
if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) {
|
||||
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
|
||||
}
|
||||
if (settings.slideshowShuffle) {
|
||||
entries.shuffle();
|
||||
}
|
||||
_slideshowCollection = CollectionLens(
|
||||
source: originalCollection.source,
|
||||
listenToSource: false,
|
||||
fixedSort: true,
|
||||
fixedSelection: entries,
|
||||
);
|
||||
}
|
||||
|
||||
void _onActionSelected(SlideshowAction action) {
|
||||
switch (action) {
|
||||
case SlideshowAction.resume:
|
||||
|
|
|
@ -37,20 +37,28 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
}
|
||||
|
||||
bool _isSlideshow(BuildContext context) => context.read<ValueNotifier<AppMode>>().value == AppMode.slideshow;
|
||||
SlideshowVideoPlayback? get videoPlaybackOverride {
|
||||
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||
switch (appMode) {
|
||||
case AppMode.screenSaver:
|
||||
return settings.screenSaverVideoPlayback;
|
||||
case AppMode.slideshow:
|
||||
return settings.slideshowVideoPlayback;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldAutoPlay(BuildContext context) {
|
||||
if (_isSlideshow(context)) {
|
||||
switch (settings.slideshowVideoPlayback) {
|
||||
case SlideshowVideoPlayback.skip:
|
||||
return false;
|
||||
case SlideshowVideoPlayback.playMuted:
|
||||
case SlideshowVideoPlayback.playWithSound:
|
||||
return true;
|
||||
}
|
||||
switch (videoPlaybackOverride) {
|
||||
case SlideshowVideoPlayback.skip:
|
||||
return false;
|
||||
case SlideshowVideoPlayback.playMuted:
|
||||
case SlideshowVideoPlayback.playWithSound:
|
||||
return true;
|
||||
case null:
|
||||
return settings.enableVideoAutoPlay;
|
||||
}
|
||||
|
||||
return settings.enableVideoAutoPlay;
|
||||
}
|
||||
|
||||
Future<void> _initVideoController(AvesEntry entry) async {
|
||||
|
@ -127,7 +135,7 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
|||
// so we play after a delay for increased stability
|
||||
await Future.delayed(const Duration(milliseconds: 300) * timeDilation);
|
||||
|
||||
if (_isSlideshow(context) && settings.slideshowVideoPlayback == SlideshowVideoPlayback.playMuted && !videoController.isMuted) {
|
||||
if (videoPlaybackOverride == SlideshowVideoPlayback.playMuted && !videoController.isMuted) {
|
||||
await videoController.toggleMute();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class FakeWindowService extends Fake implements WindowService {
|
||||
@override
|
||||
Future<bool> isActivity() => SynchronousFuture(true);
|
||||
|
||||
@override
|
||||
Future<void> keepScreenOn(bool on) => SynchronousFuture(null);
|
||||
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
{
|
||||
"de": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -58,6 +66,7 @@
|
|||
"settingsSlideshowVideoPlaybackTile",
|
||||
"settingsSlideshowVideoPlaybackTitle",
|
||||
"settingsThemeEnableDynamicColor",
|
||||
"settingsScreenSaverTitle",
|
||||
"viewerSetWallpaperButtonLabel"
|
||||
],
|
||||
|
||||
|
@ -86,10 +95,12 @@
|
|||
"settingsSlideshowIntervalTitle",
|
||||
"settingsSlideshowVideoPlaybackTile",
|
||||
"settingsSlideshowVideoPlaybackTitle",
|
||||
"settingsScreenSaverTitle",
|
||||
"viewerSetWallpaperButtonLabel"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"filterOnThisDayLabel"
|
||||
"filterOnThisDayLabel",
|
||||
"settingsScreenSaverTitle"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue