18 Commits
3.3.2 ... 3.4.4

Author SHA1 Message Date
antonio
3102bff729 gradle: bump up code version 2023-06-24 15:29:40 +02:00
antonio
992b16cc1d clean: explain user why to disable manufacturer optimizations and troubleshooting steps 2023-06-24 15:28:02 +02:00
antonio
2046872d79 gradle: bump up code version 2023-06-24 15:02:52 +02:00
antonio
7312ae20d1 fix: improved error handling during scanning phase 2023-06-24 15:02:13 +02:00
antonio
e8f2385928 gradle: bump up code version 2023-06-24 13:40:55 +02:00
antonio
204f9ff4c7 fix: addConverterFactory to retrofit client 2023-06-24 13:40:06 +02:00
antonio
17e64f3dd0 fix: cull checking 2023-06-24 13:38:45 +02:00
antonio
d09e7e7981 fix: collapse player bottom sheet on logout 2023-06-24 13:38:21 +02:00
antonio
95e9d06e47 gradle: bump up code version 2023-06-24 12:13:02 +02:00
antonio
f147c762d5 fix: reduced the frequency of the optimization prompt dialog and made it possible to disable it 2023-06-24 12:12:14 +02:00
antonio
200ce6555b Gradle: removed old search bar implementation and updated code version in preparation for the tag 2023-06-24 11:17:23 +02:00
antonio
b5e5537691 feat: Removed limit of 5 search suggestions 2023-06-24 11:16:05 +02:00
antonio
3b80725673 feat: replace old searchbar implementation with official material3 2023-06-24 11:15:04 +02:00
antonio
d7c5be834e Merge remote-tracking branch 'origin/main' 2023-06-23 18:28:31 +02:00
antonio
4a5672fc77 build: bump up code version 2023-06-23 18:28:16 +02:00
antonio
34fa2f456e fix: resolved potential issues related to missing or invalid prefixes 2023-06-23 18:23:44 +02:00
antonio
663285611b build: dependencies update 2023-06-23 09:29:10 +02:00
CappielloAntonio
a62cc78e18 Update README.md 2023-06-21 14:50:22 +02:00
22 changed files with 330 additions and 158 deletions

View File

@@ -1,5 +1,3 @@
<h1 align="center"> Tempo </h1>
<br>
<p align="center">
<img alt="Tempo" title="Tempo" src="mockup/svg/horizontal_logo.svg" width="250">
</p>

View File

@@ -10,8 +10,8 @@ android {
applicationId 'com.cappielloantonio.tempo'
minSdkVersion 24
targetSdkVersion 34
versionCode 5
versionName '3.3.0'
versionCode 11
versionName '3.4.4'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -62,7 +62,7 @@ dependencies {
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
@@ -73,10 +73,6 @@ dependencies {
// Android Material
implementation 'com.google.android.material:material:1.9.0'
// SearchBar
implementation 'com.paulrybitskyi.persistentsearchview:persistentsearchview:1.1.4'
implementation 'com.arthurivanets.adapster:adapster:1.0.13'
// Glide
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'com.github.bumptech.glide:annotations:4.15.1'
@@ -89,7 +85,7 @@ dependencies {
implementation 'androidx.media3:media3-cast:1.0.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
annotationProcessor 'androidx.room:room-compiler:2.5.1'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'

View File

@@ -12,8 +12,8 @@ import java.util.List;
@Dao
public interface RecentSearchDao {
@Query("SELECT * FROM recent_search GROUP BY search ORDER BY search DESC LIMIT :limit")
List<String> getRecent(int limit);
@Query("SELECT * FROM recent_search ORDER BY search ASC")
List<String> getRecent();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(RecentSearch search);

View File

@@ -17,8 +17,12 @@ public class ScanRepository {
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull retrofit2.Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null) {
if (response.body().getSubsonicResponse().getError() != null) {
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getMessage()));
} else if (response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
}
}
}
@@ -36,8 +40,12 @@ public class ScanRepository {
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull retrofit2.Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null) {
if (response.body().getSubsonicResponse().getError() != null) {
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getMessage()));
} else if (response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
}
}
}

View File

@@ -103,10 +103,10 @@ public class SearchingRepository {
thread.start();
}
public List<String> getRecentSearchSuggestion(int limit) {
public List<String> getRecentSearchSuggestion() {
List<String> recent = new ArrayList<>();
RecentThreadSafe suggestionsThread = new RecentThreadSafe(recentSearchDao, limit);
RecentThreadSafe suggestionsThread = new RecentThreadSafe(recentSearchDao);
Thread thread = new Thread(suggestionsThread);
thread.start();
@@ -152,17 +152,15 @@ public class SearchingRepository {
private static class RecentThreadSafe implements Runnable {
private final RecentSearchDao recentSearchDao;
private final int limit;
private List<String> recent = new ArrayList<>();
public RecentThreadSafe(RecentSearchDao recentSearchDao, int limit) {
public RecentThreadSafe(RecentSearchDao recentSearchDao) {
this.recentSearchDao = recentSearchDao;
this.limit = limit;
}
@Override
public void run() {
recent = recentSearchDao.getRecent(limit);
recent = recentSearchDao.getRecent();
}
public List<String> getRecent() {

View File

@@ -86,7 +86,7 @@ public class SongRepository {
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
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 && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs());
}

View File

@@ -22,7 +22,7 @@ public class SystemRepository {
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull retrofit2.Response<ApiResponse> response) {
if (response.body() != null) {
if (response.body().getSubsonicResponse().getStatus().equals(ResponseStatus.FAILED)) {
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getCode().getValue() + " - " + response.body().getSubsonicResponse().getError().getMessage()));
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getCode() + " - " + response.body().getSubsonicResponse().getError().getMessage()));
} else if (response.body().getSubsonicResponse().getStatus().equals(ResponseStatus.OK)) {
String password = response.raw().request().url().queryParameter("p");
String token = response.raw().request().url().queryParameter("t");

View File

@@ -16,6 +16,7 @@ class RetrofitClient(subsonic: Subsonic) {
init {
retrofit = Retrofit.Builder()
.baseUrl(subsonic.url)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()))
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.client(getOkHttpClient())
.build()

View File

@@ -4,6 +4,6 @@ import androidx.annotation.Keep
@Keep
class Error {
var code: ErrorCode? = null
var code: Int? = null
var message: String? = null
}

View File

@@ -247,6 +247,7 @@ public class MainActivity extends BaseActivity {
}
private void goToLogin() {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
setBottomNavigationBarVisibility(false);
setBottomSheetVisibility(false);

View File

@@ -2,15 +2,12 @@ package com.cappielloantonio.tempo.ui.activity.base;
import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@@ -19,9 +16,10 @@ import androidx.media3.exoplayer.offline.DownloadService;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.service.DownloaderService;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.material.elevation.SurfaceColors;
@@ -38,6 +36,8 @@ public class BaseActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
initializeCastContext();
initializeDownloader();
checkBatteryOptimization();
checkPermission();
}
@Override
@@ -47,13 +47,6 @@ public class BaseActivity extends AppCompatActivity {
initializeBrowser();
}
@Override
protected void onResume() {
super.onResume();
checkBatteryOptimization();
checkPermission();
}
@Override
protected void onStop() {
releaseBrowser();
@@ -61,7 +54,7 @@ public class BaseActivity extends AppCompatActivity {
}
private void checkBatteryOptimization() {
if (detectBatteryOptimization()) {
if (detectBatteryOptimization() && Preferences.askForOptimization()) {
showBatteryOptimizationDialog();
}
}
@@ -81,19 +74,8 @@ public class BaseActivity extends AppCompatActivity {
}
private void showBatteryOptimizationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.activity_battery_optimizations_summary)
.setTitle(R.string.activity_battery_optimizations_title)
.setNegativeButton(R.string.activity_negative_button, null)
.setPositiveButton(R.string.activity_neutral_button, (dialog, id) -> openPowerSettings())
.show();
}
private void openPowerSettings() {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(intent);
BatteryOptimizationDialog dialog = new BatteryOptimizationDialog();
dialog.show(getSupportFragmentManager(), null);
}
private void initializeBrowser() {

View File

@@ -0,0 +1,57 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.DialogFragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogBatteryOptimizationBinding;
import com.cappielloantonio.tempo.util.Preferences;
@OptIn(markerClass = UnstableApi.class)
public class BatteryOptimizationDialog extends DialogFragment {
private static final String TAG = "BatteryOptimizationDialog";
private DialogBatteryOptimizationBinding bind;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogBatteryOptimizationBinding.inflate(getLayoutInflater());
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(bind.getRoot())
.setTitle(R.string.activity_battery_optimizations_title)
.setNeutralButton(R.string.battery_optimization_neutral_button, (dialog, id) -> Preferences.dontAskForOptimization())
.setNegativeButton(R.string.battery_optimization_negative_button, null)
.setPositiveButton(R.string.battery_optimization_positive_button, (dialog, id) -> openPowerSettings());
AlertDialog popup = builder.create();
popup.setCancelable(false);
popup.setCanceledOnTouchOutside(false);
return popup;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void openPowerSettings() {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(intent);
}
}

View File

@@ -116,6 +116,11 @@ public class ServerSignupDialog extends DialogFragment {
return false;
}
if (!server.matches("^https?://(.*)")) {
bind.serverTextView.setError(getString(R.string.error_server_prefix));
return false;
}
return true;
}

View File

@@ -2,9 +2,14 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -28,17 +33,15 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.paulrybitskyi.persistentsearchview.adapters.model.SuggestionItem;
import com.paulrybitskyi.persistentsearchview.listeners.OnSuggestionChangeListener;
import com.paulrybitskyi.persistentsearchview.utils.SuggestionCreationUtil;
import java.util.Collections;
@UnstableApi
public class SearchFragment extends Fragment implements ClickCallback {
private static final String TAG = "SearchFragment";
private FragmentSearchBinding bind;
private MainActivity activity;
private SearchViewModel searchViewModel;
@@ -60,6 +63,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
initSearchResultView();
initSearchView();
inputFocus();
return view;
}
@@ -70,12 +74,6 @@ public class SearchFragment extends Fragment implements ClickCallback {
initializeMediaBrowser();
}
@Override
public void onResume() {
super.onResume();
inputFocus();
}
@Override
public void onStop() {
releaseMediaBrowser();
@@ -118,64 +116,101 @@ public class SearchFragment extends Fragment implements ClickCallback {
}
private void initSearchView() {
if (isQueryValid(searchViewModel.getQuery())) {
search(searchViewModel.getQuery());
}
setRecentSuggestions();
bind.persistentSearchView.setInputQuery(searchViewModel.getQuery());
setSuggestions();
bind.searchView
.getEditText()
.setOnEditorActionListener((textView, actionId, keyEvent) -> {
bind.persistentSearchView.setOnSearchQueryChangeListener((searchView, oldQuery, newQuery) -> {
if (!newQuery.trim().equals("") && newQuery.trim().length() > 1) {
searchViewModel.getSearchSuggestion(newQuery).observe(getViewLifecycleOwner(), suggestions -> searchView.setSuggestions(SuggestionCreationUtil.asRegularSearchSuggestions(MusicUtil.getReadableStrings(suggestions)), false));
} else {
setSuggestions();
}
});
String query = bind.searchView.getText().toString();
bind.persistentSearchView.setOnSuggestionChangeListener(new OnSuggestionChangeListener() {
@Override
public void onSuggestionPicked(SuggestionItem suggestion) {
search(suggestion.getItemModel().getText());
}
if (actionId == EditorInfo.IME_ACTION_DONE) {
if (isQueryValid(query)) {
search(bind.searchView.getText().toString());
return true;
} else {
Toast.makeText(requireContext(), getString(R.string.search_info_minimum_characters), Toast.LENGTH_SHORT).show();
return false;
}
}
@Override
public void onSuggestionRemoved(SuggestionItem suggestion) {
}
});
return false;
});
bind.persistentSearchView.setOnSearchConfirmedListener((searchView, query) -> {
if (isQueryValid(query)) {
searchView.collapse();
search(query);
} else {
Toast.makeText(requireContext(), getString(R.string.search_info_minimum_characters), Toast.LENGTH_SHORT).show();
}
});
bind.searchView
.getEditText()
.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
bind.persistentSearchView.setOnSuggestionChangeListener(new OnSuggestionChangeListener() {
@Override
public void onSuggestionPicked(SuggestionItem suggestion) {
search(suggestion.getItemModel().getText());
}
}
@Override
public void onSuggestionRemoved(SuggestionItem suggestion) {
searchViewModel.deleteRecentSearch(suggestion.getItemModel().getText());
}
});
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if (count > 1) {
setSearchSuggestions(charSequence.toString());
} else {
setRecentSuggestions();
}
}
bind.persistentSearchView.setOnClearInputBtnClickListener(v -> searchViewModel.setQuery(""));
@Override
public void afterTextChanged(Editable editable) {
}
});
}
private void setSuggestions() {
bind.persistentSearchView.setSuggestions(SuggestionCreationUtil.asRecentSearchSuggestions(searchViewModel.getRecentSearchSuggestion()), false);
public void setRecentSuggestions() {
bind.searchViewSuggestionContainer.removeAllViews();
for (String suggestion : searchViewModel.getRecentSearchSuggestion()) {
View view = LayoutInflater.from(bind.searchViewSuggestionContainer.getContext()).inflate(R.layout.item_search_suggestion, bind.searchViewSuggestionContainer, false);
ImageView leadingImageView = view.findViewById(R.id.search_suggestion_icon);
TextView titleView = view.findViewById(R.id.search_suggestion_title);
ImageView tailingImageView = view.findViewById(R.id.search_suggestion_delete_icon);
leadingImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_history, null));
titleView.setText(suggestion);
view.setOnClickListener(v -> search(suggestion));
tailingImageView.setOnClickListener(v -> {
searchViewModel.deleteRecentSearch(suggestion);
setRecentSuggestions();
});
bind.searchViewSuggestionContainer.addView(view);
}
}
public void setSearchSuggestions(String query) {
searchViewModel.getSearchSuggestion(query).observe(getViewLifecycleOwner(), suggestions -> {
bind.searchViewSuggestionContainer.removeAllViews();
for (String suggestion : suggestions) {
View view = LayoutInflater.from(bind.searchViewSuggestionContainer.getContext()).inflate(R.layout.item_search_suggestion, bind.searchViewSuggestionContainer, false);
ImageView leadingImageView = view.findViewById(R.id.search_suggestion_icon);
TextView titleView = view.findViewById(R.id.search_suggestion_title);
ImageView tailingImageView = view.findViewById(R.id.search_suggestion_delete_icon);
leadingImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_search, null));
titleView.setText(suggestion);
tailingImageView.setVisibility(View.GONE);
view.setOnClickListener(v -> search(suggestion));
bind.searchViewSuggestionContainer.addView(view);
}
});
}
public void search(String query) {
searchViewModel.setQuery(query);
bind.persistentSearchView.setInputQuery(query);
bind.searchBar.setText(query);
bind.searchView.hide();
performSearch(query);
}
@@ -216,7 +251,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
}
private void inputFocus() {
bind.persistentSearchView.expand();
bind.searchView.show();
}
private void initializeMediaBrowser() {

View File

@@ -10,6 +10,7 @@ object Preferences {
private const val TOKEN = "token"
private const val SALT = "salt"
private const val LOW_SECURITY = "low_security"
private const val BATTERY_OPTIMIZATION = "battery_optimization"
private const val SERVER_ID = "server_id"
private const val PLAYBACK_SPEED = "playback_speed"
private const val SKIP_SILENCE = "skip_silence"
@@ -100,6 +101,16 @@ object Preferences {
App.getInstance().preferences.edit().putString(SERVER_ID, serverId).apply()
}
@JvmStatic
fun askForOptimization(): Boolean? {
return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true)
}
@JvmStatic
fun dontAskForOptimization() {
App.getInstance().preferences.edit().putBoolean(BATTERY_OPTIMIZATION, false).apply()
}
@JvmStatic
fun getPlaybackSpeed(): Float {
return App.getInstance().preferences.getFloat(PLAYBACK_SPEED, 1f)

View File

@@ -56,7 +56,7 @@ public class SearchViewModel extends AndroidViewModel {
public List<String> getRecentSearchSuggestion() {
ArrayList<String> suggestions = new ArrayList<>();
suggestions.addAll(searchingRepository.getRecentSearchSuggestion(5));
suggestions.addAll(searchingRepository.getRecentSearchSuggestion());
return suggestions;
}

View File

@@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>
android:fillColor="@color/titleTextColor"
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/titleTextColor"
android:pathData="M480,840Q342,840 239.5,748.5Q137,657 122,520L204,520Q218,624 296.5,692Q375,760 480,760Q597,760 678.5,678.5Q760,597 760,480Q760,363 678.5,281.5Q597,200 480,200Q411,200 351,232Q291,264 250,320L360,320L360,400L120,400L120,160L200,160L200,254Q251,190 324.5,155Q398,120 480,120Q555,120 620.5,148.5Q686,177 734.5,225.5Q783,274 811.5,339.5Q840,405 840,480Q840,555 811.5,620.5Q783,686 734.5,734.5Q686,783 620.5,811.5Q555,840 480,840ZM592,648L440,496L440,280L520,280L520,464L648,592L592,648Z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/battery_optimization_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/activity_battery_optimizations_summary" />
<TextView
android:id="@+id/battery_optimization_secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:autoLink="web"
android:text="@string/activity_battery_optimizations_conclusion" />
</LinearLayout>

View File

@@ -1,54 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.paulrybitskyi.persistentsearchview.PersistentSearchView
android:id="@+id/persistentSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
app:areSuggestionsDisabled="false"
app:cardBackgroundColor="?attr/colorSurface"
app:cardCornerRadius="0dp"
app:cardElevation="0dp"
app:clearInputButtonDrawable="@drawable/ic_close"
app:dividerColor="@color/dividerColor"
app:isClearInputButtonEnabled="true"
app:isDismissableOnTouchOutside="true"
app:isProgressBarEnabled="true"
app:isVoiceInputButtonEnabled="false"
app:leftButtonDrawable="@drawable/ic_search"
app:progressBarColor="?attr/colorOnSurface"
app:queryInputBarIconColor="@color/searchPlaceholderColor"
app:queryInputCursorColor="?attr/colorOnSurface"
app:queryInputHint="@string/search_hint"
app:queryInputHintColor="@color/searchPlaceholderColor"
app:queryInputTextColor="@color/searchPlaceholderColor"
app:shouldDimBehind="false"
app:suggestionRecentSearchIconColor="@color/searchColor"
app:suggestionSearchSuggestionIconColor="@color/searchColor"
app:suggestionSelectedTextColor="?attr/colorPrimary"
app:suggestionTextColor="@color/searchColor"
app:suggestionIconColor="@color/searchColor" />
<View
android:id="@+id/search_bar_divider"
android:layout_width="match_parent"
android:layout_height="0.75dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_below="@+id/persistentSearchView"
android:background="?attr/colorSurfaceVariant" />
<androidx.core.widget.NestedScrollView
android:id="@+id/search_result_nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/search_bar_divider">
android:layout_height="match_parent"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
<!-- Search result -->
<LinearLayout
@@ -56,6 +16,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="64dp"
android:paddingBottom="@dimen/global_padding_bottom">
<!-- Artist -->
@@ -150,4 +111,39 @@
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_hint">
</com.google.android.material.search.SearchBar>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/search_hint"
app:layout_anchor="@id/search_bar">
<!-- Content goes here (ScrollView, RecyclerView, etc.). -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:paddingBottom="@dimen/global_padding_bottom">
<LinearLayout
android:id="@+id/search_view_suggestion_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</ScrollView>
</com.google.android.material.search.SearchView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp">
<ImageView
android:id="@+id/search_suggestion_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_history"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/search_suggestion_title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="12dp"
android:text="@string/label_placeholder"
android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintBottom_toBottomOf="@id/search_suggestion_icon"
app:layout_constraintEnd_toStartOf="@id/search_suggestion_delete_icon"
app:layout_constraintStart_toEndOf="@id/search_suggestion_icon"
app:layout_constraintTop_toTopOf="@id/search_suggestion_icon" />
<ImageView
android:id="@+id/search_suggestion_delete_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="@id/search_suggestion_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/search_suggestion_title"
app:layout_constraintTop_toTopOf="@id/search_suggestion_icon"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,9 +1,11 @@
<resources>
<string name="activity_battery_optimizations_summary">Please disable battery optimizations for media playback while the screen is off.</string>
<string name="activity_battery_optimizations_conclusion">If in trouble visit https://dontkillmyapp.com. It provides detailed instructions on how to disable any power-saving features that may affect app\'s performance.</string>
<string name="activity_battery_optimizations_title">Battery Optimizations</string>
<string name="activity_info_offline_mode">Offline mode</string>
<string name="activity_negative_button">Ignore</string>
<string name="activity_neutral_button">Disable</string>
<string name="battery_optimization_negative_button">Ignore</string>
<string name="battery_optimization_positive_button">Disable</string>
<string name="battery_optimization_neutral_button">Don\'t ask again</string>
<string name="album_bottom_sheet_add_to_queue">Add to queue</string>
<string name="album_bottom_sheet_download_all">Download all</string>
<string name="album_bottom_sheet_go_to_artist">Go to artist</string>
@@ -52,6 +54,7 @@
<string name="download_title_section">Downloads</string>
<string name="empty_string" />
<string name="error_required">Required</string>
<string name="error_server_prefix">http or https prefix required</string>
<string name="exo_download_notification_channel_name">Downloads</string>
<string name="filter_info_selection">Select two or more filters</string>
<string name="filter_title">Filter</string>