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
This commit is contained in:
zarzet committed 2026-02-03 19:53:53 +07:00
1 parent 22f001a735
commit 9c22f41a3e
29 files changed
+2969 -80

No files matched your search

+9
View File
@@ -14,6 +14,10 @@
- **Duplicate Detection in Search Results**: "In Library" badge shows on tracks that exist in your local library - **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) - Matches by ISRC (exact match) or track name + artist (fuzzy match)
- Toggle indicator visibility in Settings > Local Library - 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 - **Cloud Upload with WebDAV & SFTP**: Automatically upload downloaded files to your NAS or cloud storage
- Full WebDAV support (Synology DSM, Nextcloud, QNAP, ownCloud) - Full WebDAV support (Synology DSM, Nextcloud, QNAP, ownCloud)
- Full SFTP support (any SSH server with SFTP enabled) - Full SFTP support (any SSH server with SFTP enabled)
@@ -33,6 +37,11 @@
### Changed ### Changed
- Cloud upload passwords are now stored in secure storage instead of SharedPreferences - 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
--- ---
+1 -1
View File
@@ -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 android.useAndroidX=true
+46 -21
View File
@@ -1,8 +1,11 @@
package gobackend package gobackend
import ( import (
"fmt"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"sync" "sync"
"time" "time"
@@ -104,7 +107,16 @@ func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime {
Jar: jar, Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Validate redirect target domain against allowed domains // 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() 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) { if !ext.Manifest.IsDomainAllowed(domain) {
GoLog("[Extension:%s] Redirect blocked: domain '%s' not in allowed list\n", ext.ID, domain) GoLog("[Extension:%s] Redirect blocked: domain '%s' not in allowed list\n", ext.ID, domain)
return &RedirectBlockedError{Domain: 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 // isPrivateIP checks if a hostname resolves to a private/local IP address
func isPrivateIP(host string) bool { func isPrivateIP(host string) bool {
// Block common private network patterns hostLower := strings.ToLower(strings.TrimSpace(host))
// This is a simple check - for production, consider DNS resolution if hostLower == "" {
privatePatterns := []string{ return false
"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 := host if hostLower == "localhost" || strings.HasSuffix(hostLower, ".local") {
for _, pattern := range privatePatterns { return true
if hostLower == pattern || len(hostLower) > len(pattern) && hostLower[:len(pattern)] == pattern { }
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 return true
} }
} }
// Also block .local domains return false
if len(host) > 6 && host[len(host)-6:] == ".local" { }
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 true
} }
return false return false
} }
+28 -2
View File
@@ -41,13 +41,39 @@ func isPathInAllowedDirs(absPath string) bool {
defer allowedDownloadDirsMu.RUnlock() defer allowedDownloadDirsMu.RUnlock()
for _, allowedDir := range allowedDownloadDirs { for _, allowedDir := range allowedDownloadDirs {
if strings.HasPrefix(absPath, allowedDir) { if isPathWithinBase(allowedDir, absPath) {
return true return true
} }
} }
return false 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) { func (r *ExtensionRuntime) validatePath(path string) (string, error) {
// Check if extension has file permission // Check if extension has file permission
if !r.manifest.Permissions.File { if !r.manifest.Permissions.File {
@@ -77,7 +103,7 @@ func (r *ExtensionRuntime) validatePath(path string) (string, error) {
} }
absDataDir, _ := filepath.Abs(r.dataDir) 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) return "", fmt.Errorf("file access denied: path '%s' is outside sandbox", path)
} }
+3
View File
@@ -34,6 +34,9 @@ func (r *ExtensionRuntime) validateDomain(urlStr string) error {
} }
domain := parsed.Hostname() domain := parsed.Hostname()
if domain == "" {
return fmt.Errorf("invalid URL: hostname is required")
}
// Block private/local network access (SSRF protection) // Block private/local network access (SSRF protection)
if isPrivateIP(domain) { if isPrivateIP(domain) {
+247 -1
View File
@@ -142,7 +142,13 @@ abstract class AppLocalizations {
/// **'Home'** /// **'Home'**
String get navHome; 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: /// In en, this message translates to:
/// **'History'** /// **'History'**
@@ -3898,6 +3904,96 @@ abstract class AppLocalizations {
/// **'No stored SFTP host keys found.'** /// **'No stored SFTP host keys found.'**
String get cloudSettingsResetAllSftpHostKeysNone; 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 /// Empty queue state title
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -4455,6 +4551,156 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Selected folder does not exist'** /// **'Selected folder does not exist'**
String get libraryFolderNotExist; 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 class _AppLocalizationsDelegate
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get navHome => 'Startseite'; String get navHome => 'Startseite';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'Verlauf'; String get navHistory => 'Verlauf';
@@ -2147,6 +2150,59 @@ class AppLocalizationsDe extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2485,4 +2541,99 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsEn extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,4 +2526,99 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsEs extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,6 +2526,101 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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`). /// The translations for Spanish Castilian, as used in Spain (`es_ES`).
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsFr extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,4 +2526,99 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get navHome => 'होम'; String get navHome => 'होम';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'इतिहास'; String get navHistory => 'इतिहास';
@@ -2132,6 +2135,59 @@ class AppLocalizationsHi extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,4 +2526,99 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get navHome => 'Beranda'; String get navHome => 'Beranda';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'Riwayat'; String get navHistory => 'Riwayat';
@@ -2145,6 +2148,59 @@ class AppLocalizationsId extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'Tidak ada unduhan dalam antrian'; String get queueEmpty => 'Tidak ada unduhan dalam antrian';
@@ -2483,4 +2539,99 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get navHome => 'ホーム'; String get navHome => 'ホーム';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => '履歴'; String get navHistory => '履歴';
@@ -2119,6 +2122,59 @@ class AppLocalizationsJa extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'キューにダウンロードがありません'; String get queueEmpty => 'キューにダウンロードがありません';
@@ -2456,4 +2512,99 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsKo extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,4 +2526,99 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsNl extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,4 +2526,99 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsPt extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,6 +2526,101 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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`). /// The translations for Portuguese, as used in Portugal (`pt_PT`).
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get navHome => 'Главная'; String get navHome => 'Главная';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'История'; String get navHistory => 'История';
@@ -2171,6 +2174,59 @@ class AppLocalizationsRu extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'Нет загрузок в очереди'; String get queueEmpty => 'Нет загрузок в очереди';
@@ -2516,4 +2572,99 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get navHome => 'Ara'; String get navHome => 'Ara';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'Geçmiş'; String get navHistory => 'Geçmiş';
@@ -2147,6 +2150,59 @@ class AppLocalizationsTr extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2485,4 +2541,99 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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';
} }
+151
View File
@@ -18,6 +18,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get navHome => 'Home'; String get navHome => 'Home';
@override
String get navLibrary => 'Library';
@override @override
String get navHistory => 'History'; String get navHistory => 'History';
@@ -2132,6 +2135,59 @@ class AppLocalizationsZh extends AppLocalizations {
String get cloudSettingsResetAllSftpHostKeysNone => String get cloudSettingsResetAllSftpHostKeysNone =>
'No stored SFTP host keys found.'; '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 @override
String get queueEmpty => 'No downloads in queue'; String get queueEmpty => 'No downloads in queue';
@@ -2470,6 +2526,101 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get libraryFolderNotExist => 'Selected folder does not exist'; 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`). /// The translations for Chinese, as used in China (`zh_CN`).
+104 -2
View File
@@ -9,8 +9,10 @@
"navHome": "Home", "navHome": "Home",
"@navHome": {"description": "Bottom navigation - Home tab"}, "@navHome": {"description": "Bottom navigation - Home tab"},
"navLibrary": "Library",
"@navLibrary": {"description": "Bottom navigation - Library tab"},
"navHistory": "History", "navHistory": "History",
"@navHistory": {"description": "Bottom navigation - History tab"}, "@navHistory": {"description": "Bottom navigation - History tab (legacy)"},
"navSettings": "Settings", "navSettings": "Settings",
"@navSettings": {"description": "Bottom navigation - Settings tab"}, "@navSettings": {"description": "Bottom navigation - Settings tab"},
"navStore": "Store", "navStore": "Store",
@@ -1549,6 +1551,36 @@
"@cloudSettingsResetAllSftpHostKeysCleared": {"description": "Snackbar after clearing all SFTP host keys", "placeholders": {"count": {}}}, "@cloudSettingsResetAllSftpHostKeysCleared": {"description": "Snackbar after clearing all SFTP host keys", "placeholders": {"count": {}}},
"cloudSettingsResetAllSftpHostKeysNone": "No stored SFTP host keys found.", "cloudSettingsResetAllSftpHostKeysNone": "No stored SFTP host keys found.",
"@cloudSettingsResetAllSftpHostKeysNone": {"description": "Snackbar when no SFTP host keys exist"}, "@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": "No downloads in queue",
"@queueEmpty": {"description": "Empty queue state title"}, "@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": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.",
"@libraryStorageAccessMessage": {"description": "Dialog message for storage permission"}, "@libraryStorageAccessMessage": {"description": "Dialog message for storage permission"},
"libraryFolderNotExist": "Selected folder does not exist", "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"}
} }
+5
View File
@@ -44,6 +44,7 @@ class AppSettings {
final String cloudUsername; // Server username final String cloudUsername; // Server username
final String cloudPassword; // Server password (stored securely) final String cloudPassword; // Server password (stored securely)
final String cloudRemotePath; // Remote folder path (e.g. /Music/SpotiFLAC) final String cloudRemotePath; // Remote folder path (e.g. /Music/SpotiFLAC)
final bool cloudAllowInsecureHttp; // Allow HTTP for WebDAV (insecure)
// Local Library Settings // Local Library Settings
final bool localLibraryEnabled; // Enable local library scanning final bool localLibraryEnabled; // Enable local library scanning
@@ -90,6 +91,7 @@ class AppSettings {
this.cloudUsername = '', this.cloudUsername = '',
this.cloudPassword = '', this.cloudPassword = '',
this.cloudRemotePath = '/Music/SpotiFLAC', this.cloudRemotePath = '/Music/SpotiFLAC',
this.cloudAllowInsecureHttp = false,
// Local Library defaults // Local Library defaults
this.localLibraryEnabled = false, this.localLibraryEnabled = false,
this.localLibraryPath = '', this.localLibraryPath = '',
@@ -137,6 +139,7 @@ class AppSettings {
String? cloudUsername, String? cloudUsername,
String? cloudPassword, String? cloudPassword,
String? cloudRemotePath, String? cloudRemotePath,
bool? cloudAllowInsecureHttp,
// Local Library // Local Library
bool? localLibraryEnabled, bool? localLibraryEnabled,
String? localLibraryPath, String? localLibraryPath,
@@ -182,6 +185,8 @@ class AppSettings {
cloudUsername: cloudUsername ?? this.cloudUsername, cloudUsername: cloudUsername ?? this.cloudUsername,
cloudPassword: cloudPassword ?? this.cloudPassword, cloudPassword: cloudPassword ?? this.cloudPassword,
cloudRemotePath: cloudRemotePath ?? this.cloudRemotePath, cloudRemotePath: cloudRemotePath ?? this.cloudRemotePath,
cloudAllowInsecureHttp:
cloudAllowInsecureHttp ?? this.cloudAllowInsecureHttp,
// Local Library // Local Library
localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled, localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled,
localLibraryPath: localLibraryPath ?? this.localLibraryPath, localLibraryPath: localLibraryPath ?? this.localLibraryPath,
+3
View File
@@ -48,6 +48,8 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
cloudUsername: json['cloudUsername'] as String? ?? '', cloudUsername: json['cloudUsername'] as String? ?? '',
cloudPassword: json['cloudPassword'] as String? ?? '', cloudPassword: json['cloudPassword'] as String? ?? '',
cloudRemotePath: json['cloudRemotePath'] as String? ?? '/Music/SpotiFLAC', cloudRemotePath: json['cloudRemotePath'] as String? ?? '/Music/SpotiFLAC',
cloudAllowInsecureHttp:
json['cloudAllowInsecureHttp'] as bool? ?? false,
localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false, localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false,
localLibraryPath: json['localLibraryPath'] as String? ?? '', localLibraryPath: json['localLibraryPath'] as String? ?? '',
localLibraryShowDuplicates: localLibraryShowDuplicates:
@@ -94,6 +96,7 @@ Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
'cloudUsername': instance.cloudUsername, 'cloudUsername': instance.cloudUsername,
'cloudPassword': instance.cloudPassword, 'cloudPassword': instance.cloudPassword,
'cloudRemotePath': instance.cloudRemotePath, 'cloudRemotePath': instance.cloudRemotePath,
'cloudAllowInsecureHttp': instance.cloudAllowInsecureHttp,
'localLibraryEnabled': instance.localLibraryEnabled, 'localLibraryEnabled': instance.localLibraryEnabled,
'localLibraryPath': instance.localLibraryPath, 'localLibraryPath': instance.localLibraryPath,
'localLibraryShowDuplicates': instance.localLibraryShowDuplicates, 'localLibraryShowDuplicates': instance.localLibraryShowDuplicates,
+50 -4
View File
@@ -10,6 +10,7 @@ const _settingsKey = 'app_settings';
const _migrationVersionKey = 'settings_migration_version'; const _migrationVersionKey = 'settings_migration_version';
const _currentMigrationVersion = 1; const _currentMigrationVersion = 1;
const _cloudPasswordKey = 'cloud_password'; const _cloudPasswordKey = 'cloud_password';
const _spotifyClientSecretKey = 'spotify_client_secret';
class SettingsNotifier extends Notifier<AppSettings> { class SettingsNotifier extends Notifier<AppSettings> {
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance(); final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
@@ -31,6 +32,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
} }
await _loadCloudPassword(prefs); await _loadCloudPassword(prefs);
await _loadSpotifyClientSecret(prefs);
_applySpotifyCredentials(); _applySpotifyCredentials();
@@ -54,7 +56,10 @@ class SettingsNotifier extends Notifier<AppSettings> {
Future<void> _saveSettings() async { Future<void> _saveSettings() async {
final prefs = await _prefs; final prefs = await _prefs;
final settingsToSave = state.copyWith(cloudPassword: ''); final settingsToSave = state.copyWith(
cloudPassword: '',
spotifyClientSecret: '',
);
await prefs.setString(_settingsKey, jsonEncode(settingsToSave.toJson())); await prefs.setString(_settingsKey, jsonEncode(settingsToSave.toJson()));
} }
@@ -88,6 +93,36 @@ class SettingsNotifier extends Notifier<AppSettings> {
} }
} }
Future<void> _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<void> _storeSpotifyClientSecret(String secret) async {
if (secret.isEmpty) {
await _secureStorage.delete(key: _spotifyClientSecretKey);
} else {
await _secureStorage.write(key: _spotifyClientSecretKey, value: secret);
}
}
Future<void> _applySpotifyCredentials() async { Future<void> _applySpotifyCredentials() async {
if (state.spotifyClientId.isNotEmpty && if (state.spotifyClientId.isNotEmpty &&
state.spotifyClientSecret.isNotEmpty) { state.spotifyClientSecret.isNotEmpty) {
@@ -193,25 +228,28 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings(); _saveSettings();
} }
void setSpotifyClientSecret(String clientSecret) { Future<void> setSpotifyClientSecret(String clientSecret) async {
state = state.copyWith(spotifyClientSecret: clientSecret); state = state.copyWith(spotifyClientSecret: clientSecret);
await _storeSpotifyClientSecret(clientSecret);
_saveSettings(); _saveSettings();
} }
void setSpotifyCredentials(String clientId, String clientSecret) { Future<void> setSpotifyCredentials(String clientId, String clientSecret) async {
state = state.copyWith( state = state.copyWith(
spotifyClientId: clientId, spotifyClientId: clientId,
spotifyClientSecret: clientSecret, spotifyClientSecret: clientSecret,
); );
await _storeSpotifyClientSecret(clientSecret);
_saveSettings(); _saveSettings();
_applySpotifyCredentials(); _applySpotifyCredentials();
} }
void clearSpotifyCredentials() { Future<void> clearSpotifyCredentials() async {
state = state.copyWith( state = state.copyWith(
spotifyClientId: '', spotifyClientId: '',
spotifyClientSecret: '', spotifyClientSecret: '',
); );
await _storeSpotifyClientSecret('');
_saveSettings(); _saveSettings();
_applySpotifyCredentials(); _applySpotifyCredentials();
} }
@@ -319,6 +357,11 @@ void setUseAllFilesAccess(bool enabled) {
_saveSettings(); _saveSettings();
} }
void setCloudAllowInsecureHttp(bool allowed) {
state = state.copyWith(cloudAllowInsecureHttp: allowed);
_saveSettings();
}
Future<void> setCloudSettings({ Future<void> setCloudSettings({
bool? enabled, bool? enabled,
String? provider, String? provider,
@@ -326,6 +369,7 @@ void setUseAllFilesAccess(bool enabled) {
String? username, String? username,
String? password, String? password,
String? remotePath, String? remotePath,
bool? allowInsecureHttp,
}) async { }) async {
final nextPassword = password ?? state.cloudPassword; final nextPassword = password ?? state.cloudPassword;
state = state.copyWith( state = state.copyWith(
@@ -335,6 +379,8 @@ void setUseAllFilesAccess(bool enabled) {
cloudUsername: username ?? state.cloudUsername, cloudUsername: username ?? state.cloudUsername,
cloudPassword: nextPassword, cloudPassword: nextPassword,
cloudRemotePath: remotePath ?? state.cloudRemotePath, cloudRemotePath: remotePath ?? state.cloudRemotePath,
cloudAllowInsecureHttp:
allowInsecureHttp ?? state.cloudAllowInsecureHttp,
); );
if (password != null) { if (password != null) {
await _storeCloudPassword(nextPassword); await _storeCloudPassword(nextPassword);
+1
View File
@@ -170,6 +170,7 @@ class UploadQueueNotifier extends Notifier<UploadQueueState> {
serverUrl: settings.cloudServerUrl, serverUrl: settings.cloudServerUrl,
username: settings.cloudUsername, username: settings.cloudUsername,
password: settings.cloudPassword, password: settings.cloudPassword,
allowInsecureHttp: settings.cloudAllowInsecureHttp,
onProgress: (sent, total) { onProgress: (sent, total) {
if (total > 0) { if (total > 0) {
final progress = sent / total; final progress = sent / total;
+3 -3
View File
@@ -202,14 +202,14 @@ class _MainShellState extends ConsumerState<MainShell> {
icon: Badge( icon: Badge(
isLabelVisible: queueState > 0, isLabelVisible: queueState > 0,
label: Text('$queueState'), label: Text('$queueState'),
child: const Icon(Icons.history_outlined), child: const Icon(Icons.library_music_outlined),
), ),
selectedIcon: Badge( selectedIcon: Badge(
isLabelVisible: queueState > 0, isLabelVisible: queueState > 0,
label: Text('$queueState'), label: Text('$queueState'),
child: const Icon(Icons.history), child: const Icon(Icons.library_music),
), ),
label: l10n.navHistory, label: l10n.navLibrary,
), ),
if (showStore) if (showStore)
NavigationDestination( NavigationDestination(
+288 -2
View File
@@ -12,9 +12,82 @@ import 'package:spotiflac_android/utils/mime_utils.dart';
import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/models/download_item.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/settings_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/track_metadata_screen.dart';
import 'package:spotiflac_android/screens/downloaded_album_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 { class _GroupedAlbum {
final String albumName; final String albumName;
final String artistName; final String artistName;
@@ -664,6 +737,12 @@ final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items));
final allHistoryItems = ref.watch( final allHistoryItems = ref.watch(
downloadHistoryProvider.select((s) => s.items), 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))
: <LocalLibraryItem>[];
_ensureHistoryCaches(allHistoryItems); _ensureHistoryCaches(allHistoryItems);
final historyViewMode = ref.watch( final historyViewMode = ref.watch(
settingsProvider.select((s) => s.historyViewMode), settingsProvider.select((s) => s.historyViewMode),
@@ -720,7 +799,7 @@ final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items));
expandedTitleScale: 1.0, expandedTitleScale: 1.0,
titlePadding: const EdgeInsets.only(left: 24, bottom: 16), titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
title: Text( title: Text(
context.l10n.historyTitle, context.l10n.navLibrary,
style: TextStyle( style: TextStyle(
fontSize: 20 + (14 * expandRatio), fontSize: 20 + (14 * expandRatio),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -945,6 +1024,7 @@ const Spacer(),
queueItems: queueItems, queueItems: queueItems,
groupedAlbums: groupedAlbums, groupedAlbums: groupedAlbums,
albumCounts: historyStats.albumCounts, albumCounts: historyStats.albumCounts,
localLibraryItems: localLibraryItems,
); );
}, },
), ),
@@ -982,7 +1062,8 @@ child: _buildSelectionBottomBar(
required String historyViewMode, required String historyViewMode,
required List<DownloadItem> queueItems, required List<DownloadItem> queueItems,
required List<_GroupedAlbum> groupedAlbums, required List<_GroupedAlbum> groupedAlbums,
required Map<String, int> albumCounts, required Map<String, int> albumCounts,
required List<LocalLibraryItem> localLibraryItems,
}) { }) {
final historyItems = _resolveHistoryItems( final historyItems = _resolveHistoryItems(
filterMode: filterMode, filterMode: filterMode,
@@ -1151,8 +1232,57 @@ if (filterMode == 'albums' && filteredGroupedAlbums.isNotEmpty)
}, childCount: historyItems.length ), }, 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 && if (queueItems.isEmpty &&
historyItems.isEmpty && historyItems.isEmpty &&
localLibraryItems.isEmpty &&
(filterMode != 'albums' || filteredGroupedAlbums.isEmpty) && (filterMode != 'albums' || filteredGroupedAlbums.isEmpty) &&
!showFilteringIndicator) !showFilteringIndicator)
SliverFillRemaining( SliverFillRemaining(
@@ -2084,6 +2214,27 @@ child: CachedNetworkImage(
const SizedBox(height: 2), const SizedBox(height: 2),
Row( Row(
children: [ 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( Text(
dateStr, dateStr,
style: Theme.of(context).textTheme.labelSmall 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 { class _FilterChip extends StatelessWidget {
+98 -19
View File
@@ -204,6 +204,21 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
], ],
), ),
), ),
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 // Test Connection Button
SliverToBoxAdapter( SliverToBoxAdapter(
@@ -347,11 +362,11 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
String _getProviderName(String provider) { String _getProviderName(String provider) {
switch (provider) { switch (provider) {
case 'webdav': case 'webdav':
return 'WebDAV (Synology, Nextcloud, QNAP)'; return context.l10n.cloudProviderWebdav;
case 'sftp': case 'sftp':
return 'SFTP (SSH File Transfer)'; return context.l10n.cloudProviderSftp;
default: default:
return 'Not Configured'; return context.l10n.cloudProviderNotConfigured;
} }
} }
@@ -388,8 +403,8 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.web), leading: const Icon(Icons.web),
title: const Text('WebDAV'), title: Text(context.l10n.cloudProviderWebdavTitle),
subtitle: const Text('Synology, Nextcloud, QNAP, ownCloud'), subtitle: Text(context.l10n.cloudProviderWebdavSubtitle),
trailing: current == 'webdav' ? Icon(Icons.check, color: colorScheme.primary) : null, trailing: current == 'webdav' ? Icon(Icons.check, color: colorScheme.primary) : null,
onTap: () { onTap: () {
ref.read(settingsProvider.notifier).setCloudProvider('webdav'); ref.read(settingsProvider.notifier).setCloudProvider('webdav');
@@ -398,8 +413,8 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.terminal), leading: const Icon(Icons.terminal),
title: const Text('SFTP'), title: Text(context.l10n.cloudProviderSftpTitle),
subtitle: const Text('SSH File Transfer Protocol'), subtitle: Text(context.l10n.cloudProviderSftpSubtitle),
trailing: current == 'sftp' ? Icon(Icons.check, color: colorScheme.primary) : null, trailing: current == 'sftp' ? Icon(Icons.check, color: colorScheme.primary) : null,
onTap: () { onTap: () {
ref.read(settingsProvider.notifier).setCloudProvider('sftp'); ref.read(settingsProvider.notifier).setCloudProvider('sftp');
@@ -424,7 +439,7 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
if (settings.cloudServerUrl.isEmpty) { if (settings.cloudServerUrl.isEmpty) {
setState(() { setState(() {
_isTestingConnection = false; _isTestingConnection = false;
_connectionTestResult = 'Error: Server URL is required'; _connectionTestResult = context.l10n.errorGeneric(context.l10n.cloudTestErrorServerUrlRequired);
}); });
return; return;
} }
@@ -432,7 +447,7 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
if (settings.cloudUsername.isEmpty || settings.cloudPassword.isEmpty) { if (settings.cloudUsername.isEmpty || settings.cloudPassword.isEmpty) {
setState(() { setState(() {
_isTestingConnection = false; _isTestingConnection = false;
_connectionTestResult = 'Error: Username and password are required'; _connectionTestResult = context.l10n.errorGeneric(context.l10n.cloudTestErrorCredentialsRequired);
}); });
return; return;
} }
@@ -442,13 +457,14 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
serverUrl: settings.cloudServerUrl, serverUrl: settings.cloudServerUrl,
username: settings.cloudUsername, username: settings.cloudUsername,
password: settings.cloudPassword, password: settings.cloudPassword,
allowInsecureHttp: settings.cloudAllowInsecureHttp,
); );
setState(() { setState(() {
_isTestingConnection = false; _isTestingConnection = false;
_connectionTestResult = result.success _connectionTestResult = result.success
? 'Success: Connected to WebDAV server' ? context.l10n.connectionTestSuccess(context.l10n.cloudTestSuccessWebdav)
: 'Error: ${result.error}'; : context.l10n.errorGeneric(_localizeWebDavError(context, result));
}); });
} else if (settings.cloudProvider == 'sftp') { } else if (settings.cloudProvider == 'sftp') {
final result = await CloudUploadService.instance.testSFTPConnection( final result = await CloudUploadService.instance.testSFTPConnection(
@@ -460,17 +476,80 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
setState(() { setState(() {
_isTestingConnection = false; _isTestingConnection = false;
_connectionTestResult = result.success _connectionTestResult = result.success
? 'Success: Connected to SFTP server' ? context.l10n.connectionTestSuccess(context.l10n.cloudTestSuccessSftp)
: 'Error: ${result.error}'; : context.l10n.errorGeneric(result.error ?? '');
}); });
} else { } else {
setState(() { setState(() {
_isTestingConnection = false; _isTestingConnection = false;
_connectionTestResult = 'Error: No provider selected'; _connectionTestResult = context.l10n.errorGeneric(context.l10n.cloudTestErrorNoProvider);
}); });
} }
} }
Future<void> _handleAllowHttpChanged(bool value) async {
if (!value) {
ref.read(settingsProvider.notifier).setCloudAllowInsecureHttp(false);
return;
}
final confirmed = await showDialog<bool>(
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<void> _resetSftpHostKey() async { Future<void> _resetSftpHostKey() async {
final settings = ref.read(settingsProvider); final settings = ref.read(settingsProvider);
if (settings.cloudServerUrl.isEmpty) { if (settings.cloudServerUrl.isEmpty) {
@@ -586,28 +665,28 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
context, context,
Icons.hourglass_empty, Icons.hourglass_empty,
uploadState.pendingCount.toString(), uploadState.pendingCount.toString(),
'Pending', context.l10n.uploadStatusPending,
colorScheme.tertiary, colorScheme.tertiary,
), ),
_buildStatItem( _buildStatItem(
context, context,
Icons.cloud_upload, Icons.cloud_upload,
uploadState.uploadingCount.toString(), uploadState.uploadingCount.toString(),
'Uploading', context.l10n.uploadStatusUploading,
colorScheme.primary, colorScheme.primary,
), ),
_buildStatItem( _buildStatItem(
context, context,
Icons.check_circle, Icons.check_circle,
uploadState.completedCount.toString(), uploadState.completedCount.toString(),
'Done', context.l10n.uploadStatusDone,
Colors.green, Colors.green,
), ),
_buildStatItem( _buildStatItem(
context, context,
Icons.error, Icons.error,
uploadState.failedCount.toString(), uploadState.failedCount.toString(),
'Failed', context.l10n.uploadStatusFailed,
colorScheme.error, colorScheme.error,
), ),
], ],
@@ -729,7 +808,7 @@ class _CloudSettingsPageState extends ConsumerState<CloudSettingsPage> {
onPressed: () { onPressed: () {
ref.read(uploadQueueProvider.notifier).retryFailed(item.id); ref.read(uploadQueueProvider.notifier).retryFailed(item.id);
}, },
tooltip: 'Retry', tooltip: context.l10n.dialogRetry,
); );
break; break;
} }
@@ -410,9 +410,9 @@ class _LibraryStatusCard extends StatelessWidget {
final now = DateTime.now(); final now = DateTime.now();
final diff = now.difference(lastScannedAt!); final diff = now.difference(lastScannedAt!);
if (diff.inMinutes < 1) return 'Just now'; if (diff.inMinutes < 1) return context.l10n.timeJustNow;
if (diff.inHours < 1) return '${diff.inMinutes} minutes ago'; if (diff.inHours < 1) return context.l10n.timeMinutesAgo(diff.inMinutes);
if (diff.inDays < 1) return '${diff.inHours} hours ago'; if (diff.inDays < 1) return context.l10n.timeHoursAgo(diff.inHours);
if (diff.inDays < 7) return context.l10n.dateDaysAgo(diff.inDays); if (diff.inDays < 7) return context.l10n.dateDaysAgo(diff.inDays);
return '${lastScannedAt!.day}/${lastScannedAt!.month}/${lastScannedAt!.year}'; return '${lastScannedAt!.day}/${lastScannedAt!.month}/${lastScannedAt!.year}';
+117 -22
View File
@@ -10,11 +10,13 @@ import 'package:spotiflac_android/utils/logger.dart';
class CloudUploadResult { class CloudUploadResult {
final bool success; final bool success;
final String? error; final String? error;
final String? errorCode;
final String? remotePath; final String? remotePath;
const CloudUploadResult({ const CloudUploadResult({
required this.success, required this.success,
this.error, this.error,
this.errorCode,
this.remotePath, this.remotePath,
}); });
@@ -23,9 +25,11 @@ class CloudUploadResult {
remotePath: remotePath, remotePath: remotePath,
); );
factory CloudUploadResult.failure(String error) => CloudUploadResult( factory CloudUploadResult.failure(String error, {String? errorCode}) =>
CloudUploadResult(
success: false, success: false,
error: error, error: error,
errorCode: errorCode,
); );
} }
@@ -37,6 +41,13 @@ class SftpServerInfo {
const SftpServerInfo({required this.host, required this.port}); 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) /// Service for uploading files to cloud storage (WebDAV, SFTP)
class CloudUploadService { class CloudUploadService {
static CloudUploadService? _instance; static CloudUploadService? _instance;
@@ -51,6 +62,7 @@ class CloudUploadService {
String? _currentServerUrl; String? _currentServerUrl;
String? _currentUsername; String? _currentUsername;
String? _currentPassword; String? _currentPassword;
bool? _currentAllowInsecureHttp;
static const _sftpHostKeysKey = 'sftp_known_host_keys'; static const _sftpHostKeysKey = 'sftp_known_host_keys';
Map<String, Map<String, String>>? _knownHostKeys; Map<String, Map<String, String>>? _knownHostKeys;
@@ -78,9 +90,35 @@ class CloudUploadService {
// WebDAV Methods // WebDAV Methods
// ============================================================ // ============================================================
bool _isHttpsUrl(String url) { _WebDavError? _validateWebDavUrl(
final uri = Uri.tryParse(url); String url, {
return uri != null && uri.scheme == 'https'; 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 /// Initialize WebDAV client with server credentials
@@ -88,16 +126,22 @@ class CloudUploadService {
required String serverUrl, required String serverUrl,
required String username, required String username,
required String password, required String password,
bool allowInsecureHttp = false,
}) async { }) async {
if (!_isHttpsUrl(serverUrl)) { final urlError = _validateWebDavUrl(
throw ArgumentError('WebDAV URL must use https'); serverUrl,
allowInsecureHttp: allowInsecureHttp,
);
if (urlError != null) {
throw ArgumentError(urlError.message);
} }
// Reuse existing client if credentials haven't changed // Reuse existing client if credentials haven't changed
if (_webdavClient != null && if (_webdavClient != null &&
_currentServerUrl == serverUrl && _currentServerUrl == serverUrl &&
_currentUsername == username && _currentUsername == username &&
_currentPassword == password) { _currentPassword == password &&
_currentAllowInsecureHttp == allowInsecureHttp) {
return; return;
} }
@@ -111,6 +155,7 @@ class CloudUploadService {
_currentServerUrl = serverUrl; _currentServerUrl = serverUrl;
_currentUsername = username; _currentUsername = username;
_currentPassword = password; _currentPassword = password;
_currentAllowInsecureHttp = allowInsecureHttp;
_logInfo('CloudUpload', 'WebDAV client initialized for $serverUrl'); _logInfo('CloudUpload', 'WebDAV client initialized for $serverUrl');
} }
@@ -120,9 +165,17 @@ class CloudUploadService {
required String serverUrl, required String serverUrl,
required String username, required String username,
required String password, required String password,
bool allowInsecureHttp = false,
}) async { }) async {
if (!_isHttpsUrl(serverUrl)) { final urlError = _validateWebDavUrl(
return CloudUploadResult.failure('WebDAV URL must use https.'); serverUrl,
allowInsecureHttp: allowInsecureHttp,
);
if (urlError != null) {
return CloudUploadResult.failure(
urlError.message,
errorCode: urlError.code,
);
} }
try { try {
final client = webdav.newClient( final client = webdav.newClient(
@@ -139,7 +192,11 @@ class CloudUploadService {
return CloudUploadResult.success('/'); return CloudUploadResult.success('/');
} catch (e) { } catch (e) {
_logError('CloudUpload', 'WebDAV connection test failed', e.toString()); _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 username,
required String password, required String password,
void Function(int sent, int total)? onProgress, void Function(int sent, int total)? onProgress,
bool allowInsecureHttp = false,
}) async { }) async {
if (!_isHttpsUrl(serverUrl)) { final urlError = _validateWebDavUrl(
return CloudUploadResult.failure('WebDAV URL must use https.'); serverUrl,
allowInsecureHttp: allowInsecureHttp,
);
if (urlError != null) {
return CloudUploadResult.failure(
urlError.message,
errorCode: urlError.code,
);
} }
try { try {
// Initialize client if needed // Initialize client if needed
@@ -161,6 +226,7 @@ class CloudUploadService {
serverUrl: serverUrl, serverUrl: serverUrl,
username: username, username: username,
password: password, password: password,
allowInsecureHttp: allowInsecureHttp,
); );
final client = _webdavClient!; final client = _webdavClient!;
@@ -189,7 +255,11 @@ class CloudUploadService {
return CloudUploadResult.success(remotePath); return CloudUploadResult.success(remotePath);
} catch (e) { } catch (e) {
_logError('CloudUpload', 'WebDAV upload failed', e.toString()); _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 /// Parse WebDAV error to user-friendly message
String _parseWebDAVError(dynamic error) { _WebDavError _parseWebDAVError(dynamic error) {
final errorStr = error.toString().toLowerCase(); final errorStr = error.toString().toLowerCase();
if (errorStr.contains('401') || errorStr.contains('unauthorized')) { 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')) { 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')) { 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')) { 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')) { 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')) { 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')) { 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; _currentServerUrl = null;
_currentUsername = null; _currentUsername = null;
_currentPassword = null; _currentPassword = null;
_currentAllowInsecureHttp = null;
} }
Future<bool> clearSftpHostKey({required String serverUrl}) async { Future<bool> clearSftpHostKey({required String serverUrl}) async {