mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
7f71440039
73
lib/entities/CacheTrack.dart
Normal file
73
lib/entities/CacheTrack.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
part 'CacheTrack.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 2)
|
||||||
|
class CacheTrackEngagement {
|
||||||
|
@HiveField(0)
|
||||||
|
late int viewCount;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
late int? likeCount;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
late int? dislikeCount;
|
||||||
|
|
||||||
|
CacheTrackEngagement();
|
||||||
|
|
||||||
|
CacheTrackEngagement.fromEngagement(Engagement engagement)
|
||||||
|
: viewCount = engagement.viewCount,
|
||||||
|
likeCount = engagement.likeCount,
|
||||||
|
dislikeCount = engagement.dislikeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiveType(typeId: 1)
|
||||||
|
class CacheTrack extends HiveObject {
|
||||||
|
@HiveField(0)
|
||||||
|
late String id;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
late String title;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
late String channelId;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
late String? uploadDate;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
late String? publishDate;
|
||||||
|
|
||||||
|
@HiveField(5)
|
||||||
|
late String description;
|
||||||
|
|
||||||
|
@HiveField(6)
|
||||||
|
late String? duration;
|
||||||
|
|
||||||
|
@HiveField(7)
|
||||||
|
late List<String>? keywords;
|
||||||
|
|
||||||
|
@HiveField(8)
|
||||||
|
late CacheTrackEngagement engagement;
|
||||||
|
|
||||||
|
@HiveField(9)
|
||||||
|
late String mode;
|
||||||
|
|
||||||
|
@HiveField(10)
|
||||||
|
late String author;
|
||||||
|
|
||||||
|
CacheTrack();
|
||||||
|
|
||||||
|
CacheTrack.fromVideo(Video video, this.mode)
|
||||||
|
: id = video.id.value,
|
||||||
|
title = video.title,
|
||||||
|
author = video.author,
|
||||||
|
channelId = video.channelId.value,
|
||||||
|
uploadDate = video.uploadDate.toString(),
|
||||||
|
publishDate = video.publishDate.toString(),
|
||||||
|
description = video.description,
|
||||||
|
duration = video.duration.toString(),
|
||||||
|
keywords = video.keywords,
|
||||||
|
engagement = CacheTrackEngagement.fromEngagement(video.engagement);
|
||||||
|
}
|
109
lib/entities/CacheTrack.g.dart
Normal file
109
lib/entities/CacheTrack.g.dart
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'CacheTrack.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class CacheTrackEngagementAdapter extends TypeAdapter<CacheTrackEngagement> {
|
||||||
|
@override
|
||||||
|
final int typeId = 2;
|
||||||
|
|
||||||
|
@override
|
||||||
|
CacheTrackEngagement read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return CacheTrackEngagement()
|
||||||
|
..viewCount = fields[0] as int
|
||||||
|
..likeCount = fields[1] as int?
|
||||||
|
..dislikeCount = fields[2] as int?;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, CacheTrackEngagement obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(3)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.viewCount)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.likeCount)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.dislikeCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is CacheTrackEngagementAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheTrackAdapter extends TypeAdapter<CacheTrack> {
|
||||||
|
@override
|
||||||
|
final int typeId = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
CacheTrack read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return CacheTrack()
|
||||||
|
..id = fields[0] as String
|
||||||
|
..title = fields[1] as String
|
||||||
|
..channelId = fields[2] as String
|
||||||
|
..uploadDate = fields[3] as String?
|
||||||
|
..publishDate = fields[4] as String?
|
||||||
|
..description = fields[5] as String
|
||||||
|
..duration = fields[6] as String?
|
||||||
|
..keywords = (fields[7] as List?)?.cast<String>()
|
||||||
|
..engagement = fields[8] as CacheTrackEngagement
|
||||||
|
..mode = fields[9] as String
|
||||||
|
..author = fields[10] as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, CacheTrack obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(11)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.id)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.title)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.channelId)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.uploadDate)
|
||||||
|
..writeByte(4)
|
||||||
|
..write(obj.publishDate)
|
||||||
|
..writeByte(5)
|
||||||
|
..write(obj.description)
|
||||||
|
..writeByte(6)
|
||||||
|
..write(obj.duration)
|
||||||
|
..writeByte(7)
|
||||||
|
..write(obj.keywords)
|
||||||
|
..writeByte(8)
|
||||||
|
..write(obj.engagement)
|
||||||
|
..writeByte(9)
|
||||||
|
..write(obj.mode)
|
||||||
|
..writeByte(10)
|
||||||
|
..write(obj.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is CacheTrackAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
32
lib/extensions/yt-video-from-cache-track.dart
Normal file
32
lib/extensions/yt-video-from-cache-track.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
|
import 'package:spotube/utils/duration.dart';
|
||||||
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
extension VideoFromCacheTrackExtension on Video {
|
||||||
|
static Video fromCacheTrack(CacheTrack cacheTrack) {
|
||||||
|
return Video(
|
||||||
|
VideoId.fromString(cacheTrack.id),
|
||||||
|
cacheTrack.title,
|
||||||
|
cacheTrack.author,
|
||||||
|
ChannelId.fromString(cacheTrack.channelId),
|
||||||
|
cacheTrack.uploadDate != null
|
||||||
|
? DateTime.tryParse(cacheTrack.uploadDate!)
|
||||||
|
: null,
|
||||||
|
cacheTrack.publishDate != null
|
||||||
|
? DateTime.tryParse(cacheTrack.publishDate!)
|
||||||
|
: null,
|
||||||
|
cacheTrack.description,
|
||||||
|
cacheTrack.duration != null
|
||||||
|
? tryParseDuration(cacheTrack.duration!)
|
||||||
|
: null,
|
||||||
|
ThumbnailSet(cacheTrack.id),
|
||||||
|
cacheTrack.keywords,
|
||||||
|
Engagement(
|
||||||
|
cacheTrack.engagement.viewCount,
|
||||||
|
cacheTrack.engagement.likeCount,
|
||||||
|
cacheTrack.engagement.dislikeCount,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/helpers/contains-text-in-bracket.dart';
|
import 'package:spotube/helpers/contains-text-in-bracket.dart';
|
||||||
import 'package:spotube/helpers/getLyrics.dart';
|
import 'package:spotube/helpers/getLyrics.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
@ -7,6 +9,7 @@ import 'package:spotube/models/SpotubeTrack.dart';
|
|||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:spotube/extensions/list-sort-multiple.dart';
|
import 'package:spotube/extensions/list-sort-multiple.dart';
|
||||||
|
import 'package:spotube/extensions/yt-video-from-cache-track.dart';
|
||||||
|
|
||||||
enum AudioQuality {
|
enum AudioQuality {
|
||||||
high,
|
high,
|
||||||
@ -20,6 +23,7 @@ Future<SpotubeTrack> toSpotubeTrack({
|
|||||||
required String format,
|
required String format,
|
||||||
required SpotubeTrackMatchAlgorithm matchAlgorithm,
|
required SpotubeTrackMatchAlgorithm matchAlgorithm,
|
||||||
required AudioQuality audioQuality,
|
required AudioQuality audioQuality,
|
||||||
|
LazyBox<CacheTrack>? box,
|
||||||
}) async {
|
}) async {
|
||||||
final artistsName =
|
final artistsName =
|
||||||
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
|
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
|
||||||
@ -40,9 +44,15 @@ Future<SpotubeTrack> toSpotubeTrack({
|
|||||||
.replaceAll("\$FEATURED_ARTISTS", featuredArtists);
|
.replaceAll("\$FEATURED_ARTISTS", featuredArtists);
|
||||||
logger.v("[Youtube Search Term] $queryString");
|
logger.v("[Youtube Search Term] $queryString");
|
||||||
|
|
||||||
VideoSearchList videos = await youtube.search.search(queryString);
|
|
||||||
Video ytVideo;
|
Video ytVideo;
|
||||||
|
final cachedTrack = await box?.get(track.id);
|
||||||
|
if (cachedTrack != null && cachedTrack.mode == matchAlgorithm.name) {
|
||||||
|
logger.v(
|
||||||
|
"[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}",
|
||||||
|
);
|
||||||
|
ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
|
||||||
|
} else {
|
||||||
|
VideoSearchList videos = await youtube.search.search(queryString);
|
||||||
if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
|
if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
|
||||||
List<Map> ratedRankedVideos = videos
|
List<Map> ratedRankedVideos = videos
|
||||||
.map((video) {
|
.map((video) {
|
||||||
@ -57,7 +67,8 @@ Future<SpotubeTrack> toSpotubeTrack({
|
|||||||
track.artists?.first.name?.toLowerCase() ==
|
track.artists?.first.name?.toLowerCase() ==
|
||||||
video.author.toLowerCase();
|
video.author.toLowerCase();
|
||||||
|
|
||||||
final bool hasNoLiveInTitle = !containsTextInBracket(ytTitle, "live");
|
final bool hasNoLiveInTitle =
|
||||||
|
!containsTextInBracket(ytTitle, "live");
|
||||||
|
|
||||||
int rate = 0;
|
int rate = 0;
|
||||||
for (final el in [
|
for (final el in [
|
||||||
@ -88,6 +99,7 @@ Future<SpotubeTrack> toSpotubeTrack({
|
|||||||
} else {
|
} else {
|
||||||
ytVideo = videos.where((video) => !video.isLive).first;
|
ytVideo = videos.where((video) => !video.isLive).first;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
|
final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
|
||||||
|
|
||||||
@ -100,16 +112,25 @@ Future<SpotubeTrack> toSpotubeTrack({
|
|||||||
.where((info) => info.codec.mimeType == "audio/mp4")
|
.where((info) => info.codec.mimeType == "audio/mp4")
|
||||||
: trackManifest.audioOnly;
|
: trackManifest.audioOnly;
|
||||||
|
|
||||||
|
final ytUri = (audioQuality == AudioQuality.high
|
||||||
|
? audioManifest.withHighestBitrate()
|
||||||
|
: audioManifest.sortByBitrate().last)
|
||||||
|
.url
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
// only save when the track isn't available in the cache with same
|
||||||
|
// matchAlgorithm
|
||||||
|
if (cachedTrack == null || cachedTrack.mode != matchAlgorithm.name) {
|
||||||
|
await box?.put(
|
||||||
|
track.id!, CacheTrack.fromVideo(ytVideo, matchAlgorithm.name));
|
||||||
|
}
|
||||||
|
|
||||||
return SpotubeTrack.fromTrack(
|
return SpotubeTrack.fromTrack(
|
||||||
track: track,
|
track: track,
|
||||||
ytTrack: ytVideo,
|
ytTrack: ytVideo,
|
||||||
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
|
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
|
||||||
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
|
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
|
||||||
// codec/mimetype for those Platforms
|
// codec/mimetype for those Platforms
|
||||||
ytUri: (audioQuality == AudioQuality.high
|
ytUri: ytUri,
|
||||||
? audioManifest.withHighestBitrate()
|
|
||||||
: audioManifest.sortByBitrate().last)
|
|
||||||
.url
|
|
||||||
.toString(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/models/GoRouteDeclarations.dart';
|
import 'package:spotube/models/GoRouteDeclarations.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
@ -19,6 +21,9 @@ import 'package:spotube/utils/AudioPlayerHandler.dart';
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
await Hive.initFlutter();
|
||||||
|
Hive.registerAdapter(CacheTrackAdapter());
|
||||||
|
Hive.registerAdapter(CacheTrackEngagementAdapter());
|
||||||
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
|
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
|
||||||
builder: () => AudioPlayerHandler(),
|
builder: () => AudioPlayerHandler(),
|
||||||
config: const AudioServiceConfig(
|
config: const AudioServiceConfig(
|
||||||
|
@ -3,9 +3,10 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/helpers/artist-to-string.dart';
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/search-youtube.dart';
|
import 'package:spotube/helpers/search-youtube.dart';
|
||||||
@ -33,6 +34,9 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
AudioPlayerHandler player;
|
AudioPlayerHandler player;
|
||||||
YoutubeExplode youtube;
|
YoutubeExplode youtube;
|
||||||
Ref ref;
|
Ref ref;
|
||||||
|
|
||||||
|
LazyBox<CacheTrack>? cacheTrackBox;
|
||||||
|
|
||||||
Playback({
|
Playback({
|
||||||
required this.player,
|
required this.player,
|
||||||
required this.youtube,
|
required this.youtube,
|
||||||
@ -57,6 +61,8 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
StreamSubscription<bool>? _playingStream;
|
StreamSubscription<bool>? _playingStream;
|
||||||
|
|
||||||
void _init() async {
|
void _init() async {
|
||||||
|
cacheTrackBox = await Hive.openLazyBox<CacheTrack>("track-cache");
|
||||||
|
|
||||||
_playingStream = player.core.playingStream.listen(
|
_playingStream = player.core.playingStream.listen(
|
||||||
(playing) {
|
(playing) {
|
||||||
_isPlaying = playing;
|
_isPlaying = playing;
|
||||||
@ -111,6 +117,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
_positionStream?.cancel();
|
_positionStream?.cancel();
|
||||||
_playingStream?.cancel();
|
_playingStream?.cancel();
|
||||||
_durationStream?.cancel();
|
_durationStream?.cancel();
|
||||||
|
cacheTrackBox?.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +220,6 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence();
|
||||||
});
|
});
|
||||||
// await player.play();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
@ -223,6 +229,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
format: preferences.ytSearchFormat,
|
format: preferences.ytSearchFormat,
|
||||||
matchAlgorithm: preferences.trackMatchAlgorithm,
|
matchAlgorithm: preferences.trackMatchAlgorithm,
|
||||||
audioQuality: preferences.audioQuality,
|
audioQuality: preferences.audioQuality,
|
||||||
|
box: cacheTrackBox,
|
||||||
);
|
);
|
||||||
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
||||||
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
|
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
|
||||||
@ -237,7 +244,6 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence();
|
||||||
});
|
});
|
||||||
// await player.play();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
|
55
lib/utils/duration.dart
Normal file
55
lib/utils/duration.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/// Parses duration string formatted by Duration.toString() to [Duration].
|
||||||
|
/// The string should be of form hours:minutes:seconds.microseconds
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// parseTime('245:09:08.007006');
|
||||||
|
Duration parseDuration(String input) {
|
||||||
|
final parts = input.split(':');
|
||||||
|
|
||||||
|
if (parts.length != 3) throw FormatException('Invalid time format');
|
||||||
|
|
||||||
|
int days;
|
||||||
|
int hours;
|
||||||
|
int minutes;
|
||||||
|
int seconds;
|
||||||
|
int milliseconds;
|
||||||
|
int microseconds;
|
||||||
|
|
||||||
|
{
|
||||||
|
final p = parts[2].split('.');
|
||||||
|
|
||||||
|
if (p.length != 2) throw FormatException('Invalid time format');
|
||||||
|
|
||||||
|
final p2 = int.parse(p[1]);
|
||||||
|
microseconds = p2 % 1000;
|
||||||
|
milliseconds = p2 ~/ 1000;
|
||||||
|
|
||||||
|
seconds = int.parse(p[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
minutes = int.parse(parts[1]);
|
||||||
|
|
||||||
|
{
|
||||||
|
int p = int.parse(parts[0]);
|
||||||
|
hours = p % 24;
|
||||||
|
days = p ~/ 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO verify that there are no negative parts
|
||||||
|
|
||||||
|
return Duration(
|
||||||
|
days: days,
|
||||||
|
hours: hours,
|
||||||
|
minutes: minutes,
|
||||||
|
seconds: seconds,
|
||||||
|
milliseconds: milliseconds,
|
||||||
|
microseconds: microseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration? tryParseDuration(String input) {
|
||||||
|
try {
|
||||||
|
return parseDuration(input);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user