added cargo files
This commit is contained in:
225
PinePods-0.8.2/mobile/lib/ui/utils/local_download_utils.dart
Normal file
225
PinePods-0.8.2/mobile/lib/ui/utils/local_download_utils.dart
Normal file
@@ -0,0 +1,225 @@
|
||||
// lib/ui/utils/local_download_utils.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinepods_mobile/entities/pinepods_episode.dart';
|
||||
import 'package:pinepods_mobile/entities/episode.dart';
|
||||
import 'package:pinepods_mobile/entities/downloadable.dart';
|
||||
import 'package:pinepods_mobile/services/logging/app_logger.dart';
|
||||
import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// Utility class for managing local downloads of PinePods episodes
|
||||
class LocalDownloadUtils {
|
||||
static final Map<String, bool> _localDownloadStatusCache = {};
|
||||
|
||||
/// Generate consistent GUID for PinePods episodes for local downloads
|
||||
static String generateEpisodeGuid(PinepodsEpisode episode) {
|
||||
return 'pinepods_${episode.episodeId}';
|
||||
}
|
||||
|
||||
/// Clear the local download status cache (call on refresh)
|
||||
static void clearCache() {
|
||||
_localDownloadStatusCache.clear();
|
||||
}
|
||||
|
||||
/// Check if episode is downloaded locally with caching
|
||||
static Future<bool> isEpisodeDownloadedLocally(
|
||||
BuildContext context,
|
||||
PinepodsEpisode episode
|
||||
) async {
|
||||
final guid = generateEpisodeGuid(episode);
|
||||
final logger = AppLogger();
|
||||
logger.debug('LocalDownload', 'Checking download status for episode: ${episode.episodeTitle}, GUID: $guid');
|
||||
|
||||
// Check cache first
|
||||
if (_localDownloadStatusCache.containsKey(guid)) {
|
||||
logger.debug('LocalDownload', 'Found cached status for $guid: ${_localDownloadStatusCache[guid]}');
|
||||
return _localDownloadStatusCache[guid]!;
|
||||
}
|
||||
|
||||
try {
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context, listen: false);
|
||||
|
||||
// Get all episodes and find matches with both new and old GUID formats
|
||||
final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes();
|
||||
final matchingEpisodes = allEpisodes.where((ep) =>
|
||||
ep.guid == guid || ep.guid.startsWith('${guid}_')
|
||||
).toList();
|
||||
|
||||
logger.debug('LocalDownload', 'Repository lookup for $guid: found ${matchingEpisodes.length} matching episodes');
|
||||
|
||||
// Found matching episodes
|
||||
|
||||
// Consider downloaded if ANY matching episode is downloaded
|
||||
final isDownloaded = matchingEpisodes.any((ep) =>
|
||||
ep.downloaded || ep.downloadState == DownloadState.downloaded
|
||||
);
|
||||
|
||||
logger.debug('LocalDownload', 'Final download status for $guid: $isDownloaded');
|
||||
|
||||
// Cache the result
|
||||
_localDownloadStatusCache[guid] = isDownloaded;
|
||||
return isDownloaded;
|
||||
} catch (e) {
|
||||
final logger = AppLogger();
|
||||
logger.error('LocalDownload', 'Error checking local download status for episode: ${episode.episodeTitle}', e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update local download status cache
|
||||
static void updateLocalDownloadStatus(PinepodsEpisode episode, bool isDownloaded) {
|
||||
final guid = generateEpisodeGuid(episode);
|
||||
_localDownloadStatusCache[guid] = isDownloaded;
|
||||
}
|
||||
|
||||
/// Proactively load local download status for a list of episodes
|
||||
static Future<void> loadLocalDownloadStatuses(
|
||||
BuildContext context,
|
||||
List<PinepodsEpisode> episodes
|
||||
) async {
|
||||
final logger = AppLogger();
|
||||
logger.debug('LocalDownload', 'Loading local download statuses for ${episodes.length} episodes');
|
||||
|
||||
try {
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context, listen: false);
|
||||
|
||||
// Get all downloaded episodes from repository
|
||||
final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes();
|
||||
logger.debug('LocalDownload', 'Found ${allEpisodes.length} total episodes in repository');
|
||||
|
||||
// Filter to PinePods episodes only and log them
|
||||
final pinepodsEpisodes = allEpisodes.where((ep) => ep.guid.startsWith('pinepods_')).toList();
|
||||
logger.debug('LocalDownload', 'Found ${pinepodsEpisodes.length} PinePods episodes in repository');
|
||||
|
||||
// Found pinepods episodes in repository
|
||||
|
||||
// Now check each episode against the repository
|
||||
for (final episode in episodes) {
|
||||
final guid = generateEpisodeGuid(episode);
|
||||
|
||||
// Look for episodes with either new format (pinepods_123) or old format (pinepods_123_timestamp)
|
||||
final matchingEpisodes = allEpisodes.where((ep) =>
|
||||
ep.guid == guid || ep.guid.startsWith('${guid}_')
|
||||
).toList();
|
||||
|
||||
// Checking for matching episodes
|
||||
|
||||
// Consider downloaded if ANY matching episode is downloaded
|
||||
final isDownloaded = matchingEpisodes.any((ep) =>
|
||||
ep.downloaded || ep.downloadState == DownloadState.downloaded
|
||||
);
|
||||
|
||||
_localDownloadStatusCache[guid] = isDownloaded;
|
||||
// Episode status checked
|
||||
}
|
||||
|
||||
// Download statuses cached
|
||||
|
||||
} catch (e) {
|
||||
logger.error('LocalDownload', 'Error loading local download statuses', e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Download episode locally
|
||||
static Future<bool> localDownloadEpisode(
|
||||
BuildContext context,
|
||||
PinepodsEpisode episode
|
||||
) async {
|
||||
final logger = AppLogger();
|
||||
|
||||
try {
|
||||
// Convert PinepodsEpisode to Episode for local download
|
||||
final localEpisode = Episode(
|
||||
guid: generateEpisodeGuid(episode),
|
||||
pguid: 'pinepods_${episode.podcastName.replaceAll(' ', '_').toLowerCase()}',
|
||||
podcast: episode.podcastName,
|
||||
title: episode.episodeTitle,
|
||||
description: episode.episodeDescription,
|
||||
imageUrl: episode.episodeArtwork,
|
||||
contentUrl: episode.episodeUrl,
|
||||
duration: episode.episodeDuration,
|
||||
publicationDate: DateTime.tryParse(episode.episodePubDate),
|
||||
author: episode.podcastName,
|
||||
season: 0,
|
||||
episode: 0,
|
||||
position: episode.listenDuration ?? 0,
|
||||
played: episode.completed,
|
||||
chapters: [],
|
||||
transcriptUrls: [],
|
||||
);
|
||||
|
||||
logger.debug('LocalDownload', 'Created local episode with GUID: ${localEpisode.guid}');
|
||||
logger.debug('LocalDownload', 'Episode title: ${localEpisode.title}');
|
||||
logger.debug('LocalDownload', 'Episode URL: ${localEpisode.contentUrl}');
|
||||
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context, listen: false);
|
||||
|
||||
// First save the episode to the repository so it can be tracked
|
||||
await podcastBloc.podcastService.saveEpisode(localEpisode);
|
||||
logger.debug('LocalDownload', 'Episode saved to repository');
|
||||
|
||||
// Use the download service from podcast bloc
|
||||
final success = await podcastBloc.downloadService.downloadEpisode(localEpisode);
|
||||
logger.debug('LocalDownload', 'Download service result: $success');
|
||||
|
||||
if (success) {
|
||||
updateLocalDownloadStatus(episode, true);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
logger.error('LocalDownload', 'Error in local download for episode: ${episode.episodeTitle}', e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete local download(s) for episode
|
||||
static Future<int> deleteLocalDownload(
|
||||
BuildContext context,
|
||||
PinepodsEpisode episode
|
||||
) async {
|
||||
final logger = AppLogger();
|
||||
|
||||
try {
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context, listen: false);
|
||||
final guid = generateEpisodeGuid(episode);
|
||||
|
||||
// Get all episodes and find matches with both new and old GUID formats
|
||||
final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes();
|
||||
final matchingEpisodes = allEpisodes.where((ep) =>
|
||||
ep.guid == guid || ep.guid.startsWith('${guid}_')
|
||||
).toList();
|
||||
|
||||
logger.debug('LocalDownload', 'Found ${matchingEpisodes.length} episodes to delete for $guid');
|
||||
|
||||
if (matchingEpisodes.isNotEmpty) {
|
||||
// Delete ALL matching episodes (handles duplicates from old timestamp GUIDs)
|
||||
for (final localEpisode in matchingEpisodes) {
|
||||
logger.debug('LocalDownload', 'Deleting episode: ${localEpisode.guid}');
|
||||
await podcastBloc.podcastService.repository.deleteEpisode(localEpisode);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
updateLocalDownloadStatus(episode, false);
|
||||
|
||||
return matchingEpisodes.length;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('LocalDownload', 'Error deleting local download for episode: ${episode.episodeTitle}', e.toString());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Show snackbar with message
|
||||
static void showSnackBar(BuildContext context, String message, Color backgroundColor) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: backgroundColor,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
PinePods-0.8.2/mobile/lib/ui/utils/player_utils.dart
Normal file
43
PinePods-0.8.2/mobile/lib/ui/utils/player_utils.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinepods_mobile/entities/app_settings.dart';
|
||||
import 'package:pinepods_mobile/ui/podcast/now_playing.dart';
|
||||
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
|
||||
import 'package:pinepods_mobile/entities/pinepods_episode.dart';
|
||||
import 'package:pinepods_mobile/services/pinepods/pinepods_audio_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// If we have the 'show now playing upon play' option set to true, launch
|
||||
/// the [NowPlaying] widget automatically.
|
||||
void optionalShowNowPlaying(BuildContext context, AppSettings settings) {
|
||||
if (settings.autoOpenNowPlaying) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const NowPlaying(),
|
||||
settings: const RouteSettings(name: 'nowplaying'),
|
||||
fullscreenDialog: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to play a PinePods episode and automatically show the full screen player if enabled
|
||||
Future<void> playPinepodsEpisodeWithOptionalFullScreen(
|
||||
BuildContext context,
|
||||
PinepodsAudioService audioService,
|
||||
PinepodsEpisode episode, {
|
||||
bool resume = true,
|
||||
}) async {
|
||||
await audioService.playPinepodsEpisode(
|
||||
pinepodsEpisode: episode,
|
||||
resume: resume,
|
||||
);
|
||||
|
||||
// Show full screen player if setting is enabled
|
||||
final settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
|
||||
optionalShowNowPlaying(context, settingsBloc.currentSettings);
|
||||
}
|
||||
151
PinePods-0.8.2/mobile/lib/ui/utils/position_utils.dart
Normal file
151
PinePods-0.8.2/mobile/lib/ui/utils/position_utils.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
// lib/ui/utils/position_utils.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinepods_mobile/entities/pinepods_episode.dart';
|
||||
import 'package:pinepods_mobile/entities/episode.dart';
|
||||
import 'package:pinepods_mobile/entities/downloadable.dart';
|
||||
import 'package:pinepods_mobile/services/logging/app_logger.dart';
|
||||
import 'package:pinepods_mobile/services/pinepods/pinepods_service.dart';
|
||||
import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// Utility class for managing episode position synchronization and display
|
||||
class PositionUtils {
|
||||
static final AppLogger _logger = AppLogger();
|
||||
|
||||
/// Generate consistent GUID for PinePods episodes
|
||||
static String generateEpisodeGuid(PinepodsEpisode episode) {
|
||||
return 'pinepods_${episode.episodeId}';
|
||||
}
|
||||
|
||||
/// Get local position for episode from repository
|
||||
static Future<double?> getLocalPosition(BuildContext context, PinepodsEpisode episode) async {
|
||||
try {
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context, listen: false);
|
||||
final guid = generateEpisodeGuid(episode);
|
||||
|
||||
// Get all episodes and find matches with both new and old GUID formats
|
||||
final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes();
|
||||
final matchingEpisodes = allEpisodes.where((ep) =>
|
||||
ep.guid == guid || ep.guid.startsWith('${guid}_')
|
||||
).toList();
|
||||
|
||||
if (matchingEpisodes.isNotEmpty) {
|
||||
// Return the highest position from any matching episode (in case of duplicates)
|
||||
final positions = matchingEpisodes.map((ep) => ep.position / 1000.0).toList();
|
||||
return positions.reduce((a, b) => a > b ? a : b);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
_logger.error('PositionUtils', 'Error getting local position for episode: ${episode.episodeTitle}', e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get server position for episode (use existing data from feed)
|
||||
static Future<double?> getServerPosition(PinepodsService pinepodsService, PinepodsEpisode episode, int userId) async {
|
||||
return episode.listenDuration?.toDouble();
|
||||
}
|
||||
|
||||
/// Get the best available position (furthest of local vs server)
|
||||
static Future<PositionInfo> getBestPosition(
|
||||
BuildContext context,
|
||||
PinepodsService pinepodsService,
|
||||
PinepodsEpisode episode,
|
||||
int userId,
|
||||
) async {
|
||||
// Get both positions in parallel
|
||||
final futures = await Future.wait([
|
||||
getLocalPosition(context, episode),
|
||||
getServerPosition(pinepodsService, episode, userId),
|
||||
]);
|
||||
|
||||
final localPosition = futures[0] ?? 0.0;
|
||||
final serverPosition = futures[1] ?? episode.listenDuration?.toDouble() ?? 0.0;
|
||||
|
||||
final bestPosition = localPosition > serverPosition ? localPosition : serverPosition;
|
||||
final isLocal = localPosition >= serverPosition;
|
||||
|
||||
|
||||
return PositionInfo(
|
||||
position: bestPosition,
|
||||
isLocal: isLocal,
|
||||
localPosition: localPosition,
|
||||
serverPosition: serverPosition,
|
||||
);
|
||||
}
|
||||
|
||||
/// Enrich a single episode with the best available position
|
||||
static Future<PinepodsEpisode> enrichEpisodeWithBestPosition(
|
||||
BuildContext context,
|
||||
PinepodsService pinepodsService,
|
||||
PinepodsEpisode episode,
|
||||
int userId,
|
||||
) async {
|
||||
final positionInfo = await getBestPosition(context, pinepodsService, episode, userId);
|
||||
|
||||
// Create a new episode with updated position
|
||||
return PinepodsEpisode(
|
||||
podcastName: episode.podcastName,
|
||||
episodeTitle: episode.episodeTitle,
|
||||
episodePubDate: episode.episodePubDate,
|
||||
episodeDescription: episode.episodeDescription,
|
||||
episodeArtwork: episode.episodeArtwork,
|
||||
episodeUrl: episode.episodeUrl,
|
||||
episodeDuration: episode.episodeDuration,
|
||||
listenDuration: positionInfo.position.round(),
|
||||
episodeId: episode.episodeId,
|
||||
completed: episode.completed,
|
||||
saved: episode.saved,
|
||||
queued: episode.queued,
|
||||
downloaded: episode.downloaded,
|
||||
isYoutube: episode.isYoutube,
|
||||
podcastId: episode.podcastId,
|
||||
);
|
||||
}
|
||||
|
||||
/// Enrich a list of episodes with the best available positions
|
||||
static Future<List<PinepodsEpisode>> enrichEpisodesWithBestPositions(
|
||||
BuildContext context,
|
||||
PinepodsService pinepodsService,
|
||||
List<PinepodsEpisode> episodes,
|
||||
int userId,
|
||||
) async {
|
||||
_logger.info('PositionUtils', 'Enriching ${episodes.length} episodes with best positions');
|
||||
|
||||
final enrichedEpisodes = <PinepodsEpisode>[];
|
||||
|
||||
for (final episode in episodes) {
|
||||
try {
|
||||
final enrichedEpisode = await enrichEpisodeWithBestPosition(
|
||||
context,
|
||||
pinepodsService,
|
||||
episode,
|
||||
userId,
|
||||
);
|
||||
enrichedEpisodes.add(enrichedEpisode);
|
||||
} catch (e) {
|
||||
_logger.warning('PositionUtils', 'Failed to enrich episode ${episode.episodeTitle}, using original: ${e.toString()}');
|
||||
enrichedEpisodes.add(episode);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.info('PositionUtils', 'Successfully enriched ${enrichedEpisodes.length} episodes');
|
||||
return enrichedEpisodes;
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about episode position
|
||||
class PositionInfo {
|
||||
final double position;
|
||||
final bool isLocal;
|
||||
final double localPosition;
|
||||
final double serverPosition;
|
||||
|
||||
PositionInfo({
|
||||
required this.position,
|
||||
required this.isLocal,
|
||||
required this.localPosition,
|
||||
required this.serverPosition,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user