#437 tv: read-only, ambient mode, webview, settings
This commit is contained in:
parent
829ec201eb
commit
e5e1a8f275
27 changed files with 221 additions and 124 deletions
|
@ -11,6 +11,13 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
package="deckers.thibault.aves"
|
||||
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.
|
||||
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
|
||||
android:allowBackup="true"
|
||||
android:appCategory="image"
|
||||
android:banner="@drawable/banner"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
@ -83,6 +91,8 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
|
|
|
@ -20,10 +20,14 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
|
|||
|
||||
val window = activity.window
|
||||
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
if (on) {
|
||||
window.addFlags(flag)
|
||||
} else {
|
||||
window.clearFlags(flag)
|
||||
|
||||
val old = (window.attributes.flags and flag) != 0
|
||||
if (old != on) {
|
||||
if (on) {
|
||||
window.addFlags(flag)
|
||||
} else {
|
||||
window.clearFlags(flag)
|
||||
}
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
|
BIN
android/app/src/main/res/drawable-nodpi/banner.png
Normal file
BIN
android/app/src/main/res/drawable-nodpi/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7 KiB |
|
@ -195,6 +195,7 @@
|
|||
"nameConflictStrategySkip": "Skip",
|
||||
|
||||
"keepScreenOnNever": "Never",
|
||||
"keepScreenOnVideoPlayback": "During video playback",
|
||||
"keepScreenOnViewerOnly": "Viewer page only",
|
||||
"keepScreenOnAlways": "Always",
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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';
|
||||
|
||||
final Device device = Device._private();
|
||||
|
@ -6,7 +7,7 @@ final Device device = Device._private();
|
|||
class Device {
|
||||
late final String _userAgent;
|
||||
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;
|
||||
|
||||
|
@ -26,6 +27,10 @@ class Device {
|
|||
|
||||
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
|
||||
|
||||
bool get isReadOnly => _isTelevision;
|
||||
|
||||
bool get isTelevision => _isTelevision;
|
||||
|
||||
bool get showPinShortcutFeedback => _showPinShortcutFeedback;
|
||||
|
||||
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
|
||||
|
@ -36,6 +41,9 @@ class Device {
|
|||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
_userAgent = '${packageInfo.packageName}/${packageInfo.version}';
|
||||
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
_isTelevision = androidInfo.systemFeatures.contains('android.software.leanback');
|
||||
|
||||
final capabilities = await deviceService.getCapabilities();
|
||||
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
||||
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/geo/countries.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry_cache.dart';
|
||||
import 'package:aves/model/entry_dirs.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 canEdit => path != null && !trashed && isMediaStoreContent;
|
||||
bool get canEdit => !device.isReadOnly && path != null && !trashed && isMediaStoreContent;
|
||||
|
||||
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ enum EntryBackground { black, white, checkered }
|
|||
|
||||
enum HomePageSetting { collection, albums }
|
||||
|
||||
enum KeepScreenOn { never, viewerOnly, always }
|
||||
enum KeepScreenOn { never, videoPlayback, viewerOnly, always }
|
||||
|
||||
enum SlideshowVideoPlayback { skip, playMuted, playWithSound }
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ extension ExtraKeepScreenOn on KeepScreenOn {
|
|||
switch (this) {
|
||||
case KeepScreenOn.never:
|
||||
return context.l10n.keepScreenOnNever;
|
||||
case KeepScreenOn.videoPlayback:
|
||||
return context.l10n.keepScreenOnVideoPlayback;
|
||||
case KeepScreenOn.viewerOnly:
|
||||
return context.l10n.keepScreenOnViewerOnly;
|
||||
case KeepScreenOn.always:
|
||||
|
|
|
@ -5,7 +5,11 @@ import 'dart:math';
|
|||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/model/actions/entry_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/mime.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/settings/defaults.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/map_style.dart';
|
||||
|
@ -230,6 +234,25 @@ class Settings extends ChangeNotifier {
|
|||
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
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/services/common/services.dart';
|
|||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/durations.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/extensions/build_context.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:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class BugReport extends StatefulWidget {
|
||||
const BugReport({super.key});
|
||||
|
@ -34,7 +34,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
|||
late Future<String> _infoLoader;
|
||||
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
|
||||
void initState() {
|
||||
|
@ -184,9 +184,5 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
|||
showFeedback(context, context.l10n.genericSuccessFeedback);
|
||||
}
|
||||
|
||||
Future<void> _goToGithub() async {
|
||||
if (await canLaunchUrl(bugReportUri)) {
|
||||
await launchUrl(bugReportUri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
Future<void> _goToGithub() => AvesApp.launchUrl(bugReportUrl);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import 'package:material_color_utilities/material_color_utilities.dart';
|
|||
import 'package:overlay_support/overlay_support.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
class AvesApp extends StatefulWidget {
|
||||
final AppFlavor flavor;
|
||||
|
@ -103,6 +104,19 @@ class AvesApp extends StatefulWidget {
|
|||
static Future<void> hideSystemUI() async {
|
||||
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 {
|
||||
|
@ -207,41 +221,49 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
lightAccent = Color(tonalPalette?.get(60) ?? 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>(
|
||||
future: _shouldUseBoldFontLoader,
|
||||
builder: (context, snapshot) {
|
||||
// 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
|
||||
final shouldUseBoldFont = snapshot.data ?? false;
|
||||
return MaterialApp(
|
||||
navigatorKey: AvesApp.navigatorKey,
|
||||
home: home,
|
||||
navigatorObservers: _navigatorObservers,
|
||||
builder: (context, child) {
|
||||
if (initialized) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(context));
|
||||
}
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(boldText: shouldUseBoldFont),
|
||||
child: AvesColorsProvider(
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
pageTransitionsTheme: pageTransitionsTheme,
|
||||
),
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
// handle Android TV remote `select` button
|
||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||
},
|
||||
onGenerateTitle: (context) => context.l10n.appName,
|
||||
theme: Themes.lightTheme(lightAccent, initialized),
|
||||
darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized),
|
||||
themeMode: themeBrightness.appThemeMode,
|
||||
locale: settingsLocale,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AvesApp.supportedLocales,
|
||||
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
||||
scrollBehavior: StretchMaterialScrollBehavior(),
|
||||
child: MaterialApp(
|
||||
navigatorKey: AvesApp.navigatorKey,
|
||||
home: home,
|
||||
navigatorObservers: _navigatorObservers,
|
||||
builder: (context, child) {
|
||||
if (initialized) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(context));
|
||||
}
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(boldText: shouldUseBoldFont),
|
||||
child: AvesColorsProvider(
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
pageTransitionsTheme: pageTransitionsTheme,
|
||||
),
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onGenerateTitle: (context) => context.l10n.appName,
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: themeBrightness.appThemeMode,
|
||||
locale: settingsLocale,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AvesApp.supportedLocales,
|
||||
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
||||
scrollBehavior: StretchMaterialScrollBehavior(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
|||
|
||||
import 'package:aves/app_mode.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/filters/filters.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(
|
||||
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
|
||||
),
|
||||
if (isSelecting && !isTrash && appMode == AppMode.main)
|
||||
if (isSelecting && !device.isReadOnly && appMode == AppMode.main && !isTrash)
|
||||
PopupMenuItem<EntrySetAction>(
|
||||
enabled: canApplyEditActions,
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
|
@ -73,7 +73,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.addShortcut:
|
||||
return appMode == AppMode.main && !isSelecting && device.canPinShortcut && !isTrash;
|
||||
case EntrySetAction.emptyBin:
|
||||
return appMode == AppMode.main && isTrash;
|
||||
return !device.isReadOnly && appMode == AppMode.main && isTrash;
|
||||
// browsing or selecting
|
||||
case EntrySetAction.map:
|
||||
case EntrySetAction.slideshow:
|
||||
|
@ -82,13 +82,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.rescan:
|
||||
return appMode == AppMode.main && !isTrash;
|
||||
// selecting
|
||||
case EntrySetAction.delete:
|
||||
return appMode == AppMode.main && isSelecting;
|
||||
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.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.toggleFavourite:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
case EntrySetAction.flip:
|
||||
|
@ -98,9 +99,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.editRating:
|
||||
case EntrySetAction.editTags:
|
||||
case EntrySetAction.removeMetadata:
|
||||
return appMode == AppMode.main && isSelecting && !isTrash;
|
||||
return !device.isReadOnly && appMode == AppMode.main && isSelecting && !isTrash;
|
||||
case EntrySetAction.restore:
|
||||
return appMode == AppMode.main && isSelecting && isTrash;
|
||||
return !device.isReadOnly && appMode == AppMode.main && isSelecting && isTrash;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LinkChip extends StatelessWidget {
|
||||
final Widget? leading;
|
||||
|
@ -24,20 +24,11 @@ class LinkChip extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _urlString = urlString;
|
||||
return DefaultTextStyle.merge(
|
||||
style: (textStyle ?? const TextStyle()).copyWith(color: color),
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: onTap ??
|
||||
() async {
|
||||
if (_urlString != null) {
|
||||
final url = Uri.parse(_urlString);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTap: onTap ?? () => AvesApp.launchUrl(urlString),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MarkdownContainer extends StatelessWidget {
|
||||
final String data;
|
||||
|
@ -43,14 +43,7 @@ class MarkdownContainer extends StatelessWidget {
|
|||
child: Markdown(
|
||||
data: data,
|
||||
selectable: true,
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href != null) {
|
||||
final url = Uri.parse(href);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTapLink: (text, href, title) => AvesApp.launchUrl(href),
|
||||
shrinkWrap: true,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
import 'package:aves_map/aves_map.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class Attribution extends StatelessWidget {
|
||||
final EntryMapStyle? style;
|
||||
|
@ -37,14 +37,7 @@ class Attribution extends StatelessWidget {
|
|||
a: TextStyle(color: theme.colorScheme.secondary),
|
||||
p: theme.textTheme.bodySmall!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)),
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href != null) {
|
||||
final url = Uri.parse(href);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTapLink: (text, href, title) => AvesApp.launchUrl(href),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/actions/chip_set_actions.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/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
|
@ -77,7 +78,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
return appMode == AppMode.main && !isSelecting;
|
||||
case ChipSetAction.delete:
|
||||
case ChipSetAction.rename:
|
||||
return appMode == AppMode.main && isSelecting;
|
||||
return !device.isReadOnly && appMode == AppMode.main && isSelecting;
|
||||
default:
|
||||
return super.isVisible(
|
||||
action,
|
||||
|
|
|
@ -33,7 +33,7 @@ class DisplaySection extends SettingsSection {
|
|||
SettingsTileDisplayThemeColorMode(),
|
||||
if (device.isDynamicColorAvailable) SettingsTileDisplayEnableDynamicColor(),
|
||||
SettingsTileDisplayEnableBlurEffect(),
|
||||
SettingsTileDisplayDisplayRefreshRateMode(),
|
||||
if (!device.isTelevision) SettingsTileDisplayDisplayRefreshRateMode(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/home_page.dart';
|
||||
import 'package:aves/model/settings/enums/screen_on.dart';
|
||||
|
@ -31,11 +32,11 @@ class NavigationSection extends SettingsSection {
|
|||
@override
|
||||
FutureOr<List<SettingsTile>> tiles(BuildContext context) => [
|
||||
SettingsTileNavigationHomePage(),
|
||||
SettingsTileShowBottomNavigationBar(),
|
||||
if (!device.isTelevision) SettingsTileShowBottomNavigationBar(),
|
||||
SettingsTileNavigationDrawer(),
|
||||
SettingsTileNavigationConfirmationDialog(),
|
||||
SettingsTileNavigationKeepScreenOn(),
|
||||
SettingsTileNavigationDoubleBackExit(),
|
||||
if (!device.isTelevision) SettingsTileNavigationConfirmationDialog(),
|
||||
if (!device.isTelevision) SettingsTileNavigationKeepScreenOn(),
|
||||
if (!device.isTelevision) SettingsTileNavigationDoubleBackExit(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,11 @@ class PrivacySection extends SettingsSection {
|
|||
return [
|
||||
SettingsTilePrivacyAllowInstalledAppAccess(),
|
||||
if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(),
|
||||
if (device.canRequestManageMedia) SettingsTilePrivacyManageMedia(),
|
||||
if (!device.isTelevision && device.canRequestManageMedia) SettingsTilePrivacyManageMedia(),
|
||||
SettingsTilePrivacySaveSearchHistory(),
|
||||
SettingsTilePrivacyEnableBin(),
|
||||
if (!device.isTelevision) SettingsTilePrivacyEnableBin(),
|
||||
SettingsTilePrivacyHiddenItems(),
|
||||
if (device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(),
|
||||
if (!device.isTelevision && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
|||
import 'dart:typed_data';
|
||||
|
||||
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/ref/mime_types.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
@ -75,27 +76,28 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
|
|||
onPressed: () => _goToSearch(context),
|
||||
tooltip: MaterialLocalizations.of(context).searchFieldLabel,
|
||||
),
|
||||
MenuIconTheme(
|
||||
child: PopupMenuButton<SettingsAction>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.export,
|
||||
child: MenuRow(text: context.l10n.settingsActionExport, icon: const Icon(AIcons.fileExport)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.import,
|
||||
child: MenuRow(text: context.l10n.settingsActionImport, icon: const Icon(AIcons.fileImport)),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
_onActionSelected(action);
|
||||
},
|
||||
if (!device.isTelevision)
|
||||
MenuIconTheme(
|
||||
child: PopupMenuButton<SettingsAction>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.export,
|
||||
child: MenuRow(text: context.l10n.settingsActionExport, icon: const Icon(AIcons.fileExport)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.import,
|
||||
child: MenuRow(text: context.l10n.settingsActionImport, icon: const Icon(AIcons.fileImport)),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
_onActionSelected(action);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureAreaProtectorStack(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/video_auto_play_mode.dart';
|
||||
|
@ -42,7 +43,7 @@ class VideoSection extends SettingsSection {
|
|||
SettingsTileVideoEnableHardwareAcceleration(),
|
||||
SettingsTileVideoEnableAutoPlay(),
|
||||
SettingsTileVideoLoopMode(),
|
||||
SettingsTileVideoControls(),
|
||||
if (!device.isTelevision) SettingsTileVideoControls(),
|
||||
SettingsTileVideoSubtitleTheme(),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
@ -36,9 +37,9 @@ class ViewerSection extends SettingsSection {
|
|||
SettingsTileViewerQuickActions(),
|
||||
SettingsTileViewerOverlay(),
|
||||
SettingsTileViewerSlideshow(),
|
||||
SettingsTileViewerGestureSideTapNext(),
|
||||
if (canSetCutoutMode) SettingsTileViewerCutoutMode(),
|
||||
SettingsTileViewerMaxBrightness(),
|
||||
if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(),
|
||||
if (!device.isTelevision && canSetCutoutMode) SettingsTileViewerCutoutMode(),
|
||||
if (!device.isTelevision) SettingsTileViewerMaxBrightness(),
|
||||
SettingsTileViewerMotionPhotoAutoPlay(),
|
||||
SettingsTileViewerImageBackground(),
|
||||
];
|
||||
|
|
|
@ -72,22 +72,25 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
return collection != null;
|
||||
case EntryAction.delete:
|
||||
case EntryAction.rename:
|
||||
case EntryAction.copy:
|
||||
case EntryAction.move:
|
||||
return targetEntry.canEdit;
|
||||
case EntryAction.copy:
|
||||
return !device.isReadOnly;
|
||||
case EntryAction.rotateCCW:
|
||||
case EntryAction.rotateCW:
|
||||
return targetEntry.canRotate;
|
||||
case EntryAction.flip:
|
||||
return targetEntry.canFlip;
|
||||
case EntryAction.convert:
|
||||
return !device.isReadOnly && !targetEntry.isVideo;
|
||||
case EntryAction.print:
|
||||
return !targetEntry.isVideo && device.canPrint;
|
||||
return device.canPrint && !targetEntry.isVideo;
|
||||
case EntryAction.openMap:
|
||||
return targetEntry.hasGps;
|
||||
case EntryAction.viewSource:
|
||||
return targetEntry.isSvg;
|
||||
case EntryAction.videoCaptureFrame:
|
||||
return !device.isReadOnly && targetEntry.isVideo;
|
||||
case EntryAction.videoToggleMute:
|
||||
case EntryAction.videoSelectStreams:
|
||||
case EntryAction.videoSetSpeed:
|
||||
|
@ -98,12 +101,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
case EntryAction.openVideo:
|
||||
return targetEntry.isVideo;
|
||||
case EntryAction.rotateScreen:
|
||||
return settings.isRotationLocked;
|
||||
return !device.isTelevision && settings.isRotationLocked;
|
||||
case EntryAction.addShortcut:
|
||||
return device.canPinShortcut;
|
||||
case EntryAction.edit:
|
||||
return !device.isReadOnly;
|
||||
case EntryAction.info:
|
||||
case EntryAction.copyToClipboard:
|
||||
case EntryAction.edit:
|
||||
case EntryAction.open:
|
||||
case EntryAction.setAs:
|
||||
case EntryAction.share:
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:aves/model/actions/entry_actions.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_info.dart';
|
||||
import 'package:aves/model/entry_metadata_edition.dart';
|
||||
|
@ -37,12 +38,13 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
case EntryAction.editTags:
|
||||
case EntryAction.removeMetadata:
|
||||
case EntryAction.exportMetadata:
|
||||
return true;
|
||||
return !device.isReadOnly;
|
||||
// GeoTIFF
|
||||
case EntryAction.showGeoTiffOnMap:
|
||||
return targetEntry.isGeotiff;
|
||||
// motion photo
|
||||
case EntryAction.convertMotionPhotoToStillImage:
|
||||
return !device.isReadOnly && targetEntry.isMotionPhoto;
|
||||
case EntryAction.viewMotionPhotoVideo:
|
||||
return targetEntry.isMotionPhoto;
|
||||
default:
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/fijkplayer.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class VideoConductor {
|
||||
final List<AvesVideoController> _controllers = [];
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final bool persistPlayback;
|
||||
|
||||
static const _defaultMaxControllerCount = 3;
|
||||
|
@ -15,7 +19,13 @@ class VideoConductor {
|
|||
|
||||
Future<void> dispose() async {
|
||||
await Future.forEach<AvesVideoController>(_controllers, (controller) => controller.dispose());
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
_controllers.clear();
|
||||
if (settings.keepScreenOn == KeepScreenOn.videoPlayback) {
|
||||
await windowService.keepScreenOn(false);
|
||||
}
|
||||
}
|
||||
|
||||
AvesVideoController getOrCreateController(AvesEntry entry, {int? maxControllerCount}) {
|
||||
|
@ -24,6 +34,7 @@ class VideoConductor {
|
|||
_controllers.remove(controller);
|
||||
} else {
|
||||
controller = IjkPlayerAvesVideoController(entry, persistPlayback: persistPlayback);
|
||||
_subscriptions.add(controller.statusStream.listen(_onControllerStatusChanged));
|
||||
}
|
||||
_controllers.insert(0, controller);
|
||||
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);
|
||||
}
|
||||
|
||||
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> pauseAll() => _applyToAll((controller) => controller.pause());
|
||||
|
|
|
@ -138,6 +138,7 @@
|
|||
"nameConflictStrategyReplace",
|
||||
"nameConflictStrategySkip",
|
||||
"keepScreenOnNever",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"keepScreenOnAlways",
|
||||
"accessibilityAnimationsRemove",
|
||||
|
@ -599,7 +600,8 @@
|
|||
"de": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"el": [
|
||||
|
@ -609,13 +611,15 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"settingsViewerShowRatingTags"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
|
@ -757,6 +761,7 @@
|
|||
"nameConflictStrategyReplace",
|
||||
"nameConflictStrategySkip",
|
||||
"keepScreenOnNever",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"keepScreenOnAlways",
|
||||
"accessibilityAnimationsRemove",
|
||||
|
@ -1218,7 +1223,8 @@
|
|||
"fr": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
|
@ -1229,6 +1235,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"accessibilityAnimationsRemove",
|
||||
"accessibilityAnimationsKeep",
|
||||
"displayRefreshRatePreferHighest",
|
||||
|
@ -1693,6 +1700,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"widgetDisplayedItemRandom",
|
||||
|
@ -1710,6 +1718,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"settingsViewerShowRatingTags"
|
||||
],
|
||||
|
||||
|
@ -1722,6 +1731,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"widgetDisplayedItemRandom",
|
||||
|
@ -1735,19 +1745,22 @@
|
|||
"ko": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"nb": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
|
@ -1758,6 +1771,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"widgetDisplayedItemRandom",
|
||||
|
@ -1781,6 +1795,7 @@
|
|||
"filterTypePanoramaLabel",
|
||||
"mapStyleOsmHot",
|
||||
"mapStyleStamenToner",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"accessibilityAnimationsKeep",
|
||||
"displayRefreshRatePreferHighest",
|
||||
"displayRefreshRatePreferLowest",
|
||||
|
@ -2261,6 +2276,7 @@
|
|||
"nameConflictStrategyReplace",
|
||||
"nameConflictStrategySkip",
|
||||
"keepScreenOnNever",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"keepScreenOnAlways",
|
||||
"accessibilityAnimationsRemove",
|
||||
|
@ -2727,6 +2743,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"subtitlePositionTop",
|
||||
"subtitlePositionBottom",
|
||||
"widgetDisplayedItemRandom",
|
||||
|
@ -2744,13 +2761,15 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"settingsViewerShowRatingTags"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"th": [
|
||||
|
@ -2769,6 +2788,7 @@
|
|||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"coordinateDms",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"keepScreenOnViewerOnly",
|
||||
"accessibilityAnimationsRemove",
|
||||
"accessibilityAnimationsKeep",
|
||||
|
@ -3133,7 +3153,8 @@
|
|||
"tr": [
|
||||
"entryActionShareImageOnly",
|
||||
"entryActionShareVideoOnly",
|
||||
"entryInfoActionRemoveLocation"
|
||||
"entryInfoActionRemoveLocation",
|
||||
"keepScreenOnVideoPlayback"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -3143,6 +3164,7 @@
|
|||
"filterAspectRatioLandscapeLabel",
|
||||
"filterAspectRatioPortraitLabel",
|
||||
"filterNoAddressLabel",
|
||||
"keepScreenOnVideoPlayback",
|
||||
"settingsViewerShowRatingTags"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue