mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-15 23:50:38 +02:00
Initial commit: SpotiFLAC Android/iOS app
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
|
||||
part 'download_item.g.dart';
|
||||
|
||||
/// Download status enum
|
||||
enum DownloadStatus {
|
||||
queued,
|
||||
downloading,
|
||||
completed,
|
||||
failed,
|
||||
skipped,
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class DownloadItem {
|
||||
final String id;
|
||||
final Track track;
|
||||
final String service;
|
||||
final DownloadStatus status;
|
||||
final double progress;
|
||||
final String? filePath;
|
||||
final String? error;
|
||||
final DateTime createdAt;
|
||||
|
||||
const DownloadItem({
|
||||
required this.id,
|
||||
required this.track,
|
||||
required this.service,
|
||||
this.status = DownloadStatus.queued,
|
||||
this.progress = 0.0,
|
||||
this.filePath,
|
||||
this.error,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
DownloadItem copyWith({
|
||||
String? id,
|
||||
Track? track,
|
||||
String? service,
|
||||
DownloadStatus? status,
|
||||
double? progress,
|
||||
String? filePath,
|
||||
String? error,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return DownloadItem(
|
||||
id: id ?? this.id,
|
||||
track: track ?? this.track,
|
||||
service: service ?? this.service,
|
||||
status: status ?? this.status,
|
||||
progress: progress ?? this.progress,
|
||||
filePath: filePath ?? this.filePath,
|
||||
error: error ?? this.error,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
factory DownloadItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$DownloadItemFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$DownloadItemToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'download_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DownloadItem _$DownloadItemFromJson(Map<String, dynamic> json) => DownloadItem(
|
||||
id: json['id'] as String,
|
||||
track: Track.fromJson(json['track'] as Map<String, dynamic>),
|
||||
service: json['service'] as String,
|
||||
status: $enumDecodeNullable(_$DownloadStatusEnumMap, json['status']) ??
|
||||
DownloadStatus.queued,
|
||||
progress: (json['progress'] as num?)?.toDouble() ?? 0.0,
|
||||
filePath: json['filePath'] as String?,
|
||||
error: json['error'] as String?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DownloadItemToJson(DownloadItem instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'track': instance.track.toJson(),
|
||||
'service': instance.service,
|
||||
'status': _$DownloadStatusEnumMap[instance.status]!,
|
||||
'progress': instance.progress,
|
||||
'filePath': instance.filePath,
|
||||
'error': instance.error,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$DownloadStatusEnumMap = {
|
||||
DownloadStatus.queued: 'queued',
|
||||
DownloadStatus.downloading: 'downloading',
|
||||
DownloadStatus.completed: 'completed',
|
||||
DownloadStatus.failed: 'failed',
|
||||
DownloadStatus.skipped: 'skipped',
|
||||
};
|
||||
|
||||
K? $enumDecodeNullable<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
Object? source, {
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return enumValues.entries
|
||||
.singleWhere(
|
||||
(e) => e.value == source,
|
||||
orElse: () => throw ArgumentError(
|
||||
'`$source` is not one of the supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
),
|
||||
)
|
||||
.key;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class AppSettings {
|
||||
final String defaultService;
|
||||
final String audioQuality;
|
||||
final String filenameFormat;
|
||||
final String downloadDirectory;
|
||||
final bool autoFallback;
|
||||
final bool embedLyrics;
|
||||
final bool maxQualityCover;
|
||||
final bool isFirstLaunch;
|
||||
|
||||
const AppSettings({
|
||||
this.defaultService = 'tidal',
|
||||
this.audioQuality = 'LOSSLESS',
|
||||
this.filenameFormat = '{title} - {artist}',
|
||||
this.downloadDirectory = '',
|
||||
this.autoFallback = true,
|
||||
this.embedLyrics = true,
|
||||
this.maxQualityCover = true,
|
||||
this.isFirstLaunch = true,
|
||||
});
|
||||
|
||||
AppSettings copyWith({
|
||||
String? defaultService,
|
||||
String? audioQuality,
|
||||
String? filenameFormat,
|
||||
String? downloadDirectory,
|
||||
bool? autoFallback,
|
||||
bool? embedLyrics,
|
||||
bool? maxQualityCover,
|
||||
bool? isFirstLaunch,
|
||||
}) {
|
||||
return AppSettings(
|
||||
defaultService: defaultService ?? this.defaultService,
|
||||
audioQuality: audioQuality ?? this.audioQuality,
|
||||
filenameFormat: filenameFormat ?? this.filenameFormat,
|
||||
downloadDirectory: downloadDirectory ?? this.downloadDirectory,
|
||||
autoFallback: autoFallback ?? this.autoFallback,
|
||||
embedLyrics: embedLyrics ?? this.embedLyrics,
|
||||
maxQualityCover: maxQualityCover ?? this.maxQualityCover,
|
||||
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
|
||||
);
|
||||
}
|
||||
|
||||
factory AppSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppSettingsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AppSettingsToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
defaultService: json['defaultService'] as String? ?? 'tidal',
|
||||
audioQuality: json['audioQuality'] as String? ?? 'LOSSLESS',
|
||||
filenameFormat: json['filenameFormat'] as String? ?? '{title} - {artist}',
|
||||
downloadDirectory: json['downloadDirectory'] as String? ?? '',
|
||||
autoFallback: json['autoFallback'] as bool? ?? true,
|
||||
embedLyrics: json['embedLyrics'] as bool? ?? true,
|
||||
maxQualityCover: json['maxQualityCover'] as bool? ?? true,
|
||||
isFirstLaunch: json['isFirstLaunch'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
|
||||
<String, dynamic>{
|
||||
'defaultService': instance.defaultService,
|
||||
'audioQuality': instance.audioQuality,
|
||||
'filenameFormat': instance.filenameFormat,
|
||||
'downloadDirectory': instance.downloadDirectory,
|
||||
'autoFallback': instance.autoFallback,
|
||||
'embedLyrics': instance.embedLyrics,
|
||||
'maxQualityCover': instance.maxQualityCover,
|
||||
'isFirstLaunch': instance.isFirstLaunch,
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Storage keys for theme settings persistence
|
||||
const String kThemeModeKey = 'theme_mode';
|
||||
const String kUseDynamicColorKey = 'use_dynamic_color';
|
||||
const String kSeedColorKey = 'seed_color';
|
||||
|
||||
/// Default Spotify green color for fallback
|
||||
const int kDefaultSeedColor = 0xFF1DB954;
|
||||
|
||||
/// Theme settings model for Material Expressive 3
|
||||
class ThemeSettings {
|
||||
final ThemeMode themeMode;
|
||||
final bool useDynamicColor;
|
||||
final int seedColorValue;
|
||||
|
||||
const ThemeSettings({
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.useDynamicColor = true,
|
||||
this.seedColorValue = kDefaultSeedColor,
|
||||
});
|
||||
|
||||
/// Get seed color as Color object
|
||||
Color get seedColor => Color(seedColorValue);
|
||||
|
||||
/// Create a copy with updated values
|
||||
ThemeSettings copyWith({
|
||||
ThemeMode? themeMode,
|
||||
bool? useDynamicColor,
|
||||
int? seedColorValue,
|
||||
}) {
|
||||
return ThemeSettings(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
useDynamicColor: useDynamicColor ?? this.useDynamicColor,
|
||||
seedColorValue: seedColorValue ?? this.seedColorValue,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON map for persistence
|
||||
Map<String, dynamic> toJson() => {
|
||||
kThemeModeKey: themeMode.name,
|
||||
kUseDynamicColorKey: useDynamicColor,
|
||||
kSeedColorKey: seedColorValue,
|
||||
};
|
||||
|
||||
/// Create from JSON map
|
||||
factory ThemeSettings.fromJson(Map<String, dynamic> json) {
|
||||
return ThemeSettings(
|
||||
themeMode: _themeModeFromString(json[kThemeModeKey] as String?),
|
||||
useDynamicColor: json[kUseDynamicColorKey] as bool? ?? true,
|
||||
seedColorValue: json[kSeedColorKey] as int? ?? kDefaultSeedColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is ThemeSettings &&
|
||||
other.themeMode == themeMode &&
|
||||
other.useDynamicColor == useDynamicColor &&
|
||||
other.seedColorValue == seedColorValue;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
themeMode.hashCode ^ useDynamicColor.hashCode ^ seedColorValue.hashCode;
|
||||
}
|
||||
|
||||
/// Helper to convert string to ThemeMode
|
||||
ThemeMode _themeModeFromString(String? value) {
|
||||
if (value == null) return ThemeMode.system;
|
||||
return ThemeMode.values.firstWhere(
|
||||
(e) => e.name == value,
|
||||
orElse: () => ThemeMode.system,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'track.g.dart';
|
||||
|
||||
/// Track model representing a music track
|
||||
@JsonSerializable()
|
||||
class Track {
|
||||
final String id;
|
||||
final String name;
|
||||
final String artistName;
|
||||
final String albumName;
|
||||
final String? albumArtist;
|
||||
final String? coverUrl;
|
||||
final String? isrc;
|
||||
final int duration;
|
||||
final int? trackNumber;
|
||||
final int? discNumber;
|
||||
final String? releaseDate;
|
||||
final ServiceAvailability? availability;
|
||||
|
||||
const Track({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.artistName,
|
||||
required this.albumName,
|
||||
this.albumArtist,
|
||||
this.coverUrl,
|
||||
this.isrc,
|
||||
required this.duration,
|
||||
this.trackNumber,
|
||||
this.discNumber,
|
||||
this.releaseDate,
|
||||
this.availability,
|
||||
});
|
||||
|
||||
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TrackToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ServiceAvailability {
|
||||
final bool tidal;
|
||||
final bool qobuz;
|
||||
final bool amazon;
|
||||
final String? tidalUrl;
|
||||
final String? qobuzUrl;
|
||||
final String? amazonUrl;
|
||||
|
||||
const ServiceAvailability({
|
||||
this.tidal = false,
|
||||
this.qobuz = false,
|
||||
this.amazon = false,
|
||||
this.tidalUrl,
|
||||
this.qobuzUrl,
|
||||
this.amazonUrl,
|
||||
});
|
||||
|
||||
factory ServiceAvailability.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServiceAvailabilityFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ServiceAvailabilityToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'track.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Track _$TrackFromJson(Map<String, dynamic> json) => Track(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
artistName: json['artistName'] as String,
|
||||
albumName: json['albumName'] as String,
|
||||
albumArtist: json['albumArtist'] as String?,
|
||||
coverUrl: json['coverUrl'] as String?,
|
||||
isrc: json['isrc'] as String?,
|
||||
duration: (json['duration'] as num).toInt(),
|
||||
trackNumber: (json['trackNumber'] as num?)?.toInt(),
|
||||
discNumber: (json['discNumber'] as num?)?.toInt(),
|
||||
releaseDate: json['releaseDate'] as String?,
|
||||
availability: json['availability'] == null
|
||||
? null
|
||||
: ServiceAvailability.fromJson(
|
||||
json['availability'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'artistName': instance.artistName,
|
||||
'albumName': instance.albumName,
|
||||
'albumArtist': instance.albumArtist,
|
||||
'coverUrl': instance.coverUrl,
|
||||
'isrc': instance.isrc,
|
||||
'duration': instance.duration,
|
||||
'trackNumber': instance.trackNumber,
|
||||
'discNumber': instance.discNumber,
|
||||
'releaseDate': instance.releaseDate,
|
||||
'availability': instance.availability?.toJson(),
|
||||
};
|
||||
|
||||
ServiceAvailability _$ServiceAvailabilityFromJson(Map<String, dynamic> json) =>
|
||||
ServiceAvailability(
|
||||
tidal: json['tidal'] as bool? ?? false,
|
||||
qobuz: json['qobuz'] as bool? ?? false,
|
||||
amazon: json['amazon'] as bool? ?? false,
|
||||
tidalUrl: json['tidalUrl'] as String?,
|
||||
qobuzUrl: json['qobuzUrl'] as String?,
|
||||
amazonUrl: json['amazonUrl'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ServiceAvailabilityToJson(
|
||||
ServiceAvailability instance) =>
|
||||
<String, dynamic>{
|
||||
'tidal': instance.tidal,
|
||||
'qobuz': instance.qobuz,
|
||||
'amazon': instance.amazon,
|
||||
'tidalUrl': instance.tidalUrl,
|
||||
'qobuzUrl': instance.qobuzUrl,
|
||||
'amazonUrl': instance.amazonUrl,
|
||||
};
|
||||
Reference in New Issue
Block a user