Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaac728a26 | ||
|
|
65d2f8e33f | ||
|
|
baf4e0f0fc | ||
|
|
26c7bee106 | ||
|
|
6e51611867 | ||
|
|
d67e432731 | ||
|
|
8b61396b0f | ||
|
|
0fb6e55b12 | ||
|
|
dd7aa2291b | ||
|
|
ec33c32c89 | ||
|
|
e0ad4e3701 | ||
|
|
253f8033c5 | ||
|
|
c1aed1a4c1 | ||
|
|
23f58439ba | ||
|
|
4c99ced597 | ||
|
|
8d215a7f1c | ||
|
|
38fc4a0936 | ||
|
|
d9949349da | ||
|
|
877d29d285 | ||
|
|
9a17aa8b98 | ||
|
|
fd41395ab8 | ||
|
|
269066e036 | ||
|
|
04e692e5e9 |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -3,7 +3,34 @@
|
|||||||
## Pending release
|
## Pending release
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
## [4.9.0](https://github.com/eddyizm/tempo/releases/tag/v4.9.0) (2026-01-24)
|
## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02)
|
||||||
|
* fix: missing Replay Gain metadata from .m4a files by @pgrit in https://github.com/eddyizm/tempus/pull/396
|
||||||
|
* fix: Improve Synced Lyrics by @pgrit in https://github.com/eddyizm/tempus/pull/384
|
||||||
|
* fix: Add selector for playlist visibility by @tvillega in https://github.com/eddyizm/tempus/pull/394
|
||||||
|
* chore(i18n): set links as untranslatable by @tvillega in https://github.com/eddyizm/tempus/pull/400
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @tvillega made their first contribution in https://github.com/eddyizm/tempus/pull/394
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.5...v4.5.8
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.9.5](https://github.com/eddyizm/tempo/releases/tag/v4.9.5) (2026-01-26)
|
||||||
|
* fix: Avoid crash when server has no songs by @jaime-grj in https://github.com/eddyizm/tempus/pull/389
|
||||||
|
* fix: updated dialog import to address crashing on android 15 by @eddyizm in https://github.com/eddyizm/tempus/pull/392
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.3...v4.9.5
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.9.3](https://github.com/eddyizm/tempo/releases/tag/v4.9.3) (2026-01-25)
|
||||||
|
* fix: Proper raw stream detection by @jaime-grj in https://github.com/eddyizm/tempus/pull/382
|
||||||
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/381
|
||||||
|
* feat: add configurable timeout by @eddyizm in https://github.com/eddyizm/tempus/pull/386
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.1...v4.9.3
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.9.1](https://github.com/eddyizm/tempo/releases/tag/v4.9.1) (2026-01-24)
|
||||||
* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357
|
* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357
|
||||||
* French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356
|
* French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356
|
||||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364
|
||||||
@@ -20,7 +47,7 @@
|
|||||||
## New Contributors
|
## New Contributors
|
||||||
* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375
|
* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.0
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.1
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13)
|
## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 14
|
versionCode 17
|
||||||
versionName '4.9.0'
|
versionName '4.9.8'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
import static android.provider.Settings.System.getString;
|
|
||||||
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -107,13 +104,13 @@ public class PlaylistRepository {
|
|||||||
return playlistLiveData;
|
return playlistLiveData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||||
if (songsId.isEmpty()) {
|
if (songsId.isEmpty()) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||||
} else{
|
} else{
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
.updatePlaylist(playlistId, null, playlistVisibilityIsPublic, songsId, null)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
|||||||
@@ -283,7 +283,10 @@ public class SongRepository {
|
|||||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> songs = new ArrayList<>();
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
|
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||||
|
if (returned != null) {
|
||||||
|
songs.addAll(returned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
randomSongsSample.setValue(songs);
|
randomSongsSample.setValue(songs);
|
||||||
}
|
}
|
||||||
@@ -299,7 +302,10 @@ public class SongRepository {
|
|||||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> songs = new ArrayList<>();
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
|
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||||
|
if (returned != null) {
|
||||||
|
songs.addAll(returned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
randomSongsSample.setValue(songs);
|
randomSongsSample.setValue(songs);
|
||||||
}
|
}
|
||||||
@@ -342,7 +348,10 @@ public class SongRepository {
|
|||||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> songs = new ArrayList<>();
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||||
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs()));
|
List<Child> returned = response.body().getSubsonicResponse().getSongsByGenre().getSongs();
|
||||||
|
if (returned != null) {
|
||||||
|
songs.addAll(returned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
songsByGenre.setValue(songs);
|
songsByGenre.setValue(songs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ public class SystemClient {
|
|||||||
|
|
||||||
public Call<ApiResponse> ping() {
|
public Call<ApiResponse> ping() {
|
||||||
Log.d(TAG, "ping()");
|
Log.d(TAG, "ping()");
|
||||||
|
int timeoutSeconds = Preferences.getNetworkPingTimeout();
|
||||||
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
||||||
if (Preferences.isInUseServerAddressLocal()) {
|
if (Preferences.isInUseServerAddressLocal()) {
|
||||||
pingCall.timeout()
|
pingCall.timeout()
|
||||||
.timeout(1, TimeUnit.SECONDS);
|
.timeout(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
} else {
|
} else {
|
||||||
|
int finalTimeout = Math.min(timeoutSeconds * 2, 10);
|
||||||
pingCall.timeout()
|
pingCall.timeout()
|
||||||
.timeout(3, TimeUnit.SECONDS);
|
.timeout(finalTimeout, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
return pingCall;
|
return pingCall;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.View;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
||||||
private DialogPlaylistChooserBinding bind;
|
private DialogPlaylistChooserBinding bind;
|
||||||
@@ -35,9 +37,21 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
|||||||
|
|
||||||
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
||||||
|
|
||||||
|
String[] playlistVisibilityChoice = {
|
||||||
|
getString(R.string.playlist_chooser_dialog_visibility_public),
|
||||||
|
getString(R.string.playlist_chooser_dialog_visibility_private)
|
||||||
|
};
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(getActivity())
|
return new MaterialAlertDialogBuilder(getActivity())
|
||||||
.setView(bind.getRoot())
|
.setView(bind.getRoot())
|
||||||
.setTitle(R.string.playlist_chooser_dialog_title)
|
.setTitle(R.string.playlist_chooser_dialog_title)
|
||||||
|
.setSingleChoiceItems(
|
||||||
|
playlistVisibilityChoice,
|
||||||
|
0,
|
||||||
|
(dialog, which) -> {
|
||||||
|
boolean isPublic = (which == 0);
|
||||||
|
playlistChooserViewModel.setIsPlaylistPublic(isPublic);
|
||||||
|
})
|
||||||
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
||||||
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
||||||
.create();
|
.create();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.cappielloantonio.tempo.ui.dialog;
|
package com.cappielloantonio.tempo.ui.dialog;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import android.os.Handler;
|
|||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextPaint;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -51,6 +53,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
private Runnable syncLyricsRunnable;
|
private Runnable syncLyricsRunnable;
|
||||||
private String currentLyrics;
|
private String currentLyrics;
|
||||||
private LyricsList currentLyricsList;
|
private LyricsList currentLyricsList;
|
||||||
|
private Integer lastLineIdx;
|
||||||
private String currentDescription;
|
private String currentDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -109,6 +112,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
currentLyrics = null;
|
currentLyrics = null;
|
||||||
currentLyricsList = null;
|
currentLyricsList = null;
|
||||||
currentDescription = null;
|
currentDescription = null;
|
||||||
|
lastLineIdx = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initOverlay() {
|
private void initOverlay() {
|
||||||
@@ -162,6 +166,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
|
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
currentLyricsList = lyricsList;
|
currentLyricsList = lyricsList;
|
||||||
|
lastLineIdx = null;
|
||||||
updatePanelContent();
|
updatePanelContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,7 +199,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||||
|
|
||||||
if (hasStructuredLyrics(currentLyricsList)) {
|
if (hasStructuredLyrics(currentLyricsList)) {
|
||||||
setSyncLirics(currentLyricsList);
|
setSyncLyrics(currentLyricsList);
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
@@ -241,7 +246,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
private void setSyncLirics(LyricsList lyricsList) {
|
private void setSyncLyrics(LyricsList lyricsList) {
|
||||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
@@ -288,67 +293,75 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||||
|
|
||||||
if (hasStructuredLyrics(lyricsList)) {
|
if (hasStructuredLyrics(lyricsList)) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
if (lines == null || lines.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (lines == null || lines.isEmpty()) return;
|
// Find the index of the currently playing line
|
||||||
|
int curIdx = 0;
|
||||||
|
for (; curIdx < lines.size(); ++curIdx) {
|
||||||
|
Integer start = lines.get(curIdx).getStart();
|
||||||
|
if (start != null && start > timestamp) {
|
||||||
|
curIdx--; // Found the first line that starts after the current timestamp
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if the highlighted line has changed
|
||||||
|
if (lastLineIdx != null && curIdx == lastLineIdx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastLineIdx = curIdx;
|
||||||
|
|
||||||
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
for (Line line : lines) {
|
for (Line line : lines) {
|
||||||
lyricsBuilder.append(line.getValue().trim()).append("\n");
|
lyricsBuilder.append(line.getValue().trim()).append("\n");
|
||||||
}
|
}
|
||||||
|
String lyrics = lyricsBuilder.toString();
|
||||||
|
Spannable spannableString = new SpannableString(lyrics);
|
||||||
|
|
||||||
Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null);
|
// Make each line clickable for navigation and highlight the current one
|
||||||
|
int offset = 0;
|
||||||
|
int highlightStart = -1;
|
||||||
|
for (int i = 0; i < lines.size(); ++i) {
|
||||||
|
boolean highlight = i == curIdx;
|
||||||
|
if (highlight) highlightStart = offset;
|
||||||
|
|
||||||
if (toHighlight != null) {
|
int len = lines.get(i).getValue().length() + 1;
|
||||||
String lyrics = lyricsBuilder.toString();
|
final int lineStart = lines.get(i).getStart();
|
||||||
Spannable spannableString = new SpannableString(lyrics);
|
spannableString.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View view) {
|
||||||
|
// Seeking to 1ms after the actual start prevents scrolling / highlighting artifacts
|
||||||
|
mediaBrowser.seekTo(lineStart + 1);
|
||||||
|
}
|
||||||
|
|
||||||
int startingPosition = getStartPosition(lines, toHighlight);
|
@Override
|
||||||
int endingPosition = startingPosition + toHighlight.getValue().length();
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
|
super.updateDrawState(ds);
|
||||||
|
ds.setUnderlineText(false);
|
||||||
|
if (highlight) {
|
||||||
|
ds.setColor(requireContext().getResources().getColor(R.color.lyricsTextColor, null));
|
||||||
|
} else {
|
||||||
|
ds.setColor(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, offset, offset + len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
offset += len;
|
||||||
|
}
|
||||||
|
|
||||||
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
bind.nowPlayingSongLyricsTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
bind.nowPlayingSongLyricsTextView.setText(spannableString);
|
||||||
|
|
||||||
bind.nowPlayingSongLyricsTextView.setText(spannableString);
|
// Scroll to the highlighted line, but only if there is one
|
||||||
|
if (highlightStart >= 0 && playerBottomSheetViewModel.getSyncLyricsState()) {
|
||||||
if (playerBottomSheetViewModel.getSyncLyricsState()) {
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(highlightStart));
|
||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getStartPosition(List<Line> lines, Line toHighlight) {
|
private int getScroll(int startIndex) {
|
||||||
int start = 0;
|
|
||||||
|
|
||||||
for (Line line : lines) {
|
|
||||||
if (line != toHighlight) {
|
|
||||||
start = start + line.getValue().length() + 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getLineCount(List<Line> lines, Line toHighlight) {
|
|
||||||
int start = 0;
|
|
||||||
|
|
||||||
for (Line line : lines) {
|
|
||||||
if (line != toHighlight) {
|
|
||||||
bind.tempLyricsLineTextView.setText(line.getValue());
|
|
||||||
start = start + bind.tempLyricsLineTextView.getLineCount();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getScroll(List<Line> lines, Line toHighlight) {
|
|
||||||
int startIndex = getStartPosition(lines, toHighlight);
|
|
||||||
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
||||||
if (layout == null) return 0;
|
if (layout == null) return 0;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.media.audiofx.AudioEffect;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.text.InputFilter;
|
||||||
|
import android.text.InputType;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -27,6 +29,7 @@ import androidx.media3.common.util.UnstableApi;
|
|||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavOptions;
|
import androidx.navigation.NavOptions;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.preference.EditTextPreference;
|
||||||
import androidx.preference.ListPreference;
|
import androidx.preference.ListPreference;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceCategory;
|
import androidx.preference.PreferenceCategory;
|
||||||
@@ -141,6 +144,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
setStreamingCacheSize();
|
setStreamingCacheSize();
|
||||||
setAppLanguage();
|
setAppLanguage();
|
||||||
setVersion();
|
setVersion();
|
||||||
|
setNetorkPingTimeoutBase();
|
||||||
|
|
||||||
actionLogout();
|
actionLogout();
|
||||||
actionScan();
|
actionScan();
|
||||||
@@ -261,6 +265,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setNetorkPingTimeoutBase() {
|
||||||
|
EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base");
|
||||||
|
|
||||||
|
if (networkPingTimeoutBase != null) {
|
||||||
|
networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance());
|
||||||
|
networkPingTimeoutBase.setOnBindEditTextListener(editText -> {
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> {
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (!Character.isDigit(source.charAt(i))) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
|
networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
String input = (String) newValue;
|
||||||
|
return input != null && !input.isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setStreamingCacheSize() {
|
private void setStreamingCacheSize() {
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ class DynamicMediaSourceFactory(
|
|||||||
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
|
|
||||||
val uri = mediaItem.localConfiguration?.uri
|
val uri = mediaItem.localConfiguration?.uri
|
||||||
val isTranscoding = uri?.getQueryParameter("maxBitRate") != null ||
|
val isTranscoding = uri?.getQueryParameter("format") != null && uri.getQueryParameter("format") != "raw"
|
||||||
(uri?.getQueryParameter("format") != null && uri?.getQueryParameter("format") != "raw")
|
|
||||||
|
|
||||||
if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) {
|
if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) {
|
||||||
TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory)
|
TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory)
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ object Preferences {
|
|||||||
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
|
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
|
||||||
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
|
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
|
||||||
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
||||||
|
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServer(): String? {
|
fun getServer(): String? {
|
||||||
@@ -96,6 +98,19 @@ object Preferences {
|
|||||||
App.getInstance().preferences.edit().putString(SERVER, server).apply()
|
App.getInstance().preferences.edit().putString(SERVER, server).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getNetworkPingTimeout(): Int {
|
||||||
|
val timeoutString = App.getInstance().preferences.getString(NETWORK_PING_TIMEOUT, "2") ?: "2"
|
||||||
|
return (timeoutString.toIntOrNull() ?: 2).coerceAtLeast(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setNetworkPingTimeout(pingTimeout: String?) {
|
||||||
|
App.getInstance().preferences.edit().putString(NETWORK_PING_TIMEOUT, pingTimeout).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getUser(): String? {
|
fun getUser(): String? {
|
||||||
return App.getInstance().preferences.getString(USER, null)
|
return App.getInstance().preferences.getString(USER, null)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.media3.common.Metadata;
|
|||||||
import androidx.media3.common.Tracks;
|
import androidx.media3.common.Tracks;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.extractor.metadata.id3.InternalFrame;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.model.ReplayGain;
|
import com.cappielloantonio.tempo.model.ReplayGain;
|
||||||
|
|
||||||
@@ -82,26 +83,32 @@ public class ReplayGainUtil {
|
|||||||
private static ReplayGain setReplayGains(Metadata.Entry entry) {
|
private static ReplayGain setReplayGains(Metadata.Entry entry) {
|
||||||
ReplayGain replayGain = new ReplayGain();
|
ReplayGain replayGain = new ReplayGain();
|
||||||
|
|
||||||
if (entry.toString().contains(tags[0])) {
|
// The logic below assumes .toString() contains the dB value. That's not the case for InternalFrame
|
||||||
replayGain.setTrackGain(parseReplayGainTag(entry));
|
String str = entry.toString();
|
||||||
|
if (entry instanceof InternalFrame) {
|
||||||
|
str = ((InternalFrame) entry).description + ((InternalFrame) entry).text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.toString().contains(tags[1])) {
|
if (str.contains(tags[0])) {
|
||||||
replayGain.setAlbumGain(parseReplayGainTag(entry));
|
replayGain.setTrackGain(parseReplayGainTag(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.toString().contains(tags[2])) {
|
if (str.contains(tags[1])) {
|
||||||
replayGain.setTrackGain(parseReplayGainTag(entry) / 256f);
|
replayGain.setAlbumGain(parseReplayGainTag(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.toString().contains(tags[3])) {
|
if (str.contains(tags[2])) {
|
||||||
replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f);
|
replayGain.setTrackGain(parseReplayGainTag(str) / 256f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.contains(tags[3])) {
|
||||||
|
replayGain.setAlbumGain(parseReplayGainTag(str) / 256f);
|
||||||
}
|
}
|
||||||
|
|
||||||
return replayGain;
|
return replayGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Float parseReplayGainTag(Metadata.Entry entry) {
|
private static Float parseReplayGainTag(String entry) {
|
||||||
try {
|
try {
|
||||||
return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", ""));
|
return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", ""));
|
||||||
} catch (NumberFormatException exception) {
|
} catch (NumberFormatException exception) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.viewmodel;
|
|||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
@@ -21,8 +20,17 @@ import java.util.List;
|
|||||||
|
|
||||||
public class PlaylistChooserViewModel extends AndroidViewModel {
|
public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
private final PlaylistRepository playlistRepository;
|
private final PlaylistRepository playlistRepository;
|
||||||
|
|
||||||
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> playlistIsPublic = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
public Boolean getIsPlaylistPublic() {
|
||||||
|
return playlistIsPublic.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsPlaylistPublic(boolean isPublic) {
|
||||||
|
playlistIsPublic.setValue(isPublic);
|
||||||
|
}
|
||||||
|
|
||||||
private ArrayList<Child> toAdd = new ArrayList<>();
|
private ArrayList<Child> toAdd = new ArrayList<>();
|
||||||
|
|
||||||
public PlaylistChooserViewModel(@NonNull Application application) {
|
public PlaylistChooserViewModel(@NonNull Application application) {
|
||||||
@@ -39,7 +47,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
|||||||
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
||||||
List<String> songIds = Lists.transform(toAdd, Child::getId);
|
List<String> songIds = Lists.transform(toAdd, Child::getId);
|
||||||
if (Preferences.allowPlaylistDuplicates()) {
|
if (Preferences.allowPlaylistDuplicates()) {
|
||||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
} else {
|
} else {
|
||||||
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
|
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
|
||||||
@@ -47,7 +55,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
|||||||
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
||||||
songIds.removeAll(playlistSongIds);
|
songIds.removeAll(playlistSongIds);
|
||||||
}
|
}
|
||||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,15 @@
|
|||||||
<item>8</item>
|
<item>8</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="playlist_sort_option_titles">
|
||||||
|
<item>Por nombre</item>
|
||||||
|
<item>Aleatoriamente</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="playlist_sort_option_values">
|
||||||
|
<item>ORDER_BY_NAME</item>
|
||||||
|
<item>ORDER_BY_RANDOM</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="skip_min_star_rating_titles">
|
<string-array name="skip_min_star_rating_titles">
|
||||||
<item>0 estrellas como mínimo</item>
|
<item>0 estrellas como mínimo</item>
|
||||||
<item>1 estrella como mínimo</item>
|
<item>1 estrella como mínimo</item>
|
||||||
|
|||||||
@@ -227,6 +227,8 @@
|
|||||||
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
|
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Público</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privado</string>
|
||||||
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
|
||||||
<string name="playlist_duration">Duración • %1$s</string>
|
<string name="playlist_duration">Duración • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
|
||||||
@@ -333,6 +335,7 @@
|
|||||||
<string name="settings_music_directory_summary">Si se habilita, se mostrará la sección de carpetas de música. Tenga en cuenta que para que la navegación funcione correctamente, el servidor debe soportar esta característica.</string>
|
<string name="settings_music_directory_summary">Si se habilita, se mostrará la sección de carpetas de música. Tenga en cuenta que para que la navegación funcione correctamente, el servidor debe soportar esta característica.</string>
|
||||||
<string name="settings_podcast">Mostrar pódcasts</string>
|
<string name="settings_podcast">Mostrar pódcasts</string>
|
||||||
<string name="settings_podcast_summary">Si se habilita, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
|
<string name="settings_podcast_summary">Si se habilita, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
|
||||||
|
<string name="settings_playlist_sort">Ordenar listas de reproducción</string>
|
||||||
<string name="settings_audio_quality">Mostrar calidad de audio</string>
|
<string name="settings_audio_quality">Mostrar calidad de audio</string>
|
||||||
<string name="settings_audio_quality_summary">La tasa de bits y el formato de audio se mostrarán para cada pista de audio.</string>
|
<string name="settings_audio_quality_summary">La tasa de bits y el formato de audio se mostrarán para cada pista de audio.</string>
|
||||||
<string name="settings_song_rating_summary">Si se habilita, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
|
<string name="settings_song_rating_summary">Si se habilita, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
|
||||||
|
|||||||
@@ -236,6 +236,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Titre ajouté à la playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Titre ajouté à la playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Échec d\'ajout du titre à la playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Échec d\'ajout du titre à la playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tous les titres ont été traités comme des doublons et ignorés</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Tous les titres ont été traités comme des doublons et ignorés</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Publique</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privé</string>
|
||||||
<string name="playlist_counted_tracks">%1$d titres • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d titres • %2$s</string>
|
||||||
<string name="playlist_duration">Durée • %1$s</string>
|
<string name="playlist_duration">Durée • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Appui long pour supprimer</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Appui long pour supprimer</string>
|
||||||
|
|||||||
@@ -229,6 +229,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Pubblico</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privato</string>
|
||||||
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
|
||||||
<string name="playlist_duration">Durata • %1$s</string>
|
<string name="playlist_duration">Durata • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
||||||
|
|||||||
@@ -228,6 +228,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Dodano piosenki do playlisty</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Dodano piosenki do playlisty</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenek do playlisty</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenek do playlisty</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Pominięto wszystkie piosenki jako duplikaty</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Pominięto wszystkie piosenki jako duplikaty</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Publiczna</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Prywatna</string>
|
||||||
<string name="playlist_counted_tracks">%1$d utworów • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d utworów • %2$s</string>
|
||||||
<string name="playlist_duration">Długość • %1$s</string>
|
<string name="playlist_duration">Długość • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
|
||||||
@@ -343,6 +345,9 @@
|
|||||||
<string name="settings_image_size">Rozdzielczość obrazów</string>
|
<string name="settings_image_size">Rozdzielczość obrazów</string>
|
||||||
<string name="settings_language">Język</string>
|
<string name="settings_language">Język</string>
|
||||||
<string name="settings_logout_title">Wyloguj</string>
|
<string name="settings_logout_title">Wyloguj</string>
|
||||||
|
<string name="settings_ping_timeout_title">Timeout pingów serwera</string>
|
||||||
|
<string name="settings_ping_timeout_summary">Timeout lokalnego adresu URL. Domyślnie to 2 sekundy. (Serwer zdalny będzie używał trzykrotności tej wartości maksymalnie do 10 sekund.)</string>
|
||||||
|
<string name="settings_ping_timeout_dialog">Bazowy timeout w sekundach.</string>
|
||||||
<string name="settings_max_bitrate_download">Bitrate dla pobierania</string>
|
<string name="settings_max_bitrate_download">Bitrate dla pobierania</string>
|
||||||
<string name="settings_max_bitrate_mobile">Bitrate dla danych komórkowych</string>
|
<string name="settings_max_bitrate_mobile">Bitrate dla danych komórkowych</string>
|
||||||
<string name="settings_max_bitrate_wifi">Bitrate dla Wi-Fi</string>
|
<string name="settings_max_bitrate_wifi">Bitrate dla Wi-Fi</string>
|
||||||
|
|||||||
@@ -164,6 +164,8 @@
|
|||||||
<string name="playlist_chooser_dialog_title">Adicionar a uma playlist</string>
|
<string name="playlist_chooser_dialog_title">Adicionar a uma playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_success">Adicionada playlist de reprodução</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Adicionada playlist de reprodução</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Pública</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privada</string>
|
||||||
<string name="playlist_counted_tracks">%1$d faixas • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d faixas • %2$s</string>
|
||||||
<string name="playlist_duration">Duração • %1$s</string>
|
<string name="playlist_duration">Duração • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_hint_name">Nome da playlist</string>
|
<string name="playlist_editor_dialog_hint_name">Nome da playlist</string>
|
||||||
|
|||||||
@@ -238,6 +238,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Public</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Private</string>
|
||||||
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
||||||
<string name="playlist_duration">Duration • %1$s</string>
|
<string name="playlist_duration">Duration • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
||||||
@@ -343,7 +345,7 @@
|
|||||||
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
||||||
<string name="settings_github_summary">Follow the development</string>
|
<string name="settings_github_summary">Follow the development</string>
|
||||||
<string name="settings_github_title">Github</string>
|
<string name="settings_github_title">Github</string>
|
||||||
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</string>
|
<string name="settings_support_discussion_link" translatable="false">https://github.com/eddyizm/tempus/discussions</string>
|
||||||
<string name="settings_github_update">Updates</string>
|
<string name="settings_github_update">Updates</string>
|
||||||
<string name="settings_github_update_title">Check github for release updates</string>
|
<string name="settings_github_update_title">Check github for release updates</string>
|
||||||
<string name="settings_github_update_summary">If using the github version, by default app will check for new apk release. Toggle to disable automatic github checks</string>
|
<string name="settings_github_update_summary">If using the github version, by default app will check for new apk release. Toggle to disable automatic github checks</string>
|
||||||
@@ -353,6 +355,9 @@
|
|||||||
<string name="settings_image_size">Set image resolution</string>
|
<string name="settings_image_size">Set image resolution</string>
|
||||||
<string name="settings_language">Language</string>
|
<string name="settings_language">Language</string>
|
||||||
<string name="settings_logout_title">Log out</string>
|
<string name="settings_logout_title">Log out</string>
|
||||||
|
<string name="settings_ping_timeout_title">Server Ping Timeout</string>
|
||||||
|
<string name="settings_ping_timeout_summary">Set Local URL timeout. Default 2 seconds. (Remote server will use this value x3 up to 10 seconds max.)</string>
|
||||||
|
<string name="settings_ping_timeout_dialog">Set base timeout in seconds.</string>
|
||||||
<string name="settings_max_bitrate_download">Bitrate for downloads</string>
|
<string name="settings_max_bitrate_download">Bitrate for downloads</string>
|
||||||
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
|
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
|
||||||
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
|
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
|
||||||
@@ -484,7 +489,7 @@
|
|||||||
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
|
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
|
||||||
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
|
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
|
||||||
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
|
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
|
||||||
<string name="support_url">https://ko-fi.com/eddyizm</string>
|
<string name="support_url" translatable="false">https://ko-fi.com/eddyizm</string>
|
||||||
<string name="track_info_album">Album</string>
|
<string name="track_info_album">Album</string>
|
||||||
<string name="track_info_artist">Artist</string>
|
<string name="track_info_artist">Artist</string>
|
||||||
<string name="track_info_bit_depth">Bit depth</string>
|
<string name="track_info_bit_depth">Bit depth</string>
|
||||||
@@ -512,7 +517,7 @@
|
|||||||
<string name="track_info_year">Year</string>
|
<string name="track_info_year">Year</string>
|
||||||
<string name="undraw_page">unDraw</string>
|
<string name="undraw_page">unDraw</string>
|
||||||
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
||||||
<string name="undraw_url">https://undraw.co/</string>
|
<string name="undraw_url" translatable="false">https://undraw.co/</string>
|
||||||
<string name="widget_label">Tempus Widget</string>
|
<string name="widget_label">Tempus Widget</string>
|
||||||
<string name="widget_not_playing">Not playing</string>
|
<string name="widget_not_playing">Not playing</string>
|
||||||
<string name="widget_placeholder_subtitle">Open Tempus</string>
|
<string name="widget_placeholder_subtitle">Open Tempus</string>
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
android:key="scan_library"
|
android:key="scan_library"
|
||||||
android:title="@string/settings_scan_title" />
|
android:title="@string/settings_scan_title" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="network_ping_timeout_base"
|
||||||
|
android:title="@string/settings_ping_timeout_title"
|
||||||
|
app:summary="@string/settings_ping_timeout_summary"
|
||||||
|
android:dialogTitle="@string/settings_ping_timeout_dialog"
|
||||||
|
android:inputType="number"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:defaultValue="2" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:key="logout"
|
android:key="logout"
|
||||||
android:title="@string/settings_logout_title"/>
|
android:title="@string/settings_logout_title"/>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
chore: i18n: Add Romanian translation (including locale_config this time!)
|
||||||
|
chore: French localization update
|
||||||
|
chore(i18n): Update Spanish translation
|
||||||
|
docs: updated readme and added known issues for airsonic work around
|
||||||
|
fix: toast for made for you click indication
|
||||||
|
fix: sort playlist view
|
||||||
|
feat: sort preference for playlists
|
||||||
|
fix: use existing future when adding tracks, dialed random album tracks off in instant mix
|
||||||
|
chore(i18n): Update Polish translation
|
||||||
|
fix: Check for OpenSubsonic extensions also with password authentication, addressing lyric sync
|
||||||
|
feat: Implement duration and seeking for transcodes
|
||||||
|
feat: Playback speed controls for music
|
||||||
|
|||||||
3
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fix: Proper raw stream detection
|
||||||
|
chore(i18n): Update Spanish translation
|
||||||
|
feat: add configurable timeout
|
||||||
2
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fix: Avoid crash when server has no songs
|
||||||
|
fix: updated dialog import to address crashing on android 15
|
||||||
3
fastlane/metadata/android/en-US/changelogs/17.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/17.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fix: missing Replay Gain metadata from .m4a files
|
||||||
|
fix: Improve Synced Lyrics
|
||||||
|
fix: Add selector for playlist visibility
|
||||||
Reference in New Issue
Block a user