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"
|
<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" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ScreenSaverSettingsActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/NormalTheme" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".WallpaperActivity"
|
android:name=".WallpaperActivity"
|
||||||
android:exported="true"
|
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:description="@string/analysis_service_description"
|
||||||
android:exported="false" />
|
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 -->
|
<!-- file provider to share files having a file:// URI -->
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
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.CompletableFuture
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
open class MainActivity : FlutterActivity() {
|
||||||
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
||||||
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
|
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
|
||||||
private lateinit var intentStreamHandler: IntentStreamHandler
|
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) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_MAIN -> {
|
Intent.ACTION_MAIN -> {
|
||||||
intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page ->
|
intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page ->
|
||||||
|
@ -338,6 +338,8 @@ class MainActivity : FlutterActivity() {
|
||||||
const val INTENT_DATA_KEY_QUERY = "query"
|
const val INTENT_DATA_KEY_QUERY = "query"
|
||||||
|
|
||||||
const val INTENT_ACTION_PICK = "pick"
|
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_SEARCH = "search"
|
||||||
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
|
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
|
||||||
const val INTENT_ACTION_VIEW = "view"
|
const val INTENT_ACTION_VIEW = "view"
|
||||||
|
|
|
@ -1,4 +1,198 @@
|
||||||
package deckers.thibault.aves
|
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
|
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"
|
<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,
|
pickMultipleMediaExternal,
|
||||||
pickMediaInternal,
|
pickMediaInternal,
|
||||||
pickFilterInternal,
|
pickFilterInternal,
|
||||||
|
screenSaver,
|
||||||
setWallpaper,
|
setWallpaper,
|
||||||
slideshow,
|
slideshow,
|
||||||
view,
|
view,
|
||||||
|
|
|
@ -765,6 +765,8 @@
|
||||||
"settingsUnitSystemTile": "Units",
|
"settingsUnitSystemTile": "Units",
|
||||||
"settingsUnitSystemTitle": "Units",
|
"settingsUnitSystemTitle": "Units",
|
||||||
|
|
||||||
|
"settingsScreenSaverTitle": "Screen Saver",
|
||||||
|
|
||||||
"statsPageTitle": "Stats",
|
"statsPageTitle": "Stats",
|
||||||
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
||||||
"@statsWithGps": {
|
"@statsWithGps": {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.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');
|
debugPrint('Apply display refresh rate: $name');
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case DisplayRefreshRateMode.auto:
|
case DisplayRefreshRateMode.auto:
|
||||||
FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
||||||
break;
|
break;
|
||||||
case DisplayRefreshRateMode.highest:
|
case DisplayRefreshRateMode.highest:
|
||||||
FlutterDisplayMode.setHighRefreshRate();
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
break;
|
break;
|
||||||
case DisplayRefreshRateMode.lowest:
|
case DisplayRefreshRateMode.lowest:
|
||||||
FlutterDisplayMode.setLowRefreshRate();
|
await FlutterDisplayMode.setLowRefreshRate();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,6 +138,11 @@ class Settings extends ChangeNotifier {
|
||||||
// file picker
|
// file picker
|
||||||
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
|
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
|
// slideshow
|
||||||
static const slideshowRepeatKey = 'slideshow_loop';
|
static const slideshowRepeatKey = 'slideshow_loop';
|
||||||
static const slideshowShuffleKey = 'slideshow_shuffle';
|
static const slideshowShuffleKey = 'slideshow_shuffle';
|
||||||
|
@ -583,6 +588,20 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue);
|
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
|
// slideshow
|
||||||
|
|
||||||
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
|
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
|
||||||
|
@ -792,6 +811,9 @@ class Settings extends ChangeNotifier {
|
||||||
case unitSystemKey:
|
case unitSystemKey:
|
||||||
case accessibilityAnimationsKey:
|
case accessibilityAnimationsKey:
|
||||||
case timeToTakeActionKey:
|
case timeToTakeActionKey:
|
||||||
|
case screenSaverTransitionKey:
|
||||||
|
case screenSaverVideoPlaybackKey:
|
||||||
|
case screenSaverIntervalKey:
|
||||||
case slideshowTransitionKey:
|
case slideshowTransitionKey:
|
||||||
case slideshowVideoPlaybackKey:
|
case slideshowVideoPlaybackKey:
|
||||||
case slideshowIntervalKey:
|
case slideshowIntervalKey:
|
||||||
|
|
|
@ -370,6 +370,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
AnalysisController? analysisController,
|
AnalysisController? analysisController,
|
||||||
String? directory,
|
String? directory,
|
||||||
bool loadTopEntriesFirst = false,
|
bool loadTopEntriesFirst = false,
|
||||||
|
bool canAnalyze = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController});
|
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController});
|
||||||
|
|
|
@ -24,6 +24,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
AnalysisController? analysisController,
|
AnalysisController? analysisController,
|
||||||
String? directory,
|
String? directory,
|
||||||
bool loadTopEntriesFirst = false,
|
bool loadTopEntriesFirst = false,
|
||||||
|
bool canAnalyze = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (_initState == SourceInitializationState.none) {
|
if (_initState == SourceInitializationState.none) {
|
||||||
await _loadEssentials();
|
await _loadEssentials();
|
||||||
|
@ -35,6 +36,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
analysisController: analysisController,
|
analysisController: analysisController,
|
||||||
directory: directory,
|
directory: directory,
|
||||||
loadTopEntriesFirst: loadTopEntriesFirst,
|
loadTopEntriesFirst: loadTopEntriesFirst,
|
||||||
|
canAnalyze: canAnalyze,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +65,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
AnalysisController? analysisController,
|
AnalysisController? analysisController,
|
||||||
String? directory,
|
String? directory,
|
||||||
required bool loadTopEntriesFirst,
|
required bool loadTopEntriesFirst,
|
||||||
|
required bool canAnalyze,
|
||||||
}) async {
|
}) async {
|
||||||
debugPrint('$runtimeType refresh start');
|
debugPrint('$runtimeType refresh start');
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
@ -182,7 +185,11 @@ class MediaStoreSource extends CollectionSource {
|
||||||
if (analysisIds != null) {
|
if (analysisIds != null) {
|
||||||
analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
|
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
|
// the home page may not reflect the current derived filters
|
||||||
// as the initial addition of entries is silent,
|
// as the initial addition of entries is silent,
|
||||||
|
|
|
@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
abstract class WindowService {
|
abstract class WindowService {
|
||||||
|
Future<bool> isActivity();
|
||||||
|
|
||||||
Future<void> keepScreenOn(bool on);
|
Future<void> keepScreenOn(bool on);
|
||||||
|
|
||||||
Future<bool> isRotationLocked();
|
Future<bool> isRotationLocked();
|
||||||
|
@ -17,6 +19,17 @@ abstract class WindowService {
|
||||||
class PlatformWindowService implements WindowService {
|
class PlatformWindowService implements WindowService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/window');
|
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
|
@override
|
||||||
Future<void> keepScreenOn(bool on) async {
|
Future<void> keepScreenOn(bool on) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -230,6 +230,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
break;
|
break;
|
||||||
case AppMode.pickMediaInternal:
|
case AppMode.pickMediaInternal:
|
||||||
case AppMode.pickFilterInternal:
|
case AppMode.pickFilterInternal:
|
||||||
|
case AppMode.screenSaver:
|
||||||
case AppMode.setWallpaper:
|
case AppMode.setWallpaper:
|
||||||
case AppMode.slideshow:
|
case AppMode.slideshow:
|
||||||
case AppMode.view:
|
case AppMode.view:
|
||||||
|
|
|
@ -54,6 +54,7 @@ class InteractiveTile extends StatelessWidget {
|
||||||
Navigator.pop(context, entry);
|
Navigator.pop(context, entry);
|
||||||
break;
|
break;
|
||||||
case AppMode.pickFilterInternal:
|
case AppMode.pickFilterInternal:
|
||||||
|
case AppMode.screenSaver:
|
||||||
case AppMode.setWallpaper:
|
case AppMode.setWallpaper:
|
||||||
case AppMode.slideshow:
|
case AppMode.slideshow:
|
||||||
case AppMode.view:
|
case AppMode.view:
|
||||||
|
|
|
@ -63,6 +63,7 @@ class _InteractiveFilterTileState<T extends CollectionFilter> extends State<Inte
|
||||||
Navigator.pop<T>(context, filter);
|
Navigator.pop<T>(context, filter);
|
||||||
break;
|
break;
|
||||||
case AppMode.pickMediaInternal:
|
case AppMode.pickMediaInternal:
|
||||||
|
case AppMode.screenSaver:
|
||||||
case AppMode.setWallpaper:
|
case AppMode.setWallpaper:
|
||||||
case AppMode.slideshow:
|
case AppMode.slideshow:
|
||||||
case AppMode.view:
|
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/common/search/route.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.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/entry_viewer_page.dart';
|
||||||
|
import 'package:aves/widgets/viewer/screen_saver_page.dart';
|
||||||
import 'package:aves/widgets/wallpaper_page.dart';
|
import 'package:aves/widgets/wallpaper_page.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -49,6 +51,8 @@ class _HomePageState extends State<HomePage> {
|
||||||
Set<String>? _shortcutFilters;
|
Set<String>? _shortcutFilters;
|
||||||
|
|
||||||
static const actionPick = 'pick';
|
static const actionPick = 'pick';
|
||||||
|
static const actionScreenSaver = 'screen_saver';
|
||||||
|
static const actionScreenSaverSettings = 'screen_saver_settings';
|
||||||
static const actionSearch = 'search';
|
static const actionSearch = 'search';
|
||||||
static const actionSetWallpaper = 'set_wallpaper';
|
static const actionSetWallpaper = 'set_wallpaper';
|
||||||
static const actionView = 'view';
|
static const actionView = 'view';
|
||||||
|
@ -71,20 +75,21 @@ class _HomePageState extends State<HomePage> {
|
||||||
|
|
||||||
Future<void> _setup() async {
|
Future<void> _setup() async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
// do not check whether permission was granted,
|
if (await windowService.isActivity()) {
|
||||||
// as some app stores hide in some countries
|
// do not check whether permission was granted, because some app stores
|
||||||
// apps that force quit on permission denial
|
// hide in some countries apps that force quit on permission denial
|
||||||
await [
|
await [
|
||||||
Permission.storage,
|
Permission.storage,
|
||||||
// to access media with unredacted metadata with scoped storage (Android 10+)
|
// to access media with unredacted metadata with scoped storage (Android 10+)
|
||||||
Permission.accessMediaLocation,
|
Permission.accessMediaLocation,
|
||||||
].request();
|
].request();
|
||||||
|
}
|
||||||
|
|
||||||
var appMode = AppMode.main;
|
var appMode = AppMode.main;
|
||||||
final intentData = widget.intentData ?? await ViewerService.getIntentData();
|
final intentData = widget.intentData ?? await ViewerService.getIntentData();
|
||||||
final intentAction = intentData['action'];
|
final intentAction = intentData['action'];
|
||||||
|
|
||||||
if (intentAction != actionSetWallpaper) {
|
if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction)) {
|
||||||
await androidFileUtils.init();
|
await androidFileUtils.init();
|
||||||
if (settings.isInstalledAppAccessAllowed) {
|
if (settings.isInstalledAppAccessAllowed) {
|
||||||
unawaited(androidFileUtils.initAppNames());
|
unawaited(androidFileUtils.initAppNames());
|
||||||
|
@ -111,6 +116,13 @@ class _HomePageState extends State<HomePage> {
|
||||||
debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple');
|
debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple');
|
||||||
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
||||||
break;
|
break;
|
||||||
|
case actionScreenSaver:
|
||||||
|
appMode = AppMode.screenSaver;
|
||||||
|
_shortcutRouteName = ScreenSaverPage.routeName;
|
||||||
|
break;
|
||||||
|
case actionScreenSaverSettings:
|
||||||
|
_shortcutRouteName = ScreenSaverSettingsPage.routeName;
|
||||||
|
break;
|
||||||
case actionSearch:
|
case actionSearch:
|
||||||
_shortcutRouteName = CollectionSearchDelegate.pageRouteName;
|
_shortcutRouteName = CollectionSearchDelegate.pageRouteName;
|
||||||
_shortcutSearchQuery = intentData['query'];
|
_shortcutSearchQuery = intentData['query'];
|
||||||
|
@ -147,6 +159,12 @@ class _HomePageState extends State<HomePage> {
|
||||||
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection,
|
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case AppMode.screenSaver:
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
await source.init(
|
||||||
|
canAnalyze: false,
|
||||||
|
);
|
||||||
|
break;
|
||||||
case AppMode.view:
|
case AppMode.view:
|
||||||
if (_isViewerSourceable(_viewerEntry)) {
|
if (_isViewerSourceable(_viewerEntry)) {
|
||||||
final directory = _viewerEntry?.directory;
|
final directory = _viewerEntry?.directory;
|
||||||
|
@ -271,8 +289,20 @@ class _HomePageState extends State<HomePage> {
|
||||||
switch (routeName) {
|
switch (routeName) {
|
||||||
case AlbumListPage.routeName:
|
case AlbumListPage.routeName:
|
||||||
return DirectMaterialPageRoute(
|
return DirectMaterialPageRoute(
|
||||||
settings: const RouteSettings(name: AlbumListPage.routeName),
|
settings: RouteSettings(name: routeName),
|
||||||
builder: (_) => const AlbumListPage(),
|
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:
|
case CollectionSearchDelegate.pageRouteName:
|
||||||
return SearchPageRoute(
|
return SearchPageRoute(
|
||||||
|
@ -286,7 +316,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
case CollectionPage.routeName:
|
case CollectionPage.routeName:
|
||||||
default:
|
default:
|
||||||
return DirectMaterialPageRoute(
|
return DirectMaterialPageRoute(
|
||||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
settings: RouteSettings(name: routeName),
|
||||||
builder: (context) => CollectionPage(
|
builder: (context) => CollectionPage(
|
||||||
source: source,
|
source: source,
|
||||||
filters: filters,
|
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(),
|
_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>(
|
final infoPage = NotificationListener<ShowImageNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
widget.onImagePageRequested();
|
widget.onImagePageRequested();
|
||||||
|
|
|
@ -276,14 +276,20 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildOverlays() {
|
List<Widget> _buildOverlays() {
|
||||||
if (context.read<ValueNotifier<AppMode>>().value == AppMode.slideshow) {
|
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||||
return [_buildSlideshowBottomOverlay()];
|
switch (appMode) {
|
||||||
|
case AppMode.screenSaver:
|
||||||
|
return [];
|
||||||
|
case AppMode.slideshow:
|
||||||
|
return [
|
||||||
|
_buildSlideshowBottomOverlay(),
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
_buildViewerTopOverlay(),
|
||||||
|
_buildViewerBottomOverlay(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
|
||||||
_buildViewerTopOverlay(),
|
|
||||||
_buildViewerBottomOverlay(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSlideshowBottomOverlay() {
|
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/filters/mime.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/enums/slideshow_interval.dart';
|
import 'package:aves/model/settings/enums/slideshow_interval.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.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/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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.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_page.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
|
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class SlideshowPage extends StatefulWidget {
|
class ScreenSaverPage extends StatefulWidget {
|
||||||
static const routeName = '/collection/slideshow';
|
static const routeName = '/screen_saver';
|
||||||
|
|
||||||
final CollectionLens collection;
|
final CollectionSource source;
|
||||||
|
|
||||||
const SlideshowPage({
|
const ScreenSaverPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.collection,
|
required this.source,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SlideshowPage> createState() => _SlideshowPageState();
|
State<ScreenSaverPage> createState() => _ScreenSaverPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SlideshowPageState extends State<SlideshowPage> {
|
class _ScreenSaverPageState extends State<ScreenSaverPage> {
|
||||||
late final CollectionLens _slideshowCollection;
|
|
||||||
late final ViewerController _viewerController;
|
late final ViewerController _viewerController;
|
||||||
|
CollectionLens? _slideshowCollection;
|
||||||
|
|
||||||
|
CollectionSource get source => widget.source;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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(
|
_viewerController = ViewerController(
|
||||||
transition: settings.slideshowTransition,
|
transition: settings.screenSaverTransition,
|
||||||
repeat: settings.slideshowRepeat,
|
repeat: true,
|
||||||
autopilot: 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
source.stateNotifier.removeListener(_onSourceStateChanged);
|
||||||
_viewerController.dispose();
|
_viewerController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entries = _slideshowCollection.sortedEntries;
|
Widget child;
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onActionSelected(SlideshowAction action) {
|
final collection = _slideshowCollection;
|
||||||
switch (action) {
|
if (collection == null) {
|
||||||
case SlideshowAction.resume:
|
child = const SizedBox();
|
||||||
_viewerController.autopilot = true;
|
} else {
|
||||||
break;
|
final entries = collection.sortedEntries;
|
||||||
case SlideshowAction.showInCollection:
|
if (entries.isEmpty) {
|
||||||
_showInCollection();
|
child = EmptyContent(
|
||||||
break;
|
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() {
|
void _initSlideshowCollection() {
|
||||||
final entry = _viewerController.entryNotifier.value;
|
if (source.stateNotifier.value != SourceState.ready || _slideshowCollection != null) return;
|
||||||
if (entry == null) return;
|
|
||||||
|
|
||||||
final source = _slideshowCollection.source;
|
final originalCollection = CollectionLens(
|
||||||
final album = entry.directory;
|
source: source,
|
||||||
final uri = entry.uri;
|
// TODO TLAD [screensaver] custom filters
|
||||||
|
);
|
||||||
Navigator.pushAndRemoveUntil(
|
var entries = originalCollection.sortedEntries;
|
||||||
context,
|
if (settings.screenSaverVideoPlayback == SlideshowVideoPlayback.skip) {
|
||||||
MaterialPageRoute(
|
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
|
||||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
}
|
||||||
builder: (context) => CollectionPage(
|
entries.shuffle();
|
||||||
source: source,
|
_slideshowCollection = CollectionLens(
|
||||||
filters: album != null ? {AlbumFilter(album, source.getAlbumDisplayName(context, album))} : null,
|
source: originalCollection.source,
|
||||||
highlightTest: (entry) => entry.uri == uri,
|
listenToSource: false,
|
||||||
),
|
fixedSort: true,
|
||||||
),
|
fixedSelection: entries,
|
||||||
(route) => false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SlideshowActionNotification extends Notification {
|
|
||||||
final SlideshowAction action;
|
|
||||||
|
|
||||||
SlideshowActionNotification(this.action);
|
|
||||||
}
|
|
||||||
|
|
|
@ -32,32 +32,19 @@ class SlideshowPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SlideshowPageState extends State<SlideshowPage> {
|
class _SlideshowPageState extends State<SlideshowPage> {
|
||||||
late final CollectionLens _slideshowCollection;
|
|
||||||
late final ViewerController _viewerController;
|
late final ViewerController _viewerController;
|
||||||
|
late final CollectionLens _slideshowCollection;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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(
|
_viewerController = ViewerController(
|
||||||
transition: settings.slideshowTransition,
|
transition: settings.slideshowTransition,
|
||||||
repeat: settings.slideshowRepeat,
|
repeat: settings.slideshowRepeat,
|
||||||
autopilot: true,
|
autopilot: true,
|
||||||
autopilotInterval: settings.slideshowInterval.getDuration(),
|
autopilotInterval: settings.slideshowInterval.getDuration(),
|
||||||
);
|
);
|
||||||
|
_initSlideshowCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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) {
|
void _onActionSelected(SlideshowAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case SlideshowAction.resume:
|
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) {
|
bool _shouldAutoPlay(BuildContext context) {
|
||||||
if (_isSlideshow(context)) {
|
switch (videoPlaybackOverride) {
|
||||||
switch (settings.slideshowVideoPlayback) {
|
case SlideshowVideoPlayback.skip:
|
||||||
case SlideshowVideoPlayback.skip:
|
return false;
|
||||||
return false;
|
case SlideshowVideoPlayback.playMuted:
|
||||||
case SlideshowVideoPlayback.playMuted:
|
case SlideshowVideoPlayback.playWithSound:
|
||||||
case SlideshowVideoPlayback.playWithSound:
|
return true;
|
||||||
return true;
|
case null:
|
||||||
}
|
return settings.enableVideoAutoPlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings.enableVideoAutoPlay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initVideoController(AvesEntry entry) async {
|
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
|
// so we play after a delay for increased stability
|
||||||
await Future.delayed(const Duration(milliseconds: 300) * timeDilation);
|
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();
|
await videoController.toggleMute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,9 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
class FakeWindowService extends Fake implements WindowService {
|
class FakeWindowService extends Fake implements WindowService {
|
||||||
|
@override
|
||||||
|
Future<bool> isActivity() => SynchronousFuture(true);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> keepScreenOn(bool on) => SynchronousFuture(null);
|
Future<void> keepScreenOn(bool on) => SynchronousFuture(null);
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,42 @@
|
||||||
{
|
{
|
||||||
"de": [
|
"de": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"id": [
|
"id": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ko": [
|
"ko": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -58,6 +66,7 @@
|
||||||
"settingsSlideshowVideoPlaybackTile",
|
"settingsSlideshowVideoPlaybackTile",
|
||||||
"settingsSlideshowVideoPlaybackTitle",
|
"settingsSlideshowVideoPlaybackTitle",
|
||||||
"settingsThemeEnableDynamicColor",
|
"settingsThemeEnableDynamicColor",
|
||||||
|
"settingsScreenSaverTitle",
|
||||||
"viewerSetWallpaperButtonLabel"
|
"viewerSetWallpaperButtonLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -86,10 +95,12 @@
|
||||||
"settingsSlideshowIntervalTitle",
|
"settingsSlideshowIntervalTitle",
|
||||||
"settingsSlideshowVideoPlaybackTile",
|
"settingsSlideshowVideoPlaybackTile",
|
||||||
"settingsSlideshowVideoPlaybackTitle",
|
"settingsSlideshowVideoPlaybackTitle",
|
||||||
|
"settingsScreenSaverTitle",
|
||||||
"viewerSetWallpaperButtonLabel"
|
"viewerSetWallpaperButtonLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
"filterOnThisDayLabel"
|
"filterOnThisDayLabel",
|
||||||
|
"settingsScreenSaverTitle"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue