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