From 9c22f41a3e6c81dccf4856a01b9bcd63d6299fe3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 3 Feb 2026 19:53:53 +0700 Subject: [PATCH] feat: rename History tab to Library and show local library items - Rename bottom navigation 'History' to 'Library' - Add Local Library section showing scanned tracks below downloaded tracks - Add source badge to each item (Downloaded/Local) for clear identification - Add new localization strings for Library tab and source badges - Local library items can be played directly from the library tab --- CHANGELOG.md | 9 + android/gradle.properties | 2 +- go_backend/extension_runtime.go | 67 ++-- go_backend/extension_runtime_file.go | 30 +- go_backend/extension_runtime_http.go | 3 + lib/l10n/app_localizations.dart | 248 ++++++++++++++- lib/l10n/app_localizations_de.dart | 151 +++++++++ lib/l10n/app_localizations_en.dart | 151 +++++++++ lib/l10n/app_localizations_es.dart | 151 +++++++++ lib/l10n/app_localizations_fr.dart | 151 +++++++++ lib/l10n/app_localizations_hi.dart | 151 +++++++++ lib/l10n/app_localizations_id.dart | 151 +++++++++ lib/l10n/app_localizations_ja.dart | 151 +++++++++ lib/l10n/app_localizations_ko.dart | 151 +++++++++ lib/l10n/app_localizations_nl.dart | 151 +++++++++ lib/l10n/app_localizations_pt.dart | 151 +++++++++ lib/l10n/app_localizations_ru.dart | 151 +++++++++ lib/l10n/app_localizations_tr.dart | 151 +++++++++ lib/l10n/app_localizations_zh.dart | 151 +++++++++ lib/l10n/arb/app_en.arb | 106 ++++++- lib/models/settings.dart | 5 + lib/models/settings.g.dart | 3 + lib/providers/settings_provider.dart | 54 +++- lib/providers/upload_queue_provider.dart | 1 + lib/screens/main_shell.dart | 6 +- lib/screens/queue_tab.dart | 290 +++++++++++++++++- lib/screens/settings/cloud_settings_page.dart | 117 +++++-- .../settings/library_settings_page.dart | 6 +- lib/services/cloud_upload_service.dart | 139 +++++++-- 29 files changed, 2969 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b5a5ec..47adb363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ - **Duplicate Detection in Search Results**: "In Library" badge shows on tracks that exist in your local library - Matches by ISRC (exact match) or track name + artist (fuzzy match) - Toggle indicator visibility in Settings > Local Library +- **Unified Library Tab**: History tab renamed to Library, now shows both Downloaded and Local Library tracks + - Source badge on each item (Downloaded/Local) to identify the source + - Local Library items shown in a separate section when enabled + - Play button to open local library tracks directly - **Cloud Upload with WebDAV & SFTP**: Automatically upload downloaded files to your NAS or cloud storage - Full WebDAV support (Synology DSM, Nextcloud, QNAP, ownCloud) - Full SFTP support (any SSH server with SFTP enabled) @@ -33,6 +37,11 @@ ### Changed - Cloud upload passwords are now stored in secure storage instead of SharedPreferences +- Spotify client secrets are now stored in secure storage instead of SharedPreferences +- Extension HTTP sandbox now enforces HTTPS and blocks private IPs resolved via DNS +- Extension file sandbox now validates paths using boundary-safe checks +- WebDAV now defaults to HTTPS; insecure HTTP requires explicit opt-in +- WebDAV error messages are now localized in the UI --- diff --git a/android/gradle.properties b/android/gradle.properties index fbee1d8c..68dd758c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,2 +1,2 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index 61674062..a6b8e943 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -1,8 +1,11 @@ package gobackend import ( + "fmt" + "net" "net/http" "net/url" + "strings" "sync" "time" @@ -104,7 +107,16 @@ func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime { Jar: jar, CheckRedirect: func(req *http.Request, via []*http.Request) error { // Validate redirect target domain against allowed domains + if req.URL.Scheme != "https" { + GoLog("[Extension:%s] Redirect blocked: non-https scheme '%s'\n", ext.ID, req.URL.Scheme) + return fmt.Errorf("redirect blocked: only https is allowed") + } + domain := req.URL.Hostname() + if domain == "" { + GoLog("[Extension:%s] Redirect blocked: missing hostname\n", ext.ID) + return fmt.Errorf("redirect blocked: hostname is required") + } if !ext.Manifest.IsDomainAllowed(domain) { GoLog("[Extension:%s] Redirect blocked: domain '%s' not in allowed list\n", ext.ID, domain) return &RedirectBlockedError{Domain: domain} @@ -139,35 +151,48 @@ func (e *RedirectBlockedError) Error() string { // isPrivateIP checks if a hostname resolves to a private/local IP address func isPrivateIP(host string) bool { - // Block common private network patterns - // This is a simple check - for production, consider DNS resolution - privatePatterns := []string{ - "localhost", - "127.", - "10.", - "172.16.", "172.17.", "172.18.", "172.19.", - "172.20.", "172.21.", "172.22.", "172.23.", - "172.24.", "172.25.", "172.26.", "172.27.", - "172.28.", "172.29.", "172.30.", "172.31.", - "192.168.", - "169.254.", - "::1", - "fc00:", - "fe80:", + hostLower := strings.ToLower(strings.TrimSpace(host)) + if hostLower == "" { + return false } - hostLower := host - for _, pattern := range privatePatterns { - if hostLower == pattern || len(hostLower) > len(pattern) && hostLower[:len(pattern)] == pattern { + if hostLower == "localhost" || strings.HasSuffix(hostLower, ".local") { + return true + } + + if ip := net.ParseIP(hostLower); ip != nil { + return isPrivateIPAddr(ip) + } + + ips, err := net.LookupIP(hostLower) + if err != nil { + return false + } + + for _, ip := range ips { + if isPrivateIPAddr(ip) { return true } } - // Also block .local domains - if len(host) > 6 && host[len(host)-6:] == ".local" { + return false +} + +func isPrivateIPAddr(ip net.IP) bool { + if ip == nil { + return false + } + if ip.IsLoopback() || + ip.IsPrivate() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsMulticast() || + ip.IsUnspecified() { + return true + } + if !ip.IsGlobalUnicast() { return true } - return false } diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go index 6914230a..442aa8a6 100644 --- a/go_backend/extension_runtime_file.go +++ b/go_backend/extension_runtime_file.go @@ -41,13 +41,39 @@ func isPathInAllowedDirs(absPath string) bool { defer allowedDownloadDirsMu.RUnlock() for _, allowedDir := range allowedDownloadDirs { - if strings.HasPrefix(absPath, allowedDir) { + if isPathWithinBase(allowedDir, absPath) { return true } } return false } +func isPathWithinBase(baseDir, targetPath string) bool { + baseAbs, err := filepath.Abs(baseDir) + if err != nil { + return false + } + targetAbs, err := filepath.Abs(targetPath) + if err != nil { + return false + } + + rel, err := filepath.Rel(baseAbs, targetAbs) + if err != nil { + return false + } + rel = filepath.Clean(rel) + if rel == "." { + return true + } + + prefix := ".." + string(filepath.Separator) + if rel == ".." || strings.HasPrefix(rel, prefix) { + return false + } + return true +} + func (r *ExtensionRuntime) validatePath(path string) (string, error) { // Check if extension has file permission if !r.manifest.Permissions.File { @@ -77,7 +103,7 @@ func (r *ExtensionRuntime) validatePath(path string) (string, error) { } absDataDir, _ := filepath.Abs(r.dataDir) - if !strings.HasPrefix(absPath, absDataDir) { + if !isPathWithinBase(absDataDir, absPath) { return "", fmt.Errorf("file access denied: path '%s' is outside sandbox", path) } diff --git a/go_backend/extension_runtime_http.go b/go_backend/extension_runtime_http.go index 2d4187b5..faa5c675 100644 --- a/go_backend/extension_runtime_http.go +++ b/go_backend/extension_runtime_http.go @@ -34,6 +34,9 @@ func (r *ExtensionRuntime) validateDomain(urlStr string) error { } domain := parsed.Hostname() + if domain == "" { + return fmt.Errorf("invalid URL: hostname is required") + } // Block private/local network access (SSRF protection) if isPrivateIP(domain) { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index daed1f62..af7bde01 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -142,7 +142,13 @@ abstract class AppLocalizations { /// **'Home'** String get navHome; - /// Bottom navigation - History tab + /// Bottom navigation - Library tab + /// + /// In en, this message translates to: + /// **'Library'** + String get navLibrary; + + /// Bottom navigation - History tab (legacy) /// /// In en, this message translates to: /// **'History'** @@ -3898,6 +3904,96 @@ abstract class AppLocalizations { /// **'No stored SFTP host keys found.'** String get cloudSettingsResetAllSftpHostKeysNone; + /// Toggle/title for allowing insecure HTTP WebDAV connections + /// + /// In en, this message translates to: + /// **'Allow HTTP (Insecure)'** + String get cloudSettingsAllowHttpTitle; + + /// Subtitle warning for allowing insecure HTTP + /// + /// In en, this message translates to: + /// **'Sends credentials without TLS. Not recommended.'** + String get cloudSettingsAllowHttpSubtitle; + + /// Dialog warning message for enabling insecure HTTP + /// + /// In en, this message translates to: + /// **'HTTP does not encrypt your credentials. Only enable if you trust the network.'** + String get cloudSettingsAllowHttpMessage; + + /// Dialog confirm button for enabling insecure HTTP + /// + /// In en, this message translates to: + /// **'Allow HTTP'** + String get cloudSettingsAllowHttpConfirm; + + /// WebDAV error when URL scheme is missing + /// + /// In en, this message translates to: + /// **'Invalid URL: scheme is required'** + String get webdavErrorInvalidScheme; + + /// WebDAV error when HTTPS is required + /// + /// In en, this message translates to: + /// **'WebDAV URL must use https'** + String get webdavErrorHttpsRequired; + + /// WebDAV error when hostname is missing + /// + /// In en, this message translates to: + /// **'Invalid URL: hostname is required'** + String get webdavErrorInvalidHost; + + /// WebDAV error when authentication fails + /// + /// In en, this message translates to: + /// **'Authentication failed. Check username and password.'** + String get webdavErrorAuthFailed; + + /// WebDAV error when access is forbidden + /// + /// In en, this message translates to: + /// **'Access denied. Check permissions on the server.'** + String get webdavErrorForbidden; + + /// WebDAV error when path is not found + /// + /// In en, this message translates to: + /// **'Server path not found. Check the URL.'** + String get webdavErrorNotFound; + + /// WebDAV error when connection fails + /// + /// In en, this message translates to: + /// **'Cannot connect to server. Check URL and network.'** + String get webdavErrorConnectionFailed; + + /// WebDAV error for TLS issues + /// + /// In en, this message translates to: + /// **'SSL/TLS error. Server certificate may be invalid.'** + String get webdavErrorTlsError; + + /// WebDAV error when connection times out + /// + /// In en, this message translates to: + /// **'Connection timed out. Server may be unreachable.'** + String get webdavErrorTimeout; + + /// WebDAV error when server storage is full + /// + /// In en, this message translates to: + /// **'Insufficient storage on server.'** + String get webdavErrorInsufficientStorage; + + /// WebDAV fallback error message + /// + /// In en, this message translates to: + /// **'Upload failed. Please try again.'** + String get webdavErrorUnknown; + /// Empty queue state title /// /// In en, this message translates to: @@ -4455,6 +4551,156 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Selected folder does not exist'** String get libraryFolderNotExist; + + /// Badge for tracks downloaded via SpotiFLAC + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get librarySourceDownloaded; + + /// Badge for tracks from local library scan + /// + /// In en, this message translates to: + /// **'Local'** + String get librarySourceLocal; + + /// Filter chip - show all library items + /// + /// In en, this message translates to: + /// **'All'** + String get libraryFilterAll; + + /// Filter chip - show only downloaded items + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get libraryFilterDownloaded; + + /// Filter chip - show only local library items + /// + /// In en, this message translates to: + /// **'Local'** + String get libraryFilterLocal; + + /// Relative time - less than a minute ago + /// + /// In en, this message translates to: + /// **'Just now'** + String get timeJustNow; + + /// Relative time - minutes ago + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 minute ago} other{{count} minutes ago}}'** + String timeMinutesAgo(int count); + + /// Relative time - hours ago + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 hour ago} other{{count} hours ago}}'** + String timeHoursAgo(int count); + + /// WebDAV provider option with examples + /// + /// In en, this message translates to: + /// **'WebDAV (Synology, Nextcloud, QNAP)'** + String get cloudProviderWebdav; + + /// SFTP provider option + /// + /// In en, this message translates to: + /// **'SFTP (SSH File Transfer)'** + String get cloudProviderSftp; + + /// No cloud provider selected + /// + /// In en, this message translates to: + /// **'Not Configured'** + String get cloudProviderNotConfigured; + + /// WebDAV provider title in picker + /// + /// In en, this message translates to: + /// **'WebDAV'** + String get cloudProviderWebdavTitle; + + /// WebDAV provider subtitle in picker + /// + /// In en, this message translates to: + /// **'Synology, Nextcloud, QNAP, ownCloud'** + String get cloudProviderWebdavSubtitle; + + /// SFTP provider title in picker + /// + /// In en, this message translates to: + /// **'SFTP'** + String get cloudProviderSftpTitle; + + /// SFTP provider subtitle in picker + /// + /// In en, this message translates to: + /// **'SSH File Transfer Protocol'** + String get cloudProviderSftpSubtitle; + + /// Error when testing connection without server URL + /// + /// In en, this message translates to: + /// **'Server URL is required'** + String get cloudTestErrorServerUrlRequired; + + /// Error when testing connection without credentials + /// + /// In en, this message translates to: + /// **'Username and password are required'** + String get cloudTestErrorCredentialsRequired; + + /// Success message for WebDAV connection test + /// + /// In en, this message translates to: + /// **'Connected to WebDAV server'** + String get cloudTestSuccessWebdav; + + /// Success message for SFTP connection test + /// + /// In en, this message translates to: + /// **'Connected to SFTP server'** + String get cloudTestSuccessSftp; + + /// Error when testing connection without provider + /// + /// In en, this message translates to: + /// **'No provider selected'** + String get cloudTestErrorNoProvider; + + /// Connection test success message + /// + /// In en, this message translates to: + /// **'Success: {message}'** + String connectionTestSuccess(String message); + + /// Upload queue status - waiting + /// + /// In en, this message translates to: + /// **'Pending'** + String get uploadStatusPending; + + /// Upload queue status - in progress + /// + /// In en, this message translates to: + /// **'Uploading'** + String get uploadStatusUploading; + + /// Upload queue status - completed + /// + /// In en, this message translates to: + /// **'Done'** + String get uploadStatusDone; + + /// Upload queue status - error + /// + /// In en, this message translates to: + /// **'Failed'** + String get uploadStatusFailed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f0a6ca05..791562b0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -18,6 +18,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get navHome => 'Startseite'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'Verlauf'; @@ -2147,6 +2150,59 @@ class AppLocalizationsDe extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2485,4 +2541,99 @@ class AppLocalizationsDe extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 77cc2d41..46f508dc 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -18,6 +18,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsEn extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,4 +2526,99 @@ class AppLocalizationsEn extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 3e81d034..c3c92845 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -18,6 +18,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsEs extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,6 +2526,101 @@ class AppLocalizationsEs extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } /// 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 d14f27be..0772b206 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -18,6 +18,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsFr extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,4 +2526,99 @@ class AppLocalizationsFr extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 94709892..360b652b 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -18,6 +18,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get navHome => 'होम'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'इतिहास'; @@ -2132,6 +2135,59 @@ class AppLocalizationsHi extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,4 +2526,99 @@ class AppLocalizationsHi extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 17d2a083..249bbeb2 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -18,6 +18,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get navHome => 'Beranda'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'Riwayat'; @@ -2145,6 +2148,59 @@ class AppLocalizationsId extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'Tidak ada unduhan dalam antrian'; @@ -2483,4 +2539,99 @@ class AppLocalizationsId extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 7cc35619..07bb99c1 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -18,6 +18,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get navHome => 'ホーム'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => '履歴'; @@ -2119,6 +2122,59 @@ class AppLocalizationsJa extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'キューにダウンロードがありません'; @@ -2456,4 +2512,99 @@ class AppLocalizationsJa extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 9654897e..b9eef7d3 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -18,6 +18,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsKo extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,4 +2526,99 @@ class AppLocalizationsKo extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e4007f34..4fab0a98 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -18,6 +18,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsNl extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,4 +2526,99 @@ class AppLocalizationsNl extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index fd1c4262..85d4d995 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -18,6 +18,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsPt extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,6 +2526,101 @@ class AppLocalizationsPt extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } /// 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 06bc7a44..77a4256d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -18,6 +18,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get navHome => 'Главная'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'История'; @@ -2171,6 +2174,59 @@ class AppLocalizationsRu extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'Нет загрузок в очереди'; @@ -2516,4 +2572,99 @@ class AppLocalizationsRu extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index baf748ac..f8b29851 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -18,6 +18,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get navHome => 'Ara'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'Geçmiş'; @@ -2147,6 +2150,59 @@ class AppLocalizationsTr extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2485,4 +2541,99 @@ class AppLocalizationsTr extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 424089ca..28b3288f 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -18,6 +18,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -2132,6 +2135,59 @@ class AppLocalizationsZh extends AppLocalizations { String get cloudSettingsResetAllSftpHostKeysNone => 'No stored SFTP host keys found.'; + @override + String get cloudSettingsAllowHttpTitle => 'Allow HTTP (Insecure)'; + + @override + String get cloudSettingsAllowHttpSubtitle => + 'Sends credentials without TLS. Not recommended.'; + + @override + String get cloudSettingsAllowHttpMessage => + 'HTTP does not encrypt your credentials. Only enable if you trust the network.'; + + @override + String get cloudSettingsAllowHttpConfirm => 'Allow HTTP'; + + @override + String get webdavErrorInvalidScheme => 'Invalid URL: scheme is required'; + + @override + String get webdavErrorHttpsRequired => 'WebDAV URL must use https'; + + @override + String get webdavErrorInvalidHost => 'Invalid URL: hostname is required'; + + @override + String get webdavErrorAuthFailed => + 'Authentication failed. Check username and password.'; + + @override + String get webdavErrorForbidden => + 'Access denied. Check permissions on the server.'; + + @override + String get webdavErrorNotFound => 'Server path not found. Check the URL.'; + + @override + String get webdavErrorConnectionFailed => + 'Cannot connect to server. Check URL and network.'; + + @override + String get webdavErrorTlsError => + 'SSL/TLS error. Server certificate may be invalid.'; + + @override + String get webdavErrorTimeout => + 'Connection timed out. Server may be unreachable.'; + + @override + String get webdavErrorInsufficientStorage => + 'Insufficient storage on server.'; + + @override + String get webdavErrorUnknown => 'Upload failed. Please try again.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -2470,6 +2526,101 @@ class AppLocalizationsZh extends AppLocalizations { @override String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get cloudProviderWebdav => 'WebDAV (Synology, Nextcloud, QNAP)'; + + @override + String get cloudProviderSftp => 'SFTP (SSH File Transfer)'; + + @override + String get cloudProviderNotConfigured => 'Not Configured'; + + @override + String get cloudProviderWebdavTitle => 'WebDAV'; + + @override + String get cloudProviderWebdavSubtitle => + 'Synology, Nextcloud, QNAP, ownCloud'; + + @override + String get cloudProviderSftpTitle => 'SFTP'; + + @override + String get cloudProviderSftpSubtitle => 'SSH File Transfer Protocol'; + + @override + String get cloudTestErrorServerUrlRequired => 'Server URL is required'; + + @override + String get cloudTestErrorCredentialsRequired => + 'Username and password are required'; + + @override + String get cloudTestSuccessWebdav => 'Connected to WebDAV server'; + + @override + String get cloudTestSuccessSftp => 'Connected to SFTP server'; + + @override + String get cloudTestErrorNoProvider => 'No provider selected'; + + @override + String connectionTestSuccess(String message) { + return 'Success: $message'; + } + + @override + String get uploadStatusPending => 'Pending'; + + @override + String get uploadStatusUploading => 'Uploading'; + + @override + String get uploadStatusDone => 'Done'; + + @override + String get uploadStatusFailed => 'Failed'; } /// 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 853c14a8..e579675c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -9,8 +9,10 @@ "navHome": "Home", "@navHome": {"description": "Bottom navigation - Home tab"}, + "navLibrary": "Library", + "@navLibrary": {"description": "Bottom navigation - Library tab"}, "navHistory": "History", - "@navHistory": {"description": "Bottom navigation - History tab"}, + "@navHistory": {"description": "Bottom navigation - History tab (legacy)"}, "navSettings": "Settings", "@navSettings": {"description": "Bottom navigation - Settings tab"}, "navStore": "Store", @@ -1549,6 +1551,36 @@ "@cloudSettingsResetAllSftpHostKeysCleared": {"description": "Snackbar after clearing all SFTP host keys", "placeholders": {"count": {}}}, "cloudSettingsResetAllSftpHostKeysNone": "No stored SFTP host keys found.", "@cloudSettingsResetAllSftpHostKeysNone": {"description": "Snackbar when no SFTP host keys exist"}, + "cloudSettingsAllowHttpTitle": "Allow HTTP (Insecure)", + "@cloudSettingsAllowHttpTitle": {"description": "Toggle/title for allowing insecure HTTP WebDAV connections"}, + "cloudSettingsAllowHttpSubtitle": "Sends credentials without TLS. Not recommended.", + "@cloudSettingsAllowHttpSubtitle": {"description": "Subtitle warning for allowing insecure HTTP"}, + "cloudSettingsAllowHttpMessage": "HTTP does not encrypt your credentials. Only enable if you trust the network.", + "@cloudSettingsAllowHttpMessage": {"description": "Dialog warning message for enabling insecure HTTP"}, + "cloudSettingsAllowHttpConfirm": "Allow HTTP", + "@cloudSettingsAllowHttpConfirm": {"description": "Dialog confirm button for enabling insecure HTTP"}, + "webdavErrorInvalidScheme": "Invalid URL: scheme is required", + "@webdavErrorInvalidScheme": {"description": "WebDAV error when URL scheme is missing"}, + "webdavErrorHttpsRequired": "WebDAV URL must use https", + "@webdavErrorHttpsRequired": {"description": "WebDAV error when HTTPS is required"}, + "webdavErrorInvalidHost": "Invalid URL: hostname is required", + "@webdavErrorInvalidHost": {"description": "WebDAV error when hostname is missing"}, + "webdavErrorAuthFailed": "Authentication failed. Check username and password.", + "@webdavErrorAuthFailed": {"description": "WebDAV error when authentication fails"}, + "webdavErrorForbidden": "Access denied. Check permissions on the server.", + "@webdavErrorForbidden": {"description": "WebDAV error when access is forbidden"}, + "webdavErrorNotFound": "Server path not found. Check the URL.", + "@webdavErrorNotFound": {"description": "WebDAV error when path is not found"}, + "webdavErrorConnectionFailed": "Cannot connect to server. Check URL and network.", + "@webdavErrorConnectionFailed": {"description": "WebDAV error when connection fails"}, + "webdavErrorTlsError": "SSL/TLS error. Server certificate may be invalid.", + "@webdavErrorTlsError": {"description": "WebDAV error for TLS issues"}, + "webdavErrorTimeout": "Connection timed out. Server may be unreachable.", + "@webdavErrorTimeout": {"description": "WebDAV error when connection times out"}, + "webdavErrorInsufficientStorage": "Insufficient storage on server.", + "@webdavErrorInsufficientStorage": {"description": "WebDAV error when server storage is full"}, + "webdavErrorUnknown": "Upload failed. Please try again.", + "@webdavErrorUnknown": {"description": "WebDAV fallback error message"}, "queueEmpty": "No downloads in queue", "@queueEmpty": {"description": "Empty queue state title"}, @@ -1839,5 +1871,75 @@ "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", "@libraryStorageAccessMessage": {"description": "Dialog message for storage permission"}, "libraryFolderNotExist": "Selected folder does not exist", - "@libraryFolderNotExist": {"description": "Error when folder doesn't exist"} + "@libraryFolderNotExist": {"description": "Error when folder doesn't exist"}, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": {"description": "Badge for tracks downloaded via SpotiFLAC"}, + "librarySourceLocal": "Local", + "@librarySourceLocal": {"description": "Badge for tracks from local library scan"}, + "libraryFilterAll": "All", + "@libraryFilterAll": {"description": "Filter chip - show all library items"}, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": {"description": "Filter chip - show only downloaded items"}, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": {"description": "Filter chip - show only local library items"}, + + "timeJustNow": "Just now", + "@timeJustNow": {"description": "Relative time - less than a minute ago"}, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": {"type": "int"} + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": {"type": "int"} + } + }, + + "cloudProviderWebdav": "WebDAV (Synology, Nextcloud, QNAP)", + "@cloudProviderWebdav": {"description": "WebDAV provider option with examples"}, + "cloudProviderSftp": "SFTP (SSH File Transfer)", + "@cloudProviderSftp": {"description": "SFTP provider option"}, + "cloudProviderNotConfigured": "Not Configured", + "@cloudProviderNotConfigured": {"description": "No cloud provider selected"}, + "cloudProviderWebdavTitle": "WebDAV", + "@cloudProviderWebdavTitle": {"description": "WebDAV provider title in picker"}, + "cloudProviderWebdavSubtitle": "Synology, Nextcloud, QNAP, ownCloud", + "@cloudProviderWebdavSubtitle": {"description": "WebDAV provider subtitle in picker"}, + "cloudProviderSftpTitle": "SFTP", + "@cloudProviderSftpTitle": {"description": "SFTP provider title in picker"}, + "cloudProviderSftpSubtitle": "SSH File Transfer Protocol", + "@cloudProviderSftpSubtitle": {"description": "SFTP provider subtitle in picker"}, + + "cloudTestErrorServerUrlRequired": "Server URL is required", + "@cloudTestErrorServerUrlRequired": {"description": "Error when testing connection without server URL"}, + "cloudTestErrorCredentialsRequired": "Username and password are required", + "@cloudTestErrorCredentialsRequired": {"description": "Error when testing connection without credentials"}, + "cloudTestSuccessWebdav": "Connected to WebDAV server", + "@cloudTestSuccessWebdav": {"description": "Success message for WebDAV connection test"}, + "cloudTestSuccessSftp": "Connected to SFTP server", + "@cloudTestSuccessSftp": {"description": "Success message for SFTP connection test"}, + "cloudTestErrorNoProvider": "No provider selected", + "@cloudTestErrorNoProvider": {"description": "Error when testing connection without provider"}, + + "connectionTestSuccess": "Success: {message}", + "@connectionTestSuccess": { + "description": "Connection test success message", + "placeholders": { + "message": {"type": "String"} + } + }, + + "uploadStatusPending": "Pending", + "@uploadStatusPending": {"description": "Upload queue status - waiting"}, + "uploadStatusUploading": "Uploading", + "@uploadStatusUploading": {"description": "Upload queue status - in progress"}, + "uploadStatusDone": "Done", + "@uploadStatusDone": {"description": "Upload queue status - completed"}, + "uploadStatusFailed": "Failed", + "@uploadStatusFailed": {"description": "Upload queue status - error"} } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 369dea02..755e0d53 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -44,6 +44,7 @@ class AppSettings { final String cloudUsername; // Server username final String cloudPassword; // Server password (stored securely) final String cloudRemotePath; // Remote folder path (e.g. /Music/SpotiFLAC) + final bool cloudAllowInsecureHttp; // Allow HTTP for WebDAV (insecure) // Local Library Settings final bool localLibraryEnabled; // Enable local library scanning @@ -90,6 +91,7 @@ class AppSettings { this.cloudUsername = '', this.cloudPassword = '', this.cloudRemotePath = '/Music/SpotiFLAC', + this.cloudAllowInsecureHttp = false, // Local Library defaults this.localLibraryEnabled = false, this.localLibraryPath = '', @@ -137,6 +139,7 @@ class AppSettings { String? cloudUsername, String? cloudPassword, String? cloudRemotePath, + bool? cloudAllowInsecureHttp, // Local Library bool? localLibraryEnabled, String? localLibraryPath, @@ -182,6 +185,8 @@ class AppSettings { cloudUsername: cloudUsername ?? this.cloudUsername, cloudPassword: cloudPassword ?? this.cloudPassword, cloudRemotePath: cloudRemotePath ?? this.cloudRemotePath, + cloudAllowInsecureHttp: + cloudAllowInsecureHttp ?? this.cloudAllowInsecureHttp, // Local Library localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled, localLibraryPath: localLibraryPath ?? this.localLibraryPath, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 002152ef..3ef7c6e8 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -48,6 +48,8 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( cloudUsername: json['cloudUsername'] as String? ?? '', cloudPassword: json['cloudPassword'] as String? ?? '', cloudRemotePath: json['cloudRemotePath'] as String? ?? '/Music/SpotiFLAC', + cloudAllowInsecureHttp: + json['cloudAllowInsecureHttp'] as bool? ?? false, localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false, localLibraryPath: json['localLibraryPath'] as String? ?? '', localLibraryShowDuplicates: @@ -94,6 +96,7 @@ Map _$AppSettingsToJson(AppSettings instance) => 'cloudUsername': instance.cloudUsername, 'cloudPassword': instance.cloudPassword, 'cloudRemotePath': instance.cloudRemotePath, + 'cloudAllowInsecureHttp': instance.cloudAllowInsecureHttp, 'localLibraryEnabled': instance.localLibraryEnabled, 'localLibraryPath': instance.localLibraryPath, 'localLibraryShowDuplicates': instance.localLibraryShowDuplicates, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 10369dc4..acfb3ddd 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -10,6 +10,7 @@ const _settingsKey = 'app_settings'; const _migrationVersionKey = 'settings_migration_version'; const _currentMigrationVersion = 1; const _cloudPasswordKey = 'cloud_password'; +const _spotifyClientSecretKey = 'spotify_client_secret'; class SettingsNotifier extends Notifier { final Future _prefs = SharedPreferences.getInstance(); @@ -31,6 +32,7 @@ class SettingsNotifier extends Notifier { } await _loadCloudPassword(prefs); + await _loadSpotifyClientSecret(prefs); _applySpotifyCredentials(); @@ -54,7 +56,10 @@ class SettingsNotifier extends Notifier { Future _saveSettings() async { final prefs = await _prefs; - final settingsToSave = state.copyWith(cloudPassword: ''); + final settingsToSave = state.copyWith( + cloudPassword: '', + spotifyClientSecret: '', + ); await prefs.setString(_settingsKey, jsonEncode(settingsToSave.toJson())); } @@ -88,6 +93,36 @@ class SettingsNotifier extends Notifier { } } + Future _loadSpotifyClientSecret(SharedPreferences prefs) async { + final storedSecret = await _secureStorage.read(key: _spotifyClientSecretKey); + final prefsSecret = state.spotifyClientSecret; + + if ((storedSecret == null || storedSecret.isEmpty) && + prefsSecret.isNotEmpty) { + await _secureStorage.write(key: _spotifyClientSecretKey, value: prefsSecret); + } + + final effectiveSecret = (storedSecret != null && storedSecret.isNotEmpty) + ? storedSecret + : (prefsSecret.isNotEmpty ? prefsSecret : ''); + + if (effectiveSecret != state.spotifyClientSecret) { + state = state.copyWith(spotifyClientSecret: effectiveSecret); + } + + if (prefsSecret.isNotEmpty) { + await _saveSettings(); + } + } + + Future _storeSpotifyClientSecret(String secret) async { + if (secret.isEmpty) { + await _secureStorage.delete(key: _spotifyClientSecretKey); + } else { + await _secureStorage.write(key: _spotifyClientSecretKey, value: secret); + } + } + Future _applySpotifyCredentials() async { if (state.spotifyClientId.isNotEmpty && state.spotifyClientSecret.isNotEmpty) { @@ -193,25 +228,28 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setSpotifyClientSecret(String clientSecret) { + Future setSpotifyClientSecret(String clientSecret) async { state = state.copyWith(spotifyClientSecret: clientSecret); + await _storeSpotifyClientSecret(clientSecret); _saveSettings(); } - void setSpotifyCredentials(String clientId, String clientSecret) { + Future setSpotifyCredentials(String clientId, String clientSecret) async { state = state.copyWith( spotifyClientId: clientId, spotifyClientSecret: clientSecret, ); + await _storeSpotifyClientSecret(clientSecret); _saveSettings(); _applySpotifyCredentials(); } - void clearSpotifyCredentials() { + Future clearSpotifyCredentials() async { state = state.copyWith( spotifyClientId: '', spotifyClientSecret: '', ); + await _storeSpotifyClientSecret(''); _saveSettings(); _applySpotifyCredentials(); } @@ -319,6 +357,11 @@ void setUseAllFilesAccess(bool enabled) { _saveSettings(); } + void setCloudAllowInsecureHttp(bool allowed) { + state = state.copyWith(cloudAllowInsecureHttp: allowed); + _saveSettings(); + } + Future setCloudSettings({ bool? enabled, String? provider, @@ -326,6 +369,7 @@ void setUseAllFilesAccess(bool enabled) { String? username, String? password, String? remotePath, + bool? allowInsecureHttp, }) async { final nextPassword = password ?? state.cloudPassword; state = state.copyWith( @@ -335,6 +379,8 @@ void setUseAllFilesAccess(bool enabled) { cloudUsername: username ?? state.cloudUsername, cloudPassword: nextPassword, cloudRemotePath: remotePath ?? state.cloudRemotePath, + cloudAllowInsecureHttp: + allowInsecureHttp ?? state.cloudAllowInsecureHttp, ); if (password != null) { await _storeCloudPassword(nextPassword); diff --git a/lib/providers/upload_queue_provider.dart b/lib/providers/upload_queue_provider.dart index c9b6500f..40387773 100644 --- a/lib/providers/upload_queue_provider.dart +++ b/lib/providers/upload_queue_provider.dart @@ -170,6 +170,7 @@ class UploadQueueNotifier extends Notifier { serverUrl: settings.cloudServerUrl, username: settings.cloudUsername, password: settings.cloudPassword, + allowInsecureHttp: settings.cloudAllowInsecureHttp, onProgress: (sent, total) { if (total > 0) { final progress = sent / total; diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index f4bf87bc..2b1e6d08 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -202,14 +202,14 @@ class _MainShellState extends ConsumerState { icon: Badge( isLabelVisible: queueState > 0, label: Text('$queueState'), - child: const Icon(Icons.history_outlined), + child: const Icon(Icons.library_music_outlined), ), selectedIcon: Badge( isLabelVisible: queueState > 0, label: Text('$queueState'), - child: const Icon(Icons.history), + child: const Icon(Icons.library_music), ), - label: l10n.navHistory, + label: l10n.navLibrary, ), if (showStore) NavigationDestination( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index c1e41c8d..afb29a2d 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -12,9 +12,82 @@ import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/providers/local_library_provider.dart'; +import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; +/// Represents the source of a library item +enum LibraryItemSource { downloaded, local } + +/// Unified library item that can come from download history or local library +class UnifiedLibraryItem { + final String id; + final String trackName; + final String artistName; + final String albumName; + final String? coverUrl; + final String filePath; + final String? quality; + final DateTime addedAt; + final LibraryItemSource source; + + // Original items for navigation + final DownloadHistoryItem? historyItem; + final LocalLibraryItem? localItem; + + UnifiedLibraryItem({ + required this.id, + required this.trackName, + required this.artistName, + required this.albumName, + this.coverUrl, + required this.filePath, + this.quality, + required this.addedAt, + required this.source, + this.historyItem, + this.localItem, + }); + + factory UnifiedLibraryItem.fromDownloadHistory(DownloadHistoryItem item) { + return UnifiedLibraryItem( + id: 'dl_${item.id}', + trackName: item.trackName, + artistName: item.artistName, + albumName: item.albumName, + coverUrl: item.coverUrl, + filePath: item.filePath, + quality: item.quality, + addedAt: item.downloadedAt, + source: LibraryItemSource.downloaded, + historyItem: item, + ); + } + + factory UnifiedLibraryItem.fromLocalLibrary(LocalLibraryItem item) { + String? quality; + if (item.bitDepth != null && item.sampleRate != null) { + quality = '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; + } + return UnifiedLibraryItem( + id: 'local_${item.id}', + trackName: item.trackName, + artistName: item.artistName, + albumName: item.albumName, + coverUrl: null, // Local library doesn't have cover URLs + filePath: item.filePath, + quality: quality, + addedAt: item.scannedAt, + source: LibraryItemSource.local, + localItem: item, + ); + } + + String get searchKey => '${trackName.toLowerCase()}|${artistName.toLowerCase()}|${albumName.toLowerCase()}'; + String get albumKey => '$albumName|$artistName'; +} + class _GroupedAlbum { final String albumName; final String artistName; @@ -664,6 +737,12 @@ final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items)); final allHistoryItems = ref.watch( downloadHistoryProvider.select((s) => s.items), ); + // Watch local library items + final localLibraryEnabled = ref.watch(settingsProvider.select((s) => s.localLibraryEnabled)); + final localLibraryItems = localLibraryEnabled + ? ref.watch(localLibraryProvider.select((s) => s.items)) + : []; + _ensureHistoryCaches(allHistoryItems); final historyViewMode = ref.watch( settingsProvider.select((s) => s.historyViewMode), @@ -720,7 +799,7 @@ final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items)); expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - context.l10n.historyTitle, + context.l10n.navLibrary, style: TextStyle( fontSize: 20 + (14 * expandRatio), fontWeight: FontWeight.bold, @@ -945,6 +1024,7 @@ const Spacer(), queueItems: queueItems, groupedAlbums: groupedAlbums, albumCounts: historyStats.albumCounts, + localLibraryItems: localLibraryItems, ); }, ), @@ -982,7 +1062,8 @@ child: _buildSelectionBottomBar( required String historyViewMode, required List queueItems, required List<_GroupedAlbum> groupedAlbums, -required Map albumCounts, + required Map albumCounts, + required List localLibraryItems, }) { final historyItems = _resolveHistoryItems( filterMode: filterMode, @@ -1151,8 +1232,57 @@ if (filterMode == 'albums' && filteredGroupedAlbums.isNotEmpty) }, childCount: historyItems.length ), ), + // Local Library Section + if (localLibraryItems.isNotEmpty && filterMode == 'all') + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + children: [ + Text( + context.l10n.libraryFilterLocal, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${localLibraryItems.length}', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ), + + if (localLibraryItems.isNotEmpty && filterMode == 'all') + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final item = localLibraryItems[index]; + return KeyedSubtree( + key: ValueKey('local_${item.id}'), + child: _buildLocalLibraryItem( + context, + item, + colorScheme, + ), + ); + }, childCount: localLibraryItems.length), + ), + if (queueItems.isEmpty && historyItems.isEmpty && + localLibraryItems.isEmpty && (filterMode != 'albums' || filteredGroupedAlbums.isEmpty) && !showFilteringIndicator) SliverFillRemaining( @@ -2084,6 +2214,27 @@ child: CachedNetworkImage( const SizedBox(height: 2), Row( children: [ + // Source badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + context.l10n.librarySourceDownloaded, + style: Theme.of(context).textTheme.labelSmall + ?.copyWith( + color: colorScheme.onPrimaryContainer, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(width: 8), Text( dateStr, style: Theme.of(context).textTheme.labelSmall @@ -2158,6 +2309,141 @@ child: CachedNetworkImage( ), ); } + + Widget _buildLocalLibraryItem( + BuildContext context, + LocalLibraryItem item, + ColorScheme colorScheme, + ) { + final fileExists = _checkFileExists(item.filePath); + + // Format quality info + String? qualityStr; + if (item.bitDepth != null && item.sampleRate != null) { + qualityStr = '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; + } + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: InkWell( + onTap: () => _openFile(item.filePath), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // Placeholder for cover (local library doesn't have cover URLs) + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.folder_outlined, + color: colorScheme.onSecondaryContainer, + ), + ), + const SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.trackName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + item.artistName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Row( + children: [ + // Source badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + context.l10n.librarySourceLocal, + style: Theme.of(context).textTheme.labelSmall + ?.copyWith( + color: colorScheme.onSecondaryContainer, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ), + if (qualityStr != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: qualityStr.startsWith('24') + ? colorScheme.tertiaryContainer + : colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + qualityStr, + style: Theme.of(context).textTheme.labelSmall + ?.copyWith( + color: qualityStr.startsWith('24') + ? colorScheme.onTertiaryContainer + : colorScheme.onSurfaceVariant, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ), + ], + ), + ), + const SizedBox(width: 8), + + if (fileExists) + IconButton( + onPressed: () => _openFile(item.filePath), + icon: Icon( + Icons.play_arrow, + color: colorScheme.primary, + ), + tooltip: context.l10n.tooltipPlay, + ) + else + Icon( + Icons.error_outline, + color: colorScheme.error, + ), + ], + ), + ), + ), + ); + } } class _FilterChip extends StatelessWidget { diff --git a/lib/screens/settings/cloud_settings_page.dart b/lib/screens/settings/cloud_settings_page.dart index 910c258e..9fe49c1e 100644 --- a/lib/screens/settings/cloud_settings_page.dart +++ b/lib/screens/settings/cloud_settings_page.dart @@ -204,6 +204,21 @@ class _CloudSettingsPageState extends ConsumerState { ], ), ), + if (settings.cloudProvider == 'webdav') + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.warning_amber_outlined, + title: context.l10n.cloudSettingsAllowHttpTitle, + subtitle: context.l10n.cloudSettingsAllowHttpSubtitle, + value: settings.cloudAllowInsecureHttp, + onChanged: _handleAllowHttpChanged, + showDivider: false, + ), + ], + ), + ), // Test Connection Button SliverToBoxAdapter( @@ -347,11 +362,11 @@ class _CloudSettingsPageState extends ConsumerState { String _getProviderName(String provider) { switch (provider) { case 'webdav': - return 'WebDAV (Synology, Nextcloud, QNAP)'; + return context.l10n.cloudProviderWebdav; case 'sftp': - return 'SFTP (SSH File Transfer)'; + return context.l10n.cloudProviderSftp; default: - return 'Not Configured'; + return context.l10n.cloudProviderNotConfigured; } } @@ -388,8 +403,8 @@ class _CloudSettingsPageState extends ConsumerState { ), ListTile( leading: const Icon(Icons.web), - title: const Text('WebDAV'), - subtitle: const Text('Synology, Nextcloud, QNAP, ownCloud'), + title: Text(context.l10n.cloudProviderWebdavTitle), + subtitle: Text(context.l10n.cloudProviderWebdavSubtitle), trailing: current == 'webdav' ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { ref.read(settingsProvider.notifier).setCloudProvider('webdav'); @@ -398,8 +413,8 @@ class _CloudSettingsPageState extends ConsumerState { ), ListTile( leading: const Icon(Icons.terminal), - title: const Text('SFTP'), - subtitle: const Text('SSH File Transfer Protocol'), + title: Text(context.l10n.cloudProviderSftpTitle), + subtitle: Text(context.l10n.cloudProviderSftpSubtitle), trailing: current == 'sftp' ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { ref.read(settingsProvider.notifier).setCloudProvider('sftp'); @@ -424,7 +439,7 @@ class _CloudSettingsPageState extends ConsumerState { if (settings.cloudServerUrl.isEmpty) { setState(() { _isTestingConnection = false; - _connectionTestResult = 'Error: Server URL is required'; + _connectionTestResult = context.l10n.errorGeneric(context.l10n.cloudTestErrorServerUrlRequired); }); return; } @@ -432,7 +447,7 @@ class _CloudSettingsPageState extends ConsumerState { if (settings.cloudUsername.isEmpty || settings.cloudPassword.isEmpty) { setState(() { _isTestingConnection = false; - _connectionTestResult = 'Error: Username and password are required'; + _connectionTestResult = context.l10n.errorGeneric(context.l10n.cloudTestErrorCredentialsRequired); }); return; } @@ -442,13 +457,14 @@ class _CloudSettingsPageState extends ConsumerState { serverUrl: settings.cloudServerUrl, username: settings.cloudUsername, password: settings.cloudPassword, + allowInsecureHttp: settings.cloudAllowInsecureHttp, ); setState(() { _isTestingConnection = false; _connectionTestResult = result.success - ? 'Success: Connected to WebDAV server' - : 'Error: ${result.error}'; + ? context.l10n.connectionTestSuccess(context.l10n.cloudTestSuccessWebdav) + : context.l10n.errorGeneric(_localizeWebDavError(context, result)); }); } else if (settings.cloudProvider == 'sftp') { final result = await CloudUploadService.instance.testSFTPConnection( @@ -460,17 +476,80 @@ class _CloudSettingsPageState extends ConsumerState { setState(() { _isTestingConnection = false; _connectionTestResult = result.success - ? 'Success: Connected to SFTP server' - : 'Error: ${result.error}'; + ? context.l10n.connectionTestSuccess(context.l10n.cloudTestSuccessSftp) + : context.l10n.errorGeneric(result.error ?? ''); }); } else { setState(() { _isTestingConnection = false; - _connectionTestResult = 'Error: No provider selected'; + _connectionTestResult = context.l10n.errorGeneric(context.l10n.cloudTestErrorNoProvider); }); } } + Future _handleAllowHttpChanged(bool value) async { + if (!value) { + ref.read(settingsProvider.notifier).setCloudAllowInsecureHttp(false); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(context.l10n.cloudSettingsAllowHttpTitle), + content: Text(context.l10n.cloudSettingsAllowHttpMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.cloudSettingsAllowHttpConfirm), + ), + ], + ); + }, + ); + + if (confirmed == true) { + ref.read(settingsProvider.notifier).setCloudAllowInsecureHttp(true); + } + } + + String _localizeWebDavError( + BuildContext context, + CloudUploadResult result, + ) { + switch (result.errorCode) { + case 'webdav_invalid_scheme': + return context.l10n.webdavErrorInvalidScheme; + case 'webdav_https_required': + return context.l10n.webdavErrorHttpsRequired; + case 'webdav_invalid_host': + return context.l10n.webdavErrorInvalidHost; + case 'webdav_auth_failed': + return context.l10n.webdavErrorAuthFailed; + case 'webdav_forbidden': + return context.l10n.webdavErrorForbidden; + case 'webdav_not_found': + return context.l10n.webdavErrorNotFound; + case 'webdav_connection_failed': + return context.l10n.webdavErrorConnectionFailed; + case 'webdav_tls_error': + return context.l10n.webdavErrorTlsError; + case 'webdav_timeout': + return context.l10n.webdavErrorTimeout; + case 'webdav_insufficient_storage': + return context.l10n.webdavErrorInsufficientStorage; + case 'webdav_unknown': + return result.error ?? context.l10n.webdavErrorUnknown; + default: + return result.error ?? context.l10n.webdavErrorUnknown; + } + } + Future _resetSftpHostKey() async { final settings = ref.read(settingsProvider); if (settings.cloudServerUrl.isEmpty) { @@ -586,28 +665,28 @@ class _CloudSettingsPageState extends ConsumerState { context, Icons.hourglass_empty, uploadState.pendingCount.toString(), - 'Pending', + context.l10n.uploadStatusPending, colorScheme.tertiary, ), _buildStatItem( context, Icons.cloud_upload, uploadState.uploadingCount.toString(), - 'Uploading', + context.l10n.uploadStatusUploading, colorScheme.primary, ), _buildStatItem( context, Icons.check_circle, uploadState.completedCount.toString(), - 'Done', + context.l10n.uploadStatusDone, Colors.green, ), _buildStatItem( context, Icons.error, uploadState.failedCount.toString(), - 'Failed', + context.l10n.uploadStatusFailed, colorScheme.error, ), ], @@ -729,7 +808,7 @@ class _CloudSettingsPageState extends ConsumerState { onPressed: () { ref.read(uploadQueueProvider.notifier).retryFailed(item.id); }, - tooltip: 'Retry', + tooltip: context.l10n.dialogRetry, ); break; } diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index d2591795..160bcedd 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -410,9 +410,9 @@ class _LibraryStatusCard extends StatelessWidget { final now = DateTime.now(); final diff = now.difference(lastScannedAt!); - if (diff.inMinutes < 1) return 'Just now'; - if (diff.inHours < 1) return '${diff.inMinutes} minutes ago'; - if (diff.inDays < 1) return '${diff.inHours} hours ago'; + if (diff.inMinutes < 1) return context.l10n.timeJustNow; + if (diff.inHours < 1) return context.l10n.timeMinutesAgo(diff.inMinutes); + if (diff.inDays < 1) return context.l10n.timeHoursAgo(diff.inHours); if (diff.inDays < 7) return context.l10n.dateDaysAgo(diff.inDays); return '${lastScannedAt!.day}/${lastScannedAt!.month}/${lastScannedAt!.year}'; diff --git a/lib/services/cloud_upload_service.dart b/lib/services/cloud_upload_service.dart index f4658455..1b2d5018 100644 --- a/lib/services/cloud_upload_service.dart +++ b/lib/services/cloud_upload_service.dart @@ -10,11 +10,13 @@ import 'package:spotiflac_android/utils/logger.dart'; class CloudUploadResult { final bool success; final String? error; + final String? errorCode; final String? remotePath; const CloudUploadResult({ required this.success, this.error, + this.errorCode, this.remotePath, }); @@ -23,9 +25,11 @@ class CloudUploadResult { remotePath: remotePath, ); - factory CloudUploadResult.failure(String error) => CloudUploadResult( + factory CloudUploadResult.failure(String error, {String? errorCode}) => + CloudUploadResult( success: false, error: error, + errorCode: errorCode, ); } @@ -37,6 +41,13 @@ class SftpServerInfo { const SftpServerInfo({required this.host, required this.port}); } +class _WebDavError { + final String code; + final String message; + + const _WebDavError({required this.code, required this.message}); +} + /// Service for uploading files to cloud storage (WebDAV, SFTP) class CloudUploadService { static CloudUploadService? _instance; @@ -51,6 +62,7 @@ class CloudUploadService { String? _currentServerUrl; String? _currentUsername; String? _currentPassword; + bool? _currentAllowInsecureHttp; static const _sftpHostKeysKey = 'sftp_known_host_keys'; Map>? _knownHostKeys; @@ -78,9 +90,35 @@ class CloudUploadService { // WebDAV Methods // ============================================================ - bool _isHttpsUrl(String url) { - final uri = Uri.tryParse(url); - return uri != null && uri.scheme == 'https'; + _WebDavError? _validateWebDavUrl( + String url, { + required bool allowInsecureHttp, + }) { + final uri = Uri.tryParse(url.trim()); + if (uri == null || uri.scheme.isEmpty) { + return const _WebDavError( + code: 'webdav_invalid_scheme', + message: 'Invalid URL: scheme is required', + ); + } + final scheme = uri.scheme.toLowerCase(); + if (scheme != 'https') { + if (scheme == 'http' && allowInsecureHttp) { + // Explicitly allowed by user + } else { + return const _WebDavError( + code: 'webdav_https_required', + message: 'WebDAV URL must use https', + ); + } + } + if (uri.host.isEmpty) { + return const _WebDavError( + code: 'webdav_invalid_host', + message: 'Invalid URL: hostname is required', + ); + } + return null; } /// Initialize WebDAV client with server credentials @@ -88,16 +126,22 @@ class CloudUploadService { required String serverUrl, required String username, required String password, + bool allowInsecureHttp = false, }) async { - if (!_isHttpsUrl(serverUrl)) { - throw ArgumentError('WebDAV URL must use https'); + final urlError = _validateWebDavUrl( + serverUrl, + allowInsecureHttp: allowInsecureHttp, + ); + if (urlError != null) { + throw ArgumentError(urlError.message); } // Reuse existing client if credentials haven't changed if (_webdavClient != null && _currentServerUrl == serverUrl && _currentUsername == username && - _currentPassword == password) { + _currentPassword == password && + _currentAllowInsecureHttp == allowInsecureHttp) { return; } @@ -111,6 +155,7 @@ class CloudUploadService { _currentServerUrl = serverUrl; _currentUsername = username; _currentPassword = password; + _currentAllowInsecureHttp = allowInsecureHttp; _logInfo('CloudUpload', 'WebDAV client initialized for $serverUrl'); } @@ -120,9 +165,17 @@ class CloudUploadService { required String serverUrl, required String username, required String password, + bool allowInsecureHttp = false, }) async { - if (!_isHttpsUrl(serverUrl)) { - return CloudUploadResult.failure('WebDAV URL must use https.'); + final urlError = _validateWebDavUrl( + serverUrl, + allowInsecureHttp: allowInsecureHttp, + ); + if (urlError != null) { + return CloudUploadResult.failure( + urlError.message, + errorCode: urlError.code, + ); } try { final client = webdav.newClient( @@ -139,7 +192,11 @@ class CloudUploadService { return CloudUploadResult.success('/'); } catch (e) { _logError('CloudUpload', 'WebDAV connection test failed', e.toString()); - return CloudUploadResult.failure(_parseWebDAVError(e)); + final parsed = _parseWebDAVError(e); + return CloudUploadResult.failure( + parsed.message, + errorCode: parsed.code, + ); } } @@ -151,9 +208,17 @@ class CloudUploadService { required String username, required String password, void Function(int sent, int total)? onProgress, + bool allowInsecureHttp = false, }) async { - if (!_isHttpsUrl(serverUrl)) { - return CloudUploadResult.failure('WebDAV URL must use https.'); + final urlError = _validateWebDavUrl( + serverUrl, + allowInsecureHttp: allowInsecureHttp, + ); + if (urlError != null) { + return CloudUploadResult.failure( + urlError.message, + errorCode: urlError.code, + ); } try { // Initialize client if needed @@ -161,6 +226,7 @@ class CloudUploadService { serverUrl: serverUrl, username: username, password: password, + allowInsecureHttp: allowInsecureHttp, ); final client = _webdavClient!; @@ -189,7 +255,11 @@ class CloudUploadService { return CloudUploadResult.success(remotePath); } catch (e) { _logError('CloudUpload', 'WebDAV upload failed', e.toString()); - return CloudUploadResult.failure(_parseWebDAVError(e)); + final parsed = _parseWebDAVError(e); + return CloudUploadResult.failure( + parsed.message, + errorCode: parsed.code, + ); } } @@ -209,32 +279,56 @@ class CloudUploadService { } /// Parse WebDAV error to user-friendly message - String _parseWebDAVError(dynamic error) { + _WebDavError _parseWebDAVError(dynamic error) { final errorStr = error.toString().toLowerCase(); if (errorStr.contains('401') || errorStr.contains('unauthorized')) { - return 'Authentication failed. Check username and password.'; + return const _WebDavError( + code: 'webdav_auth_failed', + message: 'Authentication failed. Check username and password.', + ); } if (errorStr.contains('403') || errorStr.contains('forbidden')) { - return 'Access denied. Check permissions on the server.'; + return const _WebDavError( + code: 'webdav_forbidden', + message: 'Access denied. Check permissions on the server.', + ); } if (errorStr.contains('404') || errorStr.contains('not found')) { - return 'Server path not found. Check the URL.'; + return const _WebDavError( + code: 'webdav_not_found', + message: 'Server path not found. Check the URL.', + ); } if (errorStr.contains('connection refused') || errorStr.contains('socket')) { - return 'Cannot connect to server. Check URL and network.'; + return const _WebDavError( + code: 'webdav_connection_failed', + message: 'Cannot connect to server. Check URL and network.', + ); } if (errorStr.contains('certificate') || errorStr.contains('ssl') || errorStr.contains('tls')) { - return 'SSL/TLS error. Server certificate may be invalid.'; + return const _WebDavError( + code: 'webdav_tls_error', + message: 'SSL/TLS error. Server certificate may be invalid.', + ); } if (errorStr.contains('timeout')) { - return 'Connection timed out. Server may be unreachable.'; + return const _WebDavError( + code: 'webdav_timeout', + message: 'Connection timed out. Server may be unreachable.', + ); } if (errorStr.contains('507') || errorStr.contains('insufficient storage')) { - return 'Insufficient storage on server.'; + return const _WebDavError( + code: 'webdav_insufficient_storage', + message: 'Insufficient storage on server.', + ); } - return 'Upload failed: ${error.toString()}'; + return _WebDavError( + code: 'webdav_unknown', + message: 'Upload failed: ${error.toString()}', + ); } // ============================================================ @@ -508,6 +602,7 @@ class CloudUploadService { _currentServerUrl = null; _currentUsername = null; _currentPassword = null; + _currentAllowInsecureHttp = null; } Future clearSftpHostKey({required String serverUrl}) async {