added cargo files
This commit is contained in:
152
PinePods-0.8.2/mobile/lib/entities/app_settings.dart
Normal file
152
PinePods-0.8.2/mobile/lib/entities/app_settings.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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:pinepods_mobile/entities/search_providers.dart';
|
||||
|
||||
class AppSettings {
|
||||
/// The current theme name.
|
||||
final String theme;
|
||||
|
||||
/// True if episodes are marked as played when deleted.
|
||||
final bool markDeletedEpisodesAsPlayed;
|
||||
|
||||
/// True if downloaded played episodes must be deleted automatically.
|
||||
final bool deleteDownloadedPlayedEpisodes;
|
||||
|
||||
/// True if downloads should be saved to the SD card.
|
||||
final bool storeDownloadsSDCard;
|
||||
|
||||
/// The default playback speed.
|
||||
final double playbackSpeed;
|
||||
|
||||
/// The search provider: itunes or podcastindex.
|
||||
final String? searchProvider;
|
||||
|
||||
/// List of search providers: currently itunes or podcastindex.
|
||||
final List<SearchProvider> searchProviders;
|
||||
|
||||
/// True if the user has confirmed dialog accepting funding links.
|
||||
final bool externalLinkConsent;
|
||||
|
||||
/// If true the main player window will open as soon as an episode starts.
|
||||
final bool autoOpenNowPlaying;
|
||||
|
||||
/// If true the funding link icon will appear (if the podcast supports it).
|
||||
final bool showFunding;
|
||||
|
||||
/// If -1 never; 0 always; otherwise time in minutes.
|
||||
final int autoUpdateEpisodePeriod;
|
||||
|
||||
/// If true, silence in audio playback is trimmed. Currently Android only.
|
||||
final bool trimSilence;
|
||||
|
||||
/// If true, volume is boosted. Currently Android only.
|
||||
final bool volumeBoost;
|
||||
|
||||
/// If 0, list view; else grid view
|
||||
final int layout;
|
||||
|
||||
final String? pinepodsServer;
|
||||
|
||||
final String? pinepodsApiKey;
|
||||
|
||||
final int? pinepodsUserId;
|
||||
|
||||
final String? pinepodsUsername;
|
||||
|
||||
final String? pinepodsEmail;
|
||||
|
||||
/// Custom order for bottom navigation bar items
|
||||
final List<String> bottomBarOrder;
|
||||
|
||||
AppSettings({
|
||||
required this.theme,
|
||||
required this.markDeletedEpisodesAsPlayed,
|
||||
required this.deleteDownloadedPlayedEpisodes,
|
||||
required this.storeDownloadsSDCard,
|
||||
required this.playbackSpeed,
|
||||
required this.searchProvider,
|
||||
required this.searchProviders,
|
||||
required this.externalLinkConsent,
|
||||
required this.autoOpenNowPlaying,
|
||||
required this.showFunding,
|
||||
required this.autoUpdateEpisodePeriod,
|
||||
required this.trimSilence,
|
||||
required this.volumeBoost,
|
||||
required this.layout,
|
||||
this.pinepodsServer,
|
||||
this.pinepodsApiKey,
|
||||
this.pinepodsUserId,
|
||||
this.pinepodsUsername,
|
||||
this.pinepodsEmail,
|
||||
required this.bottomBarOrder,
|
||||
});
|
||||
|
||||
AppSettings.sensibleDefaults()
|
||||
: theme = 'Dark',
|
||||
markDeletedEpisodesAsPlayed = false,
|
||||
deleteDownloadedPlayedEpisodes = false,
|
||||
storeDownloadsSDCard = false,
|
||||
playbackSpeed = 1.0,
|
||||
searchProvider = 'itunes',
|
||||
searchProviders = <SearchProvider>[],
|
||||
externalLinkConsent = false,
|
||||
autoOpenNowPlaying = false,
|
||||
showFunding = true,
|
||||
autoUpdateEpisodePeriod = -1,
|
||||
trimSilence = false,
|
||||
volumeBoost = false,
|
||||
layout = 0,
|
||||
pinepodsServer = null,
|
||||
pinepodsApiKey = null,
|
||||
pinepodsUserId = null,
|
||||
pinepodsUsername = null,
|
||||
pinepodsEmail = null,
|
||||
bottomBarOrder = const ['Home', 'Feed', 'Saved', 'Podcasts', 'Downloads', 'History', 'Playlists', 'Search'];
|
||||
|
||||
AppSettings copyWith({
|
||||
String? theme,
|
||||
bool? markDeletedEpisodesAsPlayed,
|
||||
bool? deleteDownloadedPlayedEpisodes,
|
||||
bool? storeDownloadsSDCard,
|
||||
double? playbackSpeed,
|
||||
String? searchProvider,
|
||||
List<SearchProvider>? searchProviders,
|
||||
bool? externalLinkConsent,
|
||||
bool? autoOpenNowPlaying,
|
||||
bool? showFunding,
|
||||
int? autoUpdateEpisodePeriod,
|
||||
bool? trimSilence,
|
||||
bool? volumeBoost,
|
||||
int? layout,
|
||||
String? pinepodsServer,
|
||||
String? pinepodsApiKey,
|
||||
int? pinepodsUserId,
|
||||
String? pinepodsUsername,
|
||||
String? pinepodsEmail,
|
||||
List<String>? bottomBarOrder,
|
||||
}) =>
|
||||
AppSettings(
|
||||
theme: theme ?? this.theme,
|
||||
markDeletedEpisodesAsPlayed: markDeletedEpisodesAsPlayed ?? this.markDeletedEpisodesAsPlayed,
|
||||
deleteDownloadedPlayedEpisodes: deleteDownloadedPlayedEpisodes ?? this.deleteDownloadedPlayedEpisodes,
|
||||
storeDownloadsSDCard: storeDownloadsSDCard ?? this.storeDownloadsSDCard,
|
||||
playbackSpeed: playbackSpeed ?? this.playbackSpeed,
|
||||
searchProvider: searchProvider ?? this.searchProvider,
|
||||
searchProviders: searchProviders ?? this.searchProviders,
|
||||
externalLinkConsent: externalLinkConsent ?? this.externalLinkConsent,
|
||||
autoOpenNowPlaying: autoOpenNowPlaying ?? this.autoOpenNowPlaying,
|
||||
showFunding: showFunding ?? this.showFunding,
|
||||
autoUpdateEpisodePeriod: autoUpdateEpisodePeriod ?? this.autoUpdateEpisodePeriod,
|
||||
trimSilence: trimSilence ?? this.trimSilence,
|
||||
volumeBoost: volumeBoost ?? this.volumeBoost,
|
||||
layout: layout ?? this.layout,
|
||||
pinepodsServer: pinepodsServer ?? this.pinepodsServer,
|
||||
pinepodsApiKey: pinepodsApiKey ?? this.pinepodsApiKey,
|
||||
pinepodsUserId: pinepodsUserId ?? this.pinepodsUserId,
|
||||
pinepodsUsername: pinepodsUsername ?? this.pinepodsUsername,
|
||||
pinepodsEmail: pinepodsEmail ?? this.pinepodsEmail,
|
||||
bottomBarOrder: bottomBarOrder ?? this.bottomBarOrder,
|
||||
);
|
||||
}
|
||||
71
PinePods-0.8.2/mobile/lib/entities/chapter.dart
Normal file
71
PinePods-0.8.2/mobile/lib/entities/chapter.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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:pinepods_mobile/core/extensions.dart';
|
||||
|
||||
/// A class that represents an individual chapter within an [Episode].
|
||||
///
|
||||
/// Chapters may, or may not, exist for an episode.
|
||||
///
|
||||
/// Part of the [podcast namespace](https://github.com/Podcastindex-org/podcast-namespace)
|
||||
class Chapter {
|
||||
/// Title of this chapter.
|
||||
final String title;
|
||||
|
||||
/// URL for the chapter image if one is available.
|
||||
final String? imageUrl;
|
||||
|
||||
/// URL of an external link for this chapter if available.
|
||||
final String? url;
|
||||
|
||||
/// Table of contents flag. If this is false the chapter should be treated as
|
||||
/// meta data only and not be displayed.
|
||||
final bool toc;
|
||||
|
||||
/// The start time of the chapter in seconds.
|
||||
final double startTime;
|
||||
|
||||
/// The optional end time of the chapter in seconds.
|
||||
final double? endTime;
|
||||
|
||||
Chapter({
|
||||
required this.title,
|
||||
required String? imageUrl,
|
||||
required this.startTime,
|
||||
String? url,
|
||||
this.toc = true,
|
||||
this.endTime,
|
||||
}) : imageUrl = imageUrl?.forceHttps,
|
||||
url = url?.forceHttps;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'title': title,
|
||||
'imageUrl': imageUrl,
|
||||
'url': url,
|
||||
'toc': toc ? 'true' : 'false',
|
||||
'startTime': startTime.toString(),
|
||||
'endTime': endTime.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
static Chapter fromMap(Map<String, dynamic> chapter) {
|
||||
return Chapter(
|
||||
title: chapter['title'] as String,
|
||||
imageUrl: chapter['imageUrl'] as String?,
|
||||
url: chapter['url'] as String?,
|
||||
toc: chapter['toc'] == 'false' ? false : true,
|
||||
startTime: double.tryParse(chapter['startTime'] as String? ?? '0') ?? 0.0,
|
||||
endTime: double.tryParse(chapter['endTime'] as String? ?? '0') ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Chapter && runtimeType == other.runtimeType && title == other.title && startTime == other.startTime;
|
||||
|
||||
@override
|
||||
int get hashCode => title.hashCode ^ startTime.hashCode;
|
||||
}
|
||||
98
PinePods-0.8.2/mobile/lib/entities/downloadable.dart
Normal file
98
PinePods-0.8.2/mobile/lib/entities/downloadable.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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.
|
||||
|
||||
enum DownloadState { none, queued, downloading, failed, cancelled, paused, downloaded }
|
||||
|
||||
/// A Downloadble is an object that holds information about a podcast episode
|
||||
/// and its download status.
|
||||
///
|
||||
/// Downloadables can be used to determine if a download has been successful and
|
||||
/// if an episode can be played from the filesystem.
|
||||
class Downloadable {
|
||||
/// Database ID
|
||||
int? id;
|
||||
|
||||
/// Unique identifier for the download
|
||||
final String guid;
|
||||
|
||||
/// URL of the file to download
|
||||
final String url;
|
||||
|
||||
/// Destination directory
|
||||
String directory;
|
||||
|
||||
/// Name of file
|
||||
String filename;
|
||||
|
||||
/// Current task ID for the download
|
||||
String taskId;
|
||||
|
||||
/// Current state of the download
|
||||
DownloadState state;
|
||||
|
||||
/// Percentage of MP3 downloaded
|
||||
int? percentage;
|
||||
|
||||
Downloadable({
|
||||
required this.guid,
|
||||
required this.url,
|
||||
required this.directory,
|
||||
required this.filename,
|
||||
required this.taskId,
|
||||
required this.state,
|
||||
this.percentage,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'guid': guid,
|
||||
'url': url,
|
||||
'filename': filename,
|
||||
'directory': directory,
|
||||
'taskId': taskId,
|
||||
'state': state.index,
|
||||
'percentage': percentage.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
static Downloadable fromMap(Map<String, dynamic> downloadable) {
|
||||
return Downloadable(
|
||||
guid: downloadable['guid'] as String,
|
||||
url: downloadable['url'] as String,
|
||||
directory: downloadable['directory'] as String,
|
||||
filename: downloadable['filename'] as String,
|
||||
taskId: downloadable['taskId'] as String,
|
||||
state: _determineState(downloadable['state'] as int?),
|
||||
percentage: int.parse(downloadable['percentage'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
static DownloadState _determineState(int? index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return DownloadState.none;
|
||||
case 1:
|
||||
return DownloadState.queued;
|
||||
case 2:
|
||||
return DownloadState.downloading;
|
||||
case 3:
|
||||
return DownloadState.failed;
|
||||
case 4:
|
||||
return DownloadState.cancelled;
|
||||
case 5:
|
||||
return DownloadState.paused;
|
||||
case 6:
|
||||
return DownloadState.downloaded;
|
||||
}
|
||||
|
||||
return DownloadState.none;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is Downloadable && runtimeType == other.runtimeType && guid == other.guid;
|
||||
|
||||
@override
|
||||
int get hashCode => guid.hashCode;
|
||||
}
|
||||
420
PinePods-0.8.2/mobile/lib/entities/episode.dart
Normal file
420
PinePods-0.8.2/mobile/lib/entities/episode.dart
Normal file
@@ -0,0 +1,420 @@
|
||||
// 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:pinepods_mobile/core/annotations.dart';
|
||||
import 'package:pinepods_mobile/core/extensions.dart';
|
||||
import 'package:pinepods_mobile/entities/chapter.dart';
|
||||
import 'package:pinepods_mobile/entities/downloadable.dart';
|
||||
import 'package:pinepods_mobile/entities/person.dart';
|
||||
import 'package:pinepods_mobile/entities/transcript.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:html/parser.dart' show parseFragment;
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// An object that represents an individual episode of a Podcast.
|
||||
///
|
||||
/// An Episode can be used in conjunction with a [Downloadable] to
|
||||
/// determine if the Episode is available on the local filesystem.
|
||||
class Episode {
|
||||
final log = Logger('Episode');
|
||||
|
||||
/// Database ID
|
||||
int? id;
|
||||
|
||||
/// A String GUID for the episode.
|
||||
final String guid;
|
||||
|
||||
/// The GUID for an associated podcast. If an episode has been downloaded
|
||||
/// without subscribing to a podcast this may be null.
|
||||
String? pguid;
|
||||
|
||||
/// If the episode is currently being downloaded, this contains the unique
|
||||
/// ID supplied by the download manager for the episode.
|
||||
String? downloadTaskId;
|
||||
|
||||
/// The path to the directory containing the download for this episode; or null.
|
||||
String? filepath;
|
||||
|
||||
/// The filename of the downloaded episode; or null.
|
||||
String? filename;
|
||||
|
||||
/// The current downloading state of the episode.
|
||||
DownloadState downloadState = DownloadState.none;
|
||||
|
||||
/// The name of the podcast the episode is part of.
|
||||
String? podcast;
|
||||
|
||||
/// The episode title.
|
||||
String? title;
|
||||
|
||||
/// The episode description. This could be plain text or HTML.
|
||||
String? description;
|
||||
|
||||
/// More detailed description - optional.
|
||||
String? content;
|
||||
|
||||
/// External link
|
||||
String? link;
|
||||
|
||||
/// URL to the episode artwork image.
|
||||
String? imageUrl;
|
||||
|
||||
/// URL to a thumbnail version of the episode artwork image.
|
||||
String? thumbImageUrl;
|
||||
|
||||
/// The date the episode was published (if known).
|
||||
DateTime? publicationDate;
|
||||
|
||||
/// The URL for the episode location.
|
||||
String? contentUrl;
|
||||
|
||||
/// Author of the episode if known.
|
||||
String? author;
|
||||
|
||||
/// The season the episode is part of if available.
|
||||
int season;
|
||||
|
||||
/// The episode number within a season if available.
|
||||
int episode;
|
||||
|
||||
/// The duration of the episode in milliseconds. This can be populated either from
|
||||
/// the RSS if available, or determined from the MP3 file at stream/download time.
|
||||
int duration;
|
||||
|
||||
/// Stores the current position within the episode in milliseconds. Used for resuming.
|
||||
int position;
|
||||
|
||||
/// Stores the progress of the current download progress if available.
|
||||
int? downloadPercentage;
|
||||
|
||||
/// True if this episode is 'marked as played'.
|
||||
bool played;
|
||||
|
||||
/// URL pointing to a JSON file containing chapter information if available.
|
||||
String? chaptersUrl;
|
||||
|
||||
/// List of chapters for the episode if available.
|
||||
List<Chapter> chapters;
|
||||
|
||||
/// List of transcript URLs for the episode if available.
|
||||
List<TranscriptUrl> transcriptUrls;
|
||||
|
||||
List<Person> persons;
|
||||
|
||||
/// Currently downloaded or in use transcript for the episode.To minimise memory
|
||||
/// use, this is cleared when an episode download is deleted, or a streamed episode stopped.
|
||||
Transcript? transcript;
|
||||
|
||||
/// Link to a currently stored transcript for this episode.
|
||||
int? transcriptId;
|
||||
|
||||
/// Date and time episode was last updated and persisted.
|
||||
DateTime? lastUpdated;
|
||||
|
||||
/// Processed version of episode description.
|
||||
String? _descriptionText;
|
||||
|
||||
/// Index of the currently playing chapter it available. Transient.
|
||||
int? chapterIndex;
|
||||
|
||||
/// Current chapter we are listening to if this episode has chapters. Transient.
|
||||
Chapter? currentChapter;
|
||||
|
||||
/// Set to true if chapter data is currently being loaded.
|
||||
@Transient()
|
||||
bool chaptersLoading = false;
|
||||
|
||||
@Transient()
|
||||
bool highlight = false;
|
||||
|
||||
@Transient()
|
||||
bool queued = false;
|
||||
|
||||
@Transient()
|
||||
bool streaming = true;
|
||||
|
||||
Episode({
|
||||
required this.guid,
|
||||
this.pguid,
|
||||
required this.podcast,
|
||||
this.id,
|
||||
this.downloadTaskId,
|
||||
this.filepath,
|
||||
this.filename,
|
||||
this.downloadState = DownloadState.none,
|
||||
this.title,
|
||||
this.description,
|
||||
this.content,
|
||||
this.link,
|
||||
String? imageUrl,
|
||||
String? thumbImageUrl,
|
||||
this.publicationDate,
|
||||
String? contentUrl,
|
||||
this.author,
|
||||
this.season = 0,
|
||||
this.episode = 0,
|
||||
this.duration = 0,
|
||||
this.position = 0,
|
||||
this.downloadPercentage = 0,
|
||||
this.played = false,
|
||||
this.highlight = false,
|
||||
String? chaptersUrl,
|
||||
this.chapters = const <Chapter>[],
|
||||
this.transcriptUrls = const <TranscriptUrl>[],
|
||||
this.persons = const <Person>[],
|
||||
this.transcriptId = 0,
|
||||
this.lastUpdated,
|
||||
}) : imageUrl = imageUrl?.forceHttps,
|
||||
thumbImageUrl = thumbImageUrl?.forceHttps,
|
||||
contentUrl = contentUrl?.forceHttps,
|
||||
chaptersUrl = chaptersUrl?.forceHttps;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'guid': guid,
|
||||
'pguid': pguid,
|
||||
'downloadTaskId': downloadTaskId,
|
||||
'filepath': filepath,
|
||||
'filename': filename,
|
||||
'downloadState': downloadState.index,
|
||||
'podcast': podcast,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'content': content,
|
||||
'link': link,
|
||||
'imageUrl': imageUrl,
|
||||
'thumbImageUrl': thumbImageUrl,
|
||||
'publicationDate': publicationDate?.millisecondsSinceEpoch.toString(),
|
||||
'contentUrl': contentUrl,
|
||||
'author': author,
|
||||
'season': season.toString(),
|
||||
'episode': episode.toString(),
|
||||
'duration': duration.toString(),
|
||||
'position': position.toString(),
|
||||
'downloadPercentage': downloadPercentage.toString(),
|
||||
'played': played ? 'true' : 'false',
|
||||
'chaptersUrl': chaptersUrl,
|
||||
'chapters': (chapters).map((chapter) => chapter.toMap()).toList(growable: false),
|
||||
'tid': transcriptId ?? 0,
|
||||
'transcriptUrls': (transcriptUrls).map((tu) => tu.toMap()).toList(growable: false),
|
||||
'persons': (persons).map((person) => person.toMap()).toList(growable: false),
|
||||
'lastUpdated': lastUpdated?.millisecondsSinceEpoch.toString() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
static Episode fromMap(int? key, Map<String, dynamic> episode) {
|
||||
var chapters = <Chapter>[];
|
||||
var transcriptUrls = <TranscriptUrl>[];
|
||||
var persons = <Person>[];
|
||||
|
||||
// We need to perform an 'is' on each loop to prevent Dart
|
||||
// from complaining that we have not set the type for chapter.
|
||||
if (episode['chapters'] != null) {
|
||||
for (var chapter in (episode['chapters'] as List)) {
|
||||
if (chapter is Map<String, dynamic>) {
|
||||
chapters.add(Chapter.fromMap(chapter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (episode['transcriptUrls'] != null) {
|
||||
for (var transcriptUrl in (episode['transcriptUrls'] as List)) {
|
||||
if (transcriptUrl is Map<String, dynamic>) {
|
||||
transcriptUrls.add(TranscriptUrl.fromMap(transcriptUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (episode['persons'] != null) {
|
||||
for (var person in (episode['persons'] as List)) {
|
||||
if (person is Map<String, dynamic>) {
|
||||
persons.add(Person.fromMap(person));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Episode(
|
||||
id: key,
|
||||
guid: episode['guid'] as String,
|
||||
pguid: episode['pguid'] as String?,
|
||||
downloadTaskId: episode['downloadTaskId'] as String?,
|
||||
filepath: episode['filepath'] as String?,
|
||||
filename: episode['filename'] as String?,
|
||||
downloadState: _determineState(episode['downloadState'] as int?),
|
||||
podcast: episode['podcast'] as String?,
|
||||
title: episode['title'] as String?,
|
||||
description: episode['description'] as String?,
|
||||
content: episode['content'] as String?,
|
||||
link: episode['link'] as String?,
|
||||
imageUrl: episode['imageUrl'] as String?,
|
||||
thumbImageUrl: episode['thumbImageUrl'] as String?,
|
||||
publicationDate: episode['publicationDate'] == null || episode['publicationDate'] == 'null'
|
||||
? DateTime.now()
|
||||
: DateTime.fromMillisecondsSinceEpoch(int.parse(episode['publicationDate'] as String)),
|
||||
contentUrl: episode['contentUrl'] as String?,
|
||||
author: episode['author'] as String?,
|
||||
season: int.parse(episode['season'] as String? ?? '0'),
|
||||
episode: int.parse(episode['episode'] as String? ?? '0'),
|
||||
duration: int.parse(episode['duration'] as String? ?? '0'),
|
||||
position: int.parse(episode['position'] as String? ?? '0'),
|
||||
downloadPercentage: int.parse(episode['downloadPercentage'] as String? ?? '0'),
|
||||
played: episode['played'] == 'true' ? true : false,
|
||||
chaptersUrl: episode['chaptersUrl'] as String?,
|
||||
chapters: chapters,
|
||||
transcriptUrls: transcriptUrls,
|
||||
persons: persons,
|
||||
transcriptId: episode['tid'] == null ? 0 : episode['tid'] as int?,
|
||||
lastUpdated: episode['lastUpdated'] == null || episode['lastUpdated'] == 'null'
|
||||
? DateTime.now()
|
||||
: DateTime.fromMillisecondsSinceEpoch(int.parse(episode['lastUpdated'] as String)),
|
||||
);
|
||||
}
|
||||
|
||||
static DownloadState _determineState(int? index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return DownloadState.none;
|
||||
case 1:
|
||||
return DownloadState.queued;
|
||||
case 2:
|
||||
return DownloadState.downloading;
|
||||
case 3:
|
||||
return DownloadState.failed;
|
||||
case 4:
|
||||
return DownloadState.cancelled;
|
||||
case 5:
|
||||
return DownloadState.paused;
|
||||
case 6:
|
||||
return DownloadState.downloaded;
|
||||
}
|
||||
|
||||
return DownloadState.none;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is Episode &&
|
||||
runtimeType == other.runtimeType &&
|
||||
guid == other.guid &&
|
||||
pguid == other.pguid &&
|
||||
downloadTaskId == other.downloadTaskId &&
|
||||
filepath == other.filepath &&
|
||||
filename == other.filename &&
|
||||
downloadState == other.downloadState &&
|
||||
podcast == other.podcast &&
|
||||
title == other.title &&
|
||||
description == other.description &&
|
||||
content == other.content &&
|
||||
link == other.link &&
|
||||
imageUrl == other.imageUrl &&
|
||||
thumbImageUrl == other.thumbImageUrl &&
|
||||
publicationDate?.millisecondsSinceEpoch == other.publicationDate?.millisecondsSinceEpoch &&
|
||||
contentUrl == other.contentUrl &&
|
||||
author == other.author &&
|
||||
season == other.season &&
|
||||
episode == other.episode &&
|
||||
duration == other.duration &&
|
||||
position == other.position &&
|
||||
downloadPercentage == other.downloadPercentage &&
|
||||
played == other.played &&
|
||||
chaptersUrl == other.chaptersUrl &&
|
||||
transcriptId == other.transcriptId &&
|
||||
listEquals(persons, other.persons) &&
|
||||
listEquals(chapters, other.chapters);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
guid.hashCode ^
|
||||
pguid.hashCode ^
|
||||
downloadTaskId.hashCode ^
|
||||
filepath.hashCode ^
|
||||
filename.hashCode ^
|
||||
downloadState.hashCode ^
|
||||
podcast.hashCode ^
|
||||
title.hashCode ^
|
||||
description.hashCode ^
|
||||
content.hashCode ^
|
||||
link.hashCode ^
|
||||
imageUrl.hashCode ^
|
||||
thumbImageUrl.hashCode ^
|
||||
publicationDate.hashCode ^
|
||||
contentUrl.hashCode ^
|
||||
author.hashCode ^
|
||||
season.hashCode ^
|
||||
episode.hashCode ^
|
||||
duration.hashCode ^
|
||||
position.hashCode ^
|
||||
downloadPercentage.hashCode ^
|
||||
played.hashCode ^
|
||||
chaptersUrl.hashCode ^
|
||||
chapters.hashCode ^
|
||||
transcriptId.hashCode ^
|
||||
lastUpdated.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Episode{id: $id, guid: $guid, pguid: $pguid, filepath: $filepath, title: $title, contentUrl: $contentUrl, episode: $episode, duration: $duration, position: $position, downloadPercentage: $downloadPercentage, played: $played, queued: $queued}';
|
||||
}
|
||||
|
||||
bool get downloaded => downloadPercentage == 100;
|
||||
|
||||
Duration get timeRemaining {
|
||||
if (position > 0 && duration > 0) {
|
||||
var currentPosition = Duration(milliseconds: position);
|
||||
|
||||
var tr = duration - currentPosition.inSeconds;
|
||||
|
||||
return Duration(seconds: tr);
|
||||
}
|
||||
|
||||
return const Duration(seconds: 0);
|
||||
}
|
||||
|
||||
double get percentagePlayed {
|
||||
if (position > 0 && duration > 0) {
|
||||
var pc = (position / (duration * 1000)) * 100;
|
||||
|
||||
if (pc > 100.0) {
|
||||
pc = 100.0;
|
||||
}
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
String? get descriptionText {
|
||||
if (_descriptionText == null || _descriptionText!.isEmpty) {
|
||||
if (description == null || description!.isEmpty) {
|
||||
_descriptionText = '';
|
||||
} else {
|
||||
// Replace break tags with space character for readability
|
||||
var formattedDescription = description!.replaceAll(RegExp(r'(<br/?>)+'), ' ');
|
||||
_descriptionText = parseFragment(formattedDescription).text;
|
||||
}
|
||||
}
|
||||
|
||||
return _descriptionText;
|
||||
}
|
||||
|
||||
bool get hasChapters => (chaptersUrl != null && chaptersUrl!.isNotEmpty) || chapters.isNotEmpty;
|
||||
|
||||
bool get hasTranscripts => transcriptUrls.isNotEmpty;
|
||||
|
||||
bool get chaptersAreLoaded => chaptersLoading == false && chapters.isNotEmpty;
|
||||
|
||||
bool get chaptersAreNotLoaded => chaptersLoading == true && chapters.isEmpty;
|
||||
|
||||
String? get positionalImageUrl {
|
||||
if (currentChapter != null && currentChapter!.imageUrl != null && currentChapter!.imageUrl!.isNotEmpty) {
|
||||
return currentChapter!.imageUrl;
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
39
PinePods-0.8.2/mobile/lib/entities/feed.dart
Normal file
39
PinePods-0.8.2/mobile/lib/entities/feed.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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:pinepods_mobile/entities/podcast.dart';
|
||||
|
||||
/// This class is used when loading a [Podcast] feed.
|
||||
///
|
||||
/// The key information is contained within the [Podcast] instance, but as the
|
||||
/// iTunes API also returns large and thumbnail artwork within its search results
|
||||
/// this class also contains properties to represent those.
|
||||
class Feed {
|
||||
/// The podcast to load
|
||||
final Podcast podcast;
|
||||
|
||||
/// The full-size artwork for the podcast.
|
||||
String? imageUrl;
|
||||
|
||||
/// The thumbnail artwork for the podcast,
|
||||
String? thumbImageUrl;
|
||||
|
||||
/// If true the podcast is loaded regardless of if it's currently cached.
|
||||
bool refresh;
|
||||
|
||||
/// If true, will also perform an additional background refresh.
|
||||
bool backgroundFresh;
|
||||
|
||||
/// If true any error can be ignored.
|
||||
bool silently;
|
||||
|
||||
Feed({
|
||||
required this.podcast,
|
||||
this.imageUrl,
|
||||
this.thumbImageUrl,
|
||||
this.refresh = false,
|
||||
this.backgroundFresh = false,
|
||||
this.silently = false,
|
||||
});
|
||||
}
|
||||
35
PinePods-0.8.2/mobile/lib/entities/funding.dart
Normal file
35
PinePods-0.8.2/mobile/lib/entities/funding.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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:pinepods_mobile/core/extensions.dart';
|
||||
|
||||
/// part of a [Podcast].
|
||||
///
|
||||
/// Part of the [podcast namespace](https://github.com/Podcastindex-org/podcast-namespace)
|
||||
class Funding {
|
||||
/// The URL to the funding/donation/information page.
|
||||
final String url;
|
||||
|
||||
/// The label for the link which will be presented to the user.
|
||||
final String value;
|
||||
|
||||
Funding({
|
||||
required String url,
|
||||
required this.value,
|
||||
}) : url = url.forceHttps;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'url': url,
|
||||
'value': value,
|
||||
};
|
||||
}
|
||||
|
||||
static Funding fromMap(Map<String, dynamic> chapter) {
|
||||
return Funding(
|
||||
url: chapter['url'] as String,
|
||||
value: chapter['value'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
238
PinePods-0.8.2/mobile/lib/entities/home_data.dart
Normal file
238
PinePods-0.8.2/mobile/lib/entities/home_data.dart
Normal file
@@ -0,0 +1,238 @@
|
||||
// lib/entities/home_data.dart
|
||||
|
||||
class HomePodcast {
|
||||
final int podcastId;
|
||||
final String podcastName;
|
||||
final int? podcastIndexId;
|
||||
final String? artworkUrl;
|
||||
final String? author;
|
||||
final String? categories;
|
||||
final String? description;
|
||||
final int? episodeCount;
|
||||
final String? feedUrl;
|
||||
final String? websiteUrl;
|
||||
final bool? explicit;
|
||||
final bool isYoutube;
|
||||
final int playCount;
|
||||
final int? totalListenTime;
|
||||
|
||||
HomePodcast({
|
||||
required this.podcastId,
|
||||
required this.podcastName,
|
||||
this.podcastIndexId,
|
||||
this.artworkUrl,
|
||||
this.author,
|
||||
this.categories,
|
||||
this.description,
|
||||
this.episodeCount,
|
||||
this.feedUrl,
|
||||
this.websiteUrl,
|
||||
this.explicit,
|
||||
required this.isYoutube,
|
||||
required this.playCount,
|
||||
this.totalListenTime,
|
||||
});
|
||||
|
||||
factory HomePodcast.fromJson(Map<String, dynamic> json) {
|
||||
return HomePodcast(
|
||||
podcastId: json['podcastid'] ?? 0,
|
||||
podcastName: json['podcastname'] ?? '',
|
||||
podcastIndexId: json['podcastindexid'],
|
||||
artworkUrl: json['artworkurl'],
|
||||
author: json['author'],
|
||||
categories: _parseCategories(json['categories']),
|
||||
description: json['description'],
|
||||
episodeCount: json['episodecount'],
|
||||
feedUrl: json['feedurl'],
|
||||
websiteUrl: json['websiteurl'],
|
||||
explicit: json['explicit'],
|
||||
isYoutube: json['is_youtube'] ?? false,
|
||||
playCount: json['play_count'] ?? 0,
|
||||
totalListenTime: json['total_listen_time'],
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse categories from either string or Map format
|
||||
static String? _parseCategories(dynamic categories) {
|
||||
if (categories == null) return null;
|
||||
|
||||
if (categories is String) {
|
||||
// Old format - return as is
|
||||
return categories;
|
||||
} else if (categories is Map<String, dynamic>) {
|
||||
// New format - convert map values to comma-separated string
|
||||
if (categories.isEmpty) return null;
|
||||
return categories.values.join(', ');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class HomeEpisode {
|
||||
final int episodeId;
|
||||
final int podcastId;
|
||||
final String episodeTitle;
|
||||
final String episodeDescription;
|
||||
final String episodeUrl;
|
||||
final String episodeArtwork;
|
||||
final String episodePubDate;
|
||||
final int episodeDuration;
|
||||
final bool completed;
|
||||
final String podcastName;
|
||||
final bool isYoutube;
|
||||
final int? listenDuration;
|
||||
final bool saved;
|
||||
final bool queued;
|
||||
final bool downloaded;
|
||||
|
||||
HomeEpisode({
|
||||
required this.episodeId,
|
||||
required this.podcastId,
|
||||
required this.episodeTitle,
|
||||
required this.episodeDescription,
|
||||
required this.episodeUrl,
|
||||
required this.episodeArtwork,
|
||||
required this.episodePubDate,
|
||||
required this.episodeDuration,
|
||||
required this.completed,
|
||||
required this.podcastName,
|
||||
required this.isYoutube,
|
||||
this.listenDuration,
|
||||
this.saved = false,
|
||||
this.queued = false,
|
||||
this.downloaded = false,
|
||||
});
|
||||
|
||||
factory HomeEpisode.fromJson(Map<String, dynamic> json) {
|
||||
return HomeEpisode(
|
||||
episodeId: json['episodeid'] ?? 0,
|
||||
podcastId: json['podcastid'] ?? 0,
|
||||
episodeTitle: json['episodetitle'] ?? '',
|
||||
episodeDescription: json['episodedescription'] ?? '',
|
||||
episodeUrl: json['episodeurl'] ?? '',
|
||||
episodeArtwork: json['episodeartwork'] ?? '',
|
||||
episodePubDate: json['episodepubdate'] ?? '',
|
||||
episodeDuration: json['episodeduration'] ?? 0,
|
||||
completed: json['completed'] ?? false,
|
||||
podcastName: json['podcastname'] ?? '',
|
||||
isYoutube: json['is_youtube'] ?? false,
|
||||
listenDuration: json['listenduration'],
|
||||
saved: json['saved'] ?? false,
|
||||
queued: json['queued'] ?? false,
|
||||
downloaded: json['downloaded'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Format duration in seconds to MM:SS or HH:MM:SS format
|
||||
String get formattedDuration {
|
||||
if (episodeDuration <= 0) return '--:--';
|
||||
|
||||
final hours = episodeDuration ~/ 3600;
|
||||
final minutes = (episodeDuration % 3600) ~/ 60;
|
||||
final seconds = episodeDuration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Format listen duration if available
|
||||
String? get formattedListenDuration {
|
||||
if (listenDuration == null || listenDuration! <= 0) return null;
|
||||
|
||||
final duration = listenDuration!;
|
||||
final hours = duration ~/ 3600;
|
||||
final minutes = (duration % 3600) ~/ 60;
|
||||
final seconds = duration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate progress percentage for progress bar
|
||||
double get progressPercentage {
|
||||
if (episodeDuration <= 0 || listenDuration == null) return 0.0;
|
||||
return (listenDuration! / episodeDuration) * 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
class HomeOverview {
|
||||
final List<HomeEpisode> recentEpisodes;
|
||||
final List<HomeEpisode> inProgressEpisodes;
|
||||
final List<HomePodcast> topPodcasts;
|
||||
final int savedCount;
|
||||
final int downloadedCount;
|
||||
final int queueCount;
|
||||
|
||||
HomeOverview({
|
||||
required this.recentEpisodes,
|
||||
required this.inProgressEpisodes,
|
||||
required this.topPodcasts,
|
||||
required this.savedCount,
|
||||
required this.downloadedCount,
|
||||
required this.queueCount,
|
||||
});
|
||||
|
||||
factory HomeOverview.fromJson(Map<String, dynamic> json) {
|
||||
return HomeOverview(
|
||||
recentEpisodes: (json['recent_episodes'] as List<dynamic>? ?? [])
|
||||
.map((e) => HomeEpisode.fromJson(e))
|
||||
.toList(),
|
||||
inProgressEpisodes: (json['in_progress_episodes'] as List<dynamic>? ?? [])
|
||||
.map((e) => HomeEpisode.fromJson(e))
|
||||
.toList(),
|
||||
topPodcasts: (json['top_podcasts'] as List<dynamic>? ?? [])
|
||||
.map((p) => HomePodcast.fromJson(p))
|
||||
.toList(),
|
||||
savedCount: json['saved_count'] ?? 0,
|
||||
downloadedCount: json['downloaded_count'] ?? 0,
|
||||
queueCount: json['queue_count'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Playlist {
|
||||
final int playlistId;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String iconName;
|
||||
final int? episodeCount;
|
||||
|
||||
Playlist({
|
||||
required this.playlistId,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.iconName,
|
||||
this.episodeCount,
|
||||
});
|
||||
|
||||
factory Playlist.fromJson(Map<String, dynamic> json) {
|
||||
return Playlist(
|
||||
playlistId: json['playlist_id'] ?? 0,
|
||||
name: json['name'] ?? '',
|
||||
description: json['description'],
|
||||
iconName: json['icon_name'] ?? 'ph-music-notes',
|
||||
episodeCount: json['episode_count'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistResponse {
|
||||
final List<Playlist> playlists;
|
||||
|
||||
PlaylistResponse({required this.playlists});
|
||||
|
||||
factory PlaylistResponse.fromJson(Map<String, dynamic> json) {
|
||||
return PlaylistResponse(
|
||||
playlists: (json['playlists'] as List<dynamic>? ?? [])
|
||||
.map((p) => Playlist.fromJson(p))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
PinePods-0.8.2/mobile/lib/entities/persistable.dart
Normal file
81
PinePods-0.8.2/mobile/lib/entities/persistable.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2020 Ben Hills. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
enum LastState { none, completed, stopped, paused }
|
||||
|
||||
/// This class is used to persist information about the currently playing episode to disk.
|
||||
///
|
||||
/// This allows the background audio service to persist state (whilst the UI is not visible)
|
||||
/// and for the episode play and position details to be restored when the UI becomes visible
|
||||
/// again - either when bringing it to the foreground or upon next start.
|
||||
class Persistable {
|
||||
/// The Podcast GUID.
|
||||
String pguid;
|
||||
|
||||
/// The episode ID (provided by the DB layer).
|
||||
int episodeId;
|
||||
|
||||
/// The current position in seconds;
|
||||
int position;
|
||||
|
||||
/// The current playback state.
|
||||
LastState state;
|
||||
|
||||
/// Date & time episode was last updated.
|
||||
DateTime? lastUpdated;
|
||||
|
||||
Persistable({
|
||||
required this.pguid,
|
||||
required this.episodeId,
|
||||
required this.position,
|
||||
required this.state,
|
||||
this.lastUpdated,
|
||||
});
|
||||
|
||||
Persistable.empty()
|
||||
: pguid = '',
|
||||
episodeId = 0,
|
||||
position = 0,
|
||||
state = LastState.none,
|
||||
lastUpdated = DateTime.now();
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'pguid': pguid,
|
||||
'episodeId': episodeId,
|
||||
'position': position,
|
||||
'state': state.toString(),
|
||||
'lastUpdated': lastUpdated == null ? DateTime.now().millisecondsSinceEpoch : lastUpdated!.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
static Persistable fromMap(Map<String, dynamic> persistable) {
|
||||
var stateString = persistable['state'] as String?;
|
||||
var state = LastState.none;
|
||||
|
||||
if (stateString != null) {
|
||||
switch (stateString) {
|
||||
case 'LastState.completed':
|
||||
state = LastState.completed;
|
||||
break;
|
||||
case 'LastState.stopped':
|
||||
state = LastState.stopped;
|
||||
break;
|
||||
case 'LastState.paused':
|
||||
state = LastState.paused;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var lastUpdated = persistable['lastUpdated'] as int?;
|
||||
|
||||
return Persistable(
|
||||
pguid: persistable['pguid'] as String,
|
||||
episodeId: persistable['episodeId'] as int,
|
||||
position: persistable['position'] as int,
|
||||
state: state,
|
||||
lastUpdated: lastUpdated == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(lastUpdated),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
PinePods-0.8.2/mobile/lib/entities/person.dart
Normal file
64
PinePods-0.8.2/mobile/lib/entities/person.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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:pinepods_mobile/core/extensions.dart';
|
||||
|
||||
/// This class represents a person of interest to the podcast.
|
||||
///
|
||||
/// It is primarily intended to identify people like hosts, co-hosts and guests.
|
||||
class Person {
|
||||
final String name;
|
||||
final String role;
|
||||
final String group;
|
||||
final String? image;
|
||||
final String? link;
|
||||
|
||||
Person({
|
||||
required this.name,
|
||||
this.role = '',
|
||||
this.group = '',
|
||||
String? image = '',
|
||||
String? link = '',
|
||||
}) : image = image?.forceHttps,
|
||||
link = link?.forceHttps;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'name': name,
|
||||
'role': role,
|
||||
'group': group,
|
||||
'image': image,
|
||||
'link': link,
|
||||
};
|
||||
}
|
||||
|
||||
static Person fromMap(Map<String, dynamic> chapter) {
|
||||
return Person(
|
||||
name: chapter['name'] as String? ?? '',
|
||||
role: chapter['role'] as String? ?? '',
|
||||
group: chapter['group'] as String? ?? '',
|
||||
image: chapter['image'] as String? ?? '',
|
||||
link: chapter['link'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Person &&
|
||||
runtimeType == other.runtimeType &&
|
||||
name == other.name &&
|
||||
role == other.role &&
|
||||
group == other.group &&
|
||||
image == other.image &&
|
||||
link == other.link;
|
||||
|
||||
@override
|
||||
int get hashCode => name.hashCode ^ role.hashCode ^ group.hashCode ^ image.hashCode ^ link.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Person{name: $name, role: $role, group: $group, image: $image, link: $link}';
|
||||
}
|
||||
}
|
||||
142
PinePods-0.8.2/mobile/lib/entities/pinepods_episode.dart
Normal file
142
PinePods-0.8.2/mobile/lib/entities/pinepods_episode.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
class PinepodsEpisode {
|
||||
final String podcastName;
|
||||
final String episodeTitle;
|
||||
final String episodePubDate;
|
||||
final String episodeDescription;
|
||||
final String episodeArtwork;
|
||||
final String episodeUrl;
|
||||
final int episodeDuration;
|
||||
final int? listenDuration;
|
||||
final int episodeId;
|
||||
final bool completed;
|
||||
final bool saved;
|
||||
final bool queued;
|
||||
final bool downloaded;
|
||||
final bool isYoutube;
|
||||
final int? podcastId;
|
||||
|
||||
PinepodsEpisode({
|
||||
required this.podcastName,
|
||||
required this.episodeTitle,
|
||||
required this.episodePubDate,
|
||||
required this.episodeDescription,
|
||||
required this.episodeArtwork,
|
||||
required this.episodeUrl,
|
||||
required this.episodeDuration,
|
||||
this.listenDuration,
|
||||
required this.episodeId,
|
||||
required this.completed,
|
||||
required this.saved,
|
||||
required this.queued,
|
||||
required this.downloaded,
|
||||
required this.isYoutube,
|
||||
this.podcastId,
|
||||
});
|
||||
|
||||
factory PinepodsEpisode.fromJson(Map<String, dynamic> json) {
|
||||
return PinepodsEpisode(
|
||||
podcastName: json['Podcastname'] ?? json['podcastname'] ?? '',
|
||||
episodeTitle: json['Episodetitle'] ?? json['episodetitle'] ?? '',
|
||||
episodePubDate: json['Episodepubdate'] ?? json['episodepubdate'] ?? '',
|
||||
episodeDescription: json['Episodedescription'] ?? json['episodedescription'] ?? '',
|
||||
episodeArtwork: json['Episodeartwork'] ?? json['episodeartwork'] ?? '',
|
||||
episodeUrl: json['Episodeurl'] ?? json['episodeurl'] ?? '',
|
||||
episodeDuration: json['Episodeduration'] ?? json['episodeduration'] ?? 0,
|
||||
listenDuration: json['Listenduration'] ?? json['listenduration'],
|
||||
episodeId: json['Episodeid'] ?? json['episodeid'] ?? 0,
|
||||
completed: json['Completed'] ?? json['completed'] ?? false,
|
||||
saved: json['Saved'] ?? json['saved'] ?? false,
|
||||
queued: json['Queued'] ?? json['queued'] ?? false,
|
||||
downloaded: json['Downloaded'] ?? json['downloaded'] ?? false,
|
||||
isYoutube: json['Is_youtube'] ?? json['is_youtube'] ?? false,
|
||||
podcastId: json['Podcastid'] ?? json['podcastid'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'podcastname': podcastName,
|
||||
'episodetitle': episodeTitle,
|
||||
'episodepubdate': episodePubDate,
|
||||
'episodedescription': episodeDescription,
|
||||
'episodeartwork': episodeArtwork,
|
||||
'episodeurl': episodeUrl,
|
||||
'episodeduration': episodeDuration,
|
||||
'listenduration': listenDuration,
|
||||
'episodeid': episodeId,
|
||||
'completed': completed,
|
||||
'saved': saved,
|
||||
'queued': queued,
|
||||
'downloaded': downloaded,
|
||||
'is_youtube': isYoutube,
|
||||
'podcastid': podcastId,
|
||||
};
|
||||
}
|
||||
|
||||
/// Format duration from seconds to MM:SS or HH:MM:SS
|
||||
String get formattedDuration {
|
||||
if (episodeDuration <= 0) return '0:00';
|
||||
|
||||
final hours = episodeDuration ~/ 3600;
|
||||
final minutes = (episodeDuration % 3600) ~/ 60;
|
||||
final seconds = episodeDuration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Get progress percentage (0-100)
|
||||
double get progressPercentage {
|
||||
if (episodeDuration <= 0 || listenDuration == null) return 0.0;
|
||||
return (listenDuration! / episodeDuration * 100).clamp(0.0, 100.0);
|
||||
}
|
||||
|
||||
/// Check if episode has been started (has some listen duration)
|
||||
bool get isStarted {
|
||||
return listenDuration != null && listenDuration! > 0;
|
||||
}
|
||||
|
||||
/// Format listen duration from seconds to MM:SS or HH:MM:SS
|
||||
String get formattedListenDuration {
|
||||
if (listenDuration == null || listenDuration! <= 0) return '0:00';
|
||||
|
||||
final duration = listenDuration!;
|
||||
final hours = duration ~/ 3600;
|
||||
final minutes = (duration % 3600) ~/ 60;
|
||||
final seconds = duration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the publish date to a more readable format
|
||||
String get formattedPubDate {
|
||||
try {
|
||||
final date = DateTime.parse(episodePubDate);
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return 'Today';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Yesterday';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays} days ago';
|
||||
} else if (difference.inDays < 30) {
|
||||
final weeks = (difference.inDays / 7).floor();
|
||||
return weeks == 1 ? '1 week ago' : '$weeks weeks ago';
|
||||
} else {
|
||||
final months = (difference.inDays / 30).floor();
|
||||
return months == 1 ? '1 month ago' : '$months months ago';
|
||||
}
|
||||
} catch (e) {
|
||||
return episodePubDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
359
PinePods-0.8.2/mobile/lib/entities/pinepods_search.dart
Normal file
359
PinePods-0.8.2/mobile/lib/entities/pinepods_search.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
// lib/entities/pinepods_search.dart
|
||||
|
||||
class PinepodsSearchResult {
|
||||
final String? status;
|
||||
final int? resultCount;
|
||||
final List<PinepodsPodcast>? feeds;
|
||||
final List<PinepodsITunesPodcast>? results;
|
||||
|
||||
PinepodsSearchResult({
|
||||
this.status,
|
||||
this.resultCount,
|
||||
this.feeds,
|
||||
this.results,
|
||||
});
|
||||
|
||||
factory PinepodsSearchResult.fromJson(Map<String, dynamic> json) {
|
||||
return PinepodsSearchResult(
|
||||
status: json['status'] as String?,
|
||||
resultCount: json['resultCount'] as int?,
|
||||
feeds: json['feeds'] != null
|
||||
? (json['feeds'] as List)
|
||||
.map((item) => PinepodsPodcast.fromJson(item as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
results: json['results'] != null
|
||||
? (json['results'] as List)
|
||||
.map((item) => PinepodsITunesPodcast.fromJson(item as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'status': status,
|
||||
'resultCount': resultCount,
|
||||
'feeds': feeds?.map((item) => item.toJson()).toList(),
|
||||
'results': results?.map((item) => item.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
List<UnifiedPinepodsPodcast> getUnifiedPodcasts() {
|
||||
final List<UnifiedPinepodsPodcast> unified = [];
|
||||
|
||||
// Add PodcastIndex results
|
||||
if (feeds != null) {
|
||||
unified.addAll(feeds!.map((podcast) => UnifiedPinepodsPodcast.fromPodcast(podcast)));
|
||||
}
|
||||
|
||||
// Add iTunes results
|
||||
if (results != null) {
|
||||
unified.addAll(results!.map((podcast) => UnifiedPinepodsPodcast.fromITunesPodcast(podcast)));
|
||||
}
|
||||
|
||||
return unified;
|
||||
}
|
||||
}
|
||||
|
||||
class PinepodsPodcast {
|
||||
final int id;
|
||||
final String title;
|
||||
final String url;
|
||||
final String originalUrl;
|
||||
final String link;
|
||||
final String description;
|
||||
final String author;
|
||||
final String ownerName;
|
||||
final String image;
|
||||
final String artwork;
|
||||
final int lastUpdateTime;
|
||||
final Map<String, String>? categories;
|
||||
final bool explicit;
|
||||
final int episodeCount;
|
||||
|
||||
PinepodsPodcast({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.originalUrl,
|
||||
required this.link,
|
||||
required this.description,
|
||||
required this.author,
|
||||
required this.ownerName,
|
||||
required this.image,
|
||||
required this.artwork,
|
||||
required this.lastUpdateTime,
|
||||
this.categories,
|
||||
required this.explicit,
|
||||
required this.episodeCount,
|
||||
});
|
||||
|
||||
factory PinepodsPodcast.fromJson(Map<String, dynamic> json) {
|
||||
return PinepodsPodcast(
|
||||
id: json['id'] as int,
|
||||
title: json['title'] as String? ?? '',
|
||||
url: json['url'] as String? ?? '',
|
||||
originalUrl: json['originalUrl'] as String? ?? '',
|
||||
link: json['link'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
author: json['author'] as String? ?? '',
|
||||
ownerName: json['ownerName'] as String? ?? '',
|
||||
image: json['image'] as String? ?? '',
|
||||
artwork: json['artwork'] as String? ?? '',
|
||||
lastUpdateTime: json['lastUpdateTime'] as int? ?? 0,
|
||||
categories: json['categories'] != null
|
||||
? Map<String, String>.from(json['categories'] as Map)
|
||||
: null,
|
||||
explicit: json['explicit'] as bool? ?? false,
|
||||
episodeCount: json['episodeCount'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'url': url,
|
||||
'originalUrl': originalUrl,
|
||||
'link': link,
|
||||
'description': description,
|
||||
'author': author,
|
||||
'ownerName': ownerName,
|
||||
'image': image,
|
||||
'artwork': artwork,
|
||||
'lastUpdateTime': lastUpdateTime,
|
||||
'categories': categories,
|
||||
'explicit': explicit,
|
||||
'episodeCount': episodeCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PinepodsITunesPodcast {
|
||||
final String wrapperType;
|
||||
final String kind;
|
||||
final int collectionId;
|
||||
final int trackId;
|
||||
final String artistName;
|
||||
final String trackName;
|
||||
final String collectionViewUrl;
|
||||
final String feedUrl;
|
||||
final String artworkUrl100;
|
||||
final String releaseDate;
|
||||
final List<String> genres;
|
||||
final String collectionExplicitness;
|
||||
final int? trackCount;
|
||||
|
||||
PinepodsITunesPodcast({
|
||||
required this.wrapperType,
|
||||
required this.kind,
|
||||
required this.collectionId,
|
||||
required this.trackId,
|
||||
required this.artistName,
|
||||
required this.trackName,
|
||||
required this.collectionViewUrl,
|
||||
required this.feedUrl,
|
||||
required this.artworkUrl100,
|
||||
required this.releaseDate,
|
||||
required this.genres,
|
||||
required this.collectionExplicitness,
|
||||
this.trackCount,
|
||||
});
|
||||
|
||||
factory PinepodsITunesPodcast.fromJson(Map<String, dynamic> json) {
|
||||
return PinepodsITunesPodcast(
|
||||
wrapperType: json['wrapperType'] as String? ?? '',
|
||||
kind: json['kind'] as String? ?? '',
|
||||
collectionId: json['collectionId'] as int? ?? 0,
|
||||
trackId: json['trackId'] as int? ?? 0,
|
||||
artistName: json['artistName'] as String? ?? '',
|
||||
trackName: json['trackName'] as String? ?? '',
|
||||
collectionViewUrl: json['collectionViewUrl'] as String? ?? '',
|
||||
feedUrl: json['feedUrl'] as String? ?? '',
|
||||
artworkUrl100: json['artworkUrl100'] as String? ?? '',
|
||||
releaseDate: json['releaseDate'] as String? ?? '',
|
||||
genres: json['genres'] != null
|
||||
? List<String>.from(json['genres'] as List)
|
||||
: [],
|
||||
collectionExplicitness: json['collectionExplicitness'] as String? ?? '',
|
||||
trackCount: json['trackCount'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'wrapperType': wrapperType,
|
||||
'kind': kind,
|
||||
'collectionId': collectionId,
|
||||
'trackId': trackId,
|
||||
'artistName': artistName,
|
||||
'trackName': trackName,
|
||||
'collectionViewUrl': collectionViewUrl,
|
||||
'feedUrl': feedUrl,
|
||||
'artworkUrl100': artworkUrl100,
|
||||
'releaseDate': releaseDate,
|
||||
'genres': genres,
|
||||
'collectionExplicitness': collectionExplicitness,
|
||||
'trackCount': trackCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedPinepodsPodcast {
|
||||
final int id;
|
||||
final int indexId;
|
||||
final String title;
|
||||
final String url;
|
||||
final String originalUrl;
|
||||
final String link;
|
||||
final String description;
|
||||
final String author;
|
||||
final String ownerName;
|
||||
final String image;
|
||||
final String artwork;
|
||||
final int lastUpdateTime;
|
||||
final Map<String, String>? categories;
|
||||
final bool explicit;
|
||||
final int episodeCount;
|
||||
|
||||
UnifiedPinepodsPodcast({
|
||||
required this.id,
|
||||
required this.indexId,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.originalUrl,
|
||||
required this.link,
|
||||
required this.description,
|
||||
required this.author,
|
||||
required this.ownerName,
|
||||
required this.image,
|
||||
required this.artwork,
|
||||
required this.lastUpdateTime,
|
||||
this.categories,
|
||||
required this.explicit,
|
||||
required this.episodeCount,
|
||||
});
|
||||
|
||||
factory UnifiedPinepodsPodcast.fromJson(Map<String, dynamic> json) {
|
||||
return UnifiedPinepodsPodcast(
|
||||
id: json['id'] as int? ?? 0,
|
||||
indexId: json['indexId'] as int? ?? 0,
|
||||
title: json['title'] as String? ?? '',
|
||||
url: json['url'] as String? ?? '',
|
||||
originalUrl: json['originalUrl'] as String? ?? '',
|
||||
link: json['link'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
author: json['author'] as String? ?? '',
|
||||
ownerName: json['ownerName'] as String? ?? '',
|
||||
image: json['image'] as String? ?? '',
|
||||
artwork: json['artwork'] as String? ?? '',
|
||||
lastUpdateTime: json['lastUpdateTime'] as int? ?? 0,
|
||||
categories: json['categories'] != null
|
||||
? Map<String, String>.from(json['categories'] as Map)
|
||||
: null,
|
||||
explicit: json['explicit'] as bool? ?? false,
|
||||
episodeCount: json['episodeCount'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'indexId': indexId,
|
||||
'title': title,
|
||||
'url': url,
|
||||
'originalUrl': originalUrl,
|
||||
'link': link,
|
||||
'description': description,
|
||||
'author': author,
|
||||
'ownerName': ownerName,
|
||||
'image': image,
|
||||
'artwork': artwork,
|
||||
'lastUpdateTime': lastUpdateTime,
|
||||
'categories': categories,
|
||||
'explicit': explicit,
|
||||
'episodeCount': episodeCount,
|
||||
};
|
||||
}
|
||||
|
||||
factory UnifiedPinepodsPodcast.fromPodcast(PinepodsPodcast podcast) {
|
||||
return UnifiedPinepodsPodcast(
|
||||
id: 0, // Internal database ID - will be fetched when needed
|
||||
indexId: podcast.id, // Podcast index ID
|
||||
title: podcast.title,
|
||||
url: podcast.url,
|
||||
originalUrl: podcast.originalUrl,
|
||||
author: podcast.author,
|
||||
ownerName: podcast.ownerName,
|
||||
description: podcast.description,
|
||||
image: podcast.image,
|
||||
link: podcast.link,
|
||||
artwork: podcast.artwork,
|
||||
lastUpdateTime: podcast.lastUpdateTime,
|
||||
categories: podcast.categories,
|
||||
explicit: podcast.explicit,
|
||||
episodeCount: podcast.episodeCount,
|
||||
);
|
||||
}
|
||||
|
||||
factory UnifiedPinepodsPodcast.fromITunesPodcast(PinepodsITunesPodcast podcast) {
|
||||
// Convert genres list to map
|
||||
final Map<String, String> genreMap = {};
|
||||
for (int i = 0; i < podcast.genres.length; i++) {
|
||||
genreMap[i.toString()] = podcast.genres[i];
|
||||
}
|
||||
|
||||
// Parse release date to timestamp
|
||||
int timestamp = 0;
|
||||
try {
|
||||
final dateTime = DateTime.parse(podcast.releaseDate);
|
||||
timestamp = dateTime.millisecondsSinceEpoch ~/ 1000;
|
||||
} catch (e) {
|
||||
// Default to 0 if parsing fails
|
||||
}
|
||||
|
||||
return UnifiedPinepodsPodcast(
|
||||
id: podcast.trackId,
|
||||
indexId: 0,
|
||||
title: podcast.trackName,
|
||||
url: podcast.feedUrl,
|
||||
originalUrl: podcast.feedUrl,
|
||||
author: podcast.artistName,
|
||||
ownerName: podcast.artistName,
|
||||
description: 'Descriptions not provided by iTunes',
|
||||
image: podcast.artworkUrl100,
|
||||
link: podcast.collectionViewUrl,
|
||||
artwork: podcast.artworkUrl100,
|
||||
lastUpdateTime: timestamp,
|
||||
categories: genreMap,
|
||||
explicit: podcast.collectionExplicitness == 'explicit',
|
||||
episodeCount: podcast.trackCount ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SearchProvider {
|
||||
podcastIndex,
|
||||
itunes,
|
||||
}
|
||||
|
||||
extension SearchProviderExtension on SearchProvider {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case SearchProvider.podcastIndex:
|
||||
return 'Podcast Index';
|
||||
case SearchProvider.itunes:
|
||||
return 'iTunes';
|
||||
}
|
||||
}
|
||||
|
||||
String get value {
|
||||
switch (this) {
|
||||
case SearchProvider.podcastIndex:
|
||||
return 'podcast_index';
|
||||
case SearchProvider.itunes:
|
||||
return 'itunes';
|
||||
}
|
||||
}
|
||||
}
|
||||
248
PinePods-0.8.2/mobile/lib/entities/podcast.dart
Normal file
248
PinePods-0.8.2/mobile/lib/entities/podcast.dart
Normal file
@@ -0,0 +1,248 @@
|
||||
// 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:pinepods_mobile/core/extensions.dart';
|
||||
import 'package:pinepods_mobile/entities/funding.dart';
|
||||
import 'package:pinepods_mobile/entities/person.dart';
|
||||
import 'package:podcast_search/podcast_search.dart' as search;
|
||||
|
||||
import 'episode.dart';
|
||||
|
||||
enum PodcastEpisodeFilter {
|
||||
none(id: 0),
|
||||
started(id: 1),
|
||||
played(id: 2),
|
||||
notPlayed(id: 3);
|
||||
|
||||
const PodcastEpisodeFilter({required this.id});
|
||||
|
||||
final int id;
|
||||
}
|
||||
|
||||
enum PodcastEpisodeSort {
|
||||
none(id: 0),
|
||||
latestFirst(id: 1),
|
||||
earliestFirst(id: 2),
|
||||
alphabeticalAscending(id: 3),
|
||||
alphabeticalDescending(id: 4);
|
||||
|
||||
const PodcastEpisodeSort({required this.id});
|
||||
|
||||
final int id;
|
||||
}
|
||||
|
||||
/// A class that represents an instance of a podcast.
|
||||
///
|
||||
/// When persisted to disk this represents a podcast that is being followed.
|
||||
class Podcast {
|
||||
/// Database ID
|
||||
int? id;
|
||||
|
||||
/// Unique identifier for podcast.
|
||||
final String? guid;
|
||||
|
||||
/// The link to the podcast RSS feed.
|
||||
final String url;
|
||||
|
||||
/// RSS link URL.
|
||||
final String? link;
|
||||
|
||||
/// Podcast title.
|
||||
final String title;
|
||||
|
||||
/// Podcast description. Can be either plain text or HTML.
|
||||
final String? description;
|
||||
|
||||
/// URL to the full size artwork image.
|
||||
final String? imageUrl;
|
||||
|
||||
/// URL for thumbnail version of artwork image. Not contained within
|
||||
/// the RSS but may be calculated or provided within search results.
|
||||
final String? thumbImageUrl;
|
||||
|
||||
/// Copyright owner of the podcast.
|
||||
final String? copyright;
|
||||
|
||||
/// Zero or more funding links.
|
||||
final List<Funding>? funding;
|
||||
|
||||
PodcastEpisodeFilter filter;
|
||||
|
||||
PodcastEpisodeSort sort;
|
||||
|
||||
/// Date and time user subscribed to the podcast.
|
||||
DateTime? subscribedDate;
|
||||
|
||||
/// Date and time podcast was last updated/refreshed.
|
||||
DateTime? _lastUpdated;
|
||||
|
||||
/// One or more episodes for this podcast.
|
||||
List<Episode> episodes;
|
||||
|
||||
final List<Person>? persons;
|
||||
|
||||
bool newEpisodes;
|
||||
bool updatedEpisodes = false;
|
||||
|
||||
Podcast({
|
||||
required this.guid,
|
||||
required String url,
|
||||
required this.link,
|
||||
required this.title,
|
||||
this.id,
|
||||
this.description,
|
||||
String? imageUrl,
|
||||
String? thumbImageUrl,
|
||||
this.copyright,
|
||||
this.subscribedDate,
|
||||
this.funding,
|
||||
this.filter = PodcastEpisodeFilter.none,
|
||||
this.sort = PodcastEpisodeSort.none,
|
||||
this.episodes = const <Episode>[],
|
||||
this.newEpisodes = false,
|
||||
this.persons,
|
||||
DateTime? lastUpdated,
|
||||
}) : url = url.forceHttps,
|
||||
imageUrl = imageUrl?.forceHttps,
|
||||
thumbImageUrl = thumbImageUrl?.forceHttps {
|
||||
_lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
factory Podcast.fromUrl({required String url}) => Podcast(
|
||||
url: url,
|
||||
guid: '',
|
||||
link: '',
|
||||
title: '',
|
||||
description: '',
|
||||
thumbImageUrl: null,
|
||||
imageUrl: null,
|
||||
copyright: '',
|
||||
funding: <Funding>[],
|
||||
persons: <Person>[],
|
||||
);
|
||||
|
||||
factory Podcast.fromSearchResultItem(search.Item item) => Podcast(
|
||||
guid: item.guid ?? '',
|
||||
url: item.feedUrl ?? '',
|
||||
link: item.feedUrl,
|
||||
title: item.trackName!,
|
||||
description: '',
|
||||
imageUrl: item.bestArtworkUrl ?? item.artworkUrl,
|
||||
thumbImageUrl: item.thumbnailArtworkUrl,
|
||||
funding: const <Funding>[],
|
||||
copyright: item.artistName,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'guid': guid,
|
||||
'title': title,
|
||||
'copyright': copyright ?? '',
|
||||
'description': description ?? '',
|
||||
'url': url,
|
||||
'link': link ?? '',
|
||||
'imageUrl': imageUrl ?? '',
|
||||
'thumbImageUrl': thumbImageUrl ?? '',
|
||||
'subscribedDate': subscribedDate?.millisecondsSinceEpoch.toString() ?? '',
|
||||
'filter': filter.id,
|
||||
'sort': sort.id,
|
||||
'funding': (funding ?? <Funding>[]).map((funding) => funding.toMap()).toList(growable: false),
|
||||
'person': (persons ?? <Person>[]).map((persons) => persons.toMap()).toList(growable: false),
|
||||
'lastUpdated': _lastUpdated?.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
static Podcast fromMap(int key, Map<String, dynamic> podcast) {
|
||||
final sds = podcast['subscribedDate'] as String?;
|
||||
final lus = podcast['lastUpdated'] as int?;
|
||||
final funding = <Funding>[];
|
||||
final persons = <Person>[];
|
||||
var filter = PodcastEpisodeFilter.none;
|
||||
var sort = PodcastEpisodeSort.none;
|
||||
|
||||
var sd = DateTime.now();
|
||||
var lastUpdated = DateTime(1971, 1, 1);
|
||||
|
||||
if (sds != null && sds.isNotEmpty && int.tryParse(sds) != null) {
|
||||
sd = DateTime.fromMillisecondsSinceEpoch(int.parse(sds));
|
||||
}
|
||||
|
||||
if (lus != null) {
|
||||
lastUpdated = DateTime.fromMillisecondsSinceEpoch(lus);
|
||||
}
|
||||
|
||||
if (podcast['funding'] != null) {
|
||||
for (var chapter in (podcast['funding'] as List)) {
|
||||
if (chapter is Map<String, dynamic>) {
|
||||
funding.add(Funding.fromMap(chapter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (podcast['persons'] != null) {
|
||||
for (var person in (podcast['persons'] as List)) {
|
||||
if (person is Map<String, dynamic>) {
|
||||
persons.add(Person.fromMap(person));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (podcast['filter'] != null) {
|
||||
var filterValue = (podcast['filter'] as int);
|
||||
|
||||
filter = switch (filterValue) {
|
||||
1 => PodcastEpisodeFilter.started,
|
||||
2 => PodcastEpisodeFilter.played,
|
||||
3 => PodcastEpisodeFilter.notPlayed,
|
||||
_ => PodcastEpisodeFilter.none,
|
||||
};
|
||||
}
|
||||
|
||||
if (podcast['sort'] != null) {
|
||||
var sortValue = (podcast['sort'] as int);
|
||||
|
||||
sort = switch (sortValue) {
|
||||
1 => PodcastEpisodeSort.latestFirst,
|
||||
2 => PodcastEpisodeSort.earliestFirst,
|
||||
3 => PodcastEpisodeSort.alphabeticalAscending,
|
||||
4 => PodcastEpisodeSort.alphabeticalDescending,
|
||||
_ => PodcastEpisodeSort.none,
|
||||
};
|
||||
}
|
||||
|
||||
return Podcast(
|
||||
id: key,
|
||||
guid: podcast['guid'] as String,
|
||||
link: podcast['link'] as String?,
|
||||
title: podcast['title'] as String,
|
||||
copyright: podcast['copyright'] as String?,
|
||||
description: podcast['description'] as String?,
|
||||
url: podcast['url'] as String,
|
||||
imageUrl: podcast['imageUrl'] as String?,
|
||||
thumbImageUrl: podcast['thumbImageUrl'] as String?,
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
funding: funding,
|
||||
persons: persons,
|
||||
subscribedDate: sd,
|
||||
lastUpdated: lastUpdated,
|
||||
);
|
||||
}
|
||||
|
||||
bool get subscribed => id != null;
|
||||
|
||||
DateTime get lastUpdated => _lastUpdated ?? DateTime(1970, 1, 1);
|
||||
|
||||
set lastUpdated(DateTime value) {
|
||||
_lastUpdated = value;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Podcast && runtimeType == other.runtimeType && guid == other.guid && url == other.url;
|
||||
|
||||
@override
|
||||
int get hashCode => guid.hashCode ^ url.hashCode;
|
||||
}
|
||||
31
PinePods-0.8.2/mobile/lib/entities/queue.dart
Normal file
31
PinePods-0.8.2/mobile/lib/entities/queue.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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.
|
||||
|
||||
/// The current persistable queue.
|
||||
class Queue {
|
||||
List<String> guids = <String>[];
|
||||
|
||||
Queue({
|
||||
required this.guids,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'q': guids,
|
||||
};
|
||||
}
|
||||
|
||||
static Queue fromMap(int key, Map<String, dynamic> guids) {
|
||||
var g = guids['q'] as List<dynamic>?;
|
||||
var result = <String>[];
|
||||
|
||||
if (g != null) {
|
||||
result = g.map((dynamic e) => e.toString()).toList();
|
||||
}
|
||||
|
||||
return Queue(
|
||||
guids: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
PinePods-0.8.2/mobile/lib/entities/search_providers.dart
Normal file
16
PinePods-0.8.2/mobile/lib/entities/search_providers.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
/// PinePods can support multiple search providers.
|
||||
///
|
||||
/// This class represents a provider.
|
||||
class SearchProvider {
|
||||
final String key;
|
||||
final String name;
|
||||
|
||||
SearchProvider({
|
||||
required this.key,
|
||||
required this.name,
|
||||
});
|
||||
}
|
||||
32
PinePods-0.8.2/mobile/lib/entities/sleep.dart
Normal file
32
PinePods-0.8.2/mobile/lib/entities/sleep.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
enum SleepType {
|
||||
none,
|
||||
time,
|
||||
episode,
|
||||
}
|
||||
|
||||
final class Sleep {
|
||||
final SleepType type;
|
||||
final Duration duration;
|
||||
late DateTime endTime;
|
||||
|
||||
Sleep({
|
||||
required this.type,
|
||||
this.duration = const Duration(milliseconds: 0),
|
||||
}) {
|
||||
endTime = DateTime.now().add(duration);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Sleep && runtimeType == other.runtimeType && type == other.type && duration == other.duration;
|
||||
|
||||
@override
|
||||
int get hashCode => type.hashCode ^ duration.hashCode;
|
||||
|
||||
Duration get timeRemaining => endTime.difference(DateTime.now());
|
||||
}
|
||||
213
PinePods-0.8.2/mobile/lib/entities/transcript.dart
Normal file
213
PinePods-0.8.2/mobile/lib/entities/transcript.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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:pinepods_mobile/core/extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum TranscriptFormat {
|
||||
json,
|
||||
subrip,
|
||||
html,
|
||||
unsupported,
|
||||
}
|
||||
|
||||
/// This class represents a Podcasting 2.0 transcript URL.
|
||||
///
|
||||
/// [docs](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript)
|
||||
class TranscriptUrl {
|
||||
final String url;
|
||||
final TranscriptFormat type;
|
||||
final String? language;
|
||||
final String? rel;
|
||||
final DateTime? lastUpdated;
|
||||
|
||||
TranscriptUrl({
|
||||
required String url,
|
||||
required this.type,
|
||||
this.language = '',
|
||||
this.rel = '',
|
||||
this.lastUpdated,
|
||||
}) : url = url.forceHttps;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
var t = 0;
|
||||
|
||||
switch (type) {
|
||||
case TranscriptFormat.subrip:
|
||||
t = 0;
|
||||
break;
|
||||
case TranscriptFormat.json:
|
||||
t = 1;
|
||||
break;
|
||||
case TranscriptFormat.html:
|
||||
t = 2;
|
||||
break;
|
||||
case TranscriptFormat.unsupported:
|
||||
t = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
return <String, dynamic>{
|
||||
'url': url,
|
||||
'type': t,
|
||||
'lang': language,
|
||||
'rel': rel,
|
||||
'lastUpdated': DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
static TranscriptUrl fromMap(Map<String, dynamic> transcript) {
|
||||
var ts = transcript['type'] as int? ?? 2;
|
||||
var t = TranscriptFormat.unsupported;
|
||||
|
||||
switch (ts) {
|
||||
case 0:
|
||||
t = TranscriptFormat.subrip;
|
||||
break;
|
||||
case 1:
|
||||
t = TranscriptFormat.json;
|
||||
break;
|
||||
case 2:
|
||||
t = TranscriptFormat.html;
|
||||
break;
|
||||
case 3:
|
||||
t = TranscriptFormat.unsupported;
|
||||
break;
|
||||
}
|
||||
|
||||
return TranscriptUrl(
|
||||
url: transcript['url'] as String,
|
||||
language: transcript['lang'] as String?,
|
||||
rel: transcript['rel'] as String?,
|
||||
type: t,
|
||||
lastUpdated: transcript['lastUpdated'] == null
|
||||
? DateTime.now()
|
||||
: DateTime.fromMillisecondsSinceEpoch(transcript['lastUpdated'] as int),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TranscriptUrl &&
|
||||
runtimeType == other.runtimeType &&
|
||||
url == other.url &&
|
||||
type == other.type &&
|
||||
language == other.language &&
|
||||
rel == other.rel;
|
||||
|
||||
@override
|
||||
int get hashCode => url.hashCode ^ type.hashCode ^ language.hashCode ^ rel.hashCode;
|
||||
}
|
||||
|
||||
/// This class represents a Podcasting 2.0 transcript container.
|
||||
/// [docs](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript)
|
||||
class Transcript {
|
||||
int? id;
|
||||
String? guid;
|
||||
final List<Subtitle> subtitles;
|
||||
DateTime? lastUpdated;
|
||||
bool filtered;
|
||||
|
||||
Transcript({
|
||||
this.id,
|
||||
this.guid,
|
||||
this.subtitles = const <Subtitle>[],
|
||||
this.filtered = false,
|
||||
this.lastUpdated,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'guid': guid,
|
||||
'subtitles': (subtitles).map((subtitle) => subtitle.toMap()).toList(growable: false),
|
||||
'lastUpdated': DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
static Transcript fromMap(int? key, Map<String, dynamic> transcript) {
|
||||
var subtitles = <Subtitle>[];
|
||||
|
||||
if (transcript['subtitles'] != null) {
|
||||
for (var subtitle in (transcript['subtitles'] as List)) {
|
||||
if (subtitle is Map<String, dynamic>) {
|
||||
subtitles.add(Subtitle.fromMap(subtitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Transcript(
|
||||
id: key,
|
||||
guid: transcript['guid'] as String? ?? '',
|
||||
subtitles: subtitles,
|
||||
lastUpdated: transcript['lastUpdated'] == null
|
||||
? DateTime.now()
|
||||
: DateTime.fromMillisecondsSinceEpoch(transcript['lastUpdated'] as int),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Transcript &&
|
||||
runtimeType == other.runtimeType &&
|
||||
guid == other.guid &&
|
||||
listEquals(subtitles, other.subtitles);
|
||||
|
||||
@override
|
||||
int get hashCode => guid.hashCode ^ subtitles.hashCode;
|
||||
|
||||
bool get transcriptAvailable => (subtitles.isNotEmpty || filtered);
|
||||
}
|
||||
|
||||
/// Represents an individual line within a transcript.
|
||||
class Subtitle {
|
||||
final int index;
|
||||
final Duration start;
|
||||
Duration? end;
|
||||
String? data;
|
||||
String speaker;
|
||||
|
||||
Subtitle({
|
||||
required this.index,
|
||||
required this.start,
|
||||
this.end,
|
||||
this.data,
|
||||
this.speaker = '',
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'i': index,
|
||||
'start': start.inMilliseconds,
|
||||
'end': end!.inMilliseconds,
|
||||
'speaker': speaker,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
|
||||
static Subtitle fromMap(Map<String, dynamic> subtitle) {
|
||||
return Subtitle(
|
||||
index: subtitle['i'] as int? ?? 0,
|
||||
start: Duration(milliseconds: subtitle['start'] as int? ?? 0),
|
||||
end: Duration(milliseconds: subtitle['end'] as int? ?? 0),
|
||||
speaker: subtitle['speaker'] as String? ?? '',
|
||||
data: subtitle['data'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Subtitle &&
|
||||
runtimeType == other.runtimeType &&
|
||||
index == other.index &&
|
||||
start == other.start &&
|
||||
end == other.end &&
|
||||
data == other.data &&
|
||||
speaker == other.speaker;
|
||||
|
||||
@override
|
||||
int get hashCode => index.hashCode ^ start.hashCode ^ end.hashCode ^ data.hashCode ^ speaker.hashCode;
|
||||
}
|
||||
91
PinePods-0.8.2/mobile/lib/entities/user_stats.dart
Normal file
91
PinePods-0.8.2/mobile/lib/entities/user_stats.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
class UserStats {
|
||||
final String userCreated;
|
||||
final int podcastsPlayed;
|
||||
final int timeListened;
|
||||
final int podcastsAdded;
|
||||
final int episodesSaved;
|
||||
final int episodesDownloaded;
|
||||
final String gpodderUrl;
|
||||
final String podSyncType;
|
||||
|
||||
UserStats({
|
||||
required this.userCreated,
|
||||
required this.podcastsPlayed,
|
||||
required this.timeListened,
|
||||
required this.podcastsAdded,
|
||||
required this.episodesSaved,
|
||||
required this.episodesDownloaded,
|
||||
required this.gpodderUrl,
|
||||
required this.podSyncType,
|
||||
});
|
||||
|
||||
factory UserStats.fromJson(Map<String, dynamic> json) {
|
||||
return UserStats(
|
||||
userCreated: json['UserCreated'] ?? '',
|
||||
podcastsPlayed: json['PodcastsPlayed'] ?? 0,
|
||||
timeListened: json['TimeListened'] ?? 0,
|
||||
podcastsAdded: json['PodcastsAdded'] ?? 0,
|
||||
episodesSaved: json['EpisodesSaved'] ?? 0,
|
||||
episodesDownloaded: json['EpisodesDownloaded'] ?? 0,
|
||||
gpodderUrl: json['GpodderUrl'] ?? '',
|
||||
podSyncType: json['Pod_Sync_Type'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'UserCreated': userCreated,
|
||||
'PodcastsPlayed': podcastsPlayed,
|
||||
'TimeListened': timeListened,
|
||||
'PodcastsAdded': podcastsAdded,
|
||||
'EpisodesSaved': episodesSaved,
|
||||
'EpisodesDownloaded': episodesDownloaded,
|
||||
'GpodderUrl': gpodderUrl,
|
||||
'Pod_Sync_Type': podSyncType,
|
||||
};
|
||||
}
|
||||
|
||||
// Format time listened from minutes to human readable
|
||||
String get formattedTimeListened {
|
||||
if (timeListened <= 0) return '0 minutes';
|
||||
|
||||
final hours = timeListened ~/ 60;
|
||||
final minutes = timeListened % 60;
|
||||
|
||||
if (hours == 0) {
|
||||
return '$minutes minute${minutes != 1 ? 's' : ''}';
|
||||
} else if (minutes == 0) {
|
||||
return '$hours hour${hours != 1 ? 's' : ''}';
|
||||
} else {
|
||||
return '$hours hour${hours != 1 ? 's' : ''} $minutes minute${minutes != 1 ? 's' : ''}';
|
||||
}
|
||||
}
|
||||
|
||||
// Format user created date
|
||||
String get formattedUserCreated {
|
||||
try {
|
||||
final date = DateTime.parse(userCreated);
|
||||
return '${date.day}/${date.month}/${date.year}';
|
||||
} catch (e) {
|
||||
return userCreated;
|
||||
}
|
||||
}
|
||||
|
||||
// Get sync status description
|
||||
String get syncStatusDescription {
|
||||
switch (podSyncType.toLowerCase()) {
|
||||
case 'none':
|
||||
return 'Not Syncing';
|
||||
case 'gpodder':
|
||||
if (gpodderUrl == 'http://localhost:8042') {
|
||||
return 'Internal gpodder';
|
||||
} else {
|
||||
return 'External gpodder';
|
||||
}
|
||||
case 'nextcloud':
|
||||
return 'Nextcloud';
|
||||
default:
|
||||
return 'Unknown sync type';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user