diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e4930d3..9618668 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -71,15 +71,13 @@ "detail": "" }, { - "type": "flutter", + "type": "dart", "command": "dart", "args": [ "run", - "flutter_launcher_icons" + "icons_launcher:create" ], - "group": "build", - "problemMatcher": [], - "label": "flutter: flutter create launcher icons", + "label": "dart: generate launcher icons", "detail": "" } ], diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..59bd9b4 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..6f91752 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..1d0a437 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..4178538 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..fc1aa37 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/icons/fladder_macos_icon.png b/icons/fladder_macos_icon.png new file mode 100644 index 0000000..49ea563 Binary files /dev/null and b/icons/fladder_macos_icon.png differ diff --git a/icons/fladder_notification_icon.png b/icons/fladder_notification_icon.png new file mode 100644 index 0000000..c2b5467 Binary files /dev/null and b/icons/fladder_notification_icon.png differ diff --git a/icons/macos_icon.afphoto b/icons/macos_icon.afphoto new file mode 100644 index 0000000..d10d05e Binary files /dev/null and b/icons/macos_icon.afphoto differ diff --git a/icons_launcher.yaml b/icons_launcher.yaml index 56d59ab..ccf210a 100644 --- a/icons_launcher.yaml +++ b/icons_launcher.yaml @@ -5,19 +5,20 @@ icons_launcher: adaptive_foreground_image: "icons/fladder_icon.png" adaptive_background_color: "#3a2101" adaptive_monochrome_image: "icons/fladder_adaptive_icon.png" + notification_image: icons/fladder_notification_icon.png enable: true ios: - image_path: "icons/fladder_icon.png" + image_path: "icons/fladder_store_icon.jpg" enable: true windows: image_path: "icons/fladder_icon_desktop.png" enable: true macos: - image_path: "icons/fladder_icon_desktop.png" + image_path: "icons/fladder_macos_icon.png" enable: true linux: image_path: "icons/fladder_icon_desktop.png" enable: true web: - favicon_path: "icons/fladder_icon.png" + favicon_path: "icons/fladder_icon_desktop.png" enable: true diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index eabd851..d807305 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -2,117 +2,115 @@ "images": [ { "filename": "Icon-App-20x20@2x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "2x", - "size": "20x20" + "size": "20x20", + "platform": "ios" }, { "filename": "Icon-App-20x20@3x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "3x", - "size": "20x20" - }, - { - "filename": "Icon-App-29x29@1x.png", - "idiom": "iphone", - "scale": "1x", - "size": "29x29" + "size": "20x20", + "platform": "ios" }, { "filename": "Icon-App-29x29@2x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "2x", - "size": "29x29" + "size": "29x29", + "platform": "ios" }, { "filename": "Icon-App-29x29@3x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "3x", - "size": "29x29" + "size": "29x29", + "platform": "ios" + }, + { + "filename": "Icon-App-38x38@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "38x38", + "platform": "ios" + }, + { + "filename": "Icon-App-38x38@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "38x38", + "platform": "ios" }, { "filename": "Icon-App-40x40@2x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "2x", - "size": "40x40" + "size": "40x40", + "platform": "ios" }, { "filename": "Icon-App-40x40@3x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "3x", - "size": "40x40" + "size": "40x40", + "platform": "ios" }, { "filename": "Icon-App-60x60@2x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "2x", - "size": "60x60" + "size": "60x60", + "platform": "ios" }, { "filename": "Icon-App-60x60@3x.png", - "idiom": "iphone", + "idiom": "universal", "scale": "3x", - "size": "60x60" + "size": "60x60", + "platform": "ios" }, { - "filename": "Icon-App-20x20@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "20x20" - }, - { - "filename": "Icon-App-20x20@2x.png", - "idiom": "ipad", + "filename": "Icon-App-64x64@2x.png", + "idiom": "universal", "scale": "2x", - "size": "20x20" + "size": "64x64", + "platform": "ios" }, { - "filename": "Icon-App-29x29@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "29x29" + "filename": "Icon-App-64x64@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "64x64", + "platform": "ios" }, { - "filename": "Icon-App-29x29@2x.png", - "idiom": "ipad", + "filename": "Icon-App-68x68@2x.png", + "idiom": "universal", "scale": "2x", - "size": "29x29" - }, - { - "filename": "Icon-App-40x40@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "40x40" - }, - { - "filename": "Icon-App-40x40@2x.png", - "idiom": "ipad", - "scale": "2x", - "size": "40x40" - }, - { - "filename": "Icon-App-76x76@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "76x76" + "size": "68x68", + "platform": "ios" }, { "filename": "Icon-App-76x76@2x.png", - "idiom": "ipad", + "idiom": "universal", "scale": "2x", - "size": "76x76" + "size": "76x76", + "platform": "ios" }, { "filename": "Icon-App-83.5x83.5@2x.png", - "idiom": "ipad", + "idiom": "universal", "scale": "2x", - "size": "83.5x83.5" + "size": "83.5x83.5", + "platform": "ios" }, { "filename": "Icon-App-1024x1024@1x.png", - "idiom": "ios-marketing", + "idiom": "universal", "scale": "1x", - "size": "1024x1024" + "size": "1024x1024", + "platform": "ios" } ], "info": { diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index a690e5b..c7f594c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 42e1291..6ef725b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 065029f..9357ee9 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index b06d38c..844df5a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 191514f..0d68e15 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index dd9fe9a..0b57ede 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 7bbe2d2..8663b7f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png new file mode 100644 index 0000000..774da54 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png new file mode 100644 index 0000000..03f0bd0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 065029f..9357ee9 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 2576a08..25fe488 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index f2f0310..356a2ec 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index f2f0310..356a2ec 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 0cb29ab..8cbba14 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png new file mode 100644 index 0000000..0dbdd22 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png new file mode 100644 index 0000000..499f169 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png new file mode 100644 index 0000000..c022900 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index edc5e73..774da54 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 0df8d7b..94508fa 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index b86794e..38aa605 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/android_tv/main.dart b/lib/android_tv/main.dart index 7e72d46..2babcb6 100644 --- a/lib/android_tv/main.dart +++ b/lib/android_tv/main.dart @@ -1,14 +1,16 @@ -import 'package:fladder/providers/shared_provider.dart'; -import 'package:fladder/util/application_info.dart'; -import 'package:fladder/util/string_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:media_kit/media_kit.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:fladder/providers/shared_provider.dart'; +import 'package:fladder/util/application_info.dart'; +import 'package:fladder/util/string_extensions.dart'; + void main() async { _setupLogging(); WidgetsFlutterBinding.ensureInitialized(); @@ -22,8 +24,14 @@ void main() async { ProviderScope( overrides: [ sharedPreferencesProvider.overrideWith((ref) => sharedPreferences), - applicationInfoProvider.overrideWith((ref) => ApplicationInfo( - name: packageInfo.appName, version: packageInfo.version, os: defaultTargetPlatform.name.capitalize())), + applicationInfoProvider.overrideWith( + (ref) => ApplicationInfo( + name: packageInfo.appName, + buildNumber: packageInfo.buildNumber, + version: packageInfo.version, + os: defaultTargetPlatform.name.capitalize(), + ), + ), ], child: const Main(), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1466f27..5a5ed78 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -133,6 +133,16 @@ "editMetadata": "Edit metadata", "empty": "Empty", "enabled": "Enabled", + "endsAt": "ends at {date}", + "@endsAt": { + "description": "endsAt", + "placeholders": { + "date": { + "type": "DateTime", + "format": "jm" + } + } + }, "episode": "{count, plural, other{Episodes} one{Episode} }", "@episode": { "description": "episode", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 38cb55c..62b70bc 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -133,6 +133,7 @@ "editMetadata": "Editar metadatos", "empty": "Vacío", "enabled": "Habilitado", + "endsAt": "termina el {date}", "episode": "{count, plural, other{Episodios} one{Episodio}}", "@episode": { "description": "episodio", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index aa427b5..8424ca1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -133,6 +133,7 @@ "editMetadata": "Modifier les métadonnées", "empty": "Vide", "enabled": "Activé", + "endsAt": "se termine à {date}", "episode": "{count, plural, other{Épisodes} one{Épisode}}", "@episode": { "description": "épisode", diff --git a/lib/l10n/app_jp.arb b/lib/l10n/app_jp.arb index e1731f6..780dc5c 100644 --- a/lib/l10n/app_jp.arb +++ b/lib/l10n/app_jp.arb @@ -133,6 +133,7 @@ "editMetadata": "メタデータを編集", "empty": "空", "enabled": "有効", + "endsAt": "{date}に終了", "episode": "{count, plural, other{エピソード} one{エピソード}}", "@episode": { "description": "エピソード", @@ -273,7 +274,7 @@ "metadataRefreshDefault": "新しいファイルと更新されたファイルをスキャン", "metadataRefreshFull": "すべてのメタデータを置き換える", "metadataRefreshValidation": "欠落しているメタデータを検索", - "minutes": "分", + "minutes": "{count}分", "@minutes": { "description": "分", "placeholders": { @@ -466,7 +467,7 @@ } } }, - "seconds": "秒", + "seconds": "{count}秒", "@seconds": { "description": "秒", "placeholders": { diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 2f4eba5..116fda5 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -133,6 +133,7 @@ "editMetadata": "Metadata bewerken", "empty": "Leeg", "enabled": "Ingeschakeld", + "endsAt": "eindigt om {date}", "episode": "{count, plural, other{Afleveringen} one{Aflevering}}", "@episode": { "description": "aflevering", @@ -364,7 +365,7 @@ } }, "playLabel": "Afspelen", - "playVideos": "Video's afspelen", + "playVideos": "Video''s afspelen", "played": "Gespeeld", "quickConnectAction": "Voer snelverbind code in voor", "quickConnectInputACode": "Voer een code in", @@ -543,14 +544,14 @@ "showDetails": "Toon details", "showEmpty": "Toon leeg", "shuffleGallery": "Galerij shuffle", - "shuffleVideos": "Video's shuffle", + "shuffleVideos": "Video''s shuffle", "somethingWentWrong": "Er is iets misgegaan", "somethingWentWrongPasswordCheck": "Er is iets misgegaan, controleer uw wachtwoord", "sortBy": "Sorteer op", "sortName": "Naam", "sortOrder": "Sorteervolgorde", "start": "Start", - "studio": "{count, plural, other{Studio's} one{Studio}}", + "studio": "{count, plural, other{Studio''s} one{Studio}}", "@studio": { "description": "studio", "placeholders": { @@ -644,7 +645,7 @@ "videoScalingFitHeight": "Pas hoogte aan", "videoScalingFitWidth": "Pas breedte aan", "videoScalingScaleDown": "Schaal omlaag", - "viewPhotos": "Foto's bekijken", + "viewPhotos": "Foto''s bekijken", "watchOn": "Kijk op", "writer": "{count, plural, other{Schrijvers} one{Schrijver}}", "@writer": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index a720275..8e23b74 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -133,6 +133,7 @@ "editMetadata": "编辑元数据", "empty": "空", "enabled": "启用", + "endsAt": "结束于 {date}", "episode": "{count, plural, other{集} one{集}}", "@episode": { "description": "集", @@ -273,7 +274,7 @@ "metadataRefreshDefault": "扫描新文件和更新的文件", "metadataRefreshFull": "替换所有元数据", "metadataRefreshValidation": "搜索缺失的元数据", - "minutes": "分钟", + "minutes": "{count}分钟", "@minutes": { "description": "分钟", "placeholders": { @@ -466,7 +467,7 @@ } } }, - "seconds": "秒", + "seconds": "{count}秒", "@seconds": { "description": "秒", "placeholders": { diff --git a/lib/main.dart b/lib/main.dart index b79018f..f07fdc9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -84,7 +84,8 @@ void main() async { final applicationInfo = ApplicationInfo( name: packageInfo.appName.capitalize(), - version: "${packageInfo.version}(${packageInfo.buildNumber})", + version: packageInfo.version, + buildNumber: packageInfo.buildNumber, os: !kIsWeb ? defaultTargetPlatform.name.capitalize() : "${defaultTargetPlatform.name.capitalize()} Web", ); diff --git a/lib/models/item_base_model.dart b/lib/models/item_base_model.dart index 79d83d2..40665c9 100644 --- a/lib/models/item_base_model.dart +++ b/lib/models/item_base_model.dart @@ -1,34 +1,35 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; import 'package:dart_mappable/dart_mappable.dart'; import 'package:ficonsax/ficonsax.dart'; -import 'package:fladder/models/book_model.dart'; -import 'package:fladder/models/boxset_model.dart'; -import 'package:fladder/models/items/media_streams_model.dart'; -import 'package:fladder/models/library_search/library_search_options.dart'; -import 'package:fladder/models/playlist_model.dart'; -import 'package:fladder/routes/auto_router.gr.dart'; -import 'package:fladder/screens/details_screens/book_detail_screen.dart'; -import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/string_extensions.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; +import 'package:fladder/models/book_model.dart'; +import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/folder_model.dart'; import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/movie_model.dart'; import 'package:fladder/models/items/overview_model.dart'; import 'package:fladder/models/items/person_model.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/series_model.dart'; +import 'package:fladder/models/library_search/library_search_options.dart'; +import 'package:fladder/models/playlist_model.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/details_screens/book_detail_screen.dart'; import 'package:fladder/screens/details_screens/details_screens.dart'; import 'package:fladder/screens/details_screens/episode_detail_screen.dart'; import 'package:fladder/screens/details_screens/season_detail_screen.dart'; import 'package:fladder/screens/library_search/library_search_screen.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/string_extensions.dart'; part 'item_base_model.mapper.dart'; @@ -62,8 +63,6 @@ class ItemBaseModel with ItemBaseModelMappable { required this.jellyType, }); - String get title => name; - ItemBaseModel? setProgress(double progress) { return copyWith(userData: userData.copyWith(progress: progress)); } @@ -98,6 +97,8 @@ class ItemBaseModel with ItemBaseModelMappable { _ => null, }; + String get title => name; + ///Used for retrieving the correct id when fetching queue String get streamId => id; @@ -111,7 +112,7 @@ class ItemBaseModel with ItemBaseModelMappable { bool get unWatched => !userData.played && userData.progress <= 0 && userData.unPlayedItemCount == 0; - String? detailedName(BuildContext context) => null; + String? detailedName(BuildContext context) => "$name${overview.yearAired != null ? " (${overview.yearAired})" : ""}"; String? get subText => null; String? subTextShort(BuildContext context) => null; diff --git a/lib/models/items/movie_model.dart b/lib/models/items/movie_model.dart index 84125fb..32b92cb 100644 --- a/lib/models/items/movie_model.dart +++ b/lib/models/items/movie_model.dart @@ -1,7 +1,8 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first -import 'package:fladder/util/humanize_duration.dart'; import 'package:flutter/material.dart'; + +import 'package:dart_mappable/dart_mappable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; @@ -13,8 +14,7 @@ import 'package:fladder/models/items/item_stream_model.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/overview_model.dart'; import 'package:fladder/screens/details_screens/movie_detail_screen.dart'; - -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:fladder/util/humanize_duration.dart'; part 'movie_model.mapper.dart'; @@ -68,10 +68,6 @@ class MovieModel extends ItemStreamModel with MovieModelMappable { @override bool get identifiable => true; - @override - String? label(BuildContext context) => - overview.yearAired == null ? overview.runTime.humanize : "$name (${overview.yearAired})"; - @override ImageData? get bannerImage => images?.backDrop?.firstOrNull ?? images?.primary ?? getPosters?.primary; diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index f16e89b..517dba1 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -13,6 +13,7 @@ part 'client_settings_model.g.dart'; @freezed class ClientSettingsModel with _$ClientSettingsModel { + const ClientSettingsModel._(); factory ClientSettingsModel({ String? syncPath, @Default(Vector2(x: 0, y: 0)) Vector2 position, @@ -33,6 +34,14 @@ class ClientSettingsModel with _$ClientSettingsModel { }) = _ClientSettingsModel; factory ClientSettingsModel.fromJson(Map json) => _$ClientSettingsModelFromJson(json); + + Brightness statusBarBrightness(BuildContext context) { + return switch (themeMode) { + ThemeMode.dark => Brightness.light, + ThemeMode.light => Brightness.dark, + _ => MediaQuery.of(context).platformBrightness == Brightness.dark ? Brightness.light : Brightness.dark, + }; + } } class LocaleConvert implements JsonConverter { diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index 28601be..e7b3a9d 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -294,9 +294,8 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$ClientSettingsModelImpl - with DiagnosticableTreeMixin - implements _ClientSettingsModel { +class _$ClientSettingsModelImpl extends _ClientSettingsModel + with DiagnosticableTreeMixin { _$ClientSettingsModelImpl( {this.syncPath, this.position = const Vector2(x: 0, y: 0), @@ -313,7 +312,8 @@ class _$ClientSettingsModelImpl this.posterSize = 1.0, this.pinchPosterZoom = false, this.mouseDragSupport = false, - this.libraryPageSize}); + this.libraryPageSize}) + : super._(); factory _$ClientSettingsModelImpl.fromJson(Map json) => _$$ClientSettingsModelImplFromJson(json); @@ -464,7 +464,7 @@ class _$ClientSettingsModelImpl } } -abstract class _ClientSettingsModel implements ClientSettingsModel { +abstract class _ClientSettingsModel extends ClientSettingsModel { factory _ClientSettingsModel( {final String? syncPath, final Vector2 position, @@ -482,6 +482,7 @@ abstract class _ClientSettingsModel implements ClientSettingsModel { final bool pinchPosterZoom, final bool mouseDragSupport, final int? libraryPageSize}) = _$ClientSettingsModelImpl; + _ClientSettingsModel._() : super._(); factory _ClientSettingsModel.fromJson(Map json) = _$ClientSettingsModelImpl.fromJson; diff --git a/lib/providers/items/book_details_provider.dart b/lib/providers/items/book_details_provider.dart index 9a50e42..05d5094 100644 --- a/lib/providers/items/book_details_provider.dart +++ b/lib/providers/items/book_details_provider.dart @@ -98,7 +98,7 @@ class BookDetailsProviderNotifier extends StateNotifier { Future fetchDetails(BookModel book) async { state = state.copyWith( - parentModel: () => book, + parentModel: () => state.book ?? book, ); String bookId = state.book?.id ?? book.id; @@ -108,7 +108,7 @@ class BookDetailsProviderNotifier extends StateNotifier { final parentModel = parentResponse.bodyOrThrow; final getViews = await api.usersUserIdViewsGet(); - //Hacky solution more false positives so good enough for now. + //Hacky solution for determining parent views final parentIsView = getViews.body?.items?.firstWhereOrNull((element) => element.name == parentResponse.body?.name) != null; diff --git a/lib/providers/items/movies_details_provider.dart b/lib/providers/items/movies_details_provider.dart index a0ad764..4b6e9c9 100644 --- a/lib/providers/items/movies_details_provider.dart +++ b/lib/providers/items/movies_details_provider.dart @@ -18,14 +18,15 @@ class MovieDetails extends _$MovieDetails { Future fetchDetails(ItemBaseModel item) async { try { - if (item is MovieModel && state == null) { - state = item; + if (item is MovieModel) { + state = state ?? item; } + MovieModel? newState; final response = await api.usersUserIdItemsItemIdGet(itemId: item.id); if (response.body == null) return null; - state = response.bodyOrThrow as MovieModel; + newState = (response.bodyOrThrow as MovieModel).copyWith(related: state?.related); final related = await ref.read(relatedUtilityProvider).relatedContent(item.id); - state = state?.copyWith(related: related.body); + state = newState.copyWith(related: related.body); return null; } catch (e) { _tryToCreateOfflineState(item); diff --git a/lib/providers/items/movies_details_provider.g.dart b/lib/providers/items/movies_details_provider.g.dart index 2a01ad7..2a04780 100644 --- a/lib/providers/items/movies_details_provider.g.dart +++ b/lib/providers/items/movies_details_provider.g.dart @@ -6,7 +6,7 @@ part of 'movies_details_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$movieDetailsHash() => r'e5ab0af7fab9eb7a8ea50a873e8875bb572bd240'; +String _$movieDetailsHash() => r'da07dcdb6e1955119df64f8a6a5634216435982c'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/providers/items/series_details_provider.dart b/lib/providers/items/series_details_provider.dart index 554161d..f39e25f 100644 --- a/lib/providers/items/series_details_provider.dart +++ b/lib/providers/items/series_details_provider.dart @@ -27,16 +27,12 @@ class SeriesDetailViewNotifier extends StateNotifier { Future fetchDetails(ItemBaseModel seriesModel) async { try { if (seriesModel is SeriesModel) { - state = seriesModel; + state = state ?? seriesModel; } SeriesModel? newState; final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id); - if (response.body == null) { - state = seriesModel as SeriesModel; - return null; - } + if (response.body == null) return null; newState = response.bodyOrThrow as SeriesModel; - final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id); newState = newState.copyWith(seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)); diff --git a/lib/screens/details_screens/components/media_stream_information.dart b/lib/screens/details_screens/components/media_stream_information.dart index b659b27..5262a1c 100644 --- a/lib/screens/details_screens/components/media_stream_information.dart +++ b/lib/screens/details_screens/components/media_stream_information.dart @@ -1,10 +1,10 @@ -import 'package:fladder/util/localization_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/screens/details_screens/components/label_title_item.dart'; +import 'package:fladder/util/localization_helper.dart'; class MediaStreamInformation extends ConsumerWidget { final MediaStreamsModel mediaStream; @@ -27,8 +27,8 @@ class MediaStreamInformation extends ConsumerWidget { .map( (e) => PopupMenuItem( value: e, + padding: EdgeInsets.zero, child: Text(e.prettyName), - onTap: () {}, ), ) .toList(), @@ -42,7 +42,8 @@ class MediaStreamInformation extends ConsumerWidget { (e) => PopupMenuItem( value: e, padding: EdgeInsets.zero, - child: textWidget(context, selected: mediaStream.currentAudioStream == e, label: e.displayTitle), + child: textWidget(context, + selected: mediaStream.currentAudioStream?.index == e.index, label: e.displayTitle), onTap: () => onAudioIndexChanged?.call(e.index), ), ) @@ -57,7 +58,8 @@ class MediaStreamInformation extends ConsumerWidget { (e) => PopupMenuItem( value: e, padding: EdgeInsets.zero, - child: textWidget(context, selected: mediaStream.currentSubStream == e, label: e.displayTitle), + child: textWidget(context, + selected: mediaStream.currentSubStream?.index == e.index, label: e.displayTitle), onTap: () => onSubIndexChanged?.call(e.index), ), ) @@ -100,7 +102,7 @@ class _StreamOptionSelect extends StatelessWidget { @override Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme.titleMedium; - const padding = EdgeInsets.all(6.0); + const padding = EdgeInsets.all(6); final itemList = itemBuilder(context); return LabelTitleItem( title: label, @@ -110,6 +112,7 @@ class _StreamOptionSelect extends StatelessWidget { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), enabled: itemList.length > 1, itemBuilder: itemBuilder, + menuPadding: const EdgeInsets.symmetric(vertical: 16), padding: padding, child: Padding( padding: padding, diff --git a/lib/screens/details_screens/season_detail_screen.dart b/lib/screens/details_screens/season_detail_screen.dart index ec2874f..b6629f7 100644 --- a/lib/screens/details_screens/season_detail_screen.dart +++ b/lib/screens/details_screens/season_detail_screen.dart @@ -19,6 +19,7 @@ import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; +import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/util/widget_extensions.dart'; import 'package:fladder/widgets/shared/selectable_icon_button.dart'; @@ -55,12 +56,21 @@ class _SeasonDetailScreenState extends ConsumerState { ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(height: MediaQuery.of(context).size.height * 0.35), + SizedBox(height: MediaQuery.of(context).size.height * 0.25), Wrap( alignment: WrapAlignment.spaceAround, runAlignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: AspectRatio( + aspectRatio: 0.67, + child: Card( + child: FladderImage(image: details.getPosters?.primary), + ), + ), + ), ConstrainedBox( constraints: const BoxConstraints( maxWidth: 600, @@ -89,9 +99,6 @@ class _SeasonDetailScreenState extends ConsumerState { ], ), ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Card(child: FladderImage(image: details.getPosters?.primary))), ], ).padding(padding), Row( @@ -123,36 +130,40 @@ class _SeasonDetailScreenState extends ConsumerState { ), Row( children: [ - Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(200)), - child: SegmentedButton( - style: const ButtonStyle( - elevation: WidgetStatePropertyAll(5), - side: WidgetStatePropertyAll(BorderSide.none), - ), - showSelectedIcon: true, - segments: EpisodeDetailsViewType.values - .map( - (e) => ButtonSegment( - value: e, - icon: Icon(e.icon), - label: SizedBox( - height: 50, - child: Center( - child: Text( - e.name.capitalize(), - ), - )), - ), - ) - .toList(), - selected: viewOptions, - onSelectionChanged: (newOptions) { - setState(() { - viewOptions = newOptions; - }); - }, + SegmentedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.selected)) { + return context.colors.primaryContainer; + } + return context.colors.surfaceContainer; + }), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 8, horizontal: 16)), + elevation: const WidgetStatePropertyAll(5), + side: const WidgetStatePropertyAll(BorderSide.none), ), + showSelectedIcon: true, + segments: EpisodeDetailsViewType.values + .map( + (e) => ButtonSegment( + value: e, + icon: Icon(e.icon), + label: SizedBox( + height: 40, + child: Center( + child: Text( + e.name.capitalize(), + ), + )), + ), + ) + .toList(), + selected: viewOptions, + onSelectionChanged: (newOptions) { + setState(() { + viewOptions = newOptions; + }); + }, ), ], ), diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index d776fe1..2e6ea77 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -139,11 +139,12 @@ class _SettingsScreenState extends ConsumerState { label: Text(context.localized.about), subLabel: const Text("Fladder"), suffix: Opacity( - opacity: 1, - child: FladderIconOutlined( - size: 24, - color: context.colors.onSurfaceVariant, - )), + opacity: 1, + child: FladderIconOutlined( + size: 24, + color: context.colors.onSurfaceVariant, + ), + ), onTap: () => showAboutDialog( context: context, applicationIcon: const FladderIcon(size: 85), diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 03d4ee4..f22f771 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -97,8 +97,8 @@ class _PosterImageState extends ConsumerState { onEnter: (event) => setState(() => hover = true), onExit: (event) => setState(() => hover = false), child: Card( - elevation: 8, - color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.2), + elevation: 6, + color: Theme.of(context).colorScheme.secondaryContainer, shape: RoundedRectangleBorder( side: BorderSide( width: 1.0, @@ -191,6 +191,7 @@ class _PosterImageState extends ConsumerState { child: Card( color: Colors.transparent, elevation: 3, + shadowColor: Colors.transparent, child: LinearProgressIndicator( minHeight: 7.5, backgroundColor: Theme.of(context).colorScheme.onPrimary.withOpacity(0.5), diff --git a/lib/screens/video_player/components/video_player_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index 6881987..fcb0cf0 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -1,5 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/playback/direct_playback_model.dart'; @@ -20,15 +24,14 @@ import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:fladder/widgets/shared/spaced_list_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -Future showVideoPlayerOptions(BuildContext context) { +Future showVideoPlayerOptions(BuildContext context, Function() minimizePlayer) { return showBottomSheetPill( context: context, content: (context, scrollController) { return VideoOptions( controller: scrollController, + minimizePlayer: minimizePlayer, ); }, ); @@ -36,7 +39,8 @@ Future showVideoPlayerOptions(BuildContext context) { class VideoOptions extends ConsumerStatefulWidget { final ScrollController controller; - const VideoOptions({required this.controller, super.key}); + final Function() minimizePlayer; + const VideoOptions({required this.controller, required this.minimizePlayer, super.key}); @override ConsumerState createState() => _VideoOptionsMobileState(); @@ -67,16 +71,10 @@ class _VideoOptionsMobileState extends ConsumerState { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (currentItem?.title.isNotEmpty == true) - Text( - currentItem?.title ?? "", - style: Theme.of(context).textTheme.titleLarge, - ), - if (currentItem?.detailedName(context)?.isNotEmpty == true) - Text( - currentItem?.detailedName(context) ?? "", - style: Theme.of(context).textTheme.titleSmall, - ) + Text( + currentItem?.title ?? "", + style: Theme.of(context).textTheme.titleLarge, + ), ], ), const Spacer(), @@ -173,86 +171,6 @@ class _VideoOptionsMobileState extends ConsumerState { ], ), ), - // ListTile( - // title: const Text("Playback settings"), - // onTap: () => setState(() => page = 1), - // ), - ], - ); - } - - Widget itemOptions() { - final currentItem = ref.watch(playBackModel.select((value) => value?.item)); - return ListView( - shrinkWrap: true, - children: [ - navTitle("${currentItem?.title} \n${currentItem?.detailedName}"), - if (currentItem != null) ...{ - if (currentItem.type == FladderItemType.episode) - ListTile( - onTap: () { - //Pop twice once for sheet once for player - Navigator.of(context).pop(); - Navigator.of(context).pop(); - (this as EpisodeModel).parentBaseModel.navigateTo(context); - }, - title: const Text("Open show"), - ), - ListTile( - onTap: () async { - //Pop twice once for sheet once for player - Navigator.of(context).pop(); - Navigator.of(context).pop(); - await currentItem.navigateTo(context); - }, - title: const Text("Show details"), - ), - if (currentItem.type != FladderItemType.boxset) - ListTile( - onTap: () async { - await addItemToCollection(context, [currentItem]); - if (context.mounted) { - context.refreshData(); - } - }, - title: const Text("Add to collection"), - ), - if (currentItem.type != FladderItemType.playlist) - ListTile( - onTap: () async { - await addItemToPlaylist(context, [currentItem]); - if (context.mounted) { - context.refreshData(); - } - }, - title: const Text("Add to playlist"), - ), - ListTile( - onTap: () { - final favourite = !(currentItem.userData.isFavourite == true); - ref.read(userProvider.notifier).setAsFavorite(favourite, currentItem.id); - final newUserData = currentItem.userData; - final playbackModel = switch (ref.read(playBackModel)) { - DirectPlaybackModel value => value.copyWith(item: currentItem.copyWith(userData: newUserData)), - TranscodePlaybackModel value => value.copyWith(item: currentItem.copyWith(userData: newUserData)), - OfflinePlaybackModel value => value.copyWith(item: currentItem.copyWith(userData: newUserData)), - _ => null - }; - if (playbackModel != null) { - ref.read(playBackModel.notifier).update((state) => playbackModel); - } - Navigator.of(context).pop(); - }, - title: Text(currentItem.userData.isFavourite == true ? "Remove from favorites" : "Add to favourites"), - ), - ListTile( - onTap: () { - Navigator.of(context).pop(); - showInfoScreen(context, currentItem); - }, - title: const Text('Media info'), - ), - } ], ); } @@ -264,7 +182,7 @@ class _VideoOptionsMobileState extends ConsumerState { shrinkWrap: true, controller: widget.controller, children: [ - navTitle("Playback Settings"), + navTitle("Playback Settings", null), if (playbackState?.queue.isNotEmpty == true) ListTile( leading: const Icon(Icons.video_collection_rounded), @@ -294,7 +212,7 @@ class _VideoOptionsMobileState extends ConsumerState { duration: const Duration(milliseconds: 250), child: switch (page) { 1 => playbackSettings(), - 2 => itemOptions(), + 2 => itemInfo(currentItem, context), _ => mainPage(), }, ), @@ -304,7 +222,79 @@ class _VideoOptionsMobileState extends ConsumerState { ); } - Widget navTitle(String title) { + ListView itemInfo(ItemBaseModel? currentItem, BuildContext context) { + return ListView( + shrinkWrap: true, + children: [ + navTitle(currentItem?.title, currentItem?.subTextShort(context)), + if (currentItem != null) ...{ + if (currentItem.type == FladderItemType.episode) + ListTile( + onTap: () { + Navigator.of(context).pop(); + widget.minimizePlayer(); + (this as EpisodeModel).parentBaseModel.navigateTo(context); + }, + title: const Text("Open show"), + ), + ListTile( + onTap: () async { + Navigator.of(context).pop(); + widget.minimizePlayer(); + await currentItem.navigateTo(context); + }, + title: const Text("Show details"), + ), + if (currentItem.type != FladderItemType.boxset) + ListTile( + onTap: () async { + await addItemToCollection(context, [currentItem]); + if (context.mounted) { + context.refreshData(); + } + }, + title: const Text("Add to collection"), + ), + if (currentItem.type != FladderItemType.playlist) + ListTile( + onTap: () async { + await addItemToPlaylist(context, [currentItem]); + if (context.mounted) { + context.refreshData(); + } + }, + title: const Text("Add to playlist"), + ), + ListTile( + onTap: () async { + final response = await ref + .read(userProvider.notifier) + .setAsFavorite(!(currentItem.userData.isFavourite == true), currentItem.id); + final newItem = currentItem.copyWith(userData: response?.body); + final playbackModel = switch (ref.read(playBackModel)) { + DirectPlaybackModel value => value.copyWith(item: newItem), + TranscodePlaybackModel value => value.copyWith(item: newItem), + OfflinePlaybackModel value => value.copyWith(item: newItem), + _ => null + }; + ref.read(playBackModel.notifier).update((state) => playbackModel); + Navigator.of(context).pop(); + }, + title: Text(currentItem.userData.isFavourite == true ? "Remove from favorites" : "Add to favourites"), + ), + ListTile( + onTap: () { + Navigator.of(context).pop(); + showInfoScreen(context, currentItem); + }, + title: const Text('Media info'), + ), + } + ], + ); + } + + Widget navTitle(String? title, String? subText) { return Column( children: [ Row( @@ -314,10 +304,20 @@ class _VideoOptionsMobileState extends ConsumerState { onPressed: () => setState(() => page = 0), ), const SizedBox(width: 16), - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ) + Column( + children: [ + if (title != null) + Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + if (subText != null) + Text( + subText, + style: Theme.of(context).textTheme.titleMedium, + ) + ], + ), ], ), const SizedBox(height: 12), diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index d123576..e8d7017 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -1,12 +1,22 @@ import 'dart:async'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:screen_brightness/screen_brightness.dart'; +import 'package:window_manager/window_manager.dart'; + import 'package:fladder/models/items/intro_skip_model.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/default_titlebar.dart'; @@ -19,15 +29,8 @@ import 'package:fladder/screens/video_player/components/video_volume_slider.dart import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/list_padding.dart'; +import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:screen_brightness/screen_brightness.dart'; -import 'package:window_manager/window_manager.dart'; class DesktopControls extends ConsumerStatefulWidget { const DesktopControls({super.key}); @@ -45,24 +48,6 @@ class _DesktopControlsState extends ConsumerState { late final double topPadding = MediaQuery.of(context).viewPadding.top; late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom; - Future clear() async { - toggleOverlay(value: true); - if (!AdaptiveLayout.of(context).isDesktop) { - ScreenBrightness().resetScreenBrightness(); - } else { - disableFullscreen(); - } - timer.cancel(); - } - - void resetTimer() => timer.reset(); - - Future closePlayer() async { - clear(); - ref.read(videoPlayerProvider).stop(); - Navigator.of(context).pop(); - } - @override Widget build(BuildContext context) { final mediaPlayback = ref.watch(mediaPlaybackProvider); @@ -229,36 +214,38 @@ class _DesktopControlsState extends ConsumerState { child: Container( alignment: Alignment.topCenter, height: 80, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () { - clear(); - ref - .read(mediaPlaybackProvider.notifier) - .update((state) => state.copyWith(state: VideoPlayerState.minimized)); - Navigator.of(context).pop(); - }, - icon: const Icon( - IconsaxOutline.arrow_down_1, - size: 24, + child: Column( + children: [ + if (AdaptiveLayout.of(context).isDesktop) + const Flexible( + child: Align( + alignment: Alignment.topRight, + child: DefaultTitleBar(), ), ), - const SizedBox(width: 16), - if (!AdaptiveLayout.of(context).isDesktop) - Flexible( - child: Text( - currentItem?.title ?? "", - style: Theme.of(context).textTheme.titleLarge, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () => minimizePlayer(context), + icon: const Icon( + IconsaxOutline.arrow_down_1, + size: 24, + ), ), - ) - else - const Flexible(child: Align(alignment: Alignment.topRight, child: DefaultTitleBar())) - ], - ), + const SizedBox(width: 16), + Flexible( + child: Text( + currentItem?.title ?? "", + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ], + ), + ), + ], ), ), ), @@ -296,7 +283,8 @@ class _DesktopControlsState extends ConsumerState { child: Row( children: [ IconButton( - onPressed: () => showVideoPlayerOptions(context), icon: const Icon(IconsaxOutline.more)), + onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), + icon: const Icon(IconsaxOutline.more)), if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[ IconButton( onPressed: () => showSubSelection(context), @@ -416,7 +404,7 @@ class _DesktopControlsState extends ConsumerState { final List details = [ if (AdaptiveLayout.of(context).isDesktop) item?.label(context), mediaPlayback.duration.inMinutes > 1 - ? 'ends at ${DateFormat('HH:mm').format(DateTime.now().add(mediaPlayback.duration - mediaPlayback.position))}' + ? context.localized.endsAt(DateTime.now().add(mediaPlayback.duration - mediaPlayback.position)) : null ]; return Column( @@ -606,10 +594,40 @@ class _DesktopControlsState extends ConsumerState { SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, systemNavigationBarDividerColor: Colors.transparent, )); } + void minimizePlayer(BuildContext context) { + clearOverlaySettings(); + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.minimized)); + Navigator.of(context).pop(); + } + + Future clearOverlaySettings() async { + toggleOverlay(value: true); + if (!AdaptiveLayout.of(context).isDesktop) { + ScreenBrightness().resetScreenBrightness(); + } else { + disableFullscreen(); + } + + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarIconBrightness: ref.read(clientSettingsProvider.select((value) => value.statusBarBrightness(context))), + )); + + timer.cancel(); + } + + void resetTimer() => timer.reset(); + + Future closePlayer() async { + clearOverlaySettings(); + ref.read(videoPlayerProvider).stop(); + Navigator.of(context).pop(); + } + Future disableFullscreen() async { resetTimer(); final isFullScreen = await windowManager.isFullScreen(); diff --git a/lib/util/application_info.dart b/lib/util/application_info.dart index ed1d985..89b2a18 100644 --- a/lib/util/application_info.dart +++ b/lib/util/application_info.dart @@ -1,37 +1,30 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'application_info.freezed.dart'; final applicationInfoProvider = StateProvider((ref) { return ApplicationInfo( name: "", version: "", + buildNumber: "", os: "", ); }); -class ApplicationInfo { - final String name; - final String version; - final String os; - ApplicationInfo({ - required this.name, - required this.version, - required this.os, - }); +@Freezed(toJson: false, fromJson: false) +class ApplicationInfo with _$ApplicationInfo { + const ApplicationInfo._(); - ApplicationInfo copyWith({ - String? name, - String? version, - String? os, - }) { - return ApplicationInfo( - name: name ?? this.name, - version: version ?? this.version, - os: os ?? this.os, - ); - } + factory ApplicationInfo({ + required String name, + required String version, + required String buildNumber, + required String os, + }) = _ApplicationInfo; - String get versionAndPlatform => "$version ($os)"; + String get versionAndPlatform => "$version ($os)\n#$buildNumber"; @override String toString() => 'ApplicationInfo(name: $name, version: $version, os: $os)'; diff --git a/lib/util/application_info.freezed.dart b/lib/util/application_info.freezed.dart new file mode 100644 index 0000000..6813631 --- /dev/null +++ b/lib/util/application_info.freezed.dart @@ -0,0 +1,187 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'application_info.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ApplicationInfo { + String get name => throw _privateConstructorUsedError; + String get version => throw _privateConstructorUsedError; + String get buildNumber => throw _privateConstructorUsedError; + String get os => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ApplicationInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ApplicationInfoCopyWith<$Res> { + factory $ApplicationInfoCopyWith( + ApplicationInfo value, $Res Function(ApplicationInfo) then) = + _$ApplicationInfoCopyWithImpl<$Res, ApplicationInfo>; + @useResult + $Res call({String name, String version, String buildNumber, String os}); +} + +/// @nodoc +class _$ApplicationInfoCopyWithImpl<$Res, $Val extends ApplicationInfo> + implements $ApplicationInfoCopyWith<$Res> { + _$ApplicationInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? version = null, + Object? buildNumber = null, + Object? os = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as String, + buildNumber: null == buildNumber + ? _value.buildNumber + : buildNumber // ignore: cast_nullable_to_non_nullable + as String, + os: null == os + ? _value.os + : os // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ApplicationInfoImplCopyWith<$Res> + implements $ApplicationInfoCopyWith<$Res> { + factory _$$ApplicationInfoImplCopyWith(_$ApplicationInfoImpl value, + $Res Function(_$ApplicationInfoImpl) then) = + __$$ApplicationInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String version, String buildNumber, String os}); +} + +/// @nodoc +class __$$ApplicationInfoImplCopyWithImpl<$Res> + extends _$ApplicationInfoCopyWithImpl<$Res, _$ApplicationInfoImpl> + implements _$$ApplicationInfoImplCopyWith<$Res> { + __$$ApplicationInfoImplCopyWithImpl( + _$ApplicationInfoImpl _value, $Res Function(_$ApplicationInfoImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? version = null, + Object? buildNumber = null, + Object? os = null, + }) { + return _then(_$ApplicationInfoImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as String, + buildNumber: null == buildNumber + ? _value.buildNumber + : buildNumber // ignore: cast_nullable_to_non_nullable + as String, + os: null == os + ? _value.os + : os // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ApplicationInfoImpl extends _ApplicationInfo { + _$ApplicationInfoImpl( + {required this.name, + required this.version, + required this.buildNumber, + required this.os}) + : super._(); + + @override + final String name; + @override + final String version; + @override + final String buildNumber; + @override + final String os; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ApplicationInfoImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.version, version) || other.version == version) && + (identical(other.buildNumber, buildNumber) || + other.buildNumber == buildNumber) && + (identical(other.os, os) || other.os == os)); + } + + @override + int get hashCode => Object.hash(runtimeType, name, version, buildNumber, os); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ApplicationInfoImplCopyWith<_$ApplicationInfoImpl> get copyWith => + __$$ApplicationInfoImplCopyWithImpl<_$ApplicationInfoImpl>( + this, _$identity); +} + +abstract class _ApplicationInfo extends ApplicationInfo { + factory _ApplicationInfo( + {required final String name, + required final String version, + required final String buildNumber, + required final String os}) = _$ApplicationInfoImpl; + _ApplicationInfo._() : super._(); + + @override + String get name; + @override + String get version; + @override + String get buildNumber; + @override + String get os; + @override + @JsonKey(ignore: true) + _$$ApplicationInfoImplCopyWith<_$ApplicationInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/widgets/navigation_scaffold/components/navigation_body.dart b/lib/widgets/navigation_scaffold/components/navigation_body.dart index 479cbb7..4bf3d9e 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_body.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_body.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; @@ -48,6 +50,17 @@ class _NavigationBodyState extends ConsumerState { @override Widget build(BuildContext context) { final views = ref.watch(viewsProvider.select((value) => value.views)); + ref.listen( + clientSettingsProvider, + (previous, next) { + if (previous != next) { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarIconBrightness: next.statusBarBrightness(context), + )); + } + }, + ); + return switch (AdaptiveLayout.layoutOf(context)) { LayoutState.phone => MediaQuery.removePadding( context: widget.parentContext, @@ -126,7 +139,7 @@ class _NavigationBodyState extends ConsumerState { }, icon: const Icon(IconsaxBold.menu), ), - if (AdaptiveLayout.of(context).isDesktop) ...[ + if (AdaptiveLayout.of(context).size == ScreenLayout.dual) ...[ const SizedBox(height: 8), AnimatedFadeSize( child: AnimatedSwitcher( diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index 1f1c0a7..678ce36 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -92,7 +92,7 @@ class NestedNavigationDrawer extends ConsumerWidget { ), ...views.map((library) => DrawerListButton( label: library.name, - selected: checkLibrary(context, library.id), + selected: context.router.currentUrl.contains(library.id), actions: [ ItemActionButton( label: Text(context.localized.scanLibrary), @@ -151,13 +151,4 @@ class NestedNavigationDrawer extends ConsumerWidget { ], ); } - - bool checkLibrary(BuildContext context, String id) { - try { - return context.router.current.name == LibrarySearchRoute().routeName && - (context.routeData.queryParams.isNotEmpty && context.routeData.queryParams.getString('parentId') == id); - } catch (e) { - return false; - } - } } diff --git a/lib/widgets/navigation_scaffold/navigation_scaffold.dart b/lib/widgets/navigation_scaffold/navigation_scaffold.dart index bb0ef94..c0c57c3 100644 --- a/lib/widgets/navigation_scaffold/navigation_scaffold.dart +++ b/lib/widgets/navigation_scaffold/navigation_scaffold.dart @@ -65,7 +65,7 @@ class _NavigationScaffoldState extends ConsumerState { extendBody: true, floatingActionButtonLocation: playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, - floatingActionButton: AdaptiveLayout.of(context).layout == LayoutState.phone + floatingActionButton: AdaptiveLayout.of(context).size == ScreenLayout.single ? switch (playerState) { VideoPlayerState.minimized => const Padding( padding: EdgeInsets.symmetric(horizontal: 8), diff --git a/lib/wrappers/media_control_base.dart b/lib/wrappers/media_control_base.dart index 7bf82a3..8345aa7 100644 --- a/lib/wrappers/media_control_base.dart +++ b/lib/wrappers/media_control_base.dart @@ -6,7 +6,6 @@ AudioServiceConfig get audioServiceConfig => const AudioServiceConfig( androidNotificationChannelName: 'Video playback', androidNotificationOngoing: true, androidStopForegroundOnPause: true, - // androidNotificationIcon: "mipmap/ic_notification_icon", rewindInterval: Duration(seconds: 10), fastForwardInterval: Duration(seconds: 15), androidNotificationChannelDescription: "Playback", diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 9688720..2695f92 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 0996182..9fee12d 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 6c91d58..c204dd4 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 5a0e4b5..0dfd254 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index e1f03e7..b62e8e9 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index dabc6d3..398f7e5 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index b3cc25b..ad35216 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index e043262..c7ef09c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -854,10 +854,10 @@ packages: dependency: "direct main" description: name: icons_launcher - sha256: "9b514ffed6ed69b232fd2bf34c44878c8526be71fc74129a658f35c04c9d4a9d" + sha256: a7c83fbc837dc6f81944ef35c3756f533bb2aba32fcca5cbcdb2dbcd877d5ae9 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "3.0.0" image: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f47faee..b71edfa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: # Icons cupertino_icons: ^1.0.2 ficonsax: ^0.0.3 - icons_launcher: ^2.1.7 + icons_launcher: ^3.0.0 # Network and HTTP chopper: ^7.0.4 diff --git a/web/favicon.png b/web/favicon.png index 2c555c8..e1f03e7 100644 Binary files a/web/favicon.png and b/web/favicon.png differ