diff --git a/.github/workflows/build-deploy.yaml b/.github/workflows/build-deploy.yaml index 73bae47..cd14121 100644 --- a/.github/workflows/build-deploy.yaml +++ b/.github/workflows/build-deploy.yaml @@ -124,7 +124,7 @@ jobs: dart run build_runner build --delete-conflicting-outputs - name: Build iOS - run: flutter build ios --release --no-codesign + run: make build_ipa - name: Create and Pack IPA run: | @@ -178,7 +178,7 @@ jobs: dart run build_runner build --delete-conflicting-outputs - name: Build Linux - run: flutter build linux --release + run: make build_linux - name: Upload Linux artifact uses: actions/upload-artifact@v4 @@ -222,7 +222,7 @@ jobs: dart run build_runner build --delete-conflicting-outputs - name: Build MacOS - run: flutter build macos --release + run: make build_mac - name: Isolate the Build run: mkdir build/macos/Build/Products/Release/AppRelease diff --git a/Makefile b/Makefile index 9a8a863..213681f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ +.PHONY: build schema build_web build_mac build_android build_windows build_ipa build_linux + +BUILD_ID := $(or $(GITHUB_RUN_ID),dev) -.PHONY: build schema build_web build_mac build: dart run build_runner build --delete-conflicting-outputs @@ -7,13 +9,19 @@ schema: dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v1.json build_web: - flutter build web --target lib/main_web.dart --release --pwa-strategy none --wasm + flutter build web --target lib/main_web.dart --release --pwa-strategy none --wasm --dart-define=BUILD_ID=$(BUILD_ID) build_mac: - flutter build macos --target lib/main.dart --release + flutter build macos --target lib/main.dart --release --dart-define=BUILD_ID=$(BUILD_ID) build_android: - flutter build apk --release + flutter build apk --release --dart-define=BUILD_ID=$(BUILD_ID) build_windows: - flutter build windows --release + flutter build windows --release --dart-define=BUILD_ID=$(BUILD_ID) + +build_ipa: + flutter build ios --release --no-codesign --dart-define=BUILD_ID=$(BUILD_ID) + +build_linux: + flutter build linux --release --dart-define=BUILD_ID=$(BUILD_ID) diff --git a/lib/features/connections/types/stremio/stremio_base.types.dart b/lib/features/connections/types/stremio/stremio_base.types.dart index d6000bf..8d81aae 100644 --- a/lib/features/connections/types/stremio/stremio_base.types.dart +++ b/lib/features/connections/types/stremio/stremio_base.types.dart @@ -313,6 +313,8 @@ class Meta extends LibraryItem { @JsonKey(includeFromJson: false, includeToJson: false) final int? traktId; + final dynamic externalIds; + String get imdbRating { return (imdbRating_ ?? "").toString(); } @@ -329,6 +331,7 @@ class Meta extends LibraryItem { this.cast, this.traktId, this.country, + this.externalIds, this.description, this.genre, this.imdbRating_, diff --git a/lib/features/connections/widget/base/render_stream_list.dart b/lib/features/connections/widget/base/render_stream_list.dart index d8a8f6c..eb14084 100644 --- a/lib/features/connections/widget/base/render_stream_list.dart +++ b/lib/features/connections/widget/base/render_stream_list.dart @@ -38,7 +38,6 @@ class RenderStreamList extends StatefulWidget { } class _RenderStreamListState extends State { - Stream>? _stream; final Map _downloadProgress = {}; final Map _downloadError = {}; diff --git a/lib/features/connections/widget/stremio/stremio_card.dart b/lib/features/connections/widget/stremio/stremio_card.dart index 56b66c4..743cf6a 100644 --- a/lib/features/connections/widget/stremio/stremio_card.dart +++ b/lib/features/connections/widget/stremio/stremio_card.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import 'package:madari_client/features/connection/types/stremio.dart'; import 'package:madari_client/features/connections/service/base_connection_service.dart'; @@ -50,11 +51,27 @@ class StremioCard extends StatelessWidget { ); } + Video? get currentVideo { + return (item as Meta).videos?.firstWhere((episode) { + return (item as Meta).nextEpisode == episode.episode && + (item as Meta).nextSeason == episode.season; + }); + } + + bool get isInFuture { + final video = currentVideo; + return video != null && + video.firstAired != null && + video.firstAired!.isAfter(DateTime.now()); + } + _buildWideCard(BuildContext context, Meta meta) { if (meta.background == null) { return Container(); } + final video = currentVideo; + return Container( decoration: BoxDecoration( image: DecorationImage( @@ -67,6 +84,21 @@ class StremioCard extends StatelessWidget { ), child: Stack( children: [ + if (isInFuture) + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black, + Colors.transparent, + ], + begin: Alignment.topLeft, + end: Alignment.bottomCenter, + ), + ), + ), + ), Positioned.fill( child: Container( decoration: const BoxDecoration( @@ -90,6 +122,10 @@ class StremioCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Text("S${meta.nextSeason} E${meta.nextEpisode}"), + Text( + "${meta.name}", + style: Theme.of(context).textTheme.bodyLarge, + ), Text( "${meta.nextEpisodeTitle}".trim(), style: Theme.of(context).textTheme.headlineSmall, @@ -98,6 +134,38 @@ class StremioCard extends StatelessWidget { ), ), ), + if (isInFuture) + Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + getRelativeDate(video!.firstAired!), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + if (isInFuture) + const Positioned( + bottom: 0, + right: 0, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric( + horizontal: 4, + vertical: 10, + ), + child: Icon( + Icons.calendar_month, + ), + ), + ], + ), + ), + ), const Positioned( child: Center( child: IconButton.filled( @@ -176,13 +244,21 @@ class StremioCard extends StatelessWidget { return Hero( tag: "$prefix${meta.type}${item.id}", - child: AspectRatio( - aspectRatio: 2 / 3, - child: (backgroundImage == null) - ? Text("${meta.name}") - : Stack( + child: (backgroundImage == null) + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( + Text(meta.name ?? "No name"), + if (meta.description != null) Text(meta.description!), + ], + ), + ) + : Stack( + children: [ + Positioned.fill( + child: Container( decoration: BoxDecoration( image: DecorationImage( image: CachedNetworkImageProvider( @@ -230,72 +306,92 @@ class StremioCard extends StatelessWidget { ) : const SizedBox.shrink(), ), - if (meta.progress != null) - const Positioned.fill( - child: IconButton( - onPressed: null, - icon: Icon( - Icons.play_arrow, - size: 24, + ), + if (meta.progress != null) + const Positioned.fill( + child: IconButton( + onPressed: null, + icon: Icon( + Icons.play_arrow, + size: 24, + ), + ), + ), + if (meta.progress != null) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: LinearProgressIndicator( + value: meta.progress, + ), + ), + if (meta.nextEpisode != null && meta.nextSeason != null) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.grey, + Colors.transparent, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + meta.name ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(fontWeight: FontWeight.w600), + ), + Text( + "S${meta.nextSeason} E${meta.nextEpisode}", + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + ], ), ), ), - if (meta.progress != null) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: LinearProgressIndicator( - value: meta.progress, - ), - ), - if (meta.nextEpisode != null && meta.nextSeason != null) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.grey, - Colors.transparent, - ], - begin: Alignment.bottomLeft, - end: Alignment.topRight, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 4, horizontal: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - meta.name ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(fontWeight: FontWeight.w600), - ), - Text( - "S${meta.nextSeason} E${meta.nextEpisode}", - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - ], - ), - ), - ), - ) - ], - ), - ), + ) + ], + ), ); } } + +String getRelativeDate(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final tomorrow = DateTime(now.year, now.month, now.day + 1); + + final difference = date.difference(today).inDays; + + if (date.isAtSameMomentAs(today)) { + return "It's today!"; + } else if (date.isAtSameMomentAs(tomorrow)) { + return "Coming up tomorrow!"; + } else if (difference > 1 && difference < 7) { + return "Coming up in $difference days"; + } else if (difference >= 7 && difference < 14) { + return "Coming up next ${DateFormat('EEEE').format(date)}"; + } else { + return "On ${DateFormat('MM/dd/yyyy').format(date)}"; + } +} diff --git a/lib/features/doc_viewer/container/video_viewer/trakt_integration.dart b/lib/features/doc_viewer/container/video_viewer/trakt_integration.dart new file mode 100644 index 0000000..d6785bd --- /dev/null +++ b/lib/features/doc_viewer/container/video_viewer/trakt_integration.dart @@ -0,0 +1,13 @@ +import 'package:media_kit/media_kit.dart'; + +class TraktIntegrationVideo { + Player player; + + TraktIntegrationVideo({ + required this.player, + }); + + initState() {} + + dispose() {} +} diff --git a/lib/features/trakt/containers/up_next.container.dart b/lib/features/trakt/containers/up_next.container.dart index 2fc43f3..cf519e0 100644 --- a/lib/features/trakt/containers/up_next.container.dart +++ b/lib/features/trakt/containers/up_next.container.dart @@ -28,7 +28,7 @@ class _TraktContainerState extends State { key: widget.loadId, config: QueryConfig( cacheDuration: const Duration(days: 30), - refetchDuration: const Duration(minutes: 1), + refetchDuration: const Duration(minutes: 10), storageDuration: const Duration(days: 30), ), queryFn: () { diff --git a/lib/features/trakt/service/trakt.service.dart b/lib/features/trakt/service/trakt.service.dart index 0414a9c..263b98a 100644 --- a/lib/features/trakt/service/trakt.service.dart +++ b/lib/features/trakt/service/trakt.service.dart @@ -75,7 +75,7 @@ class TraktService { 'Authorization': 'Bearer $_token', }; - Future> getUpNextSeries({bool noUpNext = false}) async { + Future> getUpNextSeries() async { await initStremioService(); if (!isEnabled()) { @@ -98,17 +98,6 @@ class TraktService { final showId = show['show']['ids']['trakt']; final imdb = show['show']['ids']['imdb']; - if (noUpNext == true) { - final meta = await stremioService!.getItemById( - Meta( - type: "series", - id: imdb, - ), - ); - - return (meta as Meta).copyWith(); - } - try { final progressResponse = await http.get( Uri.parse('$_baseUrl/shows/$showId/progress/watched'), @@ -123,11 +112,13 @@ class TraktService { final nextEpisode = progress['next_episode']; - print(nextEpisode); - if (nextEpisode != null && imdb != null) { final item = await stremioService!.getItemById( - Meta(type: "series", id: imdb), + Meta( + type: "series", + id: imdb, + externalIds: show['show']['ids'], + ), ); item as Meta; @@ -203,7 +194,7 @@ class TraktService { }).toList(), ); - return result.map((res) { + return result.sublist(0, 20).map((res) { Meta returnValue = res as Meta; if (progress.containsKey(res.id)) { @@ -325,15 +316,23 @@ class TraktService { final recommendedShows = json.decode(recommendationsResponse.body) as List; - final result = await stremioService!.getBulkItem( - recommendedShows.map((show) { - final imdb = show['ids']['imdb']; - return Meta( - type: "series", - id: imdb, - ); - }).toList(), - ); + final result = (await stremioService!.getBulkItem( + recommendedShows + .map((show) { + final imdb = show['ids']?['imdb']; + + if (imdb == null) { + return null; + } + + return Meta( + type: "series", + id: imdb, + ); + }) + .whereType() + .toList(), + )); return result; } catch (e, stack) { diff --git a/lib/pages/more_tab.page.dart b/lib/pages/more_tab.page.dart index e250049..8eb15e6 100644 --- a/lib/pages/more_tab.page.dart +++ b/lib/pages/more_tab.page.dart @@ -88,6 +88,11 @@ class MoreContainer extends StatelessWidget { }, hideTrailing: true, ), + Text( + "Version ${const String.fromEnvironment('BUILD_ID', defaultValue: "Dev")}", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), ], ), ), diff --git a/lib/utils/external_player.dart b/lib/utils/external_player.dart index 44091c2..f372943 100644 --- a/lib/utils/external_player.dart +++ b/lib/utils/external_player.dart @@ -34,6 +34,10 @@ final Map> externalPlayers = { id: "com.brouken.player", name: "JustPlayer", ), + ExternalMediaPlayer( + id: "xyz.skybox.player", + name: "Skybox", + ), ], "ios": [ ExternalMediaPlayer( diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index e19c271..6a2181d 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -92,8 +92,8 @@ BEGIN VALUE "CompanyName", "media.madari" "\0" VALUE "FileDescription", "madari_client" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "madari_client" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 media.madari. All rights reserved." "\0" + VALUE "InternalName", "Madari" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 media.madari. All rights reserved." "\0" VALUE "OriginalFilename", "madari_client.exe" "\0" VALUE "ProductName", "madari_client" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 41d0ba1..c1bbad9 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"madari_client", origin, size)) { + if (!window.Create(L"Madari", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);