feat: advanced filename templates, low-RAM device profiling, responsive artist UI, and project site
- Add advanced filename template placeholders: {track_raw}, {disc_raw}, {date},
formatted numbers {track:N}/{disc:N}, and date formatting {date:%Y-%m-%d}
with strftime-to-Go layout conversion and robust date parser
- Pass date/release_date metadata to filename builder in all providers
(Amazon, Qobuz, Tidal, YouTube, extensions) and Flutter download queue
- Detect ARM32-only / low-RAM Android devices at startup and reduce image
cache size and disable overscroll effects for smoother experience
- Make artist screen selection bar responsive: compact stacked layout on
narrow screens or large text scale; add quality picker before track download
- Add advanced tags toggle in download settings filename format editor
- Fix ICU plural syntax in DE/ES/PT/RU translations (one {}=1{...} -> one {...})
- Add filenameShowAdvancedTags l10n strings (EN, ID) and regenerate dart files
- Fix featured-artist regex: remove '&' from split separators
- Add Go filename template tests (filename_test.go)
- Add GitHub Pages workflow and static project site
44
.github/workflows/pages.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'site/**'
|
||||
- '.github/workflows/pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: site
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
32
CHANGELOG.md
|
|
@ -1,5 +1,37 @@
|
|||
# Changelog
|
||||
|
||||
## [3.6.7] - 2026-02-13
|
||||
|
||||
### Added
|
||||
|
||||
- "Advanced Filename Templates" - new placeholders for custom track/disc formatting and date patterns
|
||||
- `{track_raw}` and `{disc_raw}` - unpadded raw numbers
|
||||
- `{track:N}` and `{disc:N}` - zero-padded to N digits (e.g. `{track:02}` → `01`)
|
||||
- `{date}` - full release date from metadata
|
||||
- `{date:%Y-%m-%d}` - date formatting with strftime patterns
|
||||
- "Show advanced tags" toggle in Settings > Download > Filename Format to reveal these placeholders
|
||||
- Low-RAM / ARM32-only device profiling - detects constrained devices at startup and reduces image cache (120 items / 24 MiB) and disables overscroll effects for smoother performance
|
||||
- Responsive selection bar on artist screen - switches to compact stacked layout on narrow screens (< 430dp) or large text scale (> 1.15x)
|
||||
- Quality picker dialog before downloading individual tracks from artist screen (when "Ask quality before download" is enabled)
|
||||
- Project website with GitHub Pages deployment workflow
|
||||
- Mobile burger menu navigation for all site pages
|
||||
- Go filename template test suite
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed ICU plural syntax errors in DE, ES, PT, RU translations - incorrect `=1` clause was causing missing plural forms
|
||||
- Fixed featured-artist regex incorrectly splitting on `&` character (e.g. "Simon & Garfunkel" was being split) - removed `&` from separator pattern
|
||||
- Fixed `{date}` placeholder not working in filename templates - release date was not being passed to the template builder across all providers (Amazon, Qobuz, Tidal, YouTube, extensions)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved Go backend metadata handling - filename builder now supports fallback metadata keys and automatic type conversion for more robust template rendering
|
||||
- Extension providers now pass full metadata set to filename builder (track, disc, year, date, release_date)
|
||||
- Updated translations: added filename advanced tags strings (EN, ID), regenerated all locale dart files
|
||||
- Updated app screenshot assets
|
||||
|
||||
---
|
||||
|
||||
## [3.6.6] - 2026-02-12
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 539 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 811 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 122 KiB |
|
|
@ -441,6 +441,7 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) {
|
|||
"album": req.AlbumName,
|
||||
"track": req.TrackNumber,
|
||||
"year": extractYear(req.ReleaseDate),
|
||||
"date": req.ReleaseDate,
|
||||
"disc": req.DiscNumber,
|
||||
})
|
||||
var outputPath string
|
||||
|
|
|
|||
|
|
@ -1136,8 +1136,13 @@ func buildOutputPath(req DownloadRequest) string {
|
|||
"artist": req.ArtistName,
|
||||
"album": req.AlbumName,
|
||||
"album_artist": req.AlbumArtist,
|
||||
"track": req.TrackNumber,
|
||||
"track_number": req.TrackNumber,
|
||||
"disc": req.DiscNumber,
|
||||
"disc_number": req.DiscNumber,
|
||||
"year": extractYear(req.ReleaseDate),
|
||||
"date": req.ReleaseDate,
|
||||
"release_date": req.ReleaseDate,
|
||||
"isrc": req.ISRC,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,18 @@ package gobackend
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var invalidChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`)
|
||||
var (
|
||||
invalidChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`)
|
||||
multiUnderscore = regexp.MustCompile(`_+`)
|
||||
formattedNumberPlaceholderExpr = regexp.MustCompile(`\{(track|disc):([0-9]+)\}`)
|
||||
dateFormatPlaceholderExpr = regexp.MustCompile(`\{date:([^{}]+)\}`)
|
||||
yearPattern = regexp.MustCompile(`\d{4}`)
|
||||
)
|
||||
|
||||
func sanitizeFilename(filename string) string {
|
||||
sanitized := invalidChars.ReplaceAllString(filename, "_")
|
||||
|
|
@ -14,7 +22,6 @@ func sanitizeFilename(filename string) string {
|
|||
sanitized = strings.TrimSpace(sanitized)
|
||||
sanitized = strings.Trim(sanitized, ".")
|
||||
|
||||
multiUnderscore := regexp.MustCompile(`_+`)
|
||||
sanitized = multiUnderscore.ReplaceAllString(sanitized, "_")
|
||||
|
||||
if len(sanitized) > 200 {
|
||||
|
|
@ -33,15 +40,25 @@ func buildFilenameFromTemplate(template string, metadata map[string]interface{})
|
|||
template = "{artist} - {title}"
|
||||
}
|
||||
|
||||
result := template
|
||||
result := replaceFormattedNumberPlaceholders(template, metadata)
|
||||
result = replaceDateFormatPlaceholders(result, metadata)
|
||||
|
||||
dateValue := getDateValue(metadata)
|
||||
yearValue := getString(metadata, "year")
|
||||
if yearValue == "" {
|
||||
yearValue = extractYear(dateValue)
|
||||
}
|
||||
|
||||
placeholders := map[string]string{
|
||||
"{title}": getString(metadata, "title"),
|
||||
"{artist}": getString(metadata, "artist"),
|
||||
"{album}": getString(metadata, "album"),
|
||||
"{track}": formatTrackNumber(getInt(metadata, "track")),
|
||||
"{year}": getString(metadata, "year"),
|
||||
"{track_raw}": formatRawNumber(getInt(metadata, "track")),
|
||||
"{year}": yearValue,
|
||||
"{date}": dateValue,
|
||||
"{disc}": formatDiscNumber(getInt(metadata, "disc")),
|
||||
"{disc_raw}": formatRawNumber(getInt(metadata, "disc")),
|
||||
}
|
||||
|
||||
for placeholder, value := range placeholders {
|
||||
|
|
@ -51,17 +68,75 @@ func buildFilenameFromTemplate(template string, metadata map[string]interface{})
|
|||
return result
|
||||
}
|
||||
|
||||
func replaceFormattedNumberPlaceholders(template string, metadata map[string]interface{}) string {
|
||||
return formattedNumberPlaceholderExpr.ReplaceAllStringFunc(template, func(match string) string {
|
||||
parts := formattedNumberPlaceholderExpr.FindStringSubmatch(match)
|
||||
if len(parts) != 3 {
|
||||
return ""
|
||||
}
|
||||
|
||||
number := getInt(metadata, parts[1])
|
||||
width, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatNumberWithWidth(number, width)
|
||||
})
|
||||
}
|
||||
|
||||
func replaceDateFormatPlaceholders(template string, metadata map[string]interface{}) string {
|
||||
return dateFormatPlaceholderExpr.ReplaceAllStringFunc(template, func(match string) string {
|
||||
parts := dateFormatPlaceholderExpr.FindStringSubmatch(match)
|
||||
if len(parts) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatDateWithPattern(getDateValue(metadata), parts[1])
|
||||
})
|
||||
}
|
||||
|
||||
func getDateValue(metadata map[string]interface{}) string {
|
||||
date := getString(metadata, "date")
|
||||
if date != "" {
|
||||
return date
|
||||
}
|
||||
|
||||
releaseDate := getString(metadata, "release_date")
|
||||
if releaseDate != "" {
|
||||
return releaseDate
|
||||
}
|
||||
|
||||
return getString(metadata, "year")
|
||||
}
|
||||
|
||||
func getString(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return strings.TrimSpace(s)
|
||||
switch value := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(value)
|
||||
case int:
|
||||
return strconv.Itoa(value)
|
||||
case int64:
|
||||
return strconv.FormatInt(value, 10)
|
||||
case float64:
|
||||
return strconv.Itoa(int(value))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getInt(m map[string]interface{}, key string) int {
|
||||
if v, ok := m[key]; ok {
|
||||
candidateKeys := []string{key}
|
||||
switch key {
|
||||
case "track":
|
||||
candidateKeys = append(candidateKeys, "track_number")
|
||||
case "disc":
|
||||
candidateKeys = append(candidateKeys, "disc_number")
|
||||
}
|
||||
|
||||
for _, candidate := range candidateKeys {
|
||||
if v, ok := m[candidate]; ok {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
|
|
@ -69,8 +144,15 @@ func getInt(m map[string]interface{}, key string) int {
|
|||
return int(n)
|
||||
case float64:
|
||||
return int(n)
|
||||
case string:
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(n))
|
||||
if err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +170,129 @@ func formatDiscNumber(n int) string {
|
|||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
func formatRawNumber(n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
func formatNumberWithWidth(n int, width int) string {
|
||||
if n <= 0 || width <= 0 {
|
||||
return ""
|
||||
}
|
||||
if width <= 1 {
|
||||
return formatRawNumber(n)
|
||||
}
|
||||
return fmt.Sprintf("%0*d", width, n)
|
||||
}
|
||||
|
||||
func formatDateWithPattern(rawDate string, strftimePattern string) string {
|
||||
if rawDate == "" || strftimePattern == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsedDate, ok := parseMetadataDate(rawDate)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
goLayout := convertStrftimeToGoLayout(strftimePattern)
|
||||
if goLayout == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parsedDate.Format(goLayout)
|
||||
}
|
||||
|
||||
func parseMetadataDate(rawDate string) (time.Time, bool) {
|
||||
clean := strings.TrimSpace(rawDate)
|
||||
if clean == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
layouts := []string{
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
"2006-01-02",
|
||||
"2006-01",
|
||||
"2006",
|
||||
"2006/01/02",
|
||||
"2006/01",
|
||||
"2006.01.02",
|
||||
"2006.01",
|
||||
}
|
||||
|
||||
for _, layout := range layouts {
|
||||
parsed, err := time.Parse(layout, clean)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
|
||||
if len(clean) >= 10 {
|
||||
parsed, err := time.Parse("2006-01-02", clean[:10])
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
|
||||
yearMatch := yearPattern.FindString(clean)
|
||||
if yearMatch == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(yearMatch)
|
||||
if err != nil || year <= 0 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
return time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC), true
|
||||
}
|
||||
|
||||
func convertStrftimeToGoLayout(pattern string) string {
|
||||
if pattern == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
for i := 0; i < len(pattern); i++ {
|
||||
ch := pattern[i]
|
||||
if ch != '%' {
|
||||
builder.WriteByte(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if i+1 >= len(pattern) {
|
||||
builder.WriteByte('%')
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
switch pattern[i] {
|
||||
case 'Y':
|
||||
builder.WriteString("2006")
|
||||
case 'y':
|
||||
builder.WriteString("06")
|
||||
case 'm':
|
||||
builder.WriteString("01")
|
||||
case 'd':
|
||||
builder.WriteString("02")
|
||||
case 'b':
|
||||
builder.WriteString("Jan")
|
||||
case 'B':
|
||||
builder.WriteString("January")
|
||||
case '%':
|
||||
builder.WriteByte('%')
|
||||
default:
|
||||
builder.WriteByte('%')
|
||||
builder.WriteByte(pattern[i])
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func extractYear(date string) string {
|
||||
if len(date) >= 4 {
|
||||
return date[:4]
|
||||
|
|
|
|||
85
go_backend/filename_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package gobackend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildFilenameFromTemplate_WithRawTrackAndDisc(t *testing.T) {
|
||||
metadata := map[string]interface{}{
|
||||
"title": "Song Name",
|
||||
"artist": "Artist Name",
|
||||
"album": "Album Name",
|
||||
"track": 1,
|
||||
"disc": 2,
|
||||
"year": "2025",
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate(
|
||||
"{artist} - {track} - {track_raw} - d{disc} - d{disc_raw} - {title}",
|
||||
metadata,
|
||||
)
|
||||
|
||||
expected := "Artist Name - 01 - 1 - d2 - d2 - Song Name"
|
||||
if formatted != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFilenameFromTemplate_RawPlaceholdersEmptyWhenZero(t *testing.T) {
|
||||
metadata := map[string]interface{}{
|
||||
"title": "Song Name",
|
||||
"artist": "Artist Name",
|
||||
"track": 0,
|
||||
"disc": 0,
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate("{track_raw}-{disc_raw}-{title}", metadata)
|
||||
expected := "--Song Name"
|
||||
if formatted != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFilenameFromTemplate_InlineNumberFormatting(t *testing.T) {
|
||||
metadata := map[string]interface{}{
|
||||
"track": 3,
|
||||
"disc": 2,
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate("{track:1}-{track:02}-{disc:03}", metadata)
|
||||
expected := "3-03-002"
|
||||
if formatted != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFilenameFromTemplate_DateStrftimeFormatting(t *testing.T) {
|
||||
metadata := map[string]interface{}{
|
||||
"artist": "Artist Name",
|
||||
"title": "Song Name",
|
||||
"release_date": "2024-03-09",
|
||||
"track_number": 7,
|
||||
"disc_number": 1,
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate(
|
||||
"{artist} - {track:02} - {title} - {date:%Y-%m-%d} - {year}",
|
||||
metadata,
|
||||
)
|
||||
expected := "Artist Name - 07 - Song Name - 2024-03-09 - 2024"
|
||||
if formatted != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFilenameFromTemplate_DateStrftimeFormattingWithYearOnly(t *testing.T) {
|
||||
metadata := map[string]interface{}{
|
||||
"artist": "Artist Name",
|
||||
"title": "Song Name",
|
||||
"date": "2019",
|
||||
}
|
||||
|
||||
formatted := buildFilenameFromTemplate("{date:%Y}-{date:%m}-{date:%d}", metadata)
|
||||
expected := "2019-01-01"
|
||||
if formatted != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, formatted)
|
||||
}
|
||||
}
|
||||
|
|
@ -1180,6 +1180,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) {
|
|||
"album": req.AlbumName,
|
||||
"track": req.TrackNumber,
|
||||
"year": extractYear(req.ReleaseDate),
|
||||
"date": req.ReleaseDate,
|
||||
"disc": req.DiscNumber,
|
||||
})
|
||||
var outputPath string
|
||||
|
|
|
|||
|
|
@ -1609,6 +1609,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
|
|||
"album": req.AlbumName,
|
||||
"track": req.TrackNumber,
|
||||
"year": extractYear(req.ReleaseDate),
|
||||
"date": req.ReleaseDate,
|
||||
"disc": req.DiscNumber,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -500,6 +500,7 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
|||
"album": req.AlbumName,
|
||||
"track": req.TrackNumber,
|
||||
"year": extractYear(req.ReleaseDate),
|
||||
"date": req.ReleaseDate,
|
||||
"disc": req.DiscNumber,
|
||||
})
|
||||
filename = sanitizeFilename(filename) + ext
|
||||
|
|
|
|||
26
lib/app.dart
|
|
@ -10,8 +10,12 @@ import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
|
|||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
|
||||
final _routerProvider = Provider<GoRouter>((ref) {
|
||||
final isFirstLaunch = ref.watch(settingsProvider.select((s) => s.isFirstLaunch));
|
||||
final hasCompletedTutorial = ref.watch(settingsProvider.select((s) => s.hasCompletedTutorial));
|
||||
final isFirstLaunch = ref.watch(
|
||||
settingsProvider.select((s) => s.isFirstLaunch),
|
||||
);
|
||||
final hasCompletedTutorial = ref.watch(
|
||||
settingsProvider.select((s) => s.hasCompletedTutorial),
|
||||
);
|
||||
|
||||
// Determine initial location based on app state
|
||||
String initialLocation;
|
||||
|
|
@ -26,14 +30,8 @@ final _routerProvider = Provider<GoRouter>((ref) {
|
|||
return GoRouter(
|
||||
initialLocation: initialLocation,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const MainShell(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/setup',
|
||||
builder: (context, state) => const SetupScreen(),
|
||||
),
|
||||
GoRoute(path: '/', builder: (context, state) => const MainShell()),
|
||||
GoRoute(path: '/setup', builder: (context, state) => const SetupScreen()),
|
||||
GoRoute(
|
||||
path: '/tutorial',
|
||||
builder: (context, state) => const TutorialScreen(),
|
||||
|
|
@ -43,12 +41,17 @@ final _routerProvider = Provider<GoRouter>((ref) {
|
|||
});
|
||||
|
||||
class SpotiFLACApp extends ConsumerWidget {
|
||||
const SpotiFLACApp({super.key});
|
||||
final bool disableOverscrollEffects;
|
||||
|
||||
const SpotiFLACApp({super.key, this.disableOverscrollEffects = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(_routerProvider);
|
||||
final localeString = ref.watch(settingsProvider.select((s) => s.locale));
|
||||
final scrollBehavior = disableOverscrollEffects
|
||||
? const MaterialScrollBehavior().copyWith(overscroll: false)
|
||||
: null;
|
||||
|
||||
Locale? locale;
|
||||
if (localeString != 'system') {
|
||||
|
|
@ -68,6 +71,7 @@ class SpotiFLACApp extends ConsumerWidget {
|
|||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: themeMode,
|
||||
scrollBehavior: scrollBehavior,
|
||||
themeAnimationDuration: const Duration(milliseconds: 300),
|
||||
themeAnimationCurve: Curves.easeInOut,
|
||||
routerConfig: router,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/// App version and info constants
|
||||
/// Update version here only - all other files will reference this
|
||||
class AppInfo {
|
||||
static const String version = '3.6.6';
|
||||
static const String buildNumber = '80';
|
||||
static const String version = '3.6.7';
|
||||
static const String buildNumber = '81';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2152,6 +2152,18 @@ abstract class AppLocalizations {
|
|||
/// **'{artist} - {title}'**
|
||||
String filenameHint(Object artist, Object title);
|
||||
|
||||
/// Toggle label for showing advanced filename tags
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show advanced tags'**
|
||||
String get filenameShowAdvancedTags;
|
||||
|
||||
/// Description for advanced filename tag toggle
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable formatted tags for track padding and date patterns'**
|
||||
String get filenameShowAdvancedTagsDescription;
|
||||
|
||||
/// Setting title - folder structure
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Folder Organization';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,62 +13,62 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get appDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Téléchargez des pistes Spotify en qualité sans perte de Tidal, Qobuz et Amazon Music.';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
String get navHome => 'Accueil';
|
||||
|
||||
@override
|
||||
String get navLibrary => 'Library';
|
||||
String get navLibrary => 'Bibliothèques';
|
||||
|
||||
@override
|
||||
String get navHistory => 'History';
|
||||
String get navHistory => 'Historique';
|
||||
|
||||
@override
|
||||
String get navSettings => 'Settings';
|
||||
String get navSettings => 'Paramètres';
|
||||
|
||||
@override
|
||||
String get navStore => 'Store';
|
||||
String get navStore => 'Magasin';
|
||||
|
||||
@override
|
||||
String get homeTitle => 'Home';
|
||||
String get homeTitle => 'Accueil';
|
||||
|
||||
@override
|
||||
String get homeSearchHint => 'Paste Spotify URL or search...';
|
||||
String get homeSearchHint => 'Coller l\'URL Spotify ou rechercher...';
|
||||
|
||||
@override
|
||||
String homeSearchHintExtension(String extensionName) {
|
||||
return 'Search with $extensionName...';
|
||||
return 'Rechercher avec $extensionName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
String get homeSubtitle => 'Coller un lien Spotify ou rechercher par nom';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
String get homeSupports => 'Supports: Piste, Album, Playlist, Artiste URLs';
|
||||
|
||||
@override
|
||||
String get homeRecent => 'Recent';
|
||||
String get homeRecent => 'Récent';
|
||||
|
||||
@override
|
||||
String get historyTitle => 'History';
|
||||
String get historyTitle => 'Historique';
|
||||
|
||||
@override
|
||||
String historyDownloading(int count) {
|
||||
return 'Downloading ($count)';
|
||||
return 'Téléchargement ($count)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get historyDownloaded => 'Downloaded';
|
||||
String get historyDownloaded => 'Téléchargé';
|
||||
|
||||
@override
|
||||
String get historyFilterAll => 'All';
|
||||
String get historyFilterAll => 'Tous';
|
||||
|
||||
@override
|
||||
String get historyFilterAlbums => 'Albums';
|
||||
|
||||
@override
|
||||
String get historyFilterSingles => 'Singles';
|
||||
String get historyFilterSingles => 'Titres';
|
||||
|
||||
@override
|
||||
String historyTracksCount(int count) {
|
||||
|
|
@ -93,36 +93,37 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get historyNoDownloads => 'No download history';
|
||||
String get historyNoDownloads => 'Pas d\'historique de téléchargement';
|
||||
|
||||
@override
|
||||
String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here';
|
||||
String get historyNoDownloadsSubtitle =>
|
||||
'Les pistes téléchargées apparaîtront ici';
|
||||
|
||||
@override
|
||||
String get historyNoAlbums => 'No album downloads';
|
||||
String get historyNoAlbums => 'Pas de téléchargement d\'album';
|
||||
|
||||
@override
|
||||
String get historyNoAlbumsSubtitle =>
|
||||
'Download multiple tracks from an album to see them here';
|
||||
'Téléchargez plusieurs titres d\'un album pour les voir ici';
|
||||
|
||||
@override
|
||||
String get historyNoSingles => 'No single downloads';
|
||||
String get historyNoSingles => 'Pas de téléchargements uniques';
|
||||
|
||||
@override
|
||||
String get historyNoSinglesSubtitle =>
|
||||
'Single track downloads will appear here';
|
||||
'Les téléchargements de pistes uniques apparaîtront ici';
|
||||
|
||||
@override
|
||||
String get historySearchHint => 'Search history...';
|
||||
String get historySearchHint => 'Historique de recherche...';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
String get settingsTitle => 'Paramètres';
|
||||
|
||||
@override
|
||||
String get settingsDownload => 'Download';
|
||||
String get settingsDownload => 'Télécharger';
|
||||
|
||||
@override
|
||||
String get settingsAppearance => 'Appearance';
|
||||
String get settingsAppearance => 'Apparence';
|
||||
|
||||
@override
|
||||
String get settingsOptions => 'Options';
|
||||
|
|
@ -131,51 +132,54 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get settingsExtensions => 'Extensions';
|
||||
|
||||
@override
|
||||
String get settingsAbout => 'About';
|
||||
String get settingsAbout => 'À propos';
|
||||
|
||||
@override
|
||||
String get downloadTitle => 'Download';
|
||||
String get downloadTitle => 'Télécharger';
|
||||
|
||||
@override
|
||||
String get downloadLocation => 'Download Location';
|
||||
String get downloadLocation => 'Télécharger Localisation';
|
||||
|
||||
@override
|
||||
String get downloadLocationSubtitle => 'Choose where to save files';
|
||||
String get downloadLocationSubtitle =>
|
||||
'Choisissez où enregistrer des fichiers';
|
||||
|
||||
@override
|
||||
String get downloadLocationDefault => 'Default location';
|
||||
String get downloadLocationDefault => 'Localisation par défaut';
|
||||
|
||||
@override
|
||||
String get downloadDefaultService => 'Default Service';
|
||||
String get downloadDefaultService => 'Service par défaut';
|
||||
|
||||
@override
|
||||
String get downloadDefaultServiceSubtitle => 'Service used for downloads';
|
||||
String get downloadDefaultServiceSubtitle =>
|
||||
'Service utilisé pour les téléchargements';
|
||||
|
||||
@override
|
||||
String get downloadDefaultQuality => 'Default Quality';
|
||||
String get downloadDefaultQuality => 'Qualité par défaut';
|
||||
|
||||
@override
|
||||
String get downloadAskQuality => 'Ask Quality Before Download';
|
||||
String get downloadAskQuality =>
|
||||
'Demandez La Qualité Avant Le Téléchargement';
|
||||
|
||||
@override
|
||||
String get downloadAskQualitySubtitle =>
|
||||
'Show quality picker for each download';
|
||||
'Afficher le sélecteur de qualité pour chaque téléchargement';
|
||||
|
||||
@override
|
||||
String get downloadFilenameFormat => 'Filename Format';
|
||||
String get downloadFilenameFormat => 'Nom du fichier';
|
||||
|
||||
@override
|
||||
String get downloadFolderOrganization => 'Folder Organization';
|
||||
String get downloadFolderOrganization => 'Organisation du dossier';
|
||||
|
||||
@override
|
||||
String get downloadSeparateSingles => 'Separate Singles';
|
||||
String get downloadSeparateSingles => 'Titres séparés';
|
||||
|
||||
@override
|
||||
String get downloadSeparateSinglesSubtitle =>
|
||||
'Put single tracks in a separate folder';
|
||||
'Mettre des pistes uniques dans un dossier séparé';
|
||||
|
||||
@override
|
||||
String get qualityBest => 'Best Available';
|
||||
String get qualityBest => 'Meilleur Disponible';
|
||||
|
||||
@override
|
||||
String get qualityFlac => 'FLAC';
|
||||
|
|
@ -187,69 +191,71 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get quality128 => '128 kbps';
|
||||
|
||||
@override
|
||||
String get appearanceTitle => 'Appearance';
|
||||
String get appearanceTitle => 'Apparence';
|
||||
|
||||
@override
|
||||
String get appearanceTheme => 'Theme';
|
||||
String get appearanceTheme => 'Thème';
|
||||
|
||||
@override
|
||||
String get appearanceThemeSystem => 'System';
|
||||
String get appearanceThemeSystem => 'Système';
|
||||
|
||||
@override
|
||||
String get appearanceThemeLight => 'Light';
|
||||
String get appearanceThemeLight => 'Clair';
|
||||
|
||||
@override
|
||||
String get appearanceThemeDark => 'Dark';
|
||||
String get appearanceThemeDark => 'Sombre';
|
||||
|
||||
@override
|
||||
String get appearanceDynamicColor => 'Dynamic Color';
|
||||
String get appearanceDynamicColor => 'Couleur dynamique';
|
||||
|
||||
@override
|
||||
String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper';
|
||||
String get appearanceDynamicColorSubtitle =>
|
||||
'Utilisez les couleurs de votre fond d\'écran';
|
||||
|
||||
@override
|
||||
String get appearanceAccentColor => 'Accent Color';
|
||||
String get appearanceAccentColor => 'Couleur d\'accent';
|
||||
|
||||
@override
|
||||
String get appearanceHistoryView => 'History View';
|
||||
String get appearanceHistoryView => 'Historique Vue';
|
||||
|
||||
@override
|
||||
String get appearanceHistoryViewList => 'List';
|
||||
String get appearanceHistoryViewList => '';
|
||||
|
||||
@override
|
||||
String get appearanceHistoryViewGrid => 'Grid';
|
||||
String get appearanceHistoryViewGrid => 'Grille';
|
||||
|
||||
@override
|
||||
String get optionsTitle => 'Options';
|
||||
|
||||
@override
|
||||
String get optionsSearchSource => 'Search Source';
|
||||
String get optionsSearchSource => 'Recherche Source';
|
||||
|
||||
@override
|
||||
String get optionsPrimaryProvider => 'Primary Provider';
|
||||
String get optionsPrimaryProvider => 'Fournisseur principal';
|
||||
|
||||
@override
|
||||
String get optionsPrimaryProviderSubtitle =>
|
||||
'Service used when searching by track name.';
|
||||
'Service utilisé lors de la recherche par nom de piste.';
|
||||
|
||||
@override
|
||||
String optionsUsingExtension(String extensionName) {
|
||||
return 'Using extension: $extensionName';
|
||||
return 'Utilisation de l\'extension: $extensionName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get optionsSwitchBack =>
|
||||
'Tap Deezer or Spotify to switch back from extension';
|
||||
'Appuyez sur Deezer ou Spotify pour revenir à l\'extension';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallback => 'Auto Fallback';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallbackSubtitle =>
|
||||
'Try other services if download fails';
|
||||
'Essayez d\'autres services si le téléchargement échoue';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProviders => 'Use Extension Providers';
|
||||
String get optionsUseExtensionProviders =>
|
||||
'Utiliser des fournisseurs d\'extension';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOn => 'Extensions will be tried first';
|
||||
|
|
@ -376,16 +382,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get extensionsUninstall => 'Uninstall';
|
||||
String get extensionsUninstall => 'Désinstaller';
|
||||
|
||||
@override
|
||||
String get extensionsSetAsSearch => 'Set as Search Provider';
|
||||
String get extensionsSetAsSearch => 'Défini comme fournisseur de recherche';
|
||||
|
||||
@override
|
||||
String get storeTitle => 'Extension Store';
|
||||
String get storeTitle => 'Magasin d\'extension';
|
||||
|
||||
@override
|
||||
String get storeSearch => 'Search extensions...';
|
||||
String get storeSearch => 'Recherche d\'extensions...';
|
||||
|
||||
@override
|
||||
String get storeInstall => 'Install';
|
||||
|
|
@ -567,7 +573,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get trackMetadataDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get trackMetadataQuality => 'Quality';
|
||||
String get trackMetadataQuality => '';
|
||||
|
||||
@override
|
||||
String get trackMetadataPath => 'File Path';
|
||||
|
|
@ -579,38 +585,38 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get trackMetadataService => 'Service';
|
||||
|
||||
@override
|
||||
String get trackMetadataPlay => 'Play';
|
||||
String get trackMetadataPlay => 'Jouer';
|
||||
|
||||
@override
|
||||
String get trackMetadataShare => 'Share';
|
||||
String get trackMetadataShare => 'Partager';
|
||||
|
||||
@override
|
||||
String get trackMetadataDelete => 'Delete';
|
||||
String get trackMetadataDelete => 'Supprimer';
|
||||
|
||||
@override
|
||||
String get trackMetadataRedownload => 'Re-download';
|
||||
String get trackMetadataRedownload => 'Re-télécharger';
|
||||
|
||||
@override
|
||||
String get trackMetadataOpenFolder => 'Open Folder';
|
||||
String get trackMetadataOpenFolder => 'Dossier ouvert';
|
||||
|
||||
@override
|
||||
String get setupTitle => 'Welcome to SpotiFLAC';
|
||||
String get setupTitle => 'Bienvenue chez SpotiFLAC';
|
||||
|
||||
@override
|
||||
String get setupSubtitle => 'Let\'s get you started';
|
||||
String get setupSubtitle => 'On va commencer';
|
||||
|
||||
@override
|
||||
String get setupStoragePermission => 'Storage Permission';
|
||||
String get setupStoragePermission => 'Permission de stockage';
|
||||
|
||||
@override
|
||||
String get setupStoragePermissionSubtitle =>
|
||||
'Required to save downloaded files';
|
||||
'Requis pour enregistrer les fichiers téléchargés';
|
||||
|
||||
@override
|
||||
String get setupStoragePermissionGranted => 'Permission granted';
|
||||
String get setupStoragePermissionGranted => 'Permission accordée';
|
||||
|
||||
@override
|
||||
String get setupStoragePermissionDenied => 'Permission denied';
|
||||
String get setupStoragePermissionDenied => 'Permission refusée';
|
||||
|
||||
@override
|
||||
String get setupGrantPermission => 'Grant Permission';
|
||||
|
|
@ -735,14 +741,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
'Get notified when downloads complete or require attention.';
|
||||
|
||||
@override
|
||||
String get setupFolderSelected => 'Download Folder Selected!';
|
||||
String get setupFolderSelected => 'Dossier de téléchargement sélectionné!';
|
||||
|
||||
@override
|
||||
String get setupFolderChoose => 'Choose Download Folder';
|
||||
String get setupFolderChoose => 'Choisissez le dossier pour télécharger';
|
||||
|
||||
@override
|
||||
String get setupFolderDescription =>
|
||||
'Select a folder where your downloaded music will be saved.';
|
||||
'Sélectionnez un dossier dans lequel votre musique téléchargée sera enregistrée.';
|
||||
|
||||
@override
|
||||
String get setupChangeFolder => 'Change Folder';
|
||||
|
|
@ -1182,6 +1188,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Folder Organization';
|
||||
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,13 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Folder Organization';
|
||||
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get optionsSpotifyDeprecationWarning =>
|
||||
'Pencarian Spotify akan dihentikan pada 3 Maret 2026 karena perubahan API Spotify. Silakan beralih ke Deezer.';
|
||||
'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.';
|
||||
|
||||
@override
|
||||
String get extensionsTitle => 'Ekstensi';
|
||||
|
|
@ -1188,6 +1188,13 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Tampilkan tag lanjutan';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Aktifkan tag format untuk padding nomor lagu dan pola tanggal';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Organisasi Folder';
|
||||
|
||||
|
|
@ -1941,27 +1948,26 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
String get downloadAlbumFolderStructure => 'Struktur Folder Album';
|
||||
|
||||
@override
|
||||
String get downloadUseAlbumArtistForFolders =>
|
||||
'Gunakan Album Artist untuk folder';
|
||||
String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders';
|
||||
|
||||
@override
|
||||
String get downloadUseAlbumArtistForFoldersAlbumSubtitle =>
|
||||
'Folder artis memakai Album Artist jika tersedia';
|
||||
'Artist folders use Album Artist when available';
|
||||
|
||||
@override
|
||||
String get downloadUseAlbumArtistForFoldersTrackSubtitle =>
|
||||
'Folder artis hanya memakai Track Artist';
|
||||
'Artist folders use Track Artist only';
|
||||
|
||||
@override
|
||||
String get downloadUsePrimaryArtistOnly => 'Hanya artis utama untuk folder';
|
||||
String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders';
|
||||
|
||||
@override
|
||||
String get downloadUsePrimaryArtistOnlyEnabled =>
|
||||
'Featured artist dihapus dari nama folder (misal Justin Bieber, Quavo → Justin Bieber)';
|
||||
'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)';
|
||||
|
||||
@override
|
||||
String get downloadUsePrimaryArtistOnlyDisabled =>
|
||||
'Nama artis lengkap dipakai untuk folder';
|
||||
'Full artist string used for folder name';
|
||||
|
||||
@override
|
||||
String get downloadSaveFormat => 'Simpan Format';
|
||||
|
|
@ -2200,10 +2206,10 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
String get recentTypePlaylist => 'Playlist';
|
||||
|
||||
@override
|
||||
String get recentEmpty => 'Belum ada item terbaru';
|
||||
String get recentEmpty => 'No recent items yet';
|
||||
|
||||
@override
|
||||
String get recentShowAllDownloads => 'Tampilkan Semua Download';
|
||||
String get recentShowAllDownloads => 'Show All Downloads';
|
||||
|
||||
@override
|
||||
String recentPlaylistInfo(String name) {
|
||||
|
|
@ -2312,10 +2318,10 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates';
|
||||
|
||||
@override
|
||||
String get settingsCache => 'Penyimpanan & Cache';
|
||||
String get settingsCache => 'Storage & Cache';
|
||||
|
||||
@override
|
||||
String get settingsCacheSubtitle => 'Lihat ukuran dan bersihkan data cache';
|
||||
String get settingsCacheSubtitle => 'View size and clear cached data';
|
||||
|
||||
@override
|
||||
String get libraryTitle => 'Local Library';
|
||||
|
|
@ -2590,221 +2596,219 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
String get storageModeInfo => 'Your files are stored in multiple locations';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTitle => 'Selamat Datang di SpotiFLAC!';
|
||||
String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeDesc =>
|
||||
'Mari pelajari cara mengunduh musik favorit Anda dalam kualitas lossless. Tutorial singkat ini akan menunjukkan dasar-dasarnya.';
|
||||
'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip1 =>
|
||||
'Unduh musik dari Spotify, Deezer, atau tempel URL yang didukung';
|
||||
'Download music from Spotify, Deezer, or paste any supported URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Dapatkan audio kualitas FLAC dari Tidal, Qobuz, atau Amazon Music';
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
'Metadata, cover art, dan lirik otomatis tertanam';
|
||||
'Automatic metadata, cover art, and lyrics embedding';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTitle => 'Mencari Musik';
|
||||
String get tutorialSearchTitle => 'Finding Music';
|
||||
|
||||
@override
|
||||
String get tutorialSearchDesc =>
|
||||
'Ada dua cara mudah untuk menemukan musik yang ingin Anda unduh.';
|
||||
'There are two easy ways to find music you want to download.';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTip1 =>
|
||||
'Tempel URL Spotify atau Deezer langsung di kotak pencarian';
|
||||
'Paste a Spotify or Deezer URL directly in the search box';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTip2 =>
|
||||
'Atau ketik nama lagu, artis, atau album untuk mencari';
|
||||
'Or type the song name, artist, or album to search';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTip3 =>
|
||||
'Mendukung lagu, album, playlist, dan halaman artis';
|
||||
'Supports tracks, albums, playlists, and artist pages';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTitle => 'Mengunduh Musik';
|
||||
String get tutorialDownloadTitle => 'Downloading Music';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadDesc =>
|
||||
'Mengunduh musik itu mudah dan cepat. Begini caranya.';
|
||||
'Downloading music is simple and fast. Here\'s how it works.';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTip1 =>
|
||||
'Ketuk tombol unduh di samping lagu mana pun untuk mulai mengunduh';
|
||||
'Tap the download button next to any track to start downloading';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTip2 =>
|
||||
'Pilih kualitas yang Anda inginkan (FLAC, Hi-Res, atau MP3)';
|
||||
'Choose your preferred quality (FLAC, Hi-Res, or MP3)';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTip3 =>
|
||||
'Unduh seluruh album atau playlist dengan satu ketukan';
|
||||
'Download entire albums or playlists with one tap';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTitle => 'Perpustakaan Anda';
|
||||
String get tutorialLibraryTitle => 'Your Library';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryDesc =>
|
||||
'Semua musik yang Anda unduh terorganisir di tab Perpustakaan.';
|
||||
'All your downloaded music is organized in the Library tab.';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTip1 =>
|
||||
'Lihat progres unduhan dan antrian di tab Perpustakaan';
|
||||
'View download progress and queue in the Library tab';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTip2 =>
|
||||
'Ketuk lagu mana pun untuk memutarnya dengan pemutar musik';
|
||||
'Tap any track to play it with your music player';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTip3 =>
|
||||
'Beralih antara tampilan daftar dan grid untuk penjelajahan lebih baik';
|
||||
'Switch between list and grid view for better browsing';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTitle => 'Ekstensi';
|
||||
String get tutorialExtensionsTitle => 'Extensions';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsDesc =>
|
||||
'Tingkatkan kemampuan aplikasi dengan ekstensi komunitas.';
|
||||
'Extend the app\'s capabilities with community extensions.';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
'Jelajahi tab Toko untuk menemukan ekstensi berguna';
|
||||
'Browse the Store tab to discover useful extensions';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
'Tambahkan provider unduhan atau sumber pencarian baru';
|
||||
'Add new download providers or search sources';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip3 =>
|
||||
'Dapatkan lirik, metadata lebih baik, dan fitur lainnya';
|
||||
'Get lyrics, enhanced metadata, and more features';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTitle => 'Sesuaikan Pengalaman Anda';
|
||||
String get tutorialSettingsTitle => 'Customize Your Experience';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsDesc =>
|
||||
'Personalisasi aplikasi di Pengaturan sesuai preferensi Anda.';
|
||||
'Personalize the app in Settings to match your preferences.';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTip1 =>
|
||||
'Ubah lokasi unduhan dan organisasi folder';
|
||||
'Change download location and folder organization';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTip2 =>
|
||||
'Atur kualitas audio dan preferensi format default';
|
||||
'Set default audio quality and format preferences';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTip3 => 'Sesuaikan tema dan tampilan aplikasi';
|
||||
String get tutorialSettingsTip3 => 'Customize app theme and appearance';
|
||||
|
||||
@override
|
||||
String get tutorialReadyMessage =>
|
||||
'Anda siap! Mulai unduh musik favorit Anda sekarang.';
|
||||
'You\'re all set! Start downloading your favorite music now.';
|
||||
|
||||
@override
|
||||
String get tutorialExample => 'CONTOH';
|
||||
String get tutorialExample => 'EXAMPLE';
|
||||
|
||||
@override
|
||||
String get libraryForceFullScan => 'Pindai Ulang Penuh';
|
||||
String get libraryForceFullScan => 'Force Full Scan';
|
||||
|
||||
@override
|
||||
String get libraryForceFullScanSubtitle =>
|
||||
'Pindai ulang semua file, abaikan cache';
|
||||
String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache';
|
||||
|
||||
@override
|
||||
String get cleanupOrphanedDownloads => 'Bersihkan Entri Unduhan Tidak Valid';
|
||||
String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads';
|
||||
|
||||
@override
|
||||
String get cleanupOrphanedDownloadsSubtitle =>
|
||||
'Hapus entri riwayat untuk file yang tidak ada lagi';
|
||||
'Remove history entries for files that no longer exist';
|
||||
|
||||
@override
|
||||
String cleanupOrphanedDownloadsResult(int count) {
|
||||
return 'Menghapus $count entri unduhan tidak valid dari riwayat';
|
||||
return 'Removed $count orphaned entries from history';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cleanupOrphanedDownloadsNone =>
|
||||
'Tidak ada entri unduhan tidak valid';
|
||||
String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
|
||||
|
||||
@override
|
||||
String get cacheTitle => 'Penyimpanan & Cache';
|
||||
String get cacheTitle => 'Storage & Cache';
|
||||
|
||||
@override
|
||||
String get cacheSummaryTitle => 'Ringkasan cache';
|
||||
String get cacheSummaryTitle => 'Cache overview';
|
||||
|
||||
@override
|
||||
String get cacheSummarySubtitle =>
|
||||
'Membersihkan cache tidak akan menghapus file musik yang sudah diunduh.';
|
||||
'Clearing cache will not remove downloaded music files.';
|
||||
|
||||
@override
|
||||
String cacheEstimatedTotal(String size) {
|
||||
return 'Estimasi penggunaan cache: $size';
|
||||
return 'Estimated cache usage: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cacheSectionStorage => 'Data Cache';
|
||||
String get cacheSectionStorage => 'Cached Data';
|
||||
|
||||
@override
|
||||
String get cacheSectionMaintenance => 'Perawatan';
|
||||
String get cacheSectionMaintenance => 'Maintenance';
|
||||
|
||||
@override
|
||||
String get cacheAppDirectory => 'Direktori cache aplikasi';
|
||||
String get cacheAppDirectory => 'App cache directory';
|
||||
|
||||
@override
|
||||
String get cacheAppDirectoryDesc =>
|
||||
'Respons HTTP, data WebView, dan data sementara aplikasi.';
|
||||
'HTTP responses, WebView data, and other temporary app data.';
|
||||
|
||||
@override
|
||||
String get cacheTempDirectory => 'Direktori sementara';
|
||||
String get cacheTempDirectory => 'Temporary directory';
|
||||
|
||||
@override
|
||||
String get cacheTempDirectoryDesc =>
|
||||
'File sementara dari proses download dan konversi audio.';
|
||||
'Temporary files from downloads and audio conversion.';
|
||||
|
||||
@override
|
||||
String get cacheCoverImage => 'Cache gambar cover';
|
||||
String get cacheCoverImage => 'Cover image cache';
|
||||
|
||||
@override
|
||||
String get cacheCoverImageDesc =>
|
||||
'Gambar cover album dan lagu yang diunduh. Akan diunduh ulang saat dilihat.';
|
||||
'Downloaded album and track cover art. Will re-download when viewed.';
|
||||
|
||||
@override
|
||||
String get cacheLibraryCover => 'Cache cover library';
|
||||
String get cacheLibraryCover => 'Library cover cache';
|
||||
|
||||
@override
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover dari file musik lokal. Akan diekstrak ulang saat scan berikutnya.';
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get cacheExploreFeed => 'Cache feed Explore';
|
||||
String get cacheExploreFeed => 'Explore feed cache';
|
||||
|
||||
@override
|
||||
String get cacheExploreFeedDesc =>
|
||||
'Konten tab Explore (rilis baru, trending). Akan dimuat ulang saat dikunjungi.';
|
||||
'Explore tab content (new releases, trending). Will refresh on next visit.';
|
||||
|
||||
@override
|
||||
String get cacheTrackLookup => 'Cache pencocokan lagu';
|
||||
String get cacheTrackLookup => 'Track lookup cache';
|
||||
|
||||
@override
|
||||
String get cacheTrackLookupDesc =>
|
||||
'Cache pencarian ID lagu Spotify/Deezer. Menghapus mungkin memperlambat beberapa pencarian.';
|
||||
'Spotify/Deezer track ID lookups. Clearing may slow next few searches.';
|
||||
|
||||
@override
|
||||
String get cacheCleanupUnusedDesc =>
|
||||
'Hapus entri riwayat download dan library yang filenya sudah tidak ada.';
|
||||
'Remove orphaned download history and library entries for missing files.';
|
||||
|
||||
@override
|
||||
String get cacheNoData => 'Tidak ada data cache';
|
||||
String get cacheNoData => 'No cached data';
|
||||
|
||||
@override
|
||||
String cacheSizeWithFiles(String size, int count) {
|
||||
return '$size dalam $count file';
|
||||
return '$size in $count files';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -2814,126 +2818,123 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String cacheEntries(int count) {
|
||||
return '$count entri';
|
||||
return '$count entries';
|
||||
}
|
||||
|
||||
@override
|
||||
String cacheClearSuccess(String target) {
|
||||
return 'Berhasil dibersihkan: $target';
|
||||
return 'Cleared: $target';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cacheClearConfirmTitle => 'Bersihkan cache?';
|
||||
String get cacheClearConfirmTitle => 'Clear cache?';
|
||||
|
||||
@override
|
||||
String cacheClearConfirmMessage(String target) {
|
||||
return 'Ini akan membersihkan data cache untuk $target. File musik yang sudah diunduh tidak akan dihapus.';
|
||||
return 'This will clear cached data for $target. Downloaded music files will not be deleted.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cacheClearAllConfirmTitle => 'Bersihkan semua cache?';
|
||||
String get cacheClearAllConfirmTitle => 'Clear all cache?';
|
||||
|
||||
@override
|
||||
String get cacheClearAllConfirmMessage =>
|
||||
'Ini akan membersihkan semua kategori cache di halaman ini. File musik yang sudah diunduh tidak akan dihapus.';
|
||||
'This will clear all cache categories on this page. Downloaded music files will not be deleted.';
|
||||
|
||||
@override
|
||||
String get cacheClearAll => 'Bersihkan semua cache';
|
||||
String get cacheClearAll => 'Clear all cache';
|
||||
|
||||
@override
|
||||
String get cacheCleanupUnused => 'Bersihkan data tidak terpakai';
|
||||
String get cacheCleanupUnused => 'Cleanup unused data';
|
||||
|
||||
@override
|
||||
String get cacheCleanupUnusedSubtitle =>
|
||||
'Hapus riwayat unduhan yatim dan entri library yang file-nya hilang';
|
||||
'Remove orphaned download history and missing library entries';
|
||||
|
||||
@override
|
||||
String cacheCleanupResult(int downloadCount, int libraryCount) {
|
||||
return 'Pembersihan selesai: $downloadCount unduhan yatim, $libraryCount entri library hilang';
|
||||
return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cacheRefreshStats => 'Segarkan statistik';
|
||||
String get cacheRefreshStats => 'Refresh stats';
|
||||
|
||||
@override
|
||||
String get trackSaveCoverArt => 'Simpan Cover Art';
|
||||
String get trackSaveCoverArt => 'Save Cover Art';
|
||||
|
||||
@override
|
||||
String get trackSaveCoverArtSubtitle =>
|
||||
'Simpan cover album sebagai file .jpg';
|
||||
String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file';
|
||||
|
||||
@override
|
||||
String get trackSaveLyrics => 'Simpan Lirik (.lrc)';
|
||||
String get trackSaveLyrics => 'Save Lyrics (.lrc)';
|
||||
|
||||
@override
|
||||
String get trackSaveLyricsSubtitle =>
|
||||
'Ambil dan simpan lirik sebagai file .lrc';
|
||||
String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file';
|
||||
|
||||
@override
|
||||
String get trackSaveLyricsProgress => 'Menyimpan lirik...';
|
||||
String get trackSaveLyricsProgress => 'Saving lyrics...';
|
||||
|
||||
@override
|
||||
String get trackReEnrich => 'Perkaya Ulang Metadata';
|
||||
String get trackReEnrich => 'Re-enrich Metadata';
|
||||
|
||||
@override
|
||||
String get trackReEnrichSubtitle =>
|
||||
'Tanamkan ulang metadata tanpa mengunduh ulang';
|
||||
'Re-embed metadata without re-downloading';
|
||||
|
||||
@override
|
||||
String get trackReEnrichOnlineSubtitle =>
|
||||
'Cari metadata dari internet dan tanamkan ke file';
|
||||
'Search metadata online and embed into file';
|
||||
|
||||
@override
|
||||
String get trackEditMetadata => 'Edit Metadata';
|
||||
|
||||
@override
|
||||
String trackCoverSaved(String fileName) {
|
||||
return 'Cover art disimpan ke $fileName';
|
||||
return 'Cover art saved to $fileName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoSource => 'Tidak ada sumber cover art';
|
||||
String get trackCoverNoSource => 'No cover art source available';
|
||||
|
||||
@override
|
||||
String trackLyricsSaved(String fileName) {
|
||||
return 'Lirik disimpan ke $fileName';
|
||||
return 'Lyrics saved to $fileName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackReEnrichProgress => 'Memperkaya ulang metadata...';
|
||||
String get trackReEnrichProgress => 'Re-enriching metadata...';
|
||||
|
||||
@override
|
||||
String get trackReEnrichSearching => 'Mencari metadata dari internet...';
|
||||
String get trackReEnrichSearching => 'Searching metadata online...';
|
||||
|
||||
@override
|
||||
String get trackReEnrichSuccess => 'Metadata berhasil diperkaya ulang';
|
||||
String get trackReEnrichSuccess => 'Metadata re-enriched successfully';
|
||||
|
||||
@override
|
||||
String get trackReEnrichFfmpegFailed =>
|
||||
'Gagal menanamkan metadata via FFmpeg';
|
||||
String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed';
|
||||
|
||||
@override
|
||||
String trackSaveFailed(String error) {
|
||||
return 'Gagal: $error';
|
||||
return 'Failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackConvertFormat => 'Konversi Format';
|
||||
String get trackConvertFormat => 'Convert Format';
|
||||
|
||||
@override
|
||||
String get trackConvertFormatSubtitle => 'Konversi ke MP3 atau Opus';
|
||||
String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
|
||||
|
||||
@override
|
||||
String get trackConvertTitle => 'Konversi Audio';
|
||||
String get trackConvertTitle => 'Convert Audio';
|
||||
|
||||
@override
|
||||
String get trackConvertTargetFormat => 'Format Tujuan';
|
||||
String get trackConvertTargetFormat => 'Target Format';
|
||||
|
||||
@override
|
||||
String get trackConvertBitrate => 'Bitrate';
|
||||
|
||||
@override
|
||||
String get trackConvertConfirmTitle => 'Konfirmasi Konversi';
|
||||
String get trackConvertConfirmTitle => 'Confirm Conversion';
|
||||
|
||||
@override
|
||||
String trackConvertConfirmMessage(
|
||||
|
|
@ -2941,17 +2942,17 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
String targetFormat,
|
||||
String bitrate,
|
||||
) {
|
||||
return 'Konversi dari $sourceFormat ke $targetFormat pada $bitrate?\n\nFile asli akan dihapus setelah konversi.';
|
||||
return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackConvertConverting => 'Mengkonversi audio...';
|
||||
String get trackConvertConverting => 'Converting audio...';
|
||||
|
||||
@override
|
||||
String trackConvertSuccess(String format) {
|
||||
return 'Berhasil dikonversi ke $format';
|
||||
return 'Converted to $format successfully';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackConvertFailed => 'Konversi gagal';
|
||||
String get trackConvertFailed => 'Conversion failed';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1176,6 +1176,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'フォルダ構成';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get appDescription =>
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
|
||||
'Spotify 트랙을 Tidal, Qobuz, Amazon Music에서 무손실 음질로 다운로드하세요.';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
|
|
@ -34,32 +34,32 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
String get homeTitle => 'Home';
|
||||
|
||||
@override
|
||||
String get homeSearchHint => 'Paste Spotify URL or search...';
|
||||
String get homeSearchHint => 'Spotify URL을 붙여 넣거나 검색';
|
||||
|
||||
@override
|
||||
String homeSearchHintExtension(String extensionName) {
|
||||
return 'Search with $extensionName...';
|
||||
return '$extensionName에서 검색';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
String get homeSubtitle => 'Spotify URL을 붙여 넣거나 검색';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
String get homeSupports => '지원 항목: 트랙, 앨범, 플레이리스트, 아티스트 URLs';
|
||||
|
||||
@override
|
||||
String get homeRecent => 'Recent';
|
||||
String get homeRecent => '최근 기록';
|
||||
|
||||
@override
|
||||
String get historyTitle => 'History';
|
||||
String get historyTitle => '기록';
|
||||
|
||||
@override
|
||||
String historyDownloading(int count) {
|
||||
return 'Downloading ($count)';
|
||||
return '다운로드 중... $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get historyDownloaded => 'Downloaded';
|
||||
String get historyDownloaded => '다운로드 목록';
|
||||
|
||||
@override
|
||||
String get historyFilterAll => 'All';
|
||||
|
|
@ -75,7 +75,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count tracks',
|
||||
other: '${count}tracks',
|
||||
one: '1 track',
|
||||
);
|
||||
return '$_temp0';
|
||||
|
|
@ -245,14 +245,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
String get optionsAutoFallback => 'Auto Fallback';
|
||||
|
||||
@override
|
||||
String get optionsAutoFallbackSubtitle =>
|
||||
'Try other services if download fails';
|
||||
String get optionsAutoFallbackSubtitle => '다운로드가 실패한 경우, 다른 서비스로 재시도';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProviders => 'Use Extension Providers';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOn => 'Extensions will be tried first';
|
||||
String get optionsUseExtensionProvidersOn => '확장 기능을 우선적으로 사용합니다';
|
||||
|
||||
@override
|
||||
String get optionsUseExtensionProvidersOff => 'Using built-in providers only';
|
||||
|
|
@ -1182,6 +1181,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Folder Organization';
|
||||
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Folder Organization';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get navHome => 'Главная';
|
||||
|
||||
@override
|
||||
String get navLibrary => 'Library';
|
||||
String get navLibrary => 'Библиотека';
|
||||
|
||||
@override
|
||||
String get navHistory => 'История';
|
||||
|
|
@ -356,7 +356,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get optionsSpotifyDeprecationWarning =>
|
||||
'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.';
|
||||
'Поиск Spotify устареет 3 марта 2026 года из-за изменений Spotify API. Пожалуйста, перейдите на Deezer.';
|
||||
|
||||
@override
|
||||
String get extensionsTitle => 'Расширения';
|
||||
|
|
@ -486,7 +486,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get aboutSjdonadoDesc =>
|
||||
'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!';
|
||||
'Создатель I Don\'t Have Spotify (IDHS). Резервный резолвер ссылки';
|
||||
|
||||
@override
|
||||
String get aboutDoubleDouble => 'DoubleDouble';
|
||||
|
|
@ -507,7 +507,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get aboutSpotiSaverDesc =>
|
||||
'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!';
|
||||
'Потоковая передача Tidal Hi-Res FLAC. Ключевая часть lossless головоломки!';
|
||||
|
||||
@override
|
||||
String get aboutAppDescription =>
|
||||
|
|
@ -712,7 +712,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get setupIcloudNotSupported =>
|
||||
'iCloud Drive is not supported. Please use the app Documents folder.';
|
||||
'iCloud Drive не поддерживается. Пожалуйста, используйте папку Документы.';
|
||||
|
||||
@override
|
||||
String get setupDownloadInFlac => 'Скачать Spotify треки во FLAC';
|
||||
|
|
@ -975,7 +975,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String snackbarAlreadyInLibrary(String trackName) {
|
||||
return '\"$trackName\" already exists in your library';
|
||||
return '\"$trackName\" уже есть в вашей библиотеке';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -1209,6 +1209,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Организация папок';
|
||||
|
||||
|
|
@ -1918,33 +1925,35 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get qualityLossy => 'Lossy';
|
||||
|
||||
@override
|
||||
String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)';
|
||||
String get qualityLossyMp3Subtitle =>
|
||||
'Opus 320 кбит/с (конвертировать из FLAC)';
|
||||
|
||||
@override
|
||||
String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)';
|
||||
String get qualityLossyOpusSubtitle =>
|
||||
'Opus 128 кбит/с (конвертировать из FLAC)';
|
||||
|
||||
@override
|
||||
String get enableLossyOption => 'Enable Lossy Option';
|
||||
String get enableLossyOption => 'Включить опцию Lossy';
|
||||
|
||||
@override
|
||||
String get enableLossyOptionSubtitleOn => 'Lossy quality option is available';
|
||||
String get enableLossyOptionSubtitleOn => 'Доступно качество с потерями';
|
||||
|
||||
@override
|
||||
String get enableLossyOptionSubtitleOff =>
|
||||
'Downloads FLAC then converts to lossy format';
|
||||
'Скачивать FLAC и конвертировать в MP3 320 кбит/с';
|
||||
|
||||
@override
|
||||
String get lossyFormat => 'Lossy Format';
|
||||
String get lossyFormat => 'Формат с потерями';
|
||||
|
||||
@override
|
||||
String get lossyFormatDescription => 'Choose the lossy format for conversion';
|
||||
String get lossyFormatDescription => 'Выберите Lossy формат для конвертации';
|
||||
|
||||
@override
|
||||
String get lossyFormatMp3Subtitle => '320kbps, best compatibility';
|
||||
String get lossyFormatMp3Subtitle => '320Кбит/с, лучшая совместимость';
|
||||
|
||||
@override
|
||||
String get lossyFormatOpusSubtitle =>
|
||||
'128kbps, better quality at smaller size';
|
||||
'128кбит/с, лучшее качество при меньших размерах';
|
||||
|
||||
@override
|
||||
String get qualityNote =>
|
||||
|
|
@ -1952,7 +1961,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get youtubeQualityNote =>
|
||||
'YouTube provides lossy audio only. Not part of lossless fallback.';
|
||||
'YouTube обеспечивает только звук с потерями(Lossy).';
|
||||
|
||||
@override
|
||||
String get downloadAskBeforeDownload => 'Спрашивать перед скачиванием';
|
||||
|
|
@ -1967,7 +1976,8 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get downloadAlbumFolderStructure => 'Структура папок альбома';
|
||||
|
||||
@override
|
||||
String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders';
|
||||
String get downloadUseAlbumArtistForFolders =>
|
||||
'Использовать исполнителя альбома для папок';
|
||||
|
||||
@override
|
||||
String get downloadUseAlbumArtistForFoldersAlbumSubtitle =>
|
||||
|
|
@ -1975,7 +1985,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get downloadUseAlbumArtistForFoldersTrackSubtitle =>
|
||||
'Artist folders use Track Artist only';
|
||||
'Папки исполнителя используют только трек исполнителя';
|
||||
|
||||
@override
|
||||
String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders';
|
||||
|
|
@ -2069,37 +2079,37 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
'Вы уверены, что хотите очистить все загрузки?';
|
||||
|
||||
@override
|
||||
String get queueExportFailed => 'Export';
|
||||
String get queueExportFailed => 'Экспорт';
|
||||
|
||||
@override
|
||||
String get queueExportFailedSuccess =>
|
||||
'Failed downloads exported to TXT file';
|
||||
'Сбой при экспорте загрузок в файл TXT';
|
||||
|
||||
@override
|
||||
String get queueExportFailedClear => 'Clear Failed';
|
||||
String get queueExportFailedClear => 'Не удалось очистить';
|
||||
|
||||
@override
|
||||
String get queueExportFailedError => 'Failed to export downloads';
|
||||
String get queueExportFailedError => 'Не удалось экспортировать загрузки';
|
||||
|
||||
@override
|
||||
String get settingsAutoExportFailed => 'Auto-export failed downloads';
|
||||
String get settingsAutoExportFailed => 'Автоэкспорт неудачных загрузок';
|
||||
|
||||
@override
|
||||
String get settingsAutoExportFailedSubtitle =>
|
||||
'Save failed downloads to TXT file automatically';
|
||||
'Автоматическое сохранение неудачных загрузок в TXT файл';
|
||||
|
||||
@override
|
||||
String get settingsDownloadNetwork => 'Download Network';
|
||||
String get settingsDownloadNetwork => 'Сеть для скачивания';
|
||||
|
||||
@override
|
||||
String get settingsDownloadNetworkAny => 'WiFi + Mobile Data';
|
||||
String get settingsDownloadNetworkAny => 'WiFi и мобильная сеть';
|
||||
|
||||
@override
|
||||
String get settingsDownloadNetworkWifiOnly => 'WiFi Only';
|
||||
String get settingsDownloadNetworkWifiOnly => 'Только WiFi';
|
||||
|
||||
@override
|
||||
String get settingsDownloadNetworkSubtitle =>
|
||||
'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.';
|
||||
'Выберите, какую сеть использовать для скачивания. Когда установлено значение только WiFi — скачивания через мобильную сеть будут приостановлены.';
|
||||
|
||||
@override
|
||||
String get queueEmpty => 'Нет загрузок в очереди';
|
||||
|
|
@ -2231,10 +2241,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get recentTypePlaylist => 'Плейлист';
|
||||
|
||||
@override
|
||||
String get recentEmpty => 'No recent items yet';
|
||||
String get recentEmpty => 'Нет недавних элементов';
|
||||
|
||||
@override
|
||||
String get recentShowAllDownloads => 'Show All Downloads';
|
||||
String get recentShowAllDownloads => 'Показать все загрузки';
|
||||
|
||||
@override
|
||||
String recentPlaylistInfo(String name) {
|
||||
|
|
@ -2314,234 +2324,254 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
'Не удалось получить некоторые альбомы';
|
||||
|
||||
@override
|
||||
String get sectionStorageAccess => 'Storage Access';
|
||||
String get sectionStorageAccess => 'Доступ к хранилищу';
|
||||
|
||||
@override
|
||||
String get allFilesAccess => 'All Files Access';
|
||||
String get allFilesAccess => 'Доступ ко всем файлам';
|
||||
|
||||
@override
|
||||
String get allFilesAccessEnabledSubtitle => 'Can write to any folder';
|
||||
String get allFilesAccessEnabledSubtitle => 'Можно записать в любую папку';
|
||||
|
||||
@override
|
||||
String get allFilesAccessDisabledSubtitle => 'Limited to media folders only';
|
||||
String get allFilesAccessDisabledSubtitle =>
|
||||
'Ограничено только папками медиа';
|
||||
|
||||
@override
|
||||
String get allFilesAccessDescription =>
|
||||
'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.';
|
||||
'Включите, если вы сталкиваетесь с ошибками записи при сохранении в пользовательские папки. Android 13+ по умолчанию ограничивает доступ к определенным папкам.';
|
||||
|
||||
@override
|
||||
String get allFilesAccessDeniedMessage =>
|
||||
'Permission was denied. Please enable \'All files access\' manually in system settings.';
|
||||
'В разрешении отказано. Пожалуйста, включите функцию «Доступ ко всем файлам» в настройках системы.';
|
||||
|
||||
@override
|
||||
String get allFilesAccessDisabledMessage =>
|
||||
'All Files Access disabled. The app will use limited storage access.';
|
||||
'Доступ ко всем файлам отключен. Приложение будет использовать ограниченный доступ к хранилищу.';
|
||||
|
||||
@override
|
||||
String get settingsLocalLibrary => 'Local Library';
|
||||
String get settingsLocalLibrary => 'Локальная библиотека';
|
||||
|
||||
@override
|
||||
String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates';
|
||||
String get settingsLocalLibrarySubtitle =>
|
||||
'Сканировать и обнаружить дубликаты';
|
||||
|
||||
@override
|
||||
String get settingsCache => 'Storage & Cache';
|
||||
String get settingsCache => 'Хранилище и кэш';
|
||||
|
||||
@override
|
||||
String get settingsCacheSubtitle => 'View size and clear cached data';
|
||||
String get settingsCacheSubtitle => 'Просмотреть размер и очистить кэш';
|
||||
|
||||
@override
|
||||
String get libraryTitle => 'Local Library';
|
||||
String get libraryTitle => 'Локальная библиотека';
|
||||
|
||||
@override
|
||||
String get libraryStatus => 'Library Status';
|
||||
String get libraryStatus => 'Статус Библиотеки';
|
||||
|
||||
@override
|
||||
String get libraryScanSettings => 'Scan Settings';
|
||||
String get libraryScanSettings => 'Настройки сканирования';
|
||||
|
||||
@override
|
||||
String get libraryEnableLocalLibrary => 'Enable Local Library';
|
||||
String get libraryEnableLocalLibrary => 'Включить локальную библиотеку';
|
||||
|
||||
@override
|
||||
String get libraryEnableLocalLibrarySubtitle =>
|
||||
'Scan and track your existing music';
|
||||
'Сканировать и отслеживать вашу существующую музыку';
|
||||
|
||||
@override
|
||||
String get libraryFolder => 'Library Folder';
|
||||
String get libraryFolder => 'Папка библиотеки';
|
||||
|
||||
@override
|
||||
String get libraryFolderHint => 'Tap to select folder';
|
||||
String get libraryFolderHint => 'Нажмите, чтобы выбрать папку';
|
||||
|
||||
@override
|
||||
String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator';
|
||||
String get libraryShowDuplicateIndicator => 'Показать индикатор дубликатов';
|
||||
|
||||
@override
|
||||
String get libraryShowDuplicateIndicatorSubtitle =>
|
||||
'Show when searching for existing tracks';
|
||||
'Показать при поиске существующих треков';
|
||||
|
||||
@override
|
||||
String get libraryActions => 'Actions';
|
||||
String get libraryActions => 'Действия';
|
||||
|
||||
@override
|
||||
String get libraryScan => 'Scan Library';
|
||||
String get libraryScan => 'Сканировать библиотеку';
|
||||
|
||||
@override
|
||||
String get libraryScanSubtitle => 'Scan for audio files';
|
||||
String get libraryScanSubtitle => 'Сканировать аудио файлы';
|
||||
|
||||
@override
|
||||
String get libraryScanSelectFolderFirst => 'Select a folder first';
|
||||
String get libraryScanSelectFolderFirst => 'Сначала выберите папку';
|
||||
|
||||
@override
|
||||
String get libraryCleanupMissingFiles => 'Cleanup Missing Files';
|
||||
String get libraryCleanupMissingFiles => 'Очистка отсутствующих файлов';
|
||||
|
||||
@override
|
||||
String get libraryCleanupMissingFilesSubtitle =>
|
||||
'Remove entries for files that no longer exist';
|
||||
'Удалить записи для файлов, которых больше не существует';
|
||||
|
||||
@override
|
||||
String get libraryClear => 'Clear Library';
|
||||
String get libraryClear => 'Очистить библиотеку';
|
||||
|
||||
@override
|
||||
String get libraryClearSubtitle => 'Remove all scanned tracks';
|
||||
String get libraryClearSubtitle => 'Удалить все сканированные треки';
|
||||
|
||||
@override
|
||||
String get libraryClearConfirmTitle => 'Clear Library';
|
||||
String get libraryClearConfirmTitle => 'Очистить библиотеку';
|
||||
|
||||
@override
|
||||
String get libraryClearConfirmMessage =>
|
||||
'This will remove all scanned tracks from your library. Your actual music files will not be deleted.';
|
||||
'Это удалит все сканированные треки из вашей библиотеки. Ваши фактические файлы не будут удалены.';
|
||||
|
||||
@override
|
||||
String get libraryAbout => 'About Local Library';
|
||||
String get libraryAbout => 'О локальной библиотеке';
|
||||
|
||||
@override
|
||||
String get libraryAboutDescription =>
|
||||
'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.';
|
||||
'Сканирует существующую коллекцию музыки для обнаружения дубликатов при загрузке. Поддерживает форматы FLAC, M4A, MP3, Opus и OGG. Метаданные читаются из тегов файлов, если доступны.';
|
||||
|
||||
@override
|
||||
String libraryTracksCount(int count) {
|
||||
return '$count tracks';
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String libraryLastScanned(String time) {
|
||||
return 'Last scanned: $time';
|
||||
return 'Последнее сканирование: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String get libraryLastScannedNever => 'Never';
|
||||
String get libraryLastScannedNever => 'Никогда';
|
||||
|
||||
@override
|
||||
String get libraryScanning => 'Scanning...';
|
||||
String get libraryScanning => 'Сканирование...';
|
||||
|
||||
@override
|
||||
String libraryScanProgress(String progress, int total) {
|
||||
return '$progress% of $total files';
|
||||
return '$progress% из $total файлов';
|
||||
}
|
||||
|
||||
@override
|
||||
String get libraryInLibrary => 'In Library';
|
||||
String get libraryInLibrary => 'В библиотеке';
|
||||
|
||||
@override
|
||||
String libraryRemovedMissingFiles(int count) {
|
||||
return 'Removed $count missing files from library';
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'отсутствующих файлов',
|
||||
many: 'отсутствующих файлов',
|
||||
few: 'трека',
|
||||
one: 'отсутствующий файл',
|
||||
);
|
||||
return 'Удалено $count $_temp0 в библиотеке';
|
||||
}
|
||||
|
||||
@override
|
||||
String get libraryCleared => 'Library cleared';
|
||||
String get libraryCleared => 'Библиотека очищена';
|
||||
|
||||
@override
|
||||
String get libraryStorageAccessRequired => 'Storage Access Required';
|
||||
String get libraryStorageAccessRequired => 'Требуется доступ к хранилищу';
|
||||
|
||||
@override
|
||||
String get libraryStorageAccessMessage =>
|
||||
'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.';
|
||||
'SpotiFLAC требуется доступ к хранилищу для сканирования вашей библиотеки музыки. Пожалуйста, предоставьте разрешение в настройках.';
|
||||
|
||||
@override
|
||||
String get libraryFolderNotExist => 'Selected folder does not exist';
|
||||
String get libraryFolderNotExist => 'Выбранной папки не существует';
|
||||
|
||||
@override
|
||||
String get librarySourceDownloaded => 'Downloaded';
|
||||
String get librarySourceDownloaded => 'Скачанные';
|
||||
|
||||
@override
|
||||
String get librarySourceLocal => 'Local';
|
||||
String get librarySourceLocal => 'Локальные';
|
||||
|
||||
@override
|
||||
String get libraryFilterAll => 'All';
|
||||
String get libraryFilterAll => 'Все';
|
||||
|
||||
@override
|
||||
String get libraryFilterDownloaded => 'Downloaded';
|
||||
String get libraryFilterDownloaded => 'Скачанные';
|
||||
|
||||
@override
|
||||
String get libraryFilterLocal => 'Local';
|
||||
String get libraryFilterLocal => 'Локальные';
|
||||
|
||||
@override
|
||||
String get libraryFilterTitle => 'Filters';
|
||||
String get libraryFilterTitle => 'Фильтры';
|
||||
|
||||
@override
|
||||
String get libraryFilterReset => 'Reset';
|
||||
String get libraryFilterReset => 'Сброс';
|
||||
|
||||
@override
|
||||
String get libraryFilterApply => 'Apply';
|
||||
String get libraryFilterApply => 'Применить';
|
||||
|
||||
@override
|
||||
String get libraryFilterSource => 'Source';
|
||||
String get libraryFilterSource => 'Источник';
|
||||
|
||||
@override
|
||||
String get libraryFilterQuality => 'Quality';
|
||||
String get libraryFilterQuality => 'Качество';
|
||||
|
||||
@override
|
||||
String get libraryFilterQualityHiRes => 'Hi-Res (24bit)';
|
||||
String get libraryFilterQualityHiRes => 'Hi-Res (24 бит)';
|
||||
|
||||
@override
|
||||
String get libraryFilterQualityCD => 'CD (16bit)';
|
||||
String get libraryFilterQualityCD => 'CD (16 бит)';
|
||||
|
||||
@override
|
||||
String get libraryFilterQualityLossy => 'Lossy';
|
||||
String get libraryFilterQualityLossy => 'С потерями';
|
||||
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
String get libraryFilterFormat => 'Формат';
|
||||
|
||||
@override
|
||||
String get libraryFilterDate => 'Date Added';
|
||||
String get libraryFilterDate => 'Дата добавления';
|
||||
|
||||
@override
|
||||
String get libraryFilterDateToday => 'Today';
|
||||
String get libraryFilterDateToday => 'Сегодня';
|
||||
|
||||
@override
|
||||
String get libraryFilterDateWeek => 'This Week';
|
||||
String get libraryFilterDateWeek => 'На этой неделе';
|
||||
|
||||
@override
|
||||
String get libraryFilterDateMonth => 'This Month';
|
||||
String get libraryFilterDateMonth => 'В этом месяце';
|
||||
|
||||
@override
|
||||
String get libraryFilterDateYear => 'This Year';
|
||||
String get libraryFilterDateYear => 'В этом году';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
String get libraryFilterSort => 'Сортировка';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortLatest => 'Latest';
|
||||
String get libraryFilterSortLatest => 'Последние';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
String get libraryFilterSortOldest => 'Старые';
|
||||
|
||||
@override
|
||||
String libraryFilterActive(int count) {
|
||||
return '$count filter(s) active';
|
||||
return '$count фильтр(-ов) активно';
|
||||
}
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
String get timeJustNow => 'Только что';
|
||||
|
||||
@override
|
||||
String timeMinutesAgo(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count minutes ago',
|
||||
one: '1 minute ago',
|
||||
other: '$count минут',
|
||||
many: '$count минут',
|
||||
few: '$count минуты',
|
||||
one: '$count минуту',
|
||||
);
|
||||
return '$_temp0';
|
||||
return '$_temp0 назад';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -2549,160 +2579,186 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hours ago',
|
||||
one: '1 hour ago',
|
||||
other: '$count часов',
|
||||
many: '$count часов',
|
||||
few: '$count часа',
|
||||
one: '$count час',
|
||||
);
|
||||
return '$_temp0';
|
||||
return '$_temp0 назад';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storageSwitchTitle => 'Switch Storage Mode';
|
||||
String get storageSwitchTitle => 'Сменить режим хранения';
|
||||
|
||||
@override
|
||||
String get storageSwitchToSafTitle => 'Switch to SAF Storage?';
|
||||
String get storageSwitchToSafTitle => 'Переключиться на SAF хранилище?';
|
||||
|
||||
@override
|
||||
String get storageSwitchToAppTitle => 'Switch to App Storage?';
|
||||
String get storageSwitchToAppTitle => 'Переключиться хранилище приложения?';
|
||||
|
||||
@override
|
||||
String get storageSwitchToSafMessage =>
|
||||
'Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.';
|
||||
'Ваши скачанные файлы останутся в текущем расположении и будут доступны.\n\nНовые файлы будут сохранены в выбранной вами папке SAF.';
|
||||
|
||||
@override
|
||||
String get storageSwitchToAppMessage =>
|
||||
'Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.';
|
||||
'Ваши скачанные файлы останутся в текущем выбранной вами папке SAF.\n\nНовые файлы будут сохранены в папке Music/SpotiFLAC.';
|
||||
|
||||
@override
|
||||
String get storageSwitchExistingDownloads => 'Existing Downloads';
|
||||
String get storageSwitchExistingDownloads => 'Существующие загрузки';
|
||||
|
||||
@override
|
||||
String storageSwitchExistingDownloadsInfo(int count, String mode) {
|
||||
return '$count tracks in $mode storage';
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count треков',
|
||||
many: '$count треков',
|
||||
few: '$count трека',
|
||||
one: '$count трек',
|
||||
);
|
||||
return '$_temp0 в $mode хранилище';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storageSwitchNewDownloads => 'New Downloads';
|
||||
String get storageSwitchNewDownloads => 'Новые загрузки';
|
||||
|
||||
@override
|
||||
String storageSwitchNewDownloadsLocation(String location) {
|
||||
return 'Will be saved to: $location';
|
||||
return 'Будет сохранено в: $location';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storageSwitchContinue => 'Continue';
|
||||
String get storageSwitchContinue => 'Продолжить';
|
||||
|
||||
@override
|
||||
String get storageSwitchSelectFolder => 'Select SAF Folder';
|
||||
String get storageSwitchSelectFolder => 'Выберите папку SAF';
|
||||
|
||||
@override
|
||||
String get storageAppStorage => 'App Storage';
|
||||
String get storageAppStorage => 'Хранилище приложения';
|
||||
|
||||
@override
|
||||
String get storageSafStorage => 'SAF Storage';
|
||||
String get storageSafStorage => 'Хранилище SAF';
|
||||
|
||||
@override
|
||||
String storageModeBadge(String mode) {
|
||||
return 'Storage: $mode';
|
||||
return 'Хранилище: $mode';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storageStatsTitle => 'Storage Statistics';
|
||||
String get storageStatsTitle => 'Статистика хранилища';
|
||||
|
||||
@override
|
||||
String storageStatsAppCount(int count) {
|
||||
return '$count tracks in App Storage';
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count треков',
|
||||
many: '$count треков',
|
||||
few: '$count трека',
|
||||
one: '$count трек',
|
||||
);
|
||||
return '$_temp0 в хранилище приложения';
|
||||
}
|
||||
|
||||
@override
|
||||
String storageStatsSafCount(int count) {
|
||||
return '$count tracks in SAF Storage';
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count треков',
|
||||
many: '$count треков',
|
||||
few: '$count трека',
|
||||
one: '$count трек',
|
||||
);
|
||||
return '$_temp0 в вашей папке в SAF';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storageModeInfo => 'Your files are stored in multiple locations';
|
||||
String get storageModeInfo => 'Ваши файлы хранятся в нескольких местах';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!';
|
||||
String get tutorialWelcomeTitle => 'Добро пожаловать в SpotiFLAC!';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeDesc =>
|
||||
'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.';
|
||||
'Давайте научимся скачивать свою любимую музыку в качестве без потерь. В этом кратком руководстве мы покажем вам основы.';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip1 =>
|
||||
'Download music from Spotify, Deezer, or paste any supported URL';
|
||||
'Скачивайте музыку из Spotify, Deezer, или вставьте любой поддерживаемый URL';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip2 =>
|
||||
'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
|
||||
'Скачайте FLAC с Tidal, Qobuz или Amazon Music';
|
||||
|
||||
@override
|
||||
String get tutorialWelcomeTip3 =>
|
||||
'Automatic metadata, cover art, and lyrics embedding';
|
||||
'Автоматическое встраивание метаданных, обложек и текстов песен';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTitle => 'Finding Music';
|
||||
String get tutorialSearchTitle => 'Поиск музыки';
|
||||
|
||||
@override
|
||||
String get tutorialSearchDesc =>
|
||||
'There are two easy ways to find music you want to download.';
|
||||
'Есть два простых способа найти музыку, которую вы хотите скачать.';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTip1 =>
|
||||
'Paste a Spotify or Deezer URL directly in the search box';
|
||||
'Вставьте ссылку Spotify или Deezer прямо в поле поиска';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTip2 =>
|
||||
'Or type the song name, artist, or album to search';
|
||||
'Или введите название песни, исполнителя или альбом для поиска';
|
||||
|
||||
@override
|
||||
String get tutorialSearchTip3 =>
|
||||
'Supports tracks, albums, playlists, and artist pages';
|
||||
'Поддержка треков, альбомов, плейлистов и страниц исполнителей';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTitle => 'Downloading Music';
|
||||
String get tutorialDownloadTitle => 'Скачивание музыки';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadDesc =>
|
||||
'Downloading music is simple and fast. Here\'s how it works.';
|
||||
'Скачивание музыки просто и быстро. Вот как это работает.';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTip1 =>
|
||||
'Tap the download button next to any track to start downloading';
|
||||
'Нажмите кнопку скачать рядом с любым треком, чтобы начать скачивание';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTip2 =>
|
||||
'Choose your preferred quality (FLAC, Hi-Res, or MP3)';
|
||||
'Выберите предпочитаемое качество (FLAC, Hi-Res или MP3)';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadTip3 =>
|
||||
'Download entire albums or playlists with one tap';
|
||||
'Скачать все альбомы или плейлисты одним нажатием';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTitle => 'Your Library';
|
||||
String get tutorialLibraryTitle => 'Ваша библиотека';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryDesc =>
|
||||
'All your downloaded music is organized in the Library tab.';
|
||||
'Вся скачанная музыка организована во вкладке Библиотека.';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTip1 =>
|
||||
'View download progress and queue in the Library tab';
|
||||
'Просмотр прогресса загрузки и очереди на вкладке Библиотека';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTip2 =>
|
||||
'Tap any track to play it with your music player';
|
||||
'Нажмите на любой трек, чтобы воспроизвести его с помощью вашего музыкального плеера';
|
||||
|
||||
@override
|
||||
String get tutorialLibraryTip3 =>
|
||||
'Switch between list and grid view for better browsing';
|
||||
'Переключение между списком и сеткой для лучшего просмотра';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTitle => 'Extensions';
|
||||
String get tutorialExtensionsTitle => 'Расширения';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsDesc =>
|
||||
'Extend the app\'s capabilities with community extensions.';
|
||||
'Расширьте возможности приложения с расширениями от сообщества.';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
|
|
@ -2710,14 +2766,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
'Add new download providers or search sources';
|
||||
'Добавить новых поставщиков загрузок или поиска';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip3 =>
|
||||
'Get lyrics, enhanced metadata, and more features';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTitle => 'Customize Your Experience';
|
||||
String get tutorialSettingsTitle => 'Настройте приложение под себя';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsDesc =>
|
||||
|
|
@ -2725,27 +2781,28 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get tutorialSettingsTip1 =>
|
||||
'Change download location and folder organization';
|
||||
'Изменить местоположение и организацию папок для скачивания';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTip2 =>
|
||||
'Set default audio quality and format preferences';
|
||||
'Настройте качество и формата аудиофайла по умолчанию';
|
||||
|
||||
@override
|
||||
String get tutorialSettingsTip3 => 'Customize app theme and appearance';
|
||||
String get tutorialSettingsTip3 => 'Настроить тему и внешний вид приложения';
|
||||
|
||||
@override
|
||||
String get tutorialReadyMessage =>
|
||||
'You\'re all set! Start downloading your favorite music now.';
|
||||
'Всё готово! Начните загружать любимую музыку прямо сейчас.';
|
||||
|
||||
@override
|
||||
String get tutorialExample => 'EXAMPLE';
|
||||
|
||||
@override
|
||||
String get libraryForceFullScan => 'Force Full Scan';
|
||||
String get libraryForceFullScan => 'Полное сканирование';
|
||||
|
||||
@override
|
||||
String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache';
|
||||
String get libraryForceFullScanSubtitle =>
|
||||
'Пересканировать все файлы, игнорировать кэш';
|
||||
|
||||
@override
|
||||
String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads';
|
||||
|
|
@ -2763,10 +2820,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
|
||||
|
||||
@override
|
||||
String get cacheTitle => 'Storage & Cache';
|
||||
String get cacheTitle => 'Хранилище и кэш';
|
||||
|
||||
@override
|
||||
String get cacheSummaryTitle => 'Cache overview';
|
||||
String get cacheSummaryTitle => 'Просмотр кэша';
|
||||
|
||||
@override
|
||||
String get cacheSummarySubtitle =>
|
||||
|
|
@ -2778,13 +2835,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get cacheSectionStorage => 'Cached Data';
|
||||
String get cacheSectionStorage => 'Кэшированные данные';
|
||||
|
||||
@override
|
||||
String get cacheSectionMaintenance => 'Maintenance';
|
||||
|
||||
@override
|
||||
String get cacheAppDirectory => 'App cache directory';
|
||||
String get cacheAppDirectory => 'Папка кэша приложения';
|
||||
|
||||
@override
|
||||
String get cacheAppDirectoryDesc =>
|
||||
|
|
@ -2830,11 +2887,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
'Remove orphaned download history and library entries for missing files.';
|
||||
|
||||
@override
|
||||
String get cacheNoData => 'No cached data';
|
||||
String get cacheNoData => 'Нет кэшированных данных';
|
||||
|
||||
@override
|
||||
String cacheSizeWithFiles(String size, int count) {
|
||||
return '$size in $count files';
|
||||
return '$size в $count файлах';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -2849,11 +2906,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String cacheClearSuccess(String target) {
|
||||
return 'Cleared: $target';
|
||||
return 'Очищено: $target';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cacheClearConfirmTitle => 'Clear cache?';
|
||||
String get cacheClearConfirmTitle => 'Очистить кэш?';
|
||||
|
||||
@override
|
||||
String cacheClearConfirmMessage(String target) {
|
||||
|
|
@ -2861,17 +2918,17 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get cacheClearAllConfirmTitle => 'Clear all cache?';
|
||||
String get cacheClearAllConfirmTitle => 'Очистить весь кэш?';
|
||||
|
||||
@override
|
||||
String get cacheClearAllConfirmMessage =>
|
||||
'This will clear all cache categories on this page. Downloaded music files will not be deleted.';
|
||||
'Это очистит все категории кэша на этой странице. Скачанные музыкальные файлы не будут удалены.';
|
||||
|
||||
@override
|
||||
String get cacheClearAll => 'Clear all cache';
|
||||
String get cacheClearAll => 'Очистить весь кэш';
|
||||
|
||||
@override
|
||||
String get cacheCleanupUnused => 'Cleanup unused data';
|
||||
String get cacheCleanupUnused => 'Очистка неиспользуемых данных';
|
||||
|
||||
@override
|
||||
String get cacheCleanupUnusedSubtitle =>
|
||||
|
|
@ -2883,19 +2940,20 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get cacheRefreshStats => 'Refresh stats';
|
||||
String get cacheRefreshStats => 'Обновить статистику';
|
||||
|
||||
@override
|
||||
String get trackSaveCoverArt => 'Save Cover Art';
|
||||
String get trackSaveCoverArt => 'Сохранить обложку';
|
||||
|
||||
@override
|
||||
String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file';
|
||||
String get trackSaveCoverArtSubtitle => 'Сохранить обложку как файл .jpg';
|
||||
|
||||
@override
|
||||
String get trackSaveLyrics => 'Save Lyrics (.lrc)';
|
||||
String get trackSaveLyrics => 'Сохранить текст (.lrc)';
|
||||
|
||||
@override
|
||||
String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file';
|
||||
String get trackSaveLyricsSubtitle =>
|
||||
'Получить и сохранить текст песни в формате .lrc';
|
||||
|
||||
@override
|
||||
String get trackSaveLyricsProgress => 'Saving lyrics...';
|
||||
|
|
@ -2912,36 +2970,37 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
'Search metadata online and embed into file';
|
||||
|
||||
@override
|
||||
String get trackEditMetadata => 'Edit Metadata';
|
||||
String get trackEditMetadata => 'Редактировать метаданные';
|
||||
|
||||
@override
|
||||
String trackCoverSaved(String fileName) {
|
||||
return 'Cover art saved to $fileName';
|
||||
return 'Обложка сохранена в $fileName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoSource => 'No cover art source available';
|
||||
String get trackCoverNoSource => 'Нет доступных источников обложки';
|
||||
|
||||
@override
|
||||
String trackLyricsSaved(String fileName) {
|
||||
return 'Lyrics saved to $fileName';
|
||||
return 'Текст песни сохранен в $fileName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackReEnrichProgress => 'Re-enriching metadata...';
|
||||
|
||||
@override
|
||||
String get trackReEnrichSearching => 'Searching metadata online...';
|
||||
String get trackReEnrichSearching => 'Поиск метаданных в сети...';
|
||||
|
||||
@override
|
||||
String get trackReEnrichSuccess => 'Metadata re-enriched successfully';
|
||||
|
||||
@override
|
||||
String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed';
|
||||
String get trackReEnrichFfmpegFailed =>
|
||||
'Ошибка встраивания метаданных FFmpeg';
|
||||
|
||||
@override
|
||||
String trackSaveFailed(String error) {
|
||||
return 'Failed: $error';
|
||||
return 'Ошибка: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1189,6 +1189,13 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTags => 'Show advanced tags';
|
||||
|
||||
@override
|
||||
String get filenameShowAdvancedTagsDescription =>
|
||||
'Enable formatted tags for track padding and date patterns';
|
||||
|
||||
@override
|
||||
String get folderOrganization => 'Klasör Organizasyonu';
|
||||
|
||||
|
|
|
|||
|
|
@ -1152,7 +1152,7 @@
|
|||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "Lösche {count} {count, plural, one {}=1{Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.",
|
||||
"dialogDeleteSelectedMessage": "Lösche {count} {count, plural, one {Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
|
|
@ -1231,7 +1231,7 @@
|
|||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "{count} {count, plural, one {}=1{Titel} other{Titel}}",
|
||||
"snackbarDeletedTracks": "{count} {count, plural, one {Titel} other{Titel}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
|
|
@ -1438,7 +1438,7 @@
|
|||
"@selectionTapToSelect": {
|
||||
"description": "Hint - how to select items"
|
||||
},
|
||||
"selectionDeleteTracks": "Lösche {count} {count, plural, one {}=1{Titel}other{Titel}}",
|
||||
"selectionDeleteTracks": "Lösche {count} {count, plural, one {Titel}other{Titel}}",
|
||||
"@selectionDeleteTracks": {
|
||||
"description": "Delete button with count",
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -874,6 +874,14 @@
|
|||
"@filenameAvailablePlaceholders": {"description": "Label for placeholder list"},
|
||||
"filenameHint": "{artist} - {title}",
|
||||
"@filenameHint": {"description": "Default filename format hint"},
|
||||
"filenameShowAdvancedTags": "Show advanced tags",
|
||||
"@filenameShowAdvancedTags": {
|
||||
"description": "Toggle label for showing advanced filename tags"
|
||||
},
|
||||
"filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
|
||||
"@filenameShowAdvancedTagsDescription": {
|
||||
"description": "Description for advanced filename tag toggle"
|
||||
},
|
||||
|
||||
"folderOrganization": "Folder Organization",
|
||||
"@folderOrganization": {"description": "Setting title - folder structure"},
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
"@historyFilterSingles": {
|
||||
"description": "Filter chip - show singles only"
|
||||
},
|
||||
"historyTracksCount": "{count, plural, one {}=1{1 pista} other{{count} pistas}}",
|
||||
"historyTracksCount": "{count, plural, one {1 pista} other{{count} pistas}}",
|
||||
"@historyTracksCount": {
|
||||
"description": "Track count with plural form",
|
||||
"placeholders": {
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"historyAlbumsCount": "{count, plural, one {}=1{1 álbum} other{{count} álbumes}}",
|
||||
"historyAlbumsCount": "{count, plural, one {1 álbum} other{{count} álbumes}}",
|
||||
"@historyAlbumsCount": {
|
||||
"description": "Album count with plural form",
|
||||
"placeholders": {
|
||||
|
|
@ -636,7 +636,7 @@
|
|||
"@albumTitle": {
|
||||
"description": "Album screen title"
|
||||
},
|
||||
"albumTracks": "{count, plural, one {}=1{1 pista} other{{count} pistas}}",
|
||||
"albumTracks": "{count, plural, one {1 pista} other{{count} pistas}}",
|
||||
"@albumTracks": {
|
||||
"description": "Album track count",
|
||||
"placeholders": {
|
||||
|
|
@ -673,7 +673,7 @@
|
|||
"@artistCompilations": {
|
||||
"description": "Section header for compilations"
|
||||
},
|
||||
"artistReleases": "{count, plural, one {}=1{1 lanzamiento} other{{count} lanzamientos}}",
|
||||
"artistReleases": "{count, plural, one {1 lanzamiento} other{{count} lanzamientos}}",
|
||||
"@artistReleases": {
|
||||
"description": "Artist release count",
|
||||
"placeholders": {
|
||||
|
|
@ -1152,7 +1152,7 @@
|
|||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, one {}=1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, one {pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
|
|
@ -1231,7 +1231,7 @@
|
|||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "Eliminado {count} {count, plural, one {}=1{pista} other{pistas}}",
|
||||
"snackbarDeletedTracks": "Eliminado {count} {count, plural, one {pista} other{pistas}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
|
|
@ -1438,7 +1438,7 @@
|
|||
"@selectionTapToSelect": {
|
||||
"description": "Hint - how to select items"
|
||||
},
|
||||
"selectionDeleteTracks": "¡Eliminar {count} {count, plural, one {}=1{pista} other{pistas}}",
|
||||
"selectionDeleteTracks": "¡Eliminar {count} {count, plural, one {pista} other{pistas}}",
|
||||
"@selectionDeleteTracks": {
|
||||
"description": "Delete button with count",
|
||||
"placeholders": {
|
||||
|
|
@ -2014,7 +2014,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, one {}=1{1 pista} other{{count} pistas}}",
|
||||
"tracksCount": "{count, plural, one {1 pista} other{{count} pistas}}",
|
||||
"@tracksCount": {
|
||||
"description": "Track count display",
|
||||
"placeholders": {
|
||||
|
|
@ -2758,7 +2758,7 @@
|
|||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, one {}=1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, one {pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
|
|
@ -2797,7 +2797,7 @@
|
|||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, one {}=1{pista} other{pistas}}",
|
||||
"downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, one {pista} other{pistas}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -1532,6 +1532,14 @@
|
|||
"@filenameHint": {
|
||||
"description": "Default filename format hint"
|
||||
},
|
||||
"filenameShowAdvancedTags": "Tampilkan tag lanjutan",
|
||||
"@filenameShowAdvancedTags": {
|
||||
"description": "Toggle label for showing advanced filename tags"
|
||||
},
|
||||
"filenameShowAdvancedTagsDescription": "Aktifkan tag format untuk padding nomor lagu dan pola tanggal",
|
||||
"@filenameShowAdvancedTagsDescription": {
|
||||
"description": "Description for advanced filename tag toggle"
|
||||
},
|
||||
"folderOrganization": "Organisasi Folder",
|
||||
"@folderOrganization": {
|
||||
"description": "Setting title - folder structure"
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
"@historyFilterSingles": {
|
||||
"description": "Filter chip - show singles only"
|
||||
},
|
||||
"historyTracksCount": "{count, plural, one {}=1{1 faixa} other{{count} faixas}}",
|
||||
"historyTracksCount": "{count, plural, one {1 faixa} other{{count} faixas}}",
|
||||
"@historyTracksCount": {
|
||||
"description": "Track count with plural form",
|
||||
"placeholders": {
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"historyAlbumsCount": "{count, plural, one {}=1{1 álbum} other{{count} álbuns}}",
|
||||
"historyAlbumsCount": "{count, plural, one {1 álbum} other{{count} álbuns}}",
|
||||
"@historyAlbumsCount": {
|
||||
"description": "Album count with plural form",
|
||||
"placeholders": {
|
||||
|
|
@ -636,7 +636,7 @@
|
|||
"@albumTitle": {
|
||||
"description": "Album screen title"
|
||||
},
|
||||
"albumTracks": "{count, plural, one {}=1{1 faixa} other{{count} faixas}}",
|
||||
"albumTracks": "{count, plural, one {1 faixa} other{{count} faixas}}",
|
||||
"@albumTracks": {
|
||||
"description": "Album track count",
|
||||
"placeholders": {
|
||||
|
|
@ -673,7 +673,7 @@
|
|||
"@artistCompilations": {
|
||||
"description": "Section header for compilations"
|
||||
},
|
||||
"artistReleases": "{count, plural, one {}=1{1 lançamento} other{{count} lançamentos}}",
|
||||
"artistReleases": "{count, plural, one {1 lançamento} other{{count} lançamentos}}",
|
||||
"@artistReleases": {
|
||||
"description": "Artist release count",
|
||||
"placeholders": {
|
||||
|
|
@ -1152,7 +1152,7 @@
|
|||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.",
|
||||
"dialogDeleteSelectedMessage": "Apagar {count} {count, plural, one {faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
|
|
@ -1231,7 +1231,7 @@
|
|||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "{count} {count, plural, one {}=1{faixa apagada} other{faixas apagadas}}",
|
||||
"snackbarDeletedTracks": "{count} {count, plural, one {faixa apagada} other{faixas apagadas}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
|
|
@ -1438,7 +1438,7 @@
|
|||
"@selectionTapToSelect": {
|
||||
"description": "Hint - how to select items"
|
||||
},
|
||||
"selectionDeleteTracks": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}}",
|
||||
"selectionDeleteTracks": "Apagar {count} {count, plural, one {faixa} other{faixas}}",
|
||||
"@selectionDeleteTracks": {
|
||||
"description": "Delete button with count",
|
||||
"placeholders": {
|
||||
|
|
@ -2014,7 +2014,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, one {}=1{1 faixa} other{{count} faixas}}",
|
||||
"tracksCount": "{count, plural, one {1 faixa} other{{count} faixas}}",
|
||||
"@tracksCount": {
|
||||
"description": "Track count display",
|
||||
"placeholders": {
|
||||
|
|
@ -2758,7 +2758,7 @@
|
|||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "Excluir {count} {count, plural, one {}=1{faixa} other{faixas}} deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.",
|
||||
"downloadedAlbumDeleteMessage": "Excluir {count} {count, plural, one {faixa} other{faixas}} deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
|
|
@ -2797,7 +2797,7 @@
|
|||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}}",
|
||||
"downloadedAlbumDeleteCount": "Apagar {count} {count, plural, one {faixa} other{faixas}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -3114,7 +3114,7 @@
|
|||
"@libraryAboutDescription": {
|
||||
"description": "Description of local library feature"
|
||||
},
|
||||
"libraryTracksCount": "{count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}",
|
||||
"libraryTracksCount": "{count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
|
||||
"@libraryTracksCount": {
|
||||
"description": "Track count in library",
|
||||
"placeholders": {
|
||||
|
|
@ -3156,7 +3156,7 @@
|
|||
"@libraryInLibrary": {
|
||||
"description": "Badge shown on tracks that exist in local library"
|
||||
},
|
||||
"libraryRemovedMissingFiles": "Удалено {count} {count, plural, one {отсутствующий файл} few {трека} many {отсутствующих файлов} =1{отсутствующий файл} other{отсутствующих файлов}} в библиотеке",
|
||||
"libraryRemovedMissingFiles": "Удалено {count} {count, plural, one {отсутствующий файл} few {трека} many {отсутствующих файлов} other{отсутствующих файлов}} в библиотеке",
|
||||
"@libraryRemovedMissingFiles": {
|
||||
"description": "Snackbar after cleanup",
|
||||
"placeholders": {
|
||||
|
|
@ -3282,7 +3282,7 @@
|
|||
"@timeJustNow": {
|
||||
"description": "Relative time - less than a minute ago"
|
||||
},
|
||||
"timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} =1 {1 минуту} other {{count} минут}} назад",
|
||||
"timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} other {{count} минут}} назад",
|
||||
"@timeMinutesAgo": {
|
||||
"description": "Relative time - minutes ago",
|
||||
"placeholders": {
|
||||
|
|
@ -3291,7 +3291,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} =1 {1 час} other {{count} часов}} назад",
|
||||
"timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} other {{count} часов}} назад",
|
||||
"@timeHoursAgo": {
|
||||
"description": "Relative time - hours ago",
|
||||
"placeholders": {
|
||||
|
|
@ -3324,7 +3324,7 @@
|
|||
"@storageSwitchExistingDownloads": {
|
||||
"description": "Section header for existing downloads info"
|
||||
},
|
||||
"storageSwitchExistingDownloadsInfo": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}} в {mode} хранилище",
|
||||
"storageSwitchExistingDownloadsInfo": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}} в {mode} хранилище",
|
||||
"@storageSwitchExistingDownloadsInfo": {
|
||||
"description": "Info about existing downloads count",
|
||||
"placeholders": {
|
||||
|
|
@ -3378,7 +3378,7 @@
|
|||
"@storageStatsTitle": {
|
||||
"description": "Section title for storage stats"
|
||||
},
|
||||
"storageStatsAppCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}} в хранилище приложения",
|
||||
"storageStatsAppCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}} в хранилище приложения",
|
||||
"@storageStatsAppCount": {
|
||||
"description": "Count of tracks in app storage",
|
||||
"placeholders": {
|
||||
|
|
@ -3387,7 +3387,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"storageStatsSafCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}} в вашей папке в SAF",
|
||||
"storageStatsSafCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}} в вашей папке в SAF",
|
||||
"@storageStatsSafCount": {
|
||||
"description": "Count of tracks in SAF storage",
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -11,19 +12,68 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
|||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
_configureImageCache();
|
||||
final runtimeProfile = await _resolveRuntimeProfile();
|
||||
_configureImageCache(runtimeProfile);
|
||||
|
||||
runApp(
|
||||
ProviderScope(child: const _EagerInitialization(child: SpotiFLACApp())),
|
||||
ProviderScope(
|
||||
child: _EagerInitialization(
|
||||
child: SpotiFLACApp(
|
||||
disableOverscrollEffects: runtimeProfile.disableOverscrollEffects,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _configureImageCache() {
|
||||
Future<_RuntimeProfile> _resolveRuntimeProfile() async {
|
||||
const defaults = _RuntimeProfile(
|
||||
imageCacheMaximumSize: 240,
|
||||
imageCacheMaximumSizeBytes: 60 << 20,
|
||||
disableOverscrollEffects: false,
|
||||
);
|
||||
|
||||
if (!Platform.isAndroid) return defaults;
|
||||
|
||||
try {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
final isArm32Only = androidInfo.supported64BitAbis.isEmpty;
|
||||
final isLowRamDevice =
|
||||
androidInfo.isLowRamDevice || androidInfo.physicalRamSize <= 2500;
|
||||
|
||||
if (!isArm32Only && !isLowRamDevice) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return _RuntimeProfile(
|
||||
imageCacheMaximumSize: 120,
|
||||
imageCacheMaximumSizeBytes: 24 << 20,
|
||||
disableOverscrollEffects: true,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to resolve runtime profile: $e');
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
void _configureImageCache(_RuntimeProfile runtimeProfile) {
|
||||
final imageCache = PaintingBinding.instance.imageCache;
|
||||
// Keep memory cache bounded so cover-heavy pages don't retain too many
|
||||
// full-resolution images simultaneously.
|
||||
imageCache.maximumSize = 240;
|
||||
imageCache.maximumSizeBytes = 60 << 20; // 60 MiB
|
||||
imageCache.maximumSize = runtimeProfile.imageCacheMaximumSize;
|
||||
imageCache.maximumSizeBytes = runtimeProfile.imageCacheMaximumSizeBytes;
|
||||
}
|
||||
|
||||
class _RuntimeProfile {
|
||||
final int imageCacheMaximumSize;
|
||||
final int imageCacheMaximumSizeBytes;
|
||||
final bool disableOverscrollEffects;
|
||||
|
||||
const _RuntimeProfile({
|
||||
required this.imageCacheMaximumSize,
|
||||
required this.imageCacheMaximumSizeBytes,
|
||||
required this.disableOverscrollEffects,
|
||||
});
|
||||
}
|
||||
|
||||
/// Widget to eagerly initialize providers that need to load data on startup
|
||||
|
|
|
|||
|
|
@ -1304,7 +1304,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
|||
}
|
||||
|
||||
static final _featuredArtistPattern = RegExp(
|
||||
r'\s*[,;&]\s*|\s+(?:feat\.?|ft\.?|featuring|with|x)\s+',
|
||||
r'\s*[,;]\s*|\s+(?:feat\.?|ft\.?|featuring|with|x)\s+',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
|
|
@ -2813,6 +2813,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
|||
'track': trackToDownload.trackNumber ?? 0,
|
||||
'disc': trackToDownload.discNumber ?? 0,
|
||||
'year': _extractYear(trackToDownload.releaseDate) ?? '',
|
||||
'date': trackToDownload.releaseDate ?? '',
|
||||
});
|
||||
final sanitized = await PlatformBridge.sanitizeFilename(baseName);
|
||||
safBaseName = sanitized;
|
||||
|
|
|
|||
|
|
@ -490,6 +490,9 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
0,
|
||||
(sum, a) => sum + a.totalTracks,
|
||||
);
|
||||
final textScale = MediaQuery.textScalerOf(context).scale(1.0);
|
||||
final compactLayout =
|
||||
MediaQuery.sizeOf(context).width < 430 || textScale > 1.15;
|
||||
|
||||
return Positioned(
|
||||
left: 0,
|
||||
|
|
@ -510,7 +513,12 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
child: compactLayout
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _exitSelectionMode,
|
||||
|
|
@ -524,15 +532,99 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.discographySelectedCount(selectedCount),
|
||||
context.l10n.discographySelectedCount(
|
||||
selectedCount,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
if (selectedCount > 0)
|
||||
Text(
|
||||
context.l10n.tracksCount(totalTracks),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: allSelected
|
||||
? _deselectAll
|
||||
: () => _selectAll(allAlbums),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
allSelected
|
||||
? context.l10n.actionDeselect
|
||||
: context.l10n.actionSelectAll,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: selectedCount > 0
|
||||
? () => _downloadSelectedAlbums(
|
||||
context,
|
||||
selectedAlbums,
|
||||
)
|
||||
: null,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
context.l10n.discographyDownloadSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _exitSelectionMode,
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: context.l10n.dialogCancel,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.discographySelectedCount(
|
||||
selectedCount,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
if (selectedCount > 0)
|
||||
Text(
|
||||
context.l10n.tracksCount(totalTracks),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -550,7 +642,10 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: selectedCount > 0
|
||||
? () => _downloadSelectedAlbums(context, selectedAlbums)
|
||||
? () => _downloadSelectedAlbums(
|
||||
context,
|
||||
selectedAlbums,
|
||||
)
|
||||
: null,
|
||||
icon: const Icon(Icons.download, size: 18),
|
||||
label: Text(context.l10n.discographyDownloadSelected),
|
||||
|
|
@ -1427,9 +1522,11 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
void _downloadTrack(Track track) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
void enqueue(String service, {String? quality}) {
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
.addToQueue(track, service, qualityOverride: quality);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarAddedToQueue(track.name)),
|
||||
|
|
@ -1438,6 +1535,20 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
DownloadServicePicker.show(
|
||||
context,
|
||||
onSelect: (quality, service) {
|
||||
if (!mounted) return;
|
||||
enqueue(service, quality: quality);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueue(settings.defaultService);
|
||||
}
|
||||
|
||||
Widget _buildAlbumSection(
|
||||
String title,
|
||||
List<ArtistAlbum> albums,
|
||||
|
|
@ -1468,7 +1579,12 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
final album = albums[index];
|
||||
return KeyedSubtree(
|
||||
key: ValueKey(album.id),
|
||||
child: _buildAlbumCard(album, colorScheme, tileSize: tileSize, sectionHeight: sectionHeight),
|
||||
child: _buildAlbumCard(
|
||||
album,
|
||||
colorScheme,
|
||||
tileSize: tileSize,
|
||||
sectionHeight: sectionHeight,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -1601,9 +1717,9 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
Flexible(
|
||||
child: Text(
|
||||
album.name,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -620,14 +620,28 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
final controller = TextEditingController(text: current);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final tags = [
|
||||
final basicTags = [
|
||||
'{artist}',
|
||||
'{title}',
|
||||
'{album}',
|
||||
'{track}',
|
||||
'{year}',
|
||||
'{date}',
|
||||
'{disc}',
|
||||
];
|
||||
final advancedTags = [
|
||||
'{track_raw}',
|
||||
'{track:02}',
|
||||
'{track:1}',
|
||||
'{date:%Y}',
|
||||
'{date:%Y-%m-%d}',
|
||||
'{disc_raw}',
|
||||
'{disc:02}',
|
||||
];
|
||||
var showAdvancedTags = RegExp(
|
||||
r'\{(?:track_raw|disc_raw|track:\d+|disc:\d+|date:[^}]+)\}',
|
||||
caseSensitive: false,
|
||||
).hasMatch(current);
|
||||
|
||||
void insertTag(String tag) {
|
||||
final text = controller.text;
|
||||
|
|
@ -659,7 +673,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (context) => Padding(
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setModalState) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
|
|
@ -684,9 +699,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
),
|
||||
Text(
|
||||
context.l10n.filenameFormat,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -704,9 +718,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
decoration: InputDecoration(
|
||||
hintText: '{artist} - {title}',
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
fillColor: colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.3),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
|
|
@ -726,7 +739,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: tags.map((tag) {
|
||||
children: basicTags.map((tag) {
|
||||
return ActionChip(
|
||||
label: Text(tag),
|
||||
onPressed: () => insertTag(tag),
|
||||
|
|
@ -743,6 +756,40 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile(
|
||||
value: showAdvancedTags,
|
||||
onChanged: (value) =>
|
||||
setModalState(() => showAdvancedTags = value),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.filenameShowAdvancedTags),
|
||||
subtitle: Text(
|
||||
context.l10n.filenameShowAdvancedTagsDescription,
|
||||
),
|
||||
),
|
||||
if (showAdvancedTags) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: advancedTags.map((tag) {
|
||||
return ActionChip(
|
||||
label: Text(tag),
|
||||
onPressed: () => insertTag(tag),
|
||||
backgroundColor: colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.5),
|
||||
side: BorderSide.none,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
|
|
@ -788,6 +835,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: spotiflac_android
|
||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
||||
publish_to: "none"
|
||||
version: 3.6.6+80
|
||||
version: 3.6.7+81
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
|
|
|||
484
site/downloads.html
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Downloads - SpotiFLAC Mobile</title>
|
||||
<meta name="description" content="Download the latest version of SpotiFLAC Mobile. Changelog and release history included.">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<link rel="icon" href="icon.png" type="image/png">
|
||||
|
||||
<!-- Google Sans Flex -->
|
||||
<style>
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 400; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-400-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 500; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-500-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 600; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-600-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 700; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-700-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 800; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-800-normal.woff2) format('woff2'); }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* ── M3 AMOLED surface ramp ── */
|
||||
:root {
|
||||
--green: #1DB954;
|
||||
--green-dim: #1aa34a;
|
||||
--bg: #0a0a0a;
|
||||
--bg-card: #1a1a1a;
|
||||
--bg-card-hover: #222222;
|
||||
--surface: #121212;
|
||||
--text: #e8e8e8;
|
||||
--text-dim: #999;
|
||||
--max-w: 900px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: 'Google Sans Flex', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: var(--bg); color: var(--text); line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--green); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── NAV ── */
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(18,18,18,.78);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: var(--max-w); margin: auto;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 24px; height: 64px;
|
||||
}
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1.1rem; color: var(--text); }
|
||||
.nav-brand img { width: 32px; height: 32px; border-radius: 50%; }
|
||||
.nav-links { display: flex; gap: 24px; list-style: none; }
|
||||
.nav-links a { color: var(--text-dim); font-size: .9rem; transition: color .2s; }
|
||||
.nav-links a:hover { color: var(--text); text-decoration: none; }
|
||||
.nav-links a.active { color: var(--text); font-weight: 600; }
|
||||
.nav-links .nav-icon { display: flex; align-items: center; opacity: .6; transition: opacity .2s; margin-left: -12px; }
|
||||
.nav-links .nav-icon:hover { opacity: 1; }
|
||||
.nav-links .nav-icon svg { width: 24px; height: 24px; fill: currentColor; }
|
||||
.nav-links .nav-divider { width: 1px; height: 20px; background: rgba(255,255,255,.15); margin-left: -4px; }
|
||||
|
||||
/* ── PAGE HEADER ── */
|
||||
.page-header {
|
||||
padding: 100px 24px 40px; text-align: center;
|
||||
}
|
||||
.page-header h1 { font-size: 2rem; font-weight: 800; margin-bottom: 8px; }
|
||||
.page-header p { color: var(--text-dim); font-size: 1rem; }
|
||||
|
||||
/* ── LATEST HERO ── */
|
||||
.latest-hero {
|
||||
max-width: var(--max-w); margin: 0 auto; padding: 0 24px 40px;
|
||||
}
|
||||
.latest-card {
|
||||
background: var(--bg-card-hover); border-radius: 20px;
|
||||
padding: 32px; position: relative; overflow: hidden;
|
||||
}
|
||||
.latest-header {
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;
|
||||
}
|
||||
.latest-tag { font-size: 1.6rem; font-weight: 800; }
|
||||
.latest-badge {
|
||||
font-size: .7rem; font-weight: 700; text-transform: uppercase;
|
||||
padding: 4px 12px; border-radius: 999px;
|
||||
background: var(--green); color: #000;
|
||||
}
|
||||
.latest-date { font-size: .85rem; color: var(--text-dim); margin-left: auto; }
|
||||
.latest-assets {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px; margin-bottom: 24px;
|
||||
}
|
||||
.latest-asset {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 18px; border-radius: 16px;
|
||||
background: rgba(29,185,84,.08);
|
||||
color: var(--text); transition: background .2s; text-decoration: none;
|
||||
}
|
||||
.latest-asset:hover { background: rgba(29,185,84,.15); text-decoration: none; }
|
||||
.latest-asset-icon { color: var(--green); flex-shrink: 0; }
|
||||
.latest-asset-info { min-width: 0; }
|
||||
.latest-asset-name { font-size: .85rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.latest-asset-meta { font-size: .75rem; color: var(--text-dim); }
|
||||
.latest-changelog-toggle {
|
||||
background: var(--bg-card); border: none; border-radius: 16px;
|
||||
color: var(--text-dim); padding: 10px 16px; font-size: .85rem;
|
||||
cursor: pointer; transition: background .2s; width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
.latest-changelog-toggle:hover { background: var(--surface); color: var(--text); }
|
||||
.latest-changelog {
|
||||
display: none; margin-top: 16px; padding-top: 16px;
|
||||
border-top: 1px solid rgba(255,255,255,.06);
|
||||
}
|
||||
.latest-changelog.show { display: block; }
|
||||
|
||||
/* ── OLDER RELEASES ── */
|
||||
.older-section {
|
||||
max-width: var(--max-w); margin: 0 auto; padding: 40px 24px 80px;
|
||||
}
|
||||
.older-title {
|
||||
font-size: 1.1rem; font-weight: 600; color: var(--text-dim);
|
||||
margin-bottom: 16px; padding-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ── RELEASE CARDS ── */
|
||||
.release-card {
|
||||
background: var(--bg-card); border-radius: 16px;
|
||||
margin-bottom: 8px; transition: background .2s;
|
||||
}
|
||||
.release-card:hover { background: var(--bg-card-hover); }
|
||||
.release-summary {
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||
padding: 16px 20px; cursor: pointer; list-style: none;
|
||||
}
|
||||
.release-summary::-webkit-details-marker { display: none; }
|
||||
.release-tag { font-size: 1rem; font-weight: 700; }
|
||||
.release-badge {
|
||||
font-size: .65rem; font-weight: 700; text-transform: uppercase;
|
||||
padding: 2px 8px; border-radius: 999px;
|
||||
}
|
||||
.release-badge-pre { background: #f59e0b; color: #000; }
|
||||
.release-date { font-size: .8rem; color: var(--text-dim); margin-left: auto; }
|
||||
.release-expand { color: var(--text-dim); font-size: .8rem; transition: transform .2s; }
|
||||
details[open] .release-expand { transform: rotate(180deg); }
|
||||
.release-detail { padding: 0 20px 20px; }
|
||||
.release-assets { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
||||
.release-asset {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 7px 14px; border-radius: 12px; font-size: .82rem; font-weight: 500;
|
||||
background: rgba(29,185,84,.08);
|
||||
color: var(--green); transition: background .2s; text-decoration: none;
|
||||
}
|
||||
.release-asset:hover { background: rgba(29,185,84,.15); text-decoration: none; }
|
||||
.release-asset-size { color: var(--text-dim); font-size: .72rem; }
|
||||
|
||||
/* ── CHANGELOG BODY ── */
|
||||
.release-body {
|
||||
font-size: .85rem; color: var(--text-dim); line-height: 1.7;
|
||||
max-height: 400px; overflow-y: auto;
|
||||
scrollbar-width: thin; scrollbar-color: #333 transparent;
|
||||
}
|
||||
.release-body h1, .release-body h2, .release-body h3 {
|
||||
color: var(--text); font-size: .95rem; margin: 16px 0 8px;
|
||||
}
|
||||
.release-body h1:first-child, .release-body h2:first-child, .release-body h3:first-child { margin-top: 0; }
|
||||
.release-body ul { padding-left: 20px; margin: 4px 0; }
|
||||
.release-body li { margin: 4px 0; }
|
||||
.release-body code { background: var(--bg-card-hover); padding: 2px 6px; border-radius: 4px; font-size: .8rem; }
|
||||
.release-body a { color: var(--green); }
|
||||
|
||||
/* ── FOOTER ── */
|
||||
footer {
|
||||
background: var(--surface);
|
||||
padding: 40px 24px; text-align: center;
|
||||
}
|
||||
.footer-inner { max-width: var(--max-w); margin: auto; }
|
||||
.footer-links { display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.footer-links a { color: var(--text-dim); font-size: .9rem; }
|
||||
.footer-links a:hover { color: var(--text); }
|
||||
.footer-copy { color: #555; font-size: .8rem; }
|
||||
|
||||
/* ── LOADING ── */
|
||||
.loading { text-align: center; color: var(--text-dim); padding: 60px 0; }
|
||||
.loading-spinner {
|
||||
width: 32px; height: 32px; margin: 0 auto 12px;
|
||||
border: 3px solid var(--surface); border-top-color: var(--green);
|
||||
border-radius: 50%; animation: spin .8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.all-releases-link {
|
||||
display: block; text-align: center; padding: 24px;
|
||||
color: var(--text-dim); font-size: .9rem;
|
||||
}
|
||||
|
||||
/* ── MOBILE MENU ── */
|
||||
.nav-burger {
|
||||
display: none; width: 40px; height: 40px; border-radius: 12px;
|
||||
background: none; border: none; cursor: pointer;
|
||||
align-items: center; justify-content: center; flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-burger .bar {
|
||||
display: block; width: 20px; height: 2px; background: var(--text);
|
||||
border-radius: 2px; transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .2s;
|
||||
position: absolute; left: 10px;
|
||||
}
|
||||
.nav-burger .bar:nth-child(1) { top: 12px; }
|
||||
.nav-burger .bar:nth-child(2) { top: 19px; }
|
||||
.nav-burger .bar:nth-child(3) { top: 26px; }
|
||||
.nav-burger.active .bar:nth-child(1) { top: 19px; transform: rotate(45deg); }
|
||||
.nav-burger.active .bar:nth-child(2) { opacity: 0; }
|
||||
.nav-burger.active .bar:nth-child(3) { top: 19px; transform: rotate(-45deg); }
|
||||
.mobile-overlay {
|
||||
position: fixed; top: 64px; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,.5); z-index: 98;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-overlay.open { opacity: 1; pointer-events: auto; }
|
||||
.mobile-menu {
|
||||
position: fixed; top: 64px; left: 0; right: 0;
|
||||
background: rgba(18,18,18,.95); padding: 8px 16px 16px; z-index: 99;
|
||||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||||
transform: translateY(-8px); opacity: 0; pointer-events: none;
|
||||
transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-menu.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
|
||||
.mobile-menu a {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 16px; border-radius: 12px;
|
||||
color: var(--text-dim); font-size: .95rem; font-weight: 500;
|
||||
transition: background .2s; opacity: 0; transform: translateY(-6px);
|
||||
}
|
||||
.mobile-menu.open a {
|
||||
opacity: 1; transform: translateY(0);
|
||||
transition: background .2s, opacity .3s cubic-bezier(.4,0,.2,1), transform .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-menu.open a:nth-child(1) { transition-delay: .03s; }
|
||||
.mobile-menu.open a:nth-child(2) { transition-delay: .06s; }
|
||||
.mobile-menu.open a:nth-child(3) { transition-delay: .09s; }
|
||||
.mobile-menu.open a:nth-child(4) { transition-delay: .12s; }
|
||||
.mobile-menu.open a:nth-child(5) { transition-delay: .15s; }
|
||||
.mobile-menu a:hover { background: var(--bg-card); color: var(--text); text-decoration: none; }
|
||||
.mobile-menu a.active { color: var(--text); font-weight: 600; background: var(--bg-card); }
|
||||
.mobile-menu .mobile-divider {
|
||||
height: 1px; background: rgba(255,255,255,.06); margin: 4px 0;
|
||||
opacity: 0; transition: opacity .3s .15s;
|
||||
}
|
||||
.mobile-menu.open .mobile-divider { opacity: 1; }
|
||||
.mobile-menu .mobile-icons {
|
||||
display: flex; gap: 8px; padding: 8px 16px 0;
|
||||
opacity: 0; transform: translateY(-6px);
|
||||
transition: opacity .3s cubic-bezier(.4,0,.2,1) .18s, transform .3s cubic-bezier(.4,0,.2,1) .18s;
|
||||
}
|
||||
.mobile-menu.open .mobile-icons { opacity: 1; transform: translateY(0); }
|
||||
.mobile-menu .mobile-icons a {
|
||||
padding: 10px; border-radius: 12px; background: var(--bg-card);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 1; transform: none;
|
||||
}
|
||||
.mobile-menu .mobile-icons a svg { width: 20px; height: 20px; fill: currentColor; }
|
||||
|
||||
/* ── MOBILE ── */
|
||||
@media (max-width: 640px) {
|
||||
.nav-links { display: none; }
|
||||
.nav-burger { display: flex; }
|
||||
.page-header { padding: 80px 16px 32px; }
|
||||
.latest-hero { padding: 0 16px 32px; }
|
||||
.latest-card { padding: 20px; }
|
||||
.latest-header { flex-direction: column; align-items: flex-start; gap: 6px; }
|
||||
.latest-date { margin-left: 0; }
|
||||
.latest-assets { grid-template-columns: 1fr; }
|
||||
.older-section { padding: 32px 16px 60px; }
|
||||
.release-summary { flex-direction: row; gap: 8px; }
|
||||
.release-date { margin-left: auto; }
|
||||
}
|
||||
|
||||
.icon-svg { width: 20px; height: 20px; fill: currentColor; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-brand" href="index.html">
|
||||
<img src="icon.png" alt="SpotiFLAC">
|
||||
SpotiFLAC
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html#features">Features</a></li>
|
||||
<li><a href="downloads.html" class="active">Downloads</a></li>
|
||||
<li><a href="index.html#faq">FAQ</a></li>
|
||||
<li><a href="partners.html">Partners</a></li>
|
||||
<li><a href="https://zarz.moe/docs" target="_blank">Docs</a></li>
|
||||
<li class="nav-divider"></li>
|
||||
<li><a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" class="nav-icon" aria-label="GitHub"><svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.82-.26.82-.57L9 20.86c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22l-.01 3.29c0 .31.21.69.82.57A12 12 0 0 0 12 .3"/></svg></a></li>
|
||||
<li><a href="https://t.me/spotiflac" target="_blank" class="nav-icon" aria-label="Telegram"><svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg></a></li>
|
||||
</ul>
|
||||
<button class="nav-burger" onclick="toggleMenu()" aria-label="Menu">
|
||||
<span class="bar"></span><span class="bar"></span><span class="bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- MOBILE MENU -->
|
||||
<div class="mobile-overlay" id="mobileOverlay" onclick="toggleMenu()"></div>
|
||||
<div class="mobile-menu" id="mobileMenu">
|
||||
<a href="index.html#features">Features</a>
|
||||
<a href="downloads.html" class="active">Downloads</a>
|
||||
<a href="index.html#faq">FAQ</a>
|
||||
<a href="partners.html">Partners</a>
|
||||
<a href="https://zarz.moe/docs" target="_blank">Docs</a>
|
||||
<div class="mobile-divider"></div>
|
||||
<div class="mobile-icons">
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" aria-label="GitHub">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</a>
|
||||
<a href="https://t.me/spotiflac" target="_blank" aria-label="Telegram">
|
||||
<svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Downloads</h1>
|
||||
<p>Latest releases with changelog and direct download links.</p>
|
||||
</div>
|
||||
|
||||
<div class="latest-hero" id="latest-hero">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
Loading latest release...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="older-section" id="older-section" style="display:none">
|
||||
<div class="older-title">Previous Releases</div>
|
||||
<div id="older-releases"></div>
|
||||
<a class="all-releases-link" href="https://github.com/zarzet/SpotiFLAC-Mobile/releases" target="_blank">
|
||||
View all releases on GitHub →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="footer-inner">
|
||||
<div class="footer-links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="https://zarz.moe/docs" target="_blank">Documentation</a>
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank">GitHub</a>
|
||||
<a href="https://t.me/spotiflac" target="_blank">Telegram</a>
|
||||
<a href="https://ko-fi.com/zarzet" target="_blank">Support / Ko-fi</a>
|
||||
</div>
|
||||
<p class="footer-copy">SpotiFLAC is for educational and private use only. Not affiliated with any streaming service.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
const REPO = 'zarzet/SpotiFLAC-Mobile';
|
||||
const latestEl = document.getElementById('latest-hero');
|
||||
const olderEl = document.getElementById('older-releases');
|
||||
const olderSection = document.getElementById('older-section');
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=10`);
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
const releases = await res.json();
|
||||
if (!releases.length) { latestEl.innerHTML = '<p style="text-align:center;color:#999;padding:40px">No releases found.</p>'; return; }
|
||||
|
||||
// Latest release
|
||||
const latest = releases[0];
|
||||
const latestDate = fmtDate(latest.published_at);
|
||||
const latestBody = md(latest.body || '');
|
||||
const latestAssets = (latest.assets || []).filter(a => !a.name.endsWith('.sha256'));
|
||||
|
||||
latestEl.innerHTML = `
|
||||
<div class="latest-card">
|
||||
<div class="latest-header">
|
||||
<span class="latest-tag">${latest.tag_name}</span>
|
||||
<span class="latest-badge">${latest.prerelease ? 'Pre-release' : 'Latest Release'}</span>
|
||||
<span class="latest-date">${latestDate}</span>
|
||||
</div>
|
||||
<div class="latest-assets">
|
||||
${latestAssets.map(a => `
|
||||
<a class="latest-asset" href="${a.browser_download_url}">
|
||||
<svg class="latest-asset-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
<div class="latest-asset-info">
|
||||
<div class="latest-asset-name">${a.name}</div>
|
||||
<div class="latest-asset-meta">${fmtSize(a.size)} · ${fmtCount(a.download_count)} downloads</div>
|
||||
</div>
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
${latestBody ? `
|
||||
<button class="latest-changelog-toggle" onclick="this.nextElementSibling.classList.toggle('show'); this.textContent = this.nextElementSibling.classList.contains('show') ? 'Hide changelog' : 'Show changelog'">
|
||||
Show changelog
|
||||
</button>
|
||||
<div class="latest-changelog">
|
||||
<div class="release-body">${latestBody}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Older releases
|
||||
const older = releases.slice(1);
|
||||
if (older.length) {
|
||||
olderSection.style.display = '';
|
||||
olderEl.innerHTML = older.map(r => {
|
||||
const date = fmtDate(r.published_at);
|
||||
const body = md(r.body || '');
|
||||
const assets = (r.assets || []).filter(a => !a.name.endsWith('.sha256'));
|
||||
|
||||
return `
|
||||
<details class="release-card">
|
||||
<summary class="release-summary">
|
||||
<span class="release-tag">${r.tag_name}</span>
|
||||
${r.prerelease ? '<span class="release-badge release-badge-pre">Pre-release</span>' : ''}
|
||||
<span class="release-date">${date}</span>
|
||||
<span class="release-expand">▼</span>
|
||||
</summary>
|
||||
<div class="release-detail">
|
||||
${assets.length ? `
|
||||
<div class="release-assets">
|
||||
${assets.map(a => `
|
||||
<a class="release-asset" href="${a.browser_download_url}" target="_blank">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
||||
${a.name}
|
||||
<span class="release-asset-size">${fmtSize(a.size)}</span>
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
${body ? `<div class="release-body">${body}</div>` : ''}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
latestEl.innerHTML = `<p style="text-align:center;color:#999;padding:40px">Failed to load releases. <a href="https://github.com/${REPO}/releases" target="_blank">View on GitHub</a></p>`;
|
||||
}
|
||||
|
||||
function fmtDate(d) { return new Date(d).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' }); }
|
||||
function fmtSize(b) { return b < 1048576 ? (b/1024).toFixed(1)+' KB' : (b/1048576).toFixed(1)+' MB'; }
|
||||
function fmtCount(n) { return n >= 1000 ? (n/1000).toFixed(1)+'k' : n; }
|
||||
function md(s) {
|
||||
return s
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>')
|
||||
.replace(/^[-*] (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
.replace(/\n{2,}/g, '<br>')
|
||||
.replace(/(^<ul>)|(<\/ul>$)/g, '')
|
||||
.replace(/(<li>[\s\S]*?<\/li>)(?=\s*<h|$|\s*<br>)/g, '<ul>$1</ul>');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function toggleMenu() {
|
||||
document.getElementById('mobileMenu').classList.toggle('open');
|
||||
document.getElementById('mobileOverlay').classList.toggle('open');
|
||||
document.querySelector('.nav-burger').classList.toggle('active');
|
||||
}
|
||||
document.getElementById('mobileMenu').addEventListener('click', function(e) {
|
||||
if (e.target.closest('a')) toggleMenu();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
site/icon.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
site/images/1.jpg
Normal file
|
After Width: | Height: | Size: 539 KiB |
BIN
site/images/2.jpg
Normal file
|
After Width: | Height: | Size: 811 KiB |
BIN
site/images/3.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
site/images/4.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
465
site/index.html
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SpotiFLAC Mobile - Lossless Music Downloader</title>
|
||||
<meta name="description" content="Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music. No account required. Available on Android & iOS.">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="SpotiFLAC Mobile">
|
||||
<meta property="og:description" content="Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music. No account required.">
|
||||
<meta property="og:image" content="icon.png">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<link rel="icon" href="icon.png" type="image/png">
|
||||
|
||||
<!-- Google Sans Flex -->
|
||||
<style>
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 400; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-400-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 500; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-500-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 600; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-600-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 700; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-700-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 800; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-800-normal.woff2) format('woff2'); }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* ── M3 AMOLED surface ramp ── */
|
||||
:root {
|
||||
--green: #1DB954;
|
||||
--green-dim: #1aa34a;
|
||||
--bg: #0a0a0a; /* surfaceContainerLow */
|
||||
--bg-card: #1a1a1a; /* surfaceContainerHigh */
|
||||
--bg-card-hover: #222222; /* surfaceContainerHighest */
|
||||
--surface: #121212; /* surfaceContainer */
|
||||
--text: #e8e8e8; /* onSurface */
|
||||
--text-dim: #999; /* onSurfaceVariant */
|
||||
--max-w: 1100px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: 'Google Sans Flex', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: var(--bg); color: var(--text); line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--green); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── NAV ── */
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(18,18,18,.78);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: var(--max-w); margin: auto;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 24px; height: 64px;
|
||||
}
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1.1rem; color: var(--text); }
|
||||
.nav-brand img { width: 32px; height: 32px; border-radius: 50%; }
|
||||
.nav-links { display: flex; gap: 24px; list-style: none; }
|
||||
.nav-links a { color: var(--text-dim); font-size: .9rem; transition: color .2s; }
|
||||
.nav-links a:hover { color: var(--text); text-decoration: none; }
|
||||
.nav-links a.active { color: var(--text); font-weight: 600; }
|
||||
.nav-links .nav-icon { display: flex; align-items: center; opacity: .6; transition: opacity .2s; margin-left: -12px; }
|
||||
.nav-links .nav-icon:hover { opacity: 1; }
|
||||
.nav-links .nav-icon svg { width: 24px; height: 24px; fill: currentColor; }
|
||||
.nav-links .nav-divider { width: 1px; height: 20px; background: rgba(255,255,255,.15); margin-left: -4px; }
|
||||
|
||||
/* ── HERO ── */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
text-align: center; padding: 100px 24px 60px;
|
||||
background: radial-gradient(ellipse at 50% 0%, rgba(29,185,84,.05) 0%, transparent 50%);
|
||||
}
|
||||
.hero h1 { font-size: clamp(2.2rem, 5vw, 3.5rem); font-weight: 800; letter-spacing: -1px; margin-bottom: 12px; }
|
||||
.hero h1 span { color: var(--green); }
|
||||
.hero p { font-size: 1.15rem; color: var(--text-dim); max-width: 520px; margin-bottom: 8px; }
|
||||
.hero-badges { display: flex; gap: 8px; justify-content: center; margin: 16px 0 32px; flex-wrap: wrap; }
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 16px; border-radius: 999px;
|
||||
font-size: .8rem; font-weight: 600;
|
||||
background: var(--surface); color: var(--text-dim);
|
||||
}
|
||||
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 12px 28px; border-radius: 16px;
|
||||
font-size: .95rem; font-weight: 600;
|
||||
transition: background .2s; cursor: pointer; border: none;
|
||||
}
|
||||
.btn-primary { background: var(--green); color: #000; }
|
||||
.btn-primary:hover { background: var(--green-dim); text-decoration: none; }
|
||||
.btn-secondary { background: var(--bg-card); color: var(--text); }
|
||||
.btn-secondary:hover { background: var(--bg-card-hover); text-decoration: none; }
|
||||
|
||||
/* ── SECTIONS ── */
|
||||
section { padding: 80px 24px; }
|
||||
.section-inner { max-width: var(--max-w); margin: auto; }
|
||||
.section-title { font-size: 1.8rem; font-weight: 700; text-align: center; margin-bottom: 12px; }
|
||||
.section-sub { text-align: center; color: var(--text-dim); max-width: 560px; margin: 0 auto 48px; }
|
||||
|
||||
/* ── FEATURES ── */
|
||||
.features-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.feature-card {
|
||||
background: var(--bg-card); border-radius: 20px;
|
||||
padding: 28px 24px; transition: background .2s;
|
||||
}
|
||||
.feature-card:hover { background: var(--bg-card-hover); }
|
||||
.feature-icon {
|
||||
width: 40px; height: 40px; border-radius: 12px;
|
||||
background: rgba(29,185,84,.12); color: var(--green);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 16px; font-size: 1.2rem;
|
||||
}
|
||||
.feature-card h3 { font-size: 1.05rem; margin-bottom: 6px; }
|
||||
.feature-card p { color: var(--text-dim); font-size: .9rem; }
|
||||
|
||||
/* ── FAQ ── */
|
||||
.faq-list { max-width: 700px; margin: auto; display: flex; flex-direction: column; gap: 8px; }
|
||||
.faq-item {
|
||||
background: var(--bg-card); border-radius: 16px;
|
||||
}
|
||||
.faq-item summary {
|
||||
cursor: pointer; font-weight: 600; font-size: 1rem;
|
||||
list-style: none; display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.faq-item summary::-webkit-details-marker { display: none; }
|
||||
.faq-item summary::after { content: "+"; font-size: 1.4rem; color: var(--text-dim); transition: transform .2s; }
|
||||
.faq-item[open] summary::after { content: "\2212"; }
|
||||
.faq-item .faq-answer { padding: 0 20px 18px; color: var(--text-dim); font-size: .92rem; line-height: 1.7; }
|
||||
|
||||
/* ── FOOTER ── */
|
||||
footer {
|
||||
background: var(--surface);
|
||||
padding: 40px 24px; text-align: center;
|
||||
}
|
||||
.footer-inner { max-width: var(--max-w); margin: auto; }
|
||||
.footer-links { display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.footer-links a { color: var(--text-dim); font-size: .9rem; }
|
||||
.footer-links a:hover { color: var(--text); }
|
||||
.footer-copy { color: #555; font-size: .8rem; }
|
||||
|
||||
/* ── MOBILE MENU ── */
|
||||
.nav-burger {
|
||||
display: none; width: 40px; height: 40px; border-radius: 12px;
|
||||
background: none; border: none; cursor: pointer;
|
||||
align-items: center; justify-content: center; flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-burger .bar {
|
||||
display: block; width: 20px; height: 2px; background: var(--text);
|
||||
border-radius: 2px; transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .2s;
|
||||
position: absolute; left: 10px;
|
||||
}
|
||||
.nav-burger .bar:nth-child(1) { top: 12px; }
|
||||
.nav-burger .bar:nth-child(2) { top: 19px; }
|
||||
.nav-burger .bar:nth-child(3) { top: 26px; }
|
||||
.nav-burger.active .bar:nth-child(1) { top: 19px; transform: rotate(45deg); }
|
||||
.nav-burger.active .bar:nth-child(2) { opacity: 0; }
|
||||
.nav-burger.active .bar:nth-child(3) { top: 19px; transform: rotate(-45deg); }
|
||||
.mobile-overlay {
|
||||
position: fixed; top: 64px; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,.5); z-index: 98;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-overlay.open { opacity: 1; pointer-events: auto; }
|
||||
.mobile-menu {
|
||||
position: fixed; top: 64px; left: 0; right: 0;
|
||||
background: rgba(18,18,18,.95); padding: 8px 16px 16px; z-index: 99;
|
||||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||||
transform: translateY(-8px); opacity: 0; pointer-events: none;
|
||||
transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-menu.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
|
||||
.mobile-menu a {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 16px; border-radius: 12px;
|
||||
color: var(--text-dim); font-size: .95rem; font-weight: 500;
|
||||
transition: background .2s; opacity: 0; transform: translateY(-6px);
|
||||
}
|
||||
.mobile-menu.open a {
|
||||
opacity: 1; transform: translateY(0);
|
||||
transition: background .2s, opacity .3s cubic-bezier(.4,0,.2,1), transform .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-menu.open a:nth-child(1) { transition-delay: .03s; }
|
||||
.mobile-menu.open a:nth-child(2) { transition-delay: .06s; }
|
||||
.mobile-menu.open a:nth-child(3) { transition-delay: .09s; }
|
||||
.mobile-menu.open a:nth-child(4) { transition-delay: .12s; }
|
||||
.mobile-menu.open a:nth-child(5) { transition-delay: .15s; }
|
||||
.mobile-menu a:hover { background: var(--bg-card); color: var(--text); text-decoration: none; }
|
||||
.mobile-menu a.active { color: var(--text); font-weight: 600; background: var(--bg-card); }
|
||||
.mobile-menu .mobile-divider {
|
||||
height: 1px; background: rgba(255,255,255,.06); margin: 4px 0;
|
||||
opacity: 0; transition: opacity .3s .15s;
|
||||
}
|
||||
.mobile-menu.open .mobile-divider { opacity: 1; }
|
||||
.mobile-menu .mobile-icons {
|
||||
display: flex; gap: 8px; padding: 8px 16px 0;
|
||||
opacity: 0; transform: translateY(-6px);
|
||||
transition: opacity .3s cubic-bezier(.4,0,.2,1) .18s, transform .3s cubic-bezier(.4,0,.2,1) .18s;
|
||||
}
|
||||
.mobile-menu.open .mobile-icons { opacity: 1; transform: translateY(0); }
|
||||
.mobile-menu .mobile-icons a {
|
||||
padding: 10px; border-radius: 12px; background: var(--bg-card);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 1; transform: none;
|
||||
}
|
||||
.mobile-menu .mobile-icons a svg { width: 20px; height: 20px; fill: currentColor; }
|
||||
|
||||
/* ── MOBILE ── */
|
||||
@media (max-width: 640px) {
|
||||
.nav-links { display: none; }
|
||||
.nav-burger { display: flex; }
|
||||
.hero { padding: 80px 16px 40px; }
|
||||
section { padding: 60px 16px; }
|
||||
}
|
||||
|
||||
/* ── HERO MOCKUPS ── */
|
||||
.hero-mockups {
|
||||
display: flex; gap: 20px; justify-content: center; align-items: flex-end;
|
||||
margin-top: 48px; perspective: 800px;
|
||||
}
|
||||
.phone-frame {
|
||||
width: 180px; border-radius: 28px; overflow: hidden;
|
||||
border: 3px solid #333; background: #000;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.5);
|
||||
transition: transform .3s;
|
||||
}
|
||||
.phone-frame:hover { transform: translateY(-4px); }
|
||||
.phone-frame img { width: 100%; display: block; }
|
||||
.phone-frame.phone-center {
|
||||
width: 210px;
|
||||
border-color: var(--green);
|
||||
box-shadow: 0 24px 70px rgba(0,0,0,.6), 0 0 40px rgba(29,185,84,.1);
|
||||
}
|
||||
.phone-frame.phone-side { opacity: .7; }
|
||||
@media (max-width: 640px) {
|
||||
.hero-mockups { gap: 10px; margin-top: 32px; }
|
||||
.phone-frame { width: 120px; border-radius: 20px; border-width: 2px; }
|
||||
.phone-frame.phone-center { width: 150px; }
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
.phone-frame.phone-side { display: none; }
|
||||
.phone-frame.phone-center { width: 200px; }
|
||||
}
|
||||
|
||||
/* ── SVG ICONS ── */
|
||||
.icon-svg { width: 20px; height: 20px; fill: currentColor; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-brand" href="#">
|
||||
<img src="icon.png" alt="SpotiFLAC">
|
||||
SpotiFLAC
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="downloads.html">Downloads</a></li>
|
||||
<li><a href="#faq">FAQ</a></li>
|
||||
<li><a href="partners.html">Partners</a></li>
|
||||
<li><a href="https://zarz.moe/docs" target="_blank">Docs</a></li>
|
||||
<li class="nav-divider"></li>
|
||||
<li><a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" class="nav-icon" aria-label="GitHub"><svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.82-.26.82-.57L9 20.86c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22l-.01 3.29c0 .31.21.69.82.57A12 12 0 0 0 12 .3"/></svg></a></li>
|
||||
<li><a href="https://t.me/spotiflac" target="_blank" class="nav-icon" aria-label="Telegram"><svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg></a></li>
|
||||
</ul>
|
||||
<button class="nav-burger" onclick="toggleMenu()" aria-label="Menu">
|
||||
<span class="bar"></span><span class="bar"></span><span class="bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- MOBILE MENU -->
|
||||
<div class="mobile-overlay" id="mobileOverlay" onclick="toggleMenu()"></div>
|
||||
<div class="mobile-menu" id="mobileMenu">
|
||||
<a href="#features">Features</a>
|
||||
<a href="downloads.html">Downloads</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
<a href="partners.html">Partners</a>
|
||||
<a href="https://zarz.moe/docs" target="_blank">Docs</a>
|
||||
<div class="mobile-divider"></div>
|
||||
<div class="mobile-icons">
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" aria-label="GitHub">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</a>
|
||||
<a href="https://t.me/spotiflac" target="_blank" aria-label="Telegram">
|
||||
<svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
<h1>Spoti<span>FLAC</span> Mobile</h1>
|
||||
<p>Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music — no account required.</p>
|
||||
<div class="hero-badges">
|
||||
<span class="badge">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M6 18c0 .55.45 1 1 1h1v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h2v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h1c.55 0 1-.45 1-1V7H6v11zM3.5 7C2.67 7 2 7.67 2 8.5v7c0 .83.67 1.5 1.5 1.5S5 16.33 5 15.5v-7C5 7.67 4.33 7 3.5 7zm17 0c-.83 0-1.5.67-1.5 1.5v7c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5v-7c0-.83-.67-1.5-1.5-1.5zm-4.97-5.84l1.3-1.3c.2-.2.2-.51 0-.71-.2-.2-.51-.2-.71 0l-1.48 1.48A5.84 5.84 0 0012 0c-.96 0-1.86.23-2.66.63L7.85.15c-.2-.2-.51-.2-.71 0-.2.2-.2.51 0 .71l1.31 1.31A5.983 5.983 0 006 6h12c0-2.21-1.2-4.15-2.97-5.18-.25-.14-.4-.24-.5-.36v-.3zM10 4H9V3h1v1zm5 0h-1V3h1v1z"/></svg>
|
||||
Android 7.0+
|
||||
</span>
|
||||
<span class="badge">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
|
||||
iOS 14.0+
|
||||
</span>
|
||||
<span class="badge">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
Open Source
|
||||
</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-primary" href="downloads.html">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="#000" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
||||
Download
|
||||
</a>
|
||||
<a class="btn btn-secondary" href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div class="hero-mockups">
|
||||
<div class="phone-frame phone-side"><img src="images/2.jpg" alt="Search" loading="lazy"></div>
|
||||
<div class="phone-frame phone-center"><img src="images/1.jpg" alt="Home screen" loading="lazy"></div>
|
||||
<div class="phone-frame phone-side"><img src="images/3.jpg" alt="History" loading="lazy"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section id="features">
|
||||
<div class="section-inner">
|
||||
<h2 class="section-title">Features</h2>
|
||||
<p class="section-sub">Everything you need to build a high-quality music library on your phone.</p>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3v10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</div>
|
||||
<h3>True Lossless FLAC</h3>
|
||||
<p>Download in up to 24-bit/192kHz quality. No transcoding, no quality loss. Pure studio-grade audio files.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<h3>Multiple Providers</h3>
|
||||
<p>Download from Tidal, Qobuz, Amazon Music, and more. Automatic fallback if a source is unavailable.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5a2.5 2.5 0 00-5 0V5H4c-1.1 0-2 .9-2 2v3.8h1.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7s2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5a2.5 2.5 0 000-5z"/></svg>
|
||||
</div>
|
||||
<h3>Extensions</h3>
|
||||
<p>Community-built extensions add new music sources and features. Install from the built-in Store with one tap.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
</div>
|
||||
<h3>Search by Link or Name</h3>
|
||||
<p>Paste a Spotify, Tidal, Qobuz, or Deezer link. Or just search by song name — it handles the rest.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zm-6-1l-4-4.8h3V5h2v4.2h3L14 14z"/></svg>
|
||||
</div>
|
||||
<h3>Batch & Playlist Download</h3>
|
||||
<p>Download entire albums and playlists at once. Smart queue management with concurrent downloads.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg class="icon-svg" viewBox="0 0 24 24"><path fill="currentColor" d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM5 15h14v3H5z"/></svg>
|
||||
</div>
|
||||
<h3>Rich Metadata</h3>
|
||||
<p>Full metadata embedding — album art, lyrics, genre, label, copyright, and more. All embedded in the FLAC file.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- FAQ -->
|
||||
<section id="faq">
|
||||
<div class="section-inner">
|
||||
<h2 class="section-title">FAQ</h2>
|
||||
<p class="section-sub">Common questions about SpotiFLAC Mobile.</p>
|
||||
<div class="faq-list">
|
||||
<details class="faq-item">
|
||||
<summary>Why is my download failing with "Song not found"?</summary>
|
||||
<div class="faq-answer">The track may not be available on Tidal, Qobuz, or Amazon Music. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions from the Store.</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary>Why are some tracks downloading in lower quality?</summary>
|
||||
<div class="faq-answer">Quality depends on what's available from the streaming service. Tidal offers up to 24-bit/192kHz, Qobuz up to 24-bit/192kHz, and Amazon up to 24-bit/48kHz. The app automatically selects the best available quality.</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary>Can I download entire playlists?</summary>
|
||||
<div class="faq-answer">Yes! Just paste the playlist URL in the search bar. The app will fetch all tracks and queue them for download.</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary>Why do I need to grant storage permission?</summary>
|
||||
<div class="faq-answer">The app needs permission to save downloaded files to your device. On Android 13+, you may need to grant "All files access" in Settings > Apps > SpotiFLAC > Permissions.</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary>Is this app safe?</summary>
|
||||
<div class="faq-answer">Yes, the app is fully open source. You can verify the code yourself on GitHub. Each release is scanned with VirusTotal.</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary>Download not working in my country?</summary>
|
||||
<div class="faq-answer">Some countries have restricted access to certain streaming service APIs. If downloads are failing, try using a VPN to connect through a different region.</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary>How do I create my own extension?</summary>
|
||||
<div class="faq-answer">Check out the <a href="https://zarz.moe/docs" target="_blank">Extension Development Guide</a> for complete documentation on building custom extensions.</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="footer-inner">
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile/releases" target="_blank">Download</a>
|
||||
<a href="https://zarz.moe/docs" target="_blank">Documentation</a>
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank">GitHub</a>
|
||||
<a href="https://github.com/afkarxyz/SpotiFLAC" target="_blank">Desktop Version</a>
|
||||
<a href="https://t.me/spotiflac" target="_blank">Telegram Channel</a>
|
||||
<a href="https://t.me/spotiflac_chat" target="_blank">Community</a>
|
||||
<a href="https://ko-fi.com/zarzet" target="_blank">Support / Ko-fi</a>
|
||||
<a href="https://crowdin.com/project/spotiflac-mobile" target="_blank">Help Translate</a>
|
||||
</div>
|
||||
<p class="footer-copy">SpotiFLAC is for educational and private use only. Not affiliated with any streaming service.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script>
|
||||
function toggleMenu() {
|
||||
document.getElementById('mobileMenu').classList.toggle('open');
|
||||
document.getElementById('mobileOverlay').classList.toggle('open');
|
||||
document.querySelector('.nav-burger').classList.toggle('active');
|
||||
}
|
||||
document.getElementById('mobileMenu').addEventListener('click', function(e) {
|
||||
if (e.target.closest('a')) toggleMenu();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
516
site/partners.html
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Partners & Services - SpotiFLAC Mobile</title>
|
||||
<meta name="description" content="The APIs and services that power SpotiFLAC Mobile. Giving credit to the platforms that make lossless music downloads possible.">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<link rel="icon" href="icon.png" type="image/png">
|
||||
|
||||
<!-- Google Sans Flex -->
|
||||
<style>
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 400; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-400-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 500; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-500-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 600; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-600-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 700; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-700-normal.woff2) format('woff2'); }
|
||||
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 800; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-800-normal.woff2) format('woff2'); }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* ── M3 AMOLED surface ramp ── */
|
||||
:root {
|
||||
--green: #1DB954;
|
||||
--green-dim: #1aa34a;
|
||||
--bg: #0a0a0a;
|
||||
--bg-card: #1a1a1a;
|
||||
--bg-card-hover: #222222;
|
||||
--surface: #121212;
|
||||
--text: #e8e8e8;
|
||||
--text-dim: #999;
|
||||
--max-w: 1100px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: 'Google Sans Flex', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: var(--bg); color: var(--text); line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--green); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── NAV ── */
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(18,18,18,.78);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: var(--max-w); margin: auto;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 24px; height: 64px;
|
||||
}
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1.1rem; color: var(--text); }
|
||||
.nav-brand img { width: 32px; height: 32px; border-radius: 50%; }
|
||||
.nav-links { display: flex; gap: 24px; list-style: none; }
|
||||
.nav-links a { color: var(--text-dim); font-size: .9rem; transition: color .2s; }
|
||||
.nav-links a:hover { color: var(--text); text-decoration: none; }
|
||||
.nav-links a.active { color: var(--text); font-weight: 600; }
|
||||
.nav-links .nav-icon { display: flex; align-items: center; opacity: .6; transition: opacity .2s; margin-left: -12px; }
|
||||
.nav-links .nav-icon:hover { opacity: 1; }
|
||||
.nav-links .nav-icon svg { width: 24px; height: 24px; fill: currentColor; }
|
||||
.nav-links .nav-divider { width: 1px; height: 20px; background: rgba(255,255,255,.15); margin-left: -4px; }
|
||||
|
||||
/* ── PAGE HEADER ── */
|
||||
.page-header {
|
||||
padding: 100px 24px 40px; text-align: center;
|
||||
}
|
||||
.page-header h1 { font-size: 2rem; font-weight: 800; margin-bottom: 8px; }
|
||||
.page-header p { color: var(--text-dim); font-size: 1rem; max-width: 560px; margin: 0 auto; }
|
||||
|
||||
/* ── SECTIONS ── */
|
||||
section { padding: 40px 24px 60px; }
|
||||
.section-inner { max-width: var(--max-w); margin: auto; }
|
||||
.section-label {
|
||||
font-size: .85rem; font-weight: 600;
|
||||
color: var(--green); margin-bottom: 8px;
|
||||
}
|
||||
.section-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; }
|
||||
.section-sub { color: var(--text-dim); font-size: .95rem; margin-bottom: 32px; max-width: 600px; }
|
||||
|
||||
/* ── INFRA CARDS ── */
|
||||
.infra-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.infra-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
display: flex; align-items: flex-start; gap: 16px;
|
||||
transition: background .2s;
|
||||
}
|
||||
.infra-card:hover { background: var(--bg-card-hover); }
|
||||
.infra-icon {
|
||||
width: 48px; height: 48px; border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.infra-icon svg { width: 24px; height: 24px; fill: currentColor; }
|
||||
.infra-info { flex: 1; min-width: 0; }
|
||||
.infra-name { font-size: 1.05rem; font-weight: 700; margin-bottom: 4px; }
|
||||
.infra-desc { color: var(--text-dim); font-size: .88rem; line-height: 1.6; margin-bottom: 10px; }
|
||||
.infra-link {
|
||||
font-size: .82rem; font-weight: 600; color: var(--text-dim);
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
transition: color .2s;
|
||||
}
|
||||
.infra-link:hover { color: var(--text); text-decoration: none; }
|
||||
.infra-link svg { width: 13px; height: 13px; fill: currentColor; }
|
||||
|
||||
/* ── FOOTER ── */
|
||||
footer {
|
||||
background: var(--surface);
|
||||
padding: 40px 24px; text-align: center;
|
||||
}
|
||||
.footer-inner { max-width: var(--max-w); margin: auto; }
|
||||
.footer-links { display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.footer-links a { color: var(--text-dim); font-size: .9rem; }
|
||||
.footer-links a:hover { color: var(--text); }
|
||||
.footer-copy { color: #555; font-size: .8rem; }
|
||||
|
||||
/* ── DISCLAIMER ── */
|
||||
.disclaimer {
|
||||
max-width: var(--max-w); margin: 0 auto; padding: 0 24px 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.disclaimer p {
|
||||
color: #555; font-size: .8rem; line-height: 1.6;
|
||||
max-width: 600px; margin: 0 auto;
|
||||
padding: 20px; border-radius: 16px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* ── MOBILE MENU ── */
|
||||
.nav-burger {
|
||||
display: none; width: 40px; height: 40px; border-radius: 12px;
|
||||
background: none; border: none; cursor: pointer;
|
||||
align-items: center; justify-content: center; flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-burger .bar {
|
||||
display: block; width: 20px; height: 2px; background: var(--text);
|
||||
border-radius: 2px; transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .2s;
|
||||
position: absolute; left: 10px;
|
||||
}
|
||||
.nav-burger .bar:nth-child(1) { top: 12px; }
|
||||
.nav-burger .bar:nth-child(2) { top: 19px; }
|
||||
.nav-burger .bar:nth-child(3) { top: 26px; }
|
||||
.nav-burger.active .bar:nth-child(1) { top: 19px; transform: rotate(45deg); }
|
||||
.nav-burger.active .bar:nth-child(2) { opacity: 0; }
|
||||
.nav-burger.active .bar:nth-child(3) { top: 19px; transform: rotate(-45deg); }
|
||||
.mobile-overlay {
|
||||
position: fixed; top: 64px; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,.5); z-index: 98;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-overlay.open { opacity: 1; pointer-events: auto; }
|
||||
.mobile-menu {
|
||||
position: fixed; top: 64px; left: 0; right: 0;
|
||||
background: rgba(18,18,18,.95); padding: 8px 16px 16px; z-index: 99;
|
||||
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||||
transform: translateY(-8px); opacity: 0; pointer-events: none;
|
||||
transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-menu.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
|
||||
.mobile-menu a {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 16px; border-radius: 12px;
|
||||
color: var(--text-dim); font-size: .95rem; font-weight: 500;
|
||||
transition: background .2s; opacity: 0; transform: translateY(-6px);
|
||||
}
|
||||
.mobile-menu.open a {
|
||||
opacity: 1; transform: translateY(0);
|
||||
transition: background .2s, opacity .3s cubic-bezier(.4,0,.2,1), transform .3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.mobile-menu.open a:nth-child(1) { transition-delay: .03s; }
|
||||
.mobile-menu.open a:nth-child(2) { transition-delay: .06s; }
|
||||
.mobile-menu.open a:nth-child(3) { transition-delay: .09s; }
|
||||
.mobile-menu.open a:nth-child(4) { transition-delay: .12s; }
|
||||
.mobile-menu.open a:nth-child(5) { transition-delay: .15s; }
|
||||
.mobile-menu a:hover { background: var(--bg-card); color: var(--text); text-decoration: none; }
|
||||
.mobile-menu a.active { color: var(--text); font-weight: 600; background: var(--bg-card); }
|
||||
.mobile-menu .mobile-divider {
|
||||
height: 1px; background: rgba(255,255,255,.06); margin: 4px 0;
|
||||
opacity: 0; transition: opacity .3s .15s;
|
||||
}
|
||||
.mobile-menu.open .mobile-divider { opacity: 1; }
|
||||
.mobile-menu .mobile-icons {
|
||||
display: flex; gap: 8px; padding: 8px 16px 0;
|
||||
opacity: 0; transform: translateY(-6px);
|
||||
transition: opacity .3s cubic-bezier(.4,0,.2,1) .18s, transform .3s cubic-bezier(.4,0,.2,1) .18s;
|
||||
}
|
||||
.mobile-menu.open .mobile-icons { opacity: 1; transform: translateY(0); }
|
||||
.mobile-menu .mobile-icons a {
|
||||
padding: 10px; border-radius: 12px; background: var(--bg-card);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 1; transform: none;
|
||||
}
|
||||
.mobile-menu .mobile-icons a svg { width: 20px; height: 20px; fill: currentColor; }
|
||||
|
||||
/* ── MOBILE ── */
|
||||
@media (max-width: 640px) {
|
||||
.nav-links { display: none; }
|
||||
.nav-burger { display: flex; }
|
||||
.page-header { padding: 80px 16px 32px; }
|
||||
section { padding: 32px 16px 48px; }
|
||||
.infra-grid { grid-template-columns: 1fr; }
|
||||
.disclaimer { padding: 0 16px 48px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-brand" href="index.html">
|
||||
<img src="icon.png" alt="SpotiFLAC">
|
||||
SpotiFLAC
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html#features">Features</a></li>
|
||||
<li><a href="downloads.html">Downloads</a></li>
|
||||
<li><a href="index.html#faq">FAQ</a></li>
|
||||
<li><a href="partners.html" class="active">Partners</a></li>
|
||||
<li><a href="https://zarz.moe/docs" target="_blank">Docs</a></li>
|
||||
<li class="nav-divider"></li>
|
||||
<li><a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" class="nav-icon" aria-label="GitHub"><svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.82-.26.82-.57L9 20.86c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22l-.01 3.29c0 .31.21.69.82.57A12 12 0 0 0 12 .3"/></svg></a></li>
|
||||
<li><a href="https://t.me/spotiflac" target="_blank" class="nav-icon" aria-label="Telegram"><svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg></a></li>
|
||||
</ul>
|
||||
<button class="nav-burger" onclick="toggleMenu()" aria-label="Menu">
|
||||
<span class="bar"></span><span class="bar"></span><span class="bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- MOBILE MENU -->
|
||||
<div class="mobile-overlay" id="mobileOverlay" onclick="toggleMenu()"></div>
|
||||
<div class="mobile-menu" id="mobileMenu">
|
||||
<a href="index.html#features">Features</a>
|
||||
<a href="downloads.html">Downloads</a>
|
||||
<a href="index.html#faq">FAQ</a>
|
||||
<a href="partners.html" class="active">Partners</a>
|
||||
<a href="https://zarz.moe/docs" target="_blank">Docs</a>
|
||||
<div class="mobile-divider"></div>
|
||||
<div class="mobile-icons">
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" aria-label="GitHub">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</a>
|
||||
<a href="https://t.me/spotiflac" target="_blank" aria-label="Telegram">
|
||||
<svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Partners & Services</h1>
|
||||
<p>The behind-the-scenes APIs and tools that power SpotiFLAC Mobile. We appreciate every one of them.</p>
|
||||
</div>
|
||||
|
||||
<!-- INFRASTRUCTURE -->
|
||||
<section>
|
||||
<div class="section-inner">
|
||||
<div class="section-label">Infrastructure</div>
|
||||
<h2 class="section-title">APIs & Tools</h2>
|
||||
<p class="section-sub">The services that handle link resolution, lyrics, audio extraction, and more.</p>
|
||||
|
||||
<div class="infra-grid">
|
||||
|
||||
<!-- === TRACK LINKING === -->
|
||||
|
||||
<!-- Odesli / song.link (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(99,102,241,.1); color: #6366f1;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">Odesli / song.link</div>
|
||||
<div class="infra-desc">Cross-platform link resolution. Translates any Spotify, Deezer, or streaming URL into matching Tidal, Qobuz, Amazon, and YouTube IDs — enabling SpotiFLAC to find the best lossless source for every track.</div>
|
||||
<a class="infra-link" href="https://odesli.co" target="_blank">
|
||||
odesli.co
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- I Don't Have Spotify (GitHub) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(255,255,255,.08); color: #e8e8e8;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">I Don't Have Spotify</div>
|
||||
<div class="infra-desc">Fallback link resolution service. When Odesli is rate-limited or unavailable, IDHS provides an alternative way to match Spotify links to Tidal, Qobuz, and other streaming platforms.</div>
|
||||
<a class="infra-link" href="https://github.com/sjdonado/idonthavespotify" target="_blank">
|
||||
sjdonado/idonthavespotify
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LRCLIB (GitHub) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(255,255,255,.08); color: #e8e8e8;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">LRCLIB</div>
|
||||
<div class="infra-desc">Open synced lyrics database. Provides time-stamped lyrics that get embedded directly into downloaded FLAC files, so your music player can display lyrics in sync with the music.</div>
|
||||
<a class="infra-link" href="https://github.com/tranxuanthang/lrclib" target="_blank">
|
||||
tranxuanthang/lrclib
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === TIDAL STREAM APIs === -->
|
||||
|
||||
<!-- hifi-api / Binimum (GitHub) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(255,255,255,.08); color: #e8e8e8;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">hifi-api / Binimum</div>
|
||||
<div class="infra-desc">Primary Tidal lossless stream API. Accepts a track ID and quality parameter, returns hi-res download URLs and DASH manifests. Also deployed at music.binimum.org.</div>
|
||||
<a class="infra-link" href="https://github.com/binimum/hifi-api" target="_blank">
|
||||
binimum/hifi-api
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QQDL (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(244,63,94,.1); color: #f43f5e;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">QQDL</div>
|
||||
<div class="infra-desc">Redundant Tidal API mirror cluster. Operates five parallel endpoints (vogel, maus, hund, katze, wolf) for high-availability lossless track downloads across the API pool.</div>
|
||||
<a class="infra-link" href="https://qqdl.site" target="_blank">
|
||||
qqdl.site
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Squid (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(6,182,212,.1); color: #06b6d4;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">Squid</div>
|
||||
<div class="infra-desc">Dual-purpose download API serving both Tidal and Qobuz streams. Supports multi-region retrieval (US/FR fallback for Qobuz) to maximize track availability across catalogs.</div>
|
||||
<a class="infra-link" href="https://squid.wtf" target="_blank">
|
||||
squid.wtf
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SpotiSaver (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(245,158,11,.1); color: #f59e0b;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">SpotiSaver</div>
|
||||
<div class="infra-desc">Tidal hi-fi download endpoints. Hosts two parallel instances (hifi-one, hifi-two) that provide additional redundancy in the 10-API parallel race pool.</div>
|
||||
<a class="infra-link" href="https://spotisaver.net" target="_blank">
|
||||
spotisaver.net
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === QOBUZ STREAM APIs === -->
|
||||
|
||||
<!-- DabMusic (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(139,92,246,.1); color: #8b5cf6;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">DabMusic</div>
|
||||
<div class="infra-desc">Primary Qobuz lossless stream API. Provides direct download URLs for FLAC audio at up to 24-bit/192kHz quality. Queried in parallel alongside squid.wtf for fastest response.</div>
|
||||
<a class="infra-link" href="https://dabmusic.xyz" target="_blank">
|
||||
dabmusic.xyz
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jumo DL (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(56,189,248,.1); color: #38bdf8;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">Jumo DL</div>
|
||||
<div class="infra-desc">Qobuz final fallback. A Cloudflare Pages worker tried after all standard Qobuz APIs fail, with automatic quality downgrade cascade (hi-res → CD → MP3) to maximize success rate.</div>
|
||||
<a class="infra-link" href="https://jumo-dl.pages.dev" target="_blank">
|
||||
jumo-dl.pages.dev
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === AMAZON === -->
|
||||
|
||||
<!-- AfkarXYZ (GitHub) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(255,255,255,.08); color: #e8e8e8;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">AfkarXYZ</div>
|
||||
<div class="infra-desc">Sole Amazon Music download API with stream decryption support. Also provides a SpotFetch-compatible Spotify metadata proxy used when direct API access is blocked.</div>
|
||||
<a class="infra-link" href="https://github.com/afkarxyz" target="_blank">
|
||||
afkarxyz
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- === YOUTUBE AUDIO === -->
|
||||
|
||||
<!-- Cobalt (GitHub) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(255,255,255,.08); color: #e8e8e8;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">Cobalt</div>
|
||||
<div class="infra-desc">Privacy-focused media extraction tool. The core engine behind YouTube Music downloads — accepts a video URL and returns a tunnel URL to the audio stream in opus or mp3 format.</div>
|
||||
<a class="infra-link" href="https://github.com/imputnet/cobalt" target="_blank">
|
||||
imputnet/cobalt
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qwkuns (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(16,185,129,.1); color: #10b981;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">Qwkuns</div>
|
||||
<div class="infra-desc">Cobalt-compatible API for YouTube audio extraction. Serves as the fallback download engine when the primary SpotubeDL proxy is unavailable, using the standard Cobalt protocol.</div>
|
||||
<a class="infra-link" href="https://qwkuns.me" target="_blank">
|
||||
qwkuns.me
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SpotubeDL (no GitHub — globe) -->
|
||||
<div class="infra-card">
|
||||
<div class="infra-icon" style="background: rgba(244,63,94,.1); color: #f43f5e;">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
||||
</div>
|
||||
<div class="infra-info">
|
||||
<div class="infra-name">SpotubeDL</div>
|
||||
<div class="infra-desc">Primary YouTube download proxy. Handles authentication to Cobalt download instances and serves as the first-choice engine for YouTube Music audio extraction.</div>
|
||||
<a class="infra-link" href="https://spotubedl.com" target="_blank">
|
||||
spotubedl.com
|
||||
<svg viewBox="0 0 24 24"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DISCLAIMER -->
|
||||
<div class="disclaimer">
|
||||
<p>SpotiFLAC Mobile is not affiliated with, endorsed by, or connected to any of the services listed above. All trademarks and logos belong to their respective owners. This page is meant to acknowledge and appreciate the platforms that make this project possible.</p>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="footer-inner">
|
||||
<div class="footer-links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="downloads.html">Downloads</a>
|
||||
<a href="https://zarz.moe/docs" target="_blank">Documentation</a>
|
||||
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank">GitHub</a>
|
||||
<a href="https://t.me/spotiflac" target="_blank">Telegram</a>
|
||||
<a href="https://ko-fi.com/zarzet" target="_blank">Support / Ko-fi</a>
|
||||
</div>
|
||||
<p class="footer-copy">SpotiFLAC is for educational and private use only. Not affiliated with any streaming service.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function toggleMenu() {
|
||||
document.getElementById('mobileMenu').classList.toggle('open');
|
||||
document.getElementById('mobileOverlay').classList.toggle('open');
|
||||
document.querySelector('.nav-burger').classList.toggle('active');
|
||||
}
|
||||
document.getElementById('mobileMenu').addEventListener('click', function(e) {
|
||||
if (e.target.closest('a')) toggleMenu();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||