#437 tv: read-only, ambient mode, webview, settings

This commit is contained in:
Thibault Deckers 2022-12-08 23:50:34 +01:00
parent 829ec201eb
commit e5e1a8f275
27 changed files with 221 additions and 124 deletions

View file

@ -11,6 +11,13 @@ This change eventually prevents building the app with Flutter v3.3.3.
package="deckers.thibault.aves" package="deckers.thibault.aves"
android:installLocation="auto"> android:installLocation="auto">
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<!-- <!--
Scoped storage on Android 10 is inconvenient because users need to confirm edition on each individual file. Scoped storage on Android 10 is inconvenient because users need to confirm edition on each individual file.
So we request `WRITE_EXTERNAL_STORAGE` until Android 10 (API 29), and enable `requestLegacyExternalStorage` So we request `WRITE_EXTERNAL_STORAGE` until Android 10 (API 29), and enable `requestLegacyExternalStorage`
@ -67,6 +74,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
<application <application
android:allowBackup="true" android:allowBackup="true"
android:appCategory="image" android:appCategory="image"
android:banner="@drawable/banner"
android:fullBackupOnly="true" android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -83,6 +91,8 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>

View file

@ -20,11 +20,15 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
val window = activity.window val window = activity.window
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
val old = (window.attributes.flags and flag) != 0
if (old != on) {
if (on) { if (on) {
window.addFlags(flag) window.addFlags(flag)
} else { } else {
window.clearFlags(flag) window.clearFlags(flag)
} }
}
result.success(null) result.success(null)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -195,6 +195,7 @@
"nameConflictStrategySkip": "Skip", "nameConflictStrategySkip": "Skip",
"keepScreenOnNever": "Never", "keepScreenOnNever": "Never",
"keepScreenOnVideoPlayback": "During video playback",
"keepScreenOnViewerOnly": "Viewer page only", "keepScreenOnViewerOnly": "Viewer page only",
"keepScreenOnAlways": "Always", "keepScreenOnAlways": "Always",

View file

@ -1,4 +1,5 @@
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
final Device device = Device._private(); final Device device = Device._private();
@ -6,7 +7,7 @@ final Device device = Device._private();
class Device { class Device {
late final String _userAgent; late final String _userAgent;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper; late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper;
late final bool _hasGeocoder, _isDynamicColorAvailable, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
String get userAgent => _userAgent; String get userAgent => _userAgent;
@ -26,6 +27,10 @@ class Device {
bool get isDynamicColorAvailable => _isDynamicColorAvailable; bool get isDynamicColorAvailable => _isDynamicColorAvailable;
bool get isReadOnly => _isTelevision;
bool get isTelevision => _isTelevision;
bool get showPinShortcutFeedback => _showPinShortcutFeedback; bool get showPinShortcutFeedback => _showPinShortcutFeedback;
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode; bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
@ -36,6 +41,9 @@ class Device {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
_userAgent = '${packageInfo.packageName}/${packageInfo.version}'; _userAgent = '${packageInfo.packageName}/${packageInfo.version}';
final androidInfo = await DeviceInfoPlugin().androidInfo;
_isTelevision = androidInfo.systemFeatures.contains('android.software.leanback');
final capabilities = await deviceService.getCapabilities(); final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPinShortcut = capabilities['canPinShortcut'] ?? false;

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/geo/countries.dart'; import 'package:aves/geo/countries.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/entry_cache.dart';
import 'package:aves/model/entry_dirs.dart'; import 'package:aves/model/entry_dirs.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
@ -280,7 +281,7 @@ class AvesEntry {
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
bool get canEdit => path != null && !trashed && isMediaStoreContent; bool get canEdit => !device.isReadOnly && path != null && !trashed && isMediaStoreContent;
bool get canEditDate => canEdit && (canEditExif || canEditXmp); bool get canEditDate => canEdit && (canEditExif || canEditXmp);

View file

@ -16,7 +16,7 @@ enum EntryBackground { black, white, checkered }
enum HomePageSetting { collection, albums } enum HomePageSetting { collection, albums }
enum KeepScreenOn { never, viewerOnly, always } enum KeepScreenOn { never, videoPlayback, viewerOnly, always }
enum SlideshowVideoPlayback { skip, playMuted, playWithSound } enum SlideshowVideoPlayback { skip, playMuted, playWithSound }

View file

@ -9,6 +9,8 @@ extension ExtraKeepScreenOn on KeepScreenOn {
switch (this) { switch (this) {
case KeepScreenOn.never: case KeepScreenOn.never:
return context.l10n.keepScreenOnNever; return context.l10n.keepScreenOnNever;
case KeepScreenOn.videoPlayback:
return context.l10n.keepScreenOnVideoPlayback;
case KeepScreenOn.viewerOnly: case KeepScreenOn.viewerOnly:
return context.l10n.keepScreenOnViewerOnly; return context.l10n.keepScreenOnViewerOnly;
case KeepScreenOn.always: case KeepScreenOn.always:

View file

@ -5,7 +5,11 @@ import 'dart:math';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
@ -230,6 +234,25 @@ class Settings extends ChangeNotifier {
mapStyle = styles[Random().nextInt(styles.length)]; mapStyle = styles[Random().nextInt(styles.length)];
} }
} }
if (device.isTelevision) {
drawerTypeBookmarks = [
null,
MimeFilter.video,
FavouriteFilter.instance,
RecentlyAddedFilter.instance,
];
mustBackTwiceToExit = false;
keepScreenOn = KeepScreenOn.videoPlayback;
enableBottomNavigationBar = false;
viewerGestureSideTapNext = false;
viewerUseCutout = true;
viewerMaxBrightness = false;
videoControls = VideoControls.playSeek;
videoGestureDoubleTapTogglePlay = false;
videoGestureSideDoubleTapSeek = false;
enableBin = false;
}
} }
// app // app

View file

@ -11,6 +11,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.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/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
@ -21,7 +22,6 @@ import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
class BugReport extends StatefulWidget { class BugReport extends StatefulWidget {
const BugReport({super.key}); const BugReport({super.key});
@ -34,7 +34,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
late Future<String> _infoLoader; late Future<String> _infoLoader;
bool _showInstructions = false; bool _showInstructions = false;
static final bugReportUri = Uri.parse('${Constants.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md'); static const bugReportUrl = '${Constants.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md';
@override @override
void initState() { void initState() {
@ -184,9 +184,5 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
showFeedback(context, context.l10n.genericSuccessFeedback); showFeedback(context, context.l10n.genericSuccessFeedback);
} }
Future<void> _goToGithub() async { Future<void> _goToGithub() => AvesApp.launchUrl(bugReportUrl);
if (await canLaunchUrl(bugReportUri)) {
await launchUrl(bugReportUri, mode: LaunchMode.externalApplication);
}
}
} }

View file

@ -44,6 +44,7 @@ import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart' as ul;
class AvesApp extends StatefulWidget { class AvesApp extends StatefulWidget {
final AppFlavor flavor; final AppFlavor flavor;
@ -103,6 +104,19 @@ class AvesApp extends StatefulWidget {
static Future<void> hideSystemUI() async { static Future<void> hideSystemUI() async {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} }
static Future<void> launchUrl(String? urlString) async {
if (urlString != null) {
final url = Uri.parse(urlString);
if (await ul.canLaunchUrl(url)) {
try {
await ul.launchUrl(url, mode: device.isTelevision ? ul.LaunchMode.inAppWebView : ul.LaunchMode.externalApplication);
} catch (error, stack) {
debugPrint('failed to open url=$urlString with error=$error\n$stack');
}
}
}
}
} }
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@ -207,13 +221,20 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value); lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value); darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
} }
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: _shouldUseBoldFontLoader, future: _shouldUseBoldFontLoader,
builder: (context, snapshot) { builder: (context, snapshot) {
// Flutter v3.4 already checks the system `Configuration.fontWeightAdjustment` to update `MediaQuery` // Flutter v3.4 already checks the system `Configuration.fontWeightAdjustment` to update `MediaQuery`
// but we need to also check the non-standard Samsung field `bf` representing the bold font toggle // but we need to also check the non-standard Samsung field `bf` representing the bold font toggle
final shouldUseBoldFont = snapshot.data ?? false; final shouldUseBoldFont = snapshot.data ?? false;
return MaterialApp( return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
// handle Android TV remote `select` button
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MaterialApp(
navigatorKey: AvesApp.navigatorKey, navigatorKey: AvesApp.navigatorKey,
home: home, home: home,
navigatorObservers: _navigatorObservers, navigatorObservers: _navigatorObservers,
@ -234,14 +255,15 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
); );
}, },
onGenerateTitle: (context) => context.l10n.appName, onGenerateTitle: (context) => context.l10n.appName,
theme: Themes.lightTheme(lightAccent, initialized), theme: lightTheme,
darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized), darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode, themeMode: themeBrightness.appThemeMode,
locale: settingsLocale, locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales, supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(), scrollBehavior: StretchMaterialScrollBehavior(),
),
); );
}, },
); );

View file

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
@ -307,7 +308,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( ...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection), (action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
), ),
if (isSelecting && !isTrash && appMode == AppMode.main) if (isSelecting && !device.isReadOnly && appMode == AppMode.main && !isTrash)
PopupMenuItem<EntrySetAction>( PopupMenuItem<EntrySetAction>(
enabled: canApplyEditActions, enabled: canApplyEditActions,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,

View file

@ -73,7 +73,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.addShortcut: case EntrySetAction.addShortcut:
return appMode == AppMode.main && !isSelecting && device.canPinShortcut && !isTrash; return appMode == AppMode.main && !isSelecting && device.canPinShortcut && !isTrash;
case EntrySetAction.emptyBin: case EntrySetAction.emptyBin:
return appMode == AppMode.main && isTrash; return !device.isReadOnly && appMode == AppMode.main && isTrash;
// browsing or selecting // browsing or selecting
case EntrySetAction.map: case EntrySetAction.map:
case EntrySetAction.slideshow: case EntrySetAction.slideshow:
@ -82,13 +82,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.rescan: case EntrySetAction.rescan:
return appMode == AppMode.main && !isTrash; return appMode == AppMode.main && !isTrash;
// selecting // selecting
case EntrySetAction.delete:
return appMode == AppMode.main && isSelecting;
case EntrySetAction.share: case EntrySetAction.share:
case EntrySetAction.toggleFavourite:
return appMode == AppMode.main && isSelecting && !isTrash;
case EntrySetAction.delete:
return !device.isReadOnly && appMode == AppMode.main && isSelecting;
case EntrySetAction.copy: case EntrySetAction.copy:
case EntrySetAction.move: case EntrySetAction.move:
case EntrySetAction.rename: case EntrySetAction.rename:
case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW: case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW: case EntrySetAction.rotateCW:
case EntrySetAction.flip: case EntrySetAction.flip:
@ -98,9 +99,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.editRating: case EntrySetAction.editRating:
case EntrySetAction.editTags: case EntrySetAction.editTags:
case EntrySetAction.removeMetadata: case EntrySetAction.removeMetadata:
return appMode == AppMode.main && isSelecting && !isTrash; return !device.isReadOnly && appMode == AppMode.main && isSelecting && !isTrash;
case EntrySetAction.restore: case EntrySetAction.restore:
return appMode == AppMode.main && isSelecting && isTrash; return !device.isReadOnly && appMode == AppMode.main && isSelecting && isTrash;
} }
} }

View file

@ -1,6 +1,6 @@
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class LinkChip extends StatelessWidget { class LinkChip extends StatelessWidget {
final Widget? leading; final Widget? leading;
@ -24,20 +24,11 @@ class LinkChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _urlString = urlString;
return DefaultTextStyle.merge( return DefaultTextStyle.merge(
style: (textStyle ?? const TextStyle()).copyWith(color: color), style: (textStyle ?? const TextStyle()).copyWith(color: color),
child: InkWell( child: InkWell(
borderRadius: borderRadius, borderRadius: borderRadius,
onTap: onTap ?? onTap: onTap ?? () => AvesApp.launchUrl(urlString),
() async {
if (_urlString != null) {
final url = Uri.parse(_urlString);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
}
},
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(

View file

@ -1,7 +1,7 @@
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:url_launcher/url_launcher.dart';
class MarkdownContainer extends StatelessWidget { class MarkdownContainer extends StatelessWidget {
final String data; final String data;
@ -43,14 +43,7 @@ class MarkdownContainer extends StatelessWidget {
child: Markdown( child: Markdown(
data: data, data: data,
selectable: true, selectable: true,
onTapLink: (text, href, title) async { onTapLink: (text, href, title) => AvesApp.launchUrl(href),
if (href != null) {
final url = Uri.parse(href);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
}
},
shrinkWrap: true, shrinkWrap: true,
), ),
), ),

View file

@ -1,9 +1,9 @@
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:url_launcher/url_launcher.dart';
class Attribution extends StatelessWidget { class Attribution extends StatelessWidget {
final EntryMapStyle? style; final EntryMapStyle? style;
@ -37,14 +37,7 @@ class Attribution extends StatelessWidget {
a: TextStyle(color: theme.colorScheme.secondary), a: TextStyle(color: theme.colorScheme.secondary),
p: theme.textTheme.bodySmall!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), p: theme.textTheme.bodySmall!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)),
), ),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) => AvesApp.launchUrl(href),
if (href != null) {
final url = Uri.parse(href);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
}
},
), ),
); );
} }

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
@ -77,7 +78,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
return appMode == AppMode.main && !isSelecting; return appMode == AppMode.main && !isSelecting;
case ChipSetAction.delete: case ChipSetAction.delete:
case ChipSetAction.rename: case ChipSetAction.rename:
return appMode == AppMode.main && isSelecting; return !device.isReadOnly && appMode == AppMode.main && isSelecting;
default: default:
return super.isVisible( return super.isVisible(
action, action,

View file

@ -33,7 +33,7 @@ class DisplaySection extends SettingsSection {
SettingsTileDisplayThemeColorMode(), SettingsTileDisplayThemeColorMode(),
if (device.isDynamicColorAvailable) SettingsTileDisplayEnableDynamicColor(), if (device.isDynamicColorAvailable) SettingsTileDisplayEnableDynamicColor(),
SettingsTileDisplayEnableBlurEffect(), SettingsTileDisplayEnableBlurEffect(),
SettingsTileDisplayDisplayRefreshRateMode(), if (!device.isTelevision) SettingsTileDisplayDisplayRefreshRateMode(),
]; ];
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/enums/home_page.dart';
import 'package:aves/model/settings/enums/screen_on.dart'; import 'package:aves/model/settings/enums/screen_on.dart';
@ -31,11 +32,11 @@ class NavigationSection extends SettingsSection {
@override @override
FutureOr<List<SettingsTile>> tiles(BuildContext context) => [ FutureOr<List<SettingsTile>> tiles(BuildContext context) => [
SettingsTileNavigationHomePage(), SettingsTileNavigationHomePage(),
SettingsTileShowBottomNavigationBar(), if (!device.isTelevision) SettingsTileShowBottomNavigationBar(),
SettingsTileNavigationDrawer(), SettingsTileNavigationDrawer(),
SettingsTileNavigationConfirmationDialog(), if (!device.isTelevision) SettingsTileNavigationConfirmationDialog(),
SettingsTileNavigationKeepScreenOn(), if (!device.isTelevision) SettingsTileNavigationKeepScreenOn(),
SettingsTileNavigationDoubleBackExit(), if (!device.isTelevision) SettingsTileNavigationDoubleBackExit(),
]; ];
} }

View file

@ -34,11 +34,11 @@ class PrivacySection extends SettingsSection {
return [ return [
SettingsTilePrivacyAllowInstalledAppAccess(), SettingsTilePrivacyAllowInstalledAppAccess(),
if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(), if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(),
if (device.canRequestManageMedia) SettingsTilePrivacyManageMedia(), if (!device.isTelevision && device.canRequestManageMedia) SettingsTilePrivacyManageMedia(),
SettingsTilePrivacySaveSearchHistory(), SettingsTilePrivacySaveSearchHistory(),
SettingsTilePrivacyEnableBin(), if (!device.isTelevision) SettingsTilePrivacyEnableBin(),
SettingsTilePrivacyHiddenItems(), SettingsTilePrivacyHiddenItems(),
if (device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), if (!device.isTelevision && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(),
]; ];
} }
} }

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/actions/settings_actions.dart'; import 'package:aves/model/actions/settings_actions.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -75,6 +76,7 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
onPressed: () => _goToSearch(context), onPressed: () => _goToSearch(context),
tooltip: MaterialLocalizations.of(context).searchFieldLabel, tooltip: MaterialLocalizations.of(context).searchFieldLabel,
), ),
if (!device.isTelevision)
MenuIconTheme( MenuIconTheme(
child: PopupMenuButton<SettingsAction>( child: PopupMenuButton<SettingsAction>(
itemBuilder: (context) { itemBuilder: (context) {

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/device.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/video_auto_play_mode.dart'; import 'package:aves/model/settings/enums/video_auto_play_mode.dart';
@ -42,7 +43,7 @@ class VideoSection extends SettingsSection {
SettingsTileVideoEnableHardwareAcceleration(), SettingsTileVideoEnableHardwareAcceleration(),
SettingsTileVideoEnableAutoPlay(), SettingsTileVideoEnableAutoPlay(),
SettingsTileVideoLoopMode(), SettingsTileVideoLoopMode(),
SettingsTileVideoControls(), if (!device.isTelevision) SettingsTileVideoControls(),
SettingsTileVideoSubtitleTheme(), SettingsTileVideoSubtitleTheme(),
]; ];
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -36,9 +37,9 @@ class ViewerSection extends SettingsSection {
SettingsTileViewerQuickActions(), SettingsTileViewerQuickActions(),
SettingsTileViewerOverlay(), SettingsTileViewerOverlay(),
SettingsTileViewerSlideshow(), SettingsTileViewerSlideshow(),
SettingsTileViewerGestureSideTapNext(), if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(),
if (canSetCutoutMode) SettingsTileViewerCutoutMode(), if (!device.isTelevision && canSetCutoutMode) SettingsTileViewerCutoutMode(),
SettingsTileViewerMaxBrightness(), if (!device.isTelevision) SettingsTileViewerMaxBrightness(),
SettingsTileViewerMotionPhotoAutoPlay(), SettingsTileViewerMotionPhotoAutoPlay(),
SettingsTileViewerImageBackground(), SettingsTileViewerImageBackground(),
]; ];

View file

@ -72,22 +72,25 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
return collection != null; return collection != null;
case EntryAction.delete: case EntryAction.delete:
case EntryAction.rename: case EntryAction.rename:
case EntryAction.copy:
case EntryAction.move: case EntryAction.move:
return targetEntry.canEdit; return targetEntry.canEdit;
case EntryAction.copy:
return !device.isReadOnly;
case EntryAction.rotateCCW: case EntryAction.rotateCCW:
case EntryAction.rotateCW: case EntryAction.rotateCW:
return targetEntry.canRotate; return targetEntry.canRotate;
case EntryAction.flip: case EntryAction.flip:
return targetEntry.canFlip; return targetEntry.canFlip;
case EntryAction.convert: case EntryAction.convert:
return !device.isReadOnly && !targetEntry.isVideo;
case EntryAction.print: case EntryAction.print:
return !targetEntry.isVideo && device.canPrint; return device.canPrint && !targetEntry.isVideo;
case EntryAction.openMap: case EntryAction.openMap:
return targetEntry.hasGps; return targetEntry.hasGps;
case EntryAction.viewSource: case EntryAction.viewSource:
return targetEntry.isSvg; return targetEntry.isSvg;
case EntryAction.videoCaptureFrame: case EntryAction.videoCaptureFrame:
return !device.isReadOnly && targetEntry.isVideo;
case EntryAction.videoToggleMute: case EntryAction.videoToggleMute:
case EntryAction.videoSelectStreams: case EntryAction.videoSelectStreams:
case EntryAction.videoSetSpeed: case EntryAction.videoSetSpeed:
@ -98,12 +101,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.openVideo: case EntryAction.openVideo:
return targetEntry.isVideo; return targetEntry.isVideo;
case EntryAction.rotateScreen: case EntryAction.rotateScreen:
return settings.isRotationLocked; return !device.isTelevision && settings.isRotationLocked;
case EntryAction.addShortcut: case EntryAction.addShortcut:
return device.canPinShortcut; return device.canPinShortcut;
case EntryAction.edit:
return !device.isReadOnly;
case EntryAction.info: case EntryAction.info:
case EntryAction.copyToClipboard: case EntryAction.copyToClipboard:
case EntryAction.edit:
case EntryAction.open: case EntryAction.open:
case EntryAction.setAs: case EntryAction.setAs:
case EntryAction.share: case EntryAction.share:

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/events.dart'; import 'package:aves/model/actions/events.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_info.dart'; import 'package:aves/model/entry_info.dart';
import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/entry_metadata_edition.dart';
@ -37,12 +38,13 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
case EntryAction.editTags: case EntryAction.editTags:
case EntryAction.removeMetadata: case EntryAction.removeMetadata:
case EntryAction.exportMetadata: case EntryAction.exportMetadata:
return true; return !device.isReadOnly;
// GeoTIFF // GeoTIFF
case EntryAction.showGeoTiffOnMap: case EntryAction.showGeoTiffOnMap:
return targetEntry.isGeotiff; return targetEntry.isGeotiff;
// motion photo // motion photo
case EntryAction.convertMotionPhotoToStillImage: case EntryAction.convertMotionPhotoToStillImage:
return !device.isReadOnly && targetEntry.isMotionPhoto;
case EntryAction.viewMotionPhotoVideo: case EntryAction.viewMotionPhotoVideo:
return targetEntry.isMotionPhoto; return targetEntry.isMotionPhoto;
default: default:

View file

@ -1,12 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/video/fijkplayer.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
class VideoConductor { class VideoConductor {
final List<AvesVideoController> _controllers = []; final List<AvesVideoController> _controllers = [];
final List<StreamSubscription> _subscriptions = [];
final bool persistPlayback; final bool persistPlayback;
static const _defaultMaxControllerCount = 3; static const _defaultMaxControllerCount = 3;
@ -15,7 +19,13 @@ class VideoConductor {
Future<void> dispose() async { Future<void> dispose() async {
await Future.forEach<AvesVideoController>(_controllers, (controller) => controller.dispose()); await Future.forEach<AvesVideoController>(_controllers, (controller) => controller.dispose());
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
_controllers.clear(); _controllers.clear();
if (settings.keepScreenOn == KeepScreenOn.videoPlayback) {
await windowService.keepScreenOn(false);
}
} }
AvesVideoController getOrCreateController(AvesEntry entry, {int? maxControllerCount}) { AvesVideoController getOrCreateController(AvesEntry entry, {int? maxControllerCount}) {
@ -24,6 +34,7 @@ class VideoConductor {
_controllers.remove(controller); _controllers.remove(controller);
} else { } else {
controller = IjkPlayerAvesVideoController(entry, persistPlayback: persistPlayback); controller = IjkPlayerAvesVideoController(entry, persistPlayback: persistPlayback);
_subscriptions.add(controller.statusStream.listen(_onControllerStatusChanged));
} }
_controllers.insert(0, controller); _controllers.insert(0, controller);
while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) { while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) {
@ -36,6 +47,12 @@ class VideoConductor {
return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId);
} }
Future<void> _onControllerStatusChanged(VideoStatus status) async {
if (settings.keepScreenOn == KeepScreenOn.videoPlayback) {
await windowService.keepScreenOn(status == VideoStatus.playing);
}
}
Future<void> _applyToAll(FutureOr Function(AvesVideoController controller) action) => Future.forEach<AvesVideoController>(_controllers, action); Future<void> _applyToAll(FutureOr Function(AvesVideoController controller) action) => Future.forEach<AvesVideoController>(_controllers, action);
Future<void> pauseAll() => _applyToAll((controller) => controller.pause()); Future<void> pauseAll() => _applyToAll((controller) => controller.pause());

View file

@ -138,6 +138,7 @@
"nameConflictStrategyReplace", "nameConflictStrategyReplace",
"nameConflictStrategySkip", "nameConflictStrategySkip",
"keepScreenOnNever", "keepScreenOnNever",
"keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"keepScreenOnAlways", "keepScreenOnAlways",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
@ -599,7 +600,8 @@
"de": [ "de": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"el": [ "el": [
@ -609,13 +611,15 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"settingsViewerShowRatingTags" "settingsViewerShowRatingTags"
], ],
"es": [ "es": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"fa": [ "fa": [
@ -757,6 +761,7 @@
"nameConflictStrategyReplace", "nameConflictStrategyReplace",
"nameConflictStrategySkip", "nameConflictStrategySkip",
"keepScreenOnNever", "keepScreenOnNever",
"keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"keepScreenOnAlways", "keepScreenOnAlways",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
@ -1218,7 +1223,8 @@
"fr": [ "fr": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"gl": [ "gl": [
@ -1229,6 +1235,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
"displayRefreshRatePreferHighest", "displayRefreshRatePreferHighest",
@ -1693,6 +1700,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"widgetDisplayedItemRandom", "widgetDisplayedItemRandom",
@ -1710,6 +1718,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"settingsViewerShowRatingTags" "settingsViewerShowRatingTags"
], ],
@ -1722,6 +1731,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"widgetDisplayedItemRandom", "widgetDisplayedItemRandom",
@ -1735,19 +1745,22 @@
"ko": [ "ko": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"lt": [ "lt": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"nb": [ "nb": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"nl": [ "nl": [
@ -1758,6 +1771,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"widgetDisplayedItemRandom", "widgetDisplayedItemRandom",
@ -1781,6 +1795,7 @@
"filterTypePanoramaLabel", "filterTypePanoramaLabel",
"mapStyleOsmHot", "mapStyleOsmHot",
"mapStyleStamenToner", "mapStyleStamenToner",
"keepScreenOnVideoPlayback",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
"displayRefreshRatePreferHighest", "displayRefreshRatePreferHighest",
"displayRefreshRatePreferLowest", "displayRefreshRatePreferLowest",
@ -2261,6 +2276,7 @@
"nameConflictStrategyReplace", "nameConflictStrategyReplace",
"nameConflictStrategySkip", "nameConflictStrategySkip",
"keepScreenOnNever", "keepScreenOnNever",
"keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"keepScreenOnAlways", "keepScreenOnAlways",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
@ -2727,6 +2743,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"subtitlePositionTop", "subtitlePositionTop",
"subtitlePositionBottom", "subtitlePositionBottom",
"widgetDisplayedItemRandom", "widgetDisplayedItemRandom",
@ -2744,13 +2761,15 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"settingsViewerShowRatingTags" "settingsViewerShowRatingTags"
], ],
"ru": [ "ru": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"th": [ "th": [
@ -2769,6 +2788,7 @@
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"coordinateDms", "coordinateDms",
"keepScreenOnVideoPlayback",
"keepScreenOnViewerOnly", "keepScreenOnViewerOnly",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
@ -3133,7 +3153,8 @@
"tr": [ "tr": [
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryInfoActionRemoveLocation" "entryInfoActionRemoveLocation",
"keepScreenOnVideoPlayback"
], ],
"zh": [ "zh": [
@ -3143,6 +3164,7 @@
"filterAspectRatioLandscapeLabel", "filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel", "filterAspectRatioPortraitLabel",
"filterNoAddressLabel", "filterNoAddressLabel",
"keepScreenOnVideoPlayback",
"settingsViewerShowRatingTags" "settingsViewerShowRatingTags"
] ]
} }