diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index be159c0e..881d9336 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3921,6 +3921,198 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Failed to fetch some albums'** String get discographyFailedToFetch; + + /// WebDAV settings page title + /// + /// In en, this message translates to: + /// **'WebDAV Storage'** + String get webdavTitle; + + /// WebDAV settings subtitle + /// + /// In en, this message translates to: + /// **'Upload files to WebDAV server'** + String get webdavSubtitle; + + /// Section header for WebDAV config + /// + /// In en, this message translates to: + /// **'Configuration'** + String get webdavSectionConfig; + + /// Section header for server settings + /// + /// In en, this message translates to: + /// **'Server'** + String get webdavSectionServer; + + /// Section header for WebDAV options + /// + /// In en, this message translates to: + /// **'Options'** + String get webdavSectionOptions; + + /// Section header for upload queue + /// + /// In en, this message translates to: + /// **'Upload Queue'** + String get webdavSectionQueue; + + /// Toggle to enable WebDAV uploads + /// + /// In en, this message translates to: + /// **'Enable WebDAV Upload'** + String get webdavEnable; + + /// Subtitle when WebDAV is configured + /// + /// In en, this message translates to: + /// **'Upload downloaded files to WebDAV server'** + String get webdavEnableSubtitleConfigured; + + /// Subtitle when WebDAV is not configured + /// + /// In en, this message translates to: + /// **'Configure server settings first'** + String get webdavEnableSubtitleNotConfigured; + + /// WebDAV server URL field + /// + /// In en, this message translates to: + /// **'Server URL'** + String get webdavServerUrl; + + /// WebDAV username field + /// + /// In en, this message translates to: + /// **'Username'** + String get webdavUsername; + + /// Username field placeholder + /// + /// In en, this message translates to: + /// **'Enter username'** + String get webdavUsernamePlaceholder; + + /// WebDAV password field + /// + /// In en, this message translates to: + /// **'Password'** + String get webdavPassword; + + /// Password field placeholder + /// + /// In en, this message translates to: + /// **'Enter password'** + String get webdavPasswordPlaceholder; + + /// Remote folder path on WebDAV server + /// + /// In en, this message translates to: + /// **'Remote Path'** + String get webdavRemotePath; + + /// Button to test WebDAV connection + /// + /// In en, this message translates to: + /// **'Test Connection'** + String get webdavTestConnection; + + /// Testing connection in progress + /// + /// In en, this message translates to: + /// **'Testing...'** + String get webdavTesting; + + /// Connection test passed + /// + /// In en, this message translates to: + /// **'Connection successful!'** + String get webdavConnectionSuccess; + + /// Connection test failed + /// + /// In en, this message translates to: + /// **'Connection failed: {error}'** + String webdavConnectionFailed(String error); + + /// Option to delete local files after upload + /// + /// In en, this message translates to: + /// **'Delete Local After Upload'** + String get webdavDeleteLocal; + + /// Subtitle for delete local option + /// + /// In en, this message translates to: + /// **'Remove local file after successful WebDAV upload'** + String get webdavDeleteLocalSubtitle; + + /// Option to retry failed uploads + /// + /// In en, this message translates to: + /// **'Retry on Failure'** + String get webdavRetryOnFailure; + + /// Subtitle for retry option + /// + /// In en, this message translates to: + /// **'Automatically retry up to {count} times'** + String webdavRetrySubtitle(int count); + + /// Section header for active uploads + /// + /// In en, this message translates to: + /// **'Active Uploads'** + String get webdavActiveUploads; + + /// Upload status - pending + /// + /// In en, this message translates to: + /// **'Pending'** + String get webdavPending; + + /// Upload status - uploading + /// + /// In en, this message translates to: + /// **'Uploading'** + String get webdavUploading; + + /// Upload status - completed + /// + /// In en, this message translates to: + /// **'Completed'** + String get webdavCompleted; + + /// Upload status - failed + /// + /// In en, this message translates to: + /// **'Failed'** + String get webdavFailed; + + /// Button to retry single upload + /// + /// In en, this message translates to: + /// **'Retry'** + String get webdavRetry; + + /// Button to retry all failed uploads + /// + /// In en, this message translates to: + /// **'Retry All'** + String get webdavRetryAll; + + /// Button to remove from queue + /// + /// In en, this message translates to: + /// **'Remove'** + String get webdavRemove; + + /// Button to clear completed uploads + /// + /// In en, this message translates to: + /// **'Clear Completed'** + String get webdavClearCompleted; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 0cbbbefa..f83e2cab 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2177,4 +2177,107 @@ class AppLocalizationsDe extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9eff2a37..c5ec847f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsEn extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 0a47926d..53773128 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2164,6 +2164,109 @@ class AppLocalizationsEs extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 74b8e6c0..d9068c54 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsFr extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 9a690ec5..9fe97a8f 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsHi extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 3fa36e0e..25daab00 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2177,4 +2177,107 @@ class AppLocalizationsId extends AppLocalizations { @override String get discographyFailedToFetch => 'Gagal mengambil beberapa album'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 76d048b8..5df43db9 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsJa extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index fed8e128..87c6830f 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsKo extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index eecb34ae..2dbf96c4 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsNl extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 4a1d3424..0339d36b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2164,6 +2164,109 @@ class AppLocalizationsPt extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 15ad6280..92f98882 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2209,4 +2209,107 @@ class AppLocalizationsRu extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index a1b0f73a..0759c7e6 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2164,4 +2164,107 @@ class AppLocalizationsTr extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6b15a933..c7160e62 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2164,6 +2164,109 @@ class AppLocalizationsZh extends AppLocalizations { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get webdavTitle => 'WebDAV Storage'; + + @override + String get webdavSubtitle => 'Upload files to WebDAV server'; + + @override + String get webdavSectionConfig => 'Configuration'; + + @override + String get webdavSectionServer => 'Server'; + + @override + String get webdavSectionOptions => 'Options'; + + @override + String get webdavSectionQueue => 'Upload Queue'; + + @override + String get webdavEnable => 'Enable WebDAV Upload'; + + @override + String get webdavEnableSubtitleConfigured => + 'Upload downloaded files to WebDAV server'; + + @override + String get webdavEnableSubtitleNotConfigured => + 'Configure server settings first'; + + @override + String get webdavServerUrl => 'Server URL'; + + @override + String get webdavUsername => 'Username'; + + @override + String get webdavUsernamePlaceholder => 'Enter username'; + + @override + String get webdavPassword => 'Password'; + + @override + String get webdavPasswordPlaceholder => 'Enter password'; + + @override + String get webdavRemotePath => 'Remote Path'; + + @override + String get webdavTestConnection => 'Test Connection'; + + @override + String get webdavTesting => 'Testing...'; + + @override + String get webdavConnectionSuccess => 'Connection successful!'; + + @override + String webdavConnectionFailed(String error) { + return 'Connection failed: $error'; + } + + @override + String get webdavDeleteLocal => 'Delete Local After Upload'; + + @override + String get webdavDeleteLocalSubtitle => + 'Remove local file after successful WebDAV upload'; + + @override + String get webdavRetryOnFailure => 'Retry on Failure'; + + @override + String webdavRetrySubtitle(int count) { + return 'Automatically retry up to $count times'; + } + + @override + String get webdavActiveUploads => 'Active Uploads'; + + @override + String get webdavPending => 'Pending'; + + @override + String get webdavUploading => 'Uploading'; + + @override + String get webdavCompleted => 'Completed'; + + @override + String get webdavFailed => 'Failed'; + + @override + String get webdavRetry => 'Retry'; + + @override + String get webdavRetryAll => 'Retry All'; + + @override + String get webdavRemove => 'Remove'; + + @override + String get webdavClearCompleted => 'Clear Completed'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4dd87f36..3b57ec2f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1634,5 +1634,81 @@ "discographyNoAlbums": "No albums available", "@discographyNoAlbums": {"description": "Error - no albums found for artist"}, "discographyFailedToFetch": "Failed to fetch some albums", - "@discographyFailedToFetch": {"description": "Error - some albums failed to load"} + "@discographyFailedToFetch": {"description": "Error - some albums failed to load"}, + + "webdavTitle": "WebDAV Storage", + "@webdavTitle": {"description": "WebDAV settings page title"}, + "webdavSubtitle": "Upload files to WebDAV server", + "@webdavSubtitle": {"description": "WebDAV settings subtitle"}, + "webdavSectionConfig": "Configuration", + "@webdavSectionConfig": {"description": "Section header for WebDAV config"}, + "webdavSectionServer": "Server", + "@webdavSectionServer": {"description": "Section header for server settings"}, + "webdavSectionOptions": "Options", + "@webdavSectionOptions": {"description": "Section header for WebDAV options"}, + "webdavSectionQueue": "Upload Queue", + "@webdavSectionQueue": {"description": "Section header for upload queue"}, + "webdavEnable": "Enable WebDAV Upload", + "@webdavEnable": {"description": "Toggle to enable WebDAV uploads"}, + "webdavEnableSubtitleConfigured": "Upload downloaded files to WebDAV server", + "@webdavEnableSubtitleConfigured": {"description": "Subtitle when WebDAV is configured"}, + "webdavEnableSubtitleNotConfigured": "Configure server settings first", + "@webdavEnableSubtitleNotConfigured": {"description": "Subtitle when WebDAV is not configured"}, + "webdavServerUrl": "Server URL", + "@webdavServerUrl": {"description": "WebDAV server URL field"}, + "webdavUsername": "Username", + "@webdavUsername": {"description": "WebDAV username field"}, + "webdavUsernamePlaceholder": "Enter username", + "@webdavUsernamePlaceholder": {"description": "Username field placeholder"}, + "webdavPassword": "Password", + "@webdavPassword": {"description": "WebDAV password field"}, + "webdavPasswordPlaceholder": "Enter password", + "@webdavPasswordPlaceholder": {"description": "Password field placeholder"}, + "webdavRemotePath": "Remote Path", + "@webdavRemotePath": {"description": "Remote folder path on WebDAV server"}, + "webdavTestConnection": "Test Connection", + "@webdavTestConnection": {"description": "Button to test WebDAV connection"}, + "webdavTesting": "Testing...", + "@webdavTesting": {"description": "Testing connection in progress"}, + "webdavConnectionSuccess": "Connection successful!", + "@webdavConnectionSuccess": {"description": "Connection test passed"}, + "webdavConnectionFailed": "Connection failed: {error}", + "@webdavConnectionFailed": { + "description": "Connection test failed", + "placeholders": { + "error": {"type": "String"} + } + }, + "webdavDeleteLocal": "Delete Local After Upload", + "@webdavDeleteLocal": {"description": "Option to delete local files after upload"}, + "webdavDeleteLocalSubtitle": "Remove local file after successful WebDAV upload", + "@webdavDeleteLocalSubtitle": {"description": "Subtitle for delete local option"}, + "webdavRetryOnFailure": "Retry on Failure", + "@webdavRetryOnFailure": {"description": "Option to retry failed uploads"}, + "webdavRetrySubtitle": "Automatically retry up to {count} times", + "@webdavRetrySubtitle": { + "description": "Subtitle for retry option", + "placeholders": { + "count": {"type": "int"} + } + }, + "webdavActiveUploads": "Active Uploads", + "@webdavActiveUploads": {"description": "Section header for active uploads"}, + "webdavPending": "Pending", + "@webdavPending": {"description": "Upload status - pending"}, + "webdavUploading": "Uploading", + "@webdavUploading": {"description": "Upload status - uploading"}, + "webdavCompleted": "Completed", + "@webdavCompleted": {"description": "Upload status - completed"}, + "webdavFailed": "Failed", + "@webdavFailed": {"description": "Upload status - failed"}, + "webdavRetry": "Retry", + "@webdavRetry": {"description": "Button to retry single upload"}, + "webdavRetryAll": "Retry All", + "@webdavRetryAll": {"description": "Button to retry all failed uploads"}, + "webdavRemove": "Remove", + "@webdavRemove": {"description": "Button to remove from queue"}, + "webdavClearCompleted": "Clear Completed", + "@webdavClearCompleted": {"description": "Button to clear completed uploads"} } + diff --git a/lib/models/webdav_config.dart b/lib/models/webdav_config.dart new file mode 100644 index 00000000..42c23084 --- /dev/null +++ b/lib/models/webdav_config.dart @@ -0,0 +1,124 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'webdav_config.g.dart'; + +@JsonSerializable() +class WebDavConfig { + final bool enabled; + final String serverUrl; + final String username; + final String password; + final String remotePath; + final bool deleteLocalAfterUpload; + final bool retryOnFailure; + final int maxRetries; + + const WebDavConfig({ + this.enabled = false, + this.serverUrl = '', + this.username = '', + this.password = '', + this.remotePath = '/SpotiFLAC', + this.deleteLocalAfterUpload = true, + this.retryOnFailure = true, + this.maxRetries = 3, + }); + + bool get isConfigured => + serverUrl.isNotEmpty && username.isNotEmpty && password.isNotEmpty; + + WebDavConfig copyWith({ + bool? enabled, + String? serverUrl, + String? username, + String? password, + String? remotePath, + bool? deleteLocalAfterUpload, + bool? retryOnFailure, + int? maxRetries, + }) { + return WebDavConfig( + enabled: enabled ?? this.enabled, + serverUrl: serverUrl ?? this.serverUrl, + username: username ?? this.username, + password: password ?? this.password, + remotePath: remotePath ?? this.remotePath, + deleteLocalAfterUpload: + deleteLocalAfterUpload ?? this.deleteLocalAfterUpload, + retryOnFailure: retryOnFailure ?? this.retryOnFailure, + maxRetries: maxRetries ?? this.maxRetries, + ); + } + + factory WebDavConfig.fromJson(Map json) => + _$WebDavConfigFromJson(json); + Map toJson() => _$WebDavConfigToJson(this); +} + +enum WebDavUploadStatus { pending, uploading, completed, failed } + +@JsonSerializable() +class WebDavUploadItem { + final String id; + final String localPath; + final String remotePath; + final String trackName; + final String artistName; + final String? albumName; + final WebDavUploadStatus status; + final double progress; + final String? error; + final int retryCount; + final DateTime createdAt; + final DateTime? completedAt; + + const WebDavUploadItem({ + required this.id, + required this.localPath, + required this.remotePath, + required this.trackName, + required this.artistName, + this.albumName, + this.status = WebDavUploadStatus.pending, + this.progress = 0.0, + this.error, + this.retryCount = 0, + required this.createdAt, + this.completedAt, + }); + + WebDavUploadItem copyWith({ + String? id, + String? localPath, + String? remotePath, + String? trackName, + String? artistName, + String? albumName, + WebDavUploadStatus? status, + double? progress, + String? error, + bool clearError = false, + int? retryCount, + DateTime? createdAt, + DateTime? completedAt, + }) { + return WebDavUploadItem( + id: id ?? this.id, + localPath: localPath ?? this.localPath, + remotePath: remotePath ?? this.remotePath, + trackName: trackName ?? this.trackName, + artistName: artistName ?? this.artistName, + albumName: albumName ?? this.albumName, + status: status ?? this.status, + progress: progress ?? this.progress, + error: clearError ? null : (error ?? this.error), + retryCount: retryCount ?? this.retryCount, + createdAt: createdAt ?? this.createdAt, + completedAt: completedAt ?? this.completedAt, + ); + } + + factory WebDavUploadItem.fromJson(Map json) => + _$WebDavUploadItemFromJson(json); + Map toJson() => _$WebDavUploadItemToJson(this); +} diff --git a/lib/models/webdav_config.g.dart b/lib/models/webdav_config.g.dart new file mode 100644 index 00000000..5f2c30e3 --- /dev/null +++ b/lib/models/webdav_config.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'webdav_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +WebDavConfig _$WebDavConfigFromJson(Map json) => WebDavConfig( + enabled: json['enabled'] as bool? ?? false, + serverUrl: json['serverUrl'] as String? ?? '', + username: json['username'] as String? ?? '', + password: json['password'] as String? ?? '', + remotePath: json['remotePath'] as String? ?? '/SpotiFLAC', + deleteLocalAfterUpload: json['deleteLocalAfterUpload'] as bool? ?? true, + retryOnFailure: json['retryOnFailure'] as bool? ?? true, + maxRetries: (json['maxRetries'] as num?)?.toInt() ?? 3, + ); + +Map _$WebDavConfigToJson(WebDavConfig instance) => + { + 'enabled': instance.enabled, + 'serverUrl': instance.serverUrl, + 'username': instance.username, + 'password': instance.password, + 'remotePath': instance.remotePath, + 'deleteLocalAfterUpload': instance.deleteLocalAfterUpload, + 'retryOnFailure': instance.retryOnFailure, + 'maxRetries': instance.maxRetries, + }; + +WebDavUploadItem _$WebDavUploadItemFromJson(Map json) => + WebDavUploadItem( + id: json['id'] as String, + localPath: json['localPath'] as String, + remotePath: json['remotePath'] as String, + trackName: json['trackName'] as String, + artistName: json['artistName'] as String, + albumName: json['albumName'] as String?, + status: $enumDecodeNullable(_$WebDavUploadStatusEnumMap, json['status']) ?? + WebDavUploadStatus.pending, + progress: (json['progress'] as num?)?.toDouble() ?? 0.0, + error: json['error'] as String?, + retryCount: (json['retryCount'] as num?)?.toInt() ?? 0, + createdAt: DateTime.parse(json['createdAt'] as String), + completedAt: json['completedAt'] == null + ? null + : DateTime.parse(json['completedAt'] as String), + ); + +Map _$WebDavUploadItemToJson(WebDavUploadItem instance) => + { + 'id': instance.id, + 'localPath': instance.localPath, + 'remotePath': instance.remotePath, + 'trackName': instance.trackName, + 'artistName': instance.artistName, + 'albumName': instance.albumName, + 'status': _$WebDavUploadStatusEnumMap[instance.status]!, + 'progress': instance.progress, + 'error': instance.error, + 'retryCount': instance.retryCount, + 'createdAt': instance.createdAt.toIso8601String(), + 'completedAt': instance.completedAt?.toIso8601String(), + }; + +const _$WebDavUploadStatusEnumMap = { + WebDavUploadStatus.pending: 'pending', + WebDavUploadStatus.uploading: 'uploading', + WebDavUploadStatus.completed: 'completed', + WebDavUploadStatus.failed: 'failed', +}; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 37da93dc..1eb1212c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -10,6 +10,7 @@ import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/providers/webdav_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; import 'package:spotiflac_android/services/notification_service.dart'; @@ -2103,6 +2104,17 @@ class DownloadQueueNotifier extends Notifier { ), ); + // Add to WebDAV upload queue if configured + final webDavState = ref.read(webDavProvider); + if (webDavState.config.enabled && webDavState.config.isConfigured) { + ref.read(webDavProvider.notifier).addToQueue( + localPath: filePath, + trackName: trackToDownload.name, + artistName: trackToDownload.artistName, + albumName: trackToDownload.albumName, + ); + } + removeItem(item.id); } } else { diff --git a/lib/providers/webdav_provider.dart b/lib/providers/webdav_provider.dart new file mode 100644 index 00000000..429f76ab --- /dev/null +++ b/lib/providers/webdav_provider.dart @@ -0,0 +1,417 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotiflac_android/models/webdav_config.dart'; +import 'package:spotiflac_android/services/webdav_service.dart'; +import 'package:spotiflac_android/utils/logger.dart'; + +final _log = AppLogger('WebDavUploadQueue'); + +const _configKey = 'webdav_config'; +const _queueKey = 'webdav_upload_queue'; + +class WebDavState { + final WebDavConfig config; + final List queue; + final bool isProcessing; + final WebDavUploadItem? currentUpload; + + const WebDavState({ + this.config = const WebDavConfig(), + this.queue = const [], + this.isProcessing = false, + this.currentUpload, + }); + + WebDavState copyWith({ + WebDavConfig? config, + List? queue, + bool? isProcessing, + WebDavUploadItem? currentUpload, + bool clearCurrentUpload = false, + }) { + return WebDavState( + config: config ?? this.config, + queue: queue ?? this.queue, + isProcessing: isProcessing ?? this.isProcessing, + currentUpload: clearCurrentUpload + ? null + : (currentUpload ?? this.currentUpload), + ); + } + + int get pendingCount => + queue.where((i) => i.status == WebDavUploadStatus.pending).length; + int get uploadingCount => + queue.where((i) => i.status == WebDavUploadStatus.uploading).length; + int get completedCount => + queue.where((i) => i.status == WebDavUploadStatus.completed).length; + int get failedCount => + queue.where((i) => i.status == WebDavUploadStatus.failed).length; + + List get activeItems => + queue.where((i) => i.status != WebDavUploadStatus.completed).toList(); + + List get failedItems => + queue.where((i) => i.status == WebDavUploadStatus.failed).toList(); +} + +class WebDavNotifier extends Notifier { + final Future _prefs = SharedPreferences.getInstance(); + final WebDavService _service = WebDavService(); + Timer? _processTimer; + + @override + WebDavState build() { + ref.onDispose(() { + _processTimer?.cancel(); + }); + _loadConfig(); + _loadQueue(); + return const WebDavState(); + } + + Future _loadConfig() async { + final prefs = await _prefs; + final json = prefs.getString(_configKey); + if (json != null) { + try { + final config = WebDavConfig.fromJson(jsonDecode(json)); + state = state.copyWith(config: config); + _service.configure(config); + _log.d('Loaded WebDAV config: enabled=${config.enabled}'); + } catch (e) { + _log.e('Failed to load WebDAV config: $e'); + } + } + } + + Future _loadQueue() async { + final prefs = await _prefs; + final json = prefs.getString(_queueKey); + if (json != null) { + try { + final List list = jsonDecode(json); + final queue = list.map((e) => WebDavUploadItem.fromJson(e)).toList(); + + // Reset any uploading items to pending (app may have been killed mid-upload) + final resetQueue = queue.map((item) { + if (item.status == WebDavUploadStatus.uploading) { + return item.copyWith(status: WebDavUploadStatus.pending); + } + return item; + }).toList(); + + state = state.copyWith(queue: resetQueue); + _log.d('Loaded ${resetQueue.length} items from WebDAV upload queue'); + + // Start processing if there are pending items + if (state.config.enabled && state.pendingCount > 0) { + _startProcessing(); + } + } catch (e) { + _log.e('Failed to load WebDAV queue: $e'); + } + } + } + + Future _saveConfig() async { + final prefs = await _prefs; + await prefs.setString(_configKey, jsonEncode(state.config.toJson())); + } + + Future _saveQueue() async { + final prefs = await _prefs; + final json = jsonEncode(state.queue.map((e) => e.toJson()).toList()); + await prefs.setString(_queueKey, json); + } + + // Configuration methods + Future updateConfig(WebDavConfig config) async { + state = state.copyWith(config: config); + _service.configure(config); + await _saveConfig(); + + if (config.enabled && config.isConfigured && state.pendingCount > 0) { + _startProcessing(); + } + } + + void setEnabled(bool enabled) { + updateConfig(state.config.copyWith(enabled: enabled)); + } + + void setServerUrl(String url) { + updateConfig(state.config.copyWith(serverUrl: url.trim())); + } + + void setUsername(String username) { + updateConfig(state.config.copyWith(username: username.trim())); + } + + void setPassword(String password) { + updateConfig(state.config.copyWith(password: password)); + } + + void setRemotePath(String path) { + var cleanPath = path.trim(); + if (!cleanPath.startsWith('/')) { + cleanPath = '/$cleanPath'; + } + updateConfig(state.config.copyWith(remotePath: cleanPath)); + } + + void setDeleteLocalAfterUpload(bool delete) { + updateConfig(state.config.copyWith(deleteLocalAfterUpload: delete)); + } + + void setRetryOnFailure(bool retry) { + updateConfig(state.config.copyWith(retryOnFailure: retry)); + } + + void setMaxRetries(int maxRetries) { + updateConfig(state.config.copyWith(maxRetries: maxRetries.clamp(1, 10))); + } + + Future<({bool success, String? error})> testConnection() async { + _service.configure(state.config); + return await _service.testConnection(); + } + + // Queue methods + Future addToQueue({ + required String localPath, + required String trackName, + required String artistName, + String? albumName, + }) async { + if (!state.config.enabled || !state.config.isConfigured) { + _log.d('WebDAV not enabled or configured, skipping upload queue'); + return; + } + + // Check if file exists + final file = File(localPath); + if (!await file.exists()) { + _log.w('File does not exist, not adding to queue: $localPath'); + return; + } + + final remotePath = _service.buildRemotePath( + localPath, + albumName: albumName, + artistName: artistName, + ); + + final item = WebDavUploadItem( + id: '${DateTime.now().millisecondsSinceEpoch}_${localPath.hashCode}', + localPath: localPath, + remotePath: remotePath, + trackName: trackName, + artistName: artistName, + albumName: albumName, + createdAt: DateTime.now(), + ); + + state = state.copyWith(queue: [...state.queue, item]); + await _saveQueue(); + + _log.i('Added to WebDAV upload queue: $trackName'); + _startProcessing(); + } + + void removeFromQueue(String id) { + state = state.copyWith( + queue: state.queue.where((i) => i.id != id).toList(), + ); + _saveQueue(); + } + + void clearCompleted() { + state = state.copyWith( + queue: state.queue + .where((i) => i.status != WebDavUploadStatus.completed) + .toList(), + ); + _saveQueue(); + } + + void clearAll() { + state = state.copyWith(queue: []); + _saveQueue(); + } + + Future retryFailed() async { + final updatedQueue = state.queue.map((item) { + if (item.status == WebDavUploadStatus.failed) { + return item.copyWith( + status: WebDavUploadStatus.pending, + clearError: true, + retryCount: 0, + ); + } + return item; + }).toList(); + + state = state.copyWith(queue: updatedQueue); + await _saveQueue(); + _startProcessing(); + } + + Future retryItem(String id) async { + final updatedQueue = state.queue.map((item) { + if (item.id == id && item.status == WebDavUploadStatus.failed) { + return item.copyWith( + status: WebDavUploadStatus.pending, + clearError: true, + ); + } + return item; + }).toList(); + + state = state.copyWith(queue: updatedQueue); + await _saveQueue(); + _startProcessing(); + } + + void _startProcessing() { + if (state.isProcessing) return; + if (!state.config.enabled || !state.config.isConfigured) return; + + _processTimer?.cancel(); + _processTimer = Timer(const Duration(milliseconds: 500), () { + _processQueue(); + }); + } + + Future _processQueue() async { + if (state.isProcessing) return; + if (!state.config.enabled || !state.config.isConfigured) return; + + state = state.copyWith(isProcessing: true); + + // Process items in a loop to catch any new items added during processing + while (state.config.enabled && state.config.isConfigured) { + final pendingItems = state.queue + .where((i) => i.status == WebDavUploadStatus.pending) + .toList(); + + if (pendingItems.isEmpty) { + _log.d('No pending items in upload queue'); + break; + } + + // Process one item at a time to allow new items to be picked up + await _uploadItem(pendingItems.first); + } + + state = state.copyWith(isProcessing: false, clearCurrentUpload: true); + } + + Future _uploadItem(WebDavUploadItem item) async { + _log.i('Starting upload: ${item.trackName}'); + + // Update status to uploading + _updateItem( + item.id, + (i) => i.copyWith(status: WebDavUploadStatus.uploading, progress: 0.0), + ); + state = state.copyWith( + currentUpload: state.queue.firstWhere((i) => i.id == item.id), + ); + + try { + await _service.uploadFile( + item.localPath, + item.remotePath, + onProgress: (progress) { + _updateItem(item.id, (i) => i.copyWith(progress: progress)); + final updatedItem = state.queue.firstWhere( + (i) => i.id == item.id, + orElse: () => item, + ); + state = state.copyWith(currentUpload: updatedItem); + }, + ); + + // Upload successful + _updateItem( + item.id, + (i) => i.copyWith( + status: WebDavUploadStatus.completed, + progress: 1.0, + completedAt: DateTime.now(), + ), + ); + + _log.i('Upload completed: ${item.trackName}'); + + // Delete local file if configured + if (state.config.deleteLocalAfterUpload) { + final deleted = await _service.deleteLocalFile(item.localPath); + if (deleted) { + _log.d('Deleted local file after upload: ${item.localPath}'); + } + } + + await _saveQueue(); + } catch (e) { + _log.e('Upload failed: ${item.trackName} - $e'); + + final currentItem = state.queue.firstWhere( + (i) => i.id == item.id, + orElse: () => item, + ); + final newRetryCount = currentItem.retryCount + 1; + + if (state.config.retryOnFailure && + newRetryCount < state.config.maxRetries) { + // Mark as pending for retry + _updateItem( + item.id, + (i) => i.copyWith( + status: WebDavUploadStatus.pending, + progress: 0.0, + retryCount: newRetryCount, + error: 'Retry $newRetryCount/${state.config.maxRetries}: $e', + ), + ); + _log.d( + 'Will retry upload ($newRetryCount/${state.config.maxRetries}): ${item.trackName}', + ); + } else { + // Mark as failed + _updateItem( + item.id, + (i) => i.copyWith( + status: WebDavUploadStatus.failed, + error: e.toString(), + retryCount: newRetryCount, + ), + ); + } + + await _saveQueue(); + } + } + + void _updateItem( + String id, + WebDavUploadItem Function(WebDavUploadItem) updater, + ) { + state = state.copyWith( + queue: state.queue.map((item) { + if (item.id == id) { + return updater(item); + } + return item; + }).toList(), + ); + } +} + +final webDavProvider = NotifierProvider( + WebDavNotifier.new, +); diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index 5bf00423..34be4e5f 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -6,6 +6,7 @@ import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart import 'package:spotiflac_android/screens/settings/download_settings_page.dart'; import 'package:spotiflac_android/screens/settings/extensions_page.dart'; import 'package:spotiflac_android/screens/settings/options_settings_page.dart'; +import 'package:spotiflac_android/screens/settings/webdav_settings_page.dart'; import 'package:spotiflac_android/screens/settings/about_page.dart'; import 'package:spotiflac_android/screens/settings/log_screen.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -84,6 +85,12 @@ class SettingsTab extends ConsumerWidget { title: l10n.settingsExtensions, subtitle: l10n.settingsExtensionsSubtitle, onTap: () => _navigateTo(context, const ExtensionsPage()), + ), + SettingsItem( + icon: Icons.cloud_upload_outlined, + title: l10n.webdavTitle, + subtitle: l10n.webdavSubtitle, + onTap: () => _navigateTo(context, const WebDavSettingsPage()), showDivider: false, ), ], diff --git a/lib/screens/settings/webdav_settings_page.dart b/lib/screens/settings/webdav_settings_page.dart new file mode 100644 index 00000000..441425ab --- /dev/null +++ b/lib/screens/settings/webdav_settings_page.dart @@ -0,0 +1,547 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/webdav_config.dart'; +import 'package:spotiflac_android/providers/webdav_provider.dart'; +import 'package:spotiflac_android/widgets/settings_group.dart'; + +class WebDavSettingsPage extends ConsumerStatefulWidget { + const WebDavSettingsPage({super.key}); + + @override + ConsumerState createState() => _WebDavSettingsPageState(); +} + +class _WebDavSettingsPageState extends ConsumerState { + final _serverUrlController = TextEditingController(); + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + final _remotePathController = TextEditingController(); + bool _obscurePassword = true; + bool _isTesting = false; + + @override + void initState() { + super.initState(); + // Load existing config + final config = ref.read(webDavProvider).config; + _serverUrlController.text = config.serverUrl; + _usernameController.text = config.username; + _passwordController.text = config.password; + _remotePathController.text = config.remotePath; + } + + @override + void dispose() { + _serverUrlController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + _remotePathController.dispose(); + super.dispose(); + } + + Future _testConnection() async { + setState(() => _isTesting = true); + + final result = await ref.read(webDavProvider.notifier).testConnection(); + + if (!mounted) return; + setState(() => _isTesting = false); + + if (result.success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.webdavConnectionSuccess), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.webdavConnectionFailed( + result.error ?? 'Unknown error', + ), + ), + backgroundColor: Colors.red, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final webDavState = ref.watch(webDavProvider); + final config = webDavState.config; + final colorScheme = Theme.of(context).colorScheme; + final topPadding = MediaQuery.of(context).padding.top; + final l10n = context.l10n; + + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 120 + topPadding, + collapsedHeight: kToolbarHeight, + floating: false, + pinned: true, + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final maxHeight = 120 + topPadding; + final minHeight = kToolbarHeight + topPadding; + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); + return FlexibleSpaceBar( + expandedTitleScale: 1.0, + titlePadding: EdgeInsets.only( + left: 56 - (32 * expandRatio), + bottom: 16, + ), + title: Text( + l10n.webdavTitle, + style: TextStyle( + fontSize: 20 + (8 * expandRatio), + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ); + }, + ), + ), + + // Enable toggle + SliverToBoxAdapter( + child: SettingsSectionHeader(title: l10n.webdavSectionConfig), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.cloud_upload, + title: l10n.webdavEnable, + subtitle: config.isConfigured + ? l10n.webdavEnableSubtitleConfigured + : l10n.webdavEnableSubtitleNotConfigured, + value: config.enabled, + enabled: config.isConfigured, + onChanged: (value) { + ref.read(webDavProvider.notifier).setEnabled(value); + }, + showDivider: false, + ), + ], + ), + ), + + // Server configuration + SliverToBoxAdapter( + child: SettingsSectionHeader(title: l10n.webdavSectionServer), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + _buildTextField( + controller: _serverUrlController, + icon: Icons.link, + label: l10n.webdavServerUrl, + hint: 'https://webdav.example.com/dav', + onChanged: (value) { + ref.read(webDavProvider.notifier).setServerUrl(value); + }, + keyboardType: TextInputType.url, + ), + _buildTextField( + controller: _usernameController, + icon: Icons.person, + label: l10n.webdavUsername, + hint: l10n.webdavUsernamePlaceholder, + onChanged: (value) { + ref.read(webDavProvider.notifier).setUsername(value); + }, + ), + _buildTextField( + controller: _passwordController, + icon: Icons.lock, + label: l10n.webdavPassword, + hint: l10n.webdavPasswordPlaceholder, + obscureText: _obscurePassword, + onChanged: (value) { + ref.read(webDavProvider.notifier).setPassword(value); + }, + suffix: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () => + setState(() => _obscurePassword = !_obscurePassword), + ), + showDivider: false, + ), + ], + ), + ), + + // Test connection button + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FilledButton.icon( + onPressed: config.isConfigured && !_isTesting + ? _testConnection + : null, + icon: _isTesting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.wifi_find), + label: Text( + _isTesting ? l10n.webdavTesting : l10n.webdavTestConnection, + ), + ), + ), + ), + + // Remote path and options + SliverToBoxAdapter( + child: SettingsSectionHeader(title: l10n.webdavSectionOptions), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + _buildTextField( + controller: _remotePathController, + icon: Icons.folder, + label: l10n.webdavRemotePath, + hint: '/SpotiFLAC', + onChanged: (value) { + ref.read(webDavProvider.notifier).setRemotePath(value); + }, + ), + SettingsSwitchItem( + icon: Icons.delete_outline, + title: l10n.webdavDeleteLocal, + subtitle: l10n.webdavDeleteLocalSubtitle, + value: config.deleteLocalAfterUpload, + onChanged: (value) { + ref + .read(webDavProvider.notifier) + .setDeleteLocalAfterUpload(value); + }, + ), + SettingsSwitchItem( + icon: Icons.refresh, + title: l10n.webdavRetryOnFailure, + subtitle: l10n.webdavRetrySubtitle(config.maxRetries), + value: config.retryOnFailure, + onChanged: (value) { + ref.read(webDavProvider.notifier).setRetryOnFailure(value); + }, + showDivider: false, + ), + ], + ), + ), + + // Upload Queue section + SliverToBoxAdapter( + child: SettingsSectionHeader(title: l10n.webdavSectionQueue), + ), + SliverToBoxAdapter( + child: _buildQueueSummary(webDavState, l10n, colorScheme), + ), + + // Queue items + if (webDavState.activeItems.isNotEmpty) ...[ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + children: [ + Text( + l10n.webdavActiveUploads, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const Spacer(), + if (webDavState.failedCount > 0) + TextButton.icon( + onPressed: () { + ref.read(webDavProvider.notifier).retryFailed(); + }, + icon: const Icon(Icons.refresh, size: 18), + label: Text(l10n.webdavRetryAll), + ), + ], + ), + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final item = webDavState.activeItems[index]; + return _buildQueueItem(item, colorScheme, l10n); + }, childCount: webDavState.activeItems.length), + ), + ], + + // Clear completed button + if (webDavState.completedCount > 0) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16), + child: OutlinedButton.icon( + onPressed: () { + ref.read(webDavProvider.notifier).clearCompleted(); + }, + icon: const Icon(Icons.clear_all), + label: Text(l10n.webdavClearCompleted), + ), + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required IconData icon, + required String label, + String? hint, + bool obscureText = false, + ValueChanged? onChanged, + Widget? suffix, + TextInputType? keyboardType, + bool showDivider = true, + }) { + final colorScheme = Theme.of(context).colorScheme; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: colorScheme.onSurfaceVariant, size: 24), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: label, + hintText: hint, + border: const OutlineInputBorder(), + suffixIcon: suffix, + ), + onChanged: onChanged, + ), + ), + ], + ), + ), + if (showDivider) + Divider( + height: 1, + thickness: 1, + indent: 56, + endIndent: 20, + color: colorScheme.outlineVariant.withValues(alpha: 0.3), + ), + ], + ); + } + + Widget _buildQueueSummary( + WebDavState webDavState, + AppLocalizations l10n, + ColorScheme colorScheme, + ) { + return SettingsGroup( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem( + icon: Icons.schedule, + count: webDavState.pendingCount, + label: l10n.webdavPending, + color: colorScheme.outline, + ), + _buildStatItem( + icon: Icons.cloud_upload, + count: webDavState.uploadingCount, + label: l10n.webdavUploading, + color: colorScheme.primary, + ), + _buildStatItem( + icon: Icons.check_circle, + count: webDavState.completedCount, + label: l10n.webdavCompleted, + color: Colors.green, + ), + _buildStatItem( + icon: Icons.error, + count: webDavState.failedCount, + label: l10n.webdavFailed, + color: Colors.red, + ), + ], + ), + ), + ], + ); + } + + Widget _buildStatItem({ + required IconData icon, + required int count, + required String label, + required Color color, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 28), + const SizedBox(height: 4), + Text( + count.toString(), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: color, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } + + Widget _buildQueueItem( + WebDavUploadItem item, + ColorScheme colorScheme, + AppLocalizations l10n, + ) { + final statusIcon = switch (item.status) { + WebDavUploadStatus.pending => Icons.schedule, + WebDavUploadStatus.uploading => Icons.cloud_upload, + WebDavUploadStatus.completed => Icons.check_circle, + WebDavUploadStatus.failed => Icons.error, + }; + + final statusColor = switch (item.status) { + WebDavUploadStatus.pending => colorScheme.outline, + WebDavUploadStatus.uploading => colorScheme.primary, + WebDavUploadStatus.completed => Colors.green, + WebDavUploadStatus.failed => Colors.red, + }; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Card( + child: ListTile( + leading: Stack( + alignment: Alignment.center, + children: [ + if (item.status == WebDavUploadStatus.uploading) + SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + value: item.progress, + strokeWidth: 3, + color: colorScheme.primary, + ), + ), + Icon(statusIcon, color: statusColor), + ], + ), + title: Text( + item.trackName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.artistName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + if (item.status == WebDavUploadStatus.uploading) + Padding( + padding: const EdgeInsets.only(top: 4), + child: LinearProgressIndicator( + value: item.progress, + borderRadius: BorderRadius.circular(2), + ), + ), + if (item.error != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + item.error!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.status == WebDavUploadStatus.uploading) + Text('${(item.progress * 100).toInt()}%'), + if (item.status == WebDavUploadStatus.failed) + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + ref.read(webDavProvider.notifier).retryItem(item.id); + }, + tooltip: l10n.webdavRetry, + ), + if (item.status != WebDavUploadStatus.uploading) + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + ref.read(webDavProvider.notifier).removeFromQueue(item.id); + }, + tooltip: l10n.webdavRemove, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/services/webdav_service.dart b/lib/services/webdav_service.dart new file mode 100644 index 00000000..d2cafc93 --- /dev/null +++ b/lib/services/webdav_service.dart @@ -0,0 +1,180 @@ +import 'dart:io'; +import 'package:webdav_client/webdav_client.dart' as webdav; +import 'package:spotiflac_android/models/webdav_config.dart'; +import 'package:spotiflac_android/utils/logger.dart'; +import 'package:path/path.dart' as p; + +final _log = AppLogger('WebDavService'); + +class WebDavService { + webdav.Client? _client; + WebDavConfig? _config; + + static final WebDavService _instance = WebDavService._internal(); + factory WebDavService() => _instance; + WebDavService._internal(); + + void configure(WebDavConfig config) { + _config = config; + if (config.isConfigured) { + _client = webdav.newClient( + config.serverUrl, + user: config.username, + password: config.password, + debug: false, + ); + _log.i('WebDAV client configured for: ${config.serverUrl}'); + } else { + _client = null; + _log.d('WebDAV client cleared - configuration incomplete'); + } + } + + bool get isConfigured => _config?.isConfigured ?? false; + bool get isEnabled => _config?.enabled ?? false; + WebDavConfig? get config => _config; + + /// Test the WebDAV connection + Future<({bool success, String? error})> testConnection() async { + if (_client == null || _config == null) { + return (success: false, error: 'WebDAV not configured'); + } + + try { + // Try to ping the server + await _client!.ping(); + _log.i('WebDAV connection test successful'); + return (success: true, error: null); + } catch (e) { + _log.e('WebDAV connection test failed: $e'); + return (success: false, error: e.toString()); + } + } + + /// Ensure the remote directory exists + Future _ensureRemoteDir(String remotePath) async { + if (_client == null) return; + + final parts = remotePath.split('/').where((p) => p.isNotEmpty).toList(); + var currentPath = ''; + + for (final part in parts) { + currentPath = '$currentPath/$part'; + try { + await _client!.mkdir(currentPath); + } catch (e) { + // Directory might already exist, ignore error + _log.d('mkdir $currentPath: $e'); + } + } + } + + /// Upload a file to WebDAV server + /// Returns the remote path on success, or throws an exception on failure + Future uploadFile( + String localPath, + String remotePath, { + void Function(double progress)? onProgress, + CancelToken? cancelToken, + }) async { + if (_client == null || _config == null) { + throw Exception('WebDAV not configured'); + } + + final file = File(localPath); + if (!await file.exists()) { + throw Exception('Local file does not exist: $localPath'); + } + + final fileSize = await file.length(); + final remoteDir = p.dirname(remotePath); + + _log.d( + 'Uploading $localPath to $remotePath (${(fileSize / 1024 / 1024).toStringAsFixed(2)} MB)', + ); + + // Ensure remote directory exists + await _ensureRemoteDir(remoteDir); + + // Upload the file + var lastReportedProgress = 0.0; + + try { + await _client!.writeFromFile( + localPath, + remotePath, + onProgress: (current, total) { + if (cancelToken?.isCancelled ?? false) { + throw Exception('Upload cancelled'); + } + final progress = total > 0 ? current / total : 0.0; + if (progress - lastReportedProgress >= 0.01 || progress == 1.0) { + lastReportedProgress = progress; + onProgress?.call(progress); + } + }, + ); + + _log.i('Successfully uploaded to: $remotePath'); + return remotePath; + } catch (e) { + _log.e('Upload failed: $e'); + rethrow; + } + } + + /// Delete a local file after successful upload + Future deleteLocalFile(String localPath) async { + try { + final file = File(localPath); + if (await file.exists()) { + await file.delete(); + _log.d('Deleted local file: $localPath'); + return true; + } + return false; + } catch (e) { + _log.e('Failed to delete local file: $e'); + return false; + } + } + + /// Build the remote path for a downloaded file + String buildRemotePath( + String localFilePath, { + String? albumName, + String? artistName, + }) { + final basePath = _config?.remotePath ?? '/SpotiFLAC'; + final fileName = p.basename(localFilePath); + + if (albumName != null && artistName != null) { + final sanitizedArtist = _sanitizePathComponent(artistName); + final sanitizedAlbum = _sanitizePathComponent(albumName); + return '$basePath/$sanitizedArtist/$sanitizedAlbum/$fileName'; + } else if (artistName != null) { + final sanitizedArtist = _sanitizePathComponent(artistName); + return '$basePath/$sanitizedArtist/$fileName'; + } + + return '$basePath/$fileName'; + } + + String _sanitizePathComponent(String name) { + return name + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + .replaceAll(RegExp(r'\.+$'), '') + .trim(); + } +} + +/// A simple cancel token for upload operations +class CancelToken { + bool _isCancelled = false; + + bool get isCancelled => _isCancelled; + + void cancel() { + _isCancelled = true; + } +} diff --git a/pubspec.lock b/pubspec.lock index 13c08c36..28a4d0a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1306,6 +1306,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webdav_client: + dependency: "direct main" + description: + name: webdav_client + sha256: "682fffc50b61dc0e8f46717171db03bf9caaa17347be41c0c91e297553bf86b2" + url: "https://pub.dev" + source: hosted + version: "1.2.2" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2481b533..b745a7b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,9 @@ dependencies: # Notifications flutter_local_notifications: ^19.0.0 + # WebDAV + webdav_client: ^1.2.2 + dev_dependencies: flutter_test: sdk: flutter