mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-15 08:50:35 +03:00
Merge remote-tracking branch 'origin/develop' into 4.8.0
This commit is contained in:
commit
7f4f944d79
9
fastlane/metadata/android/en-US/changelogs/130.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/130.txt
Normal file
@ -0,0 +1,9 @@
|
||||
### Features
|
||||
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
|
||||
- Properly handling nested directory structures (Android Auto)
|
||||
- Add a toast when adding tracks to the playlist
|
||||
- Allow pinning when offline
|
||||
|
||||
### Dependencies
|
||||
- Update koin
|
||||
- Update media3 to v1.1.0
|
@ -2,45 +2,45 @@
|
||||
# You need to run ./gradlew wrapper after updating the version
|
||||
gradle = "8.1.1"
|
||||
|
||||
navigation = "2.6.0"
|
||||
gradlePlugin = "8.1.0"
|
||||
navigation = "2.7.3"
|
||||
gradlePlugin = "8.1.2"
|
||||
androidxcar = "1.2.0"
|
||||
androidxcore = "1.10.1"
|
||||
androidxcore = "1.12.0"
|
||||
ktlint = "0.43.2"
|
||||
ktlintGradle = "11.5.0"
|
||||
ktlintGradle = "11.6.0"
|
||||
detekt = "1.23.0"
|
||||
preferences = "1.2.1"
|
||||
media3 = "1.1.0"
|
||||
media3 = "1.1.1"
|
||||
|
||||
androidSupport = "1.6.0"
|
||||
androidSupport = "1.7.0"
|
||||
materialDesign = "1.9.0"
|
||||
constraintLayout = "2.1.4"
|
||||
multidex = "2.0.1"
|
||||
room = "2.5.2"
|
||||
kotlin = "1.8.22"
|
||||
ksp = "1.8.22-1.0.11"
|
||||
kotlin = "1.9.10"
|
||||
ksp = "1.9.10-1.0.13"
|
||||
kotlinxCoroutines = "1.7.3"
|
||||
viewModelKtx = "2.6.1"
|
||||
viewModelKtx = "2.6.2"
|
||||
swipeRefresh = "1.1.0"
|
||||
|
||||
retrofit = "2.9.0"
|
||||
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
||||
jackson = "2.13.5"
|
||||
okhttp = "4.11.0"
|
||||
koin = "3.4.3"
|
||||
koin = "3.5.0"
|
||||
picasso = "2.8"
|
||||
|
||||
junit4 = "4.13.2"
|
||||
junit5 = "5.10.0"
|
||||
mockito = "5.4.0"
|
||||
mockitoKotlin = "5.0.0"
|
||||
mockito = "5.5.0"
|
||||
mockitoKotlin = "5.1.0"
|
||||
kluent = "1.73"
|
||||
apacheCodecs = "1.16.0"
|
||||
robolectric = "4.10.3"
|
||||
timber = "5.0.1"
|
||||
fastScroll = "2.0.1"
|
||||
colorPicker = "2.2.4"
|
||||
rxJava = "3.1.6"
|
||||
rxJava = "3.1.8"
|
||||
rxAndroid = "3.0.2"
|
||||
multiType = "4.3.0"
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
ext.versions = [
|
||||
minSdk : 21,
|
||||
targetSdk : 33,
|
||||
compileSdk : 33,
|
||||
]
|
||||
compileSdk : 34,
|
||||
]
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
3
gradlew
vendored
3
gradlew
vendored
@ -83,7 +83,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -1,38 +0,0 @@
|
||||
package org.moire.ultrasonic.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import timber.log.Timber;
|
||||
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class UltrasonicIntentReceiver extends BroadcastReceiver
|
||||
{
|
||||
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
String intentAction = intent.getAction();
|
||||
Timber.i("Received Ultrasonic Intent: %s", intentAction);
|
||||
|
||||
try
|
||||
{
|
||||
lifecycleSupport.getValue().receiveIntent(intent);
|
||||
|
||||
if (isOrderedBroadcast())
|
||||
{
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.moire.ultrasonic.app.UApp;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Monitors the state of the mobile's external storage
|
||||
*/
|
||||
public class ExternalStorageMonitor
|
||||
{
|
||||
private BroadcastReceiver ejectEventReceiver;
|
||||
private boolean externalStorageAvailable = true;
|
||||
|
||||
public void onCreate(final Runnable ejectedCallback)
|
||||
{
|
||||
// Stop when SD card is ejected.
|
||||
ejectEventReceiver = new BroadcastReceiver()
|
||||
{
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction());
|
||||
if (!externalStorageAvailable)
|
||||
{
|
||||
Timber.i("External media is ejecting. Stopping playback.");
|
||||
ejectedCallback.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
Timber.i("External media is available.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
|
||||
ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
|
||||
ejectFilter.addDataScheme("file");
|
||||
UApp.Companion.applicationContext().registerReceiver(ejectEventReceiver, ejectFilter);
|
||||
}
|
||||
|
||||
public void onDestroy()
|
||||
{
|
||||
UApp.Companion.applicationContext().unregisterReceiver(ejectEventReceiver);
|
||||
}
|
||||
|
||||
public boolean isExternalStorageAvailable() { return externalStorageAvailable; }
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
import timber.log.Timber;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.Track;
|
||||
|
||||
/**
|
||||
* Scrobbles played songs to Last.fm.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
public class Scrobbler
|
||||
{
|
||||
private String lastSubmission;
|
||||
private String lastNowPlaying;
|
||||
|
||||
public void scrobble(final Track song, final boolean submission)
|
||||
{
|
||||
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
|
||||
|
||||
final String id = song.getId();
|
||||
|
||||
// Avoid duplicate registrations.
|
||||
if (submission && id.equals(lastSubmission)) return;
|
||||
|
||||
if (!submission && id.equals(lastNowPlaying)) return;
|
||||
|
||||
if (submission) lastSubmission = id;
|
||||
else lastNowPlaying = id;
|
||||
|
||||
new Thread(String.format("Scrobble %s", song))
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
MusicService service = MusicServiceFactory.getMusicService();
|
||||
try
|
||||
{
|
||||
service.scrobble(id, submission);
|
||||
Timber.i("Scrobbled '%s' for %s", submission ? "submission" : "now playing", song);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
Timber.i(x, "Failed to scrobble'%s' for %s", submission ? "submission" : "now playing", song);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
/**
|
||||
* Abstract class for supplying items to a consumer
|
||||
* @param <T> The type of the item supplied
|
||||
*/
|
||||
public abstract class Supplier<T>
|
||||
{
|
||||
public abstract T get();
|
||||
}
|
||||
|
||||
|
@ -1,159 +0,0 @@
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.ChatMessage;
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader;
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class ChatAdapter extends ArrayAdapter<ChatMessage>
|
||||
{
|
||||
private final Context context;
|
||||
private final List<ChatMessage> messages;
|
||||
|
||||
private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})";
|
||||
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
|
||||
|
||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
|
||||
|
||||
public ChatAdapter(Context context, List<ChatMessage> messages)
|
||||
{
|
||||
super(context, R.layout.chat_item, messages);
|
||||
this.context = context;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount()
|
||||
{
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
ChatMessage message = this.getItem(position);
|
||||
|
||||
ViewHolder holder;
|
||||
int layout;
|
||||
|
||||
String messageUser = message.getUsername();
|
||||
Date messageTime = new java.util.Date(message.getTime());
|
||||
String messageText = message.getMessage();
|
||||
|
||||
String me = activeServerProvider.getValue().getActiveServer().getUserName();
|
||||
|
||||
layout = messageUser.equals(me) ? R.layout.chat_item_reverse : R.layout.chat_item;
|
||||
|
||||
if (convertView == null)
|
||||
{
|
||||
convertView = inflateView(layout, parent);
|
||||
holder = createViewHolder(layout, convertView);
|
||||
}
|
||||
else
|
||||
{
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
|
||||
if (!holder.chatMessage.equals(message))
|
||||
{
|
||||
convertView = inflateView(layout, parent);
|
||||
holder = createViewHolder(layout, convertView);
|
||||
}
|
||||
}
|
||||
|
||||
holder.chatMessage = message;
|
||||
|
||||
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
||||
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
|
||||
|
||||
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
|
||||
|
||||
if (holder.avatar != null && !TextUtils.isEmpty(messageUser))
|
||||
{
|
||||
imageLoader.loadAvatarImage(holder.avatar, messageUser);
|
||||
}
|
||||
|
||||
holder.username.setText(messageUser);
|
||||
holder.message.setText(messageText);
|
||||
holder.time.setText(messageTimeFormatted);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private View inflateView(int layout, ViewGroup parent)
|
||||
{
|
||||
return LayoutInflater.from(context).inflate(layout, parent, false);
|
||||
}
|
||||
|
||||
private static ViewHolder createViewHolder(int layout, View convertView)
|
||||
{
|
||||
ViewHolder holder = new ViewHolder();
|
||||
holder.layout = layout;
|
||||
|
||||
TextView usernameView;
|
||||
TextView timeView;
|
||||
TextView messageView;
|
||||
ImageView imageView;
|
||||
|
||||
if (convertView != null)
|
||||
{
|
||||
usernameView = (TextView) convertView.findViewById(R.id.chat_username);
|
||||
timeView = (TextView) convertView.findViewById(R.id.chat_time);
|
||||
messageView = (TextView) convertView.findViewById(R.id.chat_message);
|
||||
imageView = (ImageView) convertView.findViewById(R.id.chat_avatar);
|
||||
|
||||
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
Linkify.addLinks(messageView, Linkify.ALL);
|
||||
Linkify.addLinks(messageView, phoneMatcher, "tel:");
|
||||
|
||||
holder.avatar = imageView;
|
||||
holder.message = messageView;
|
||||
holder.username = usernameView;
|
||||
holder.time = timeView;
|
||||
|
||||
convertView.setTag(holder);
|
||||
}
|
||||
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static class ViewHolder
|
||||
{
|
||||
int layout;
|
||||
ImageView avatar;
|
||||
TextView message;
|
||||
TextView username;
|
||||
TextView time;
|
||||
ChatMessage chatMessage;
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2010 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.SectionIndexer;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Genre;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class GenreAdapter extends ArrayAdapter<Genre> implements SectionIndexer
|
||||
{
|
||||
private final LayoutInflater layoutInflater;
|
||||
// Both arrays are indexed by section ID.
|
||||
private final Object[] sections;
|
||||
private final Integer[] positions;
|
||||
|
||||
public GenreAdapter(@NonNull Context context, List<Genre> genres)
|
||||
{
|
||||
super(context, R.layout.list_item_generic, genres);
|
||||
|
||||
layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
Collection<String> sectionSet = new LinkedHashSet<String>(30);
|
||||
List<Integer> positionList = new ArrayList<Integer>(30);
|
||||
|
||||
for (int i = 0; i < genres.size(); i++)
|
||||
{
|
||||
Genre genre = genres.get(i);
|
||||
String index = genre.getIndex();
|
||||
if (!sectionSet.contains(index))
|
||||
{
|
||||
sectionSet.add(index);
|
||||
positionList.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
sections = sectionSet.toArray(new Object[0]);
|
||||
positions = positionList.toArray(new Integer[0]);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View rowView = convertView;
|
||||
if (rowView == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false);
|
||||
}
|
||||
|
||||
((TextView) rowView).setText(getItem(position).getName());
|
||||
|
||||
return rowView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getSections()
|
||||
{
|
||||
return sections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionForSection(int section)
|
||||
{
|
||||
return positions[section];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionForPosition(int pos)
|
||||
{
|
||||
for (int i = 0; i < sections.length - 1; i++)
|
||||
{
|
||||
if (pos < positions[i + 1])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return sections.length - 1;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Share;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class ShareAdapter extends ArrayAdapter<Share>
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
public ShareAdapter(Context context, List<Share> Shares)
|
||||
{
|
||||
super(context, R.layout.share_list_item, Shares);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
Share entry = getItem(position);
|
||||
ShareView view;
|
||||
|
||||
if (convertView instanceof ShareView)
|
||||
{
|
||||
ShareView currentView = (ShareView) convertView;
|
||||
|
||||
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
|
||||
view = currentView;
|
||||
view.setViewHolder(viewHolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
view = new ShareView(context);
|
||||
view.setLayout();
|
||||
}
|
||||
|
||||
view.setShare(entry);
|
||||
return view;
|
||||
}
|
||||
|
||||
static class ViewHolder
|
||||
{
|
||||
TextView url;
|
||||
TextView description;
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Share;
|
||||
|
||||
/**
|
||||
* Used to display playlists in a {@code ListView}.
|
||||
*
|
||||
* @author Joshua Bahnsen
|
||||
*/
|
||||
public class ShareView extends LinearLayout
|
||||
{
|
||||
private final Context context;
|
||||
private ShareAdapter.ViewHolder viewHolder;
|
||||
|
||||
public ShareView(Context context)
|
||||
{
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setLayout()
|
||||
{
|
||||
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
|
||||
viewHolder = new ShareAdapter.ViewHolder();
|
||||
viewHolder.url = findViewById(R.id.share_url);
|
||||
viewHolder.description = findViewById(R.id.share_description);
|
||||
setTag(viewHolder);
|
||||
}
|
||||
|
||||
public void setViewHolder(ShareAdapter.ViewHolder viewHolder)
|
||||
{
|
||||
this.viewHolder = viewHolder;
|
||||
setTag(this.viewHolder);
|
||||
}
|
||||
|
||||
public void setShare(Share share)
|
||||
{
|
||||
viewHolder.url.setText(share.getName());
|
||||
viewHolder.description.setText(share.getDescription());
|
||||
}
|
||||
}
|
@ -1219,14 +1219,14 @@ class PlayerFragment :
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun onFling(
|
||||
e1: MotionEvent,
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
val e1X = e1.x
|
||||
val e1X = e1?.x ?: 0F
|
||||
val e2X = e2.x
|
||||
val e1Y = e1.y
|
||||
val e1Y = e1?.y ?: 0F
|
||||
val e2Y = e2.y
|
||||
val absX = abs(velocityX)
|
||||
val absY = abs(velocityY)
|
||||
@ -1263,7 +1263,7 @@ class PlayerFragment :
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {}
|
||||
override fun onScroll(
|
||||
e1: MotionEvent,
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
|
@ -108,7 +108,9 @@ class SettingsFragment :
|
||||
preferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == null || sharedPreferences == null) return
|
||||
|
||||
Timber.d("Preference changed: %s", key)
|
||||
updateCustomPreferences()
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SharesFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
@ -58,7 +58,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
private var sharesListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var shareAdapter: ShareAdapter? = null
|
||||
private val downloadHandler = inject<DownloadHandler>()
|
||||
private val downloadHandler: DownloadHandler by inject()
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
@ -110,8 +110,9 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
}
|
||||
|
||||
override fun done(result: List<Share>) {
|
||||
sharesListView!!.adapter = ShareAdapter(context, result).also { shareAdapter = it }
|
||||
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
shareAdapter = ShareAdapter(requireContext(), result)
|
||||
sharesListView?.adapter = shareAdapter
|
||||
emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
@ -132,7 +133,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
val share = sharesListView!!.getItemAtPosition(info.position) as Share
|
||||
when (menuItem.itemId) {
|
||||
R.id.share_menu_pin -> {
|
||||
downloadHandler.value.justDownload(
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.PIN,
|
||||
fragment = this,
|
||||
id = share.id,
|
||||
@ -142,7 +143,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_unpin -> {
|
||||
downloadHandler.value.justDownload(
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.UNPIN,
|
||||
fragment = this,
|
||||
id = share.id,
|
||||
@ -152,7 +153,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_download -> {
|
||||
downloadHandler.value.justDownload(
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.DOWNLOAD,
|
||||
fragment = this,
|
||||
id = share.id,
|
||||
@ -162,7 +163,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_now -> {
|
||||
downloadHandler.value.fetchTracksAndAddToController(
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
@ -172,7 +173,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_shuffled -> {
|
||||
downloadHandler.value.fetchTracksAndAddToController(
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* BluetoothIntentReceiver.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
@ -10,9 +10,6 @@ package org.moire.ultrasonic.receiver
|
||||
import android.Manifest
|
||||
import android.bluetooth.BluetoothA2dp
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothDevice.ACTION_ACL_CONNECTED
|
||||
import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECTED
|
||||
import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@ -22,9 +19,6 @@ import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_A2DP
|
||||
import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_ALL
|
||||
import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_DISABLED
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import timber.log.Timber
|
||||
|
||||
@ -42,27 +36,27 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
||||
Timber.d("Bluetooth device: $name; State: $state; Action: $action")
|
||||
|
||||
// In these flags we store what kind of device (any or a2dp) has (dis)connected
|
||||
var connectionStatus = PREFERENCE_VALUE_DISABLED
|
||||
var disconnectionStatus = PREFERENCE_VALUE_DISABLED
|
||||
var connectionStatus = Constants.PREFERENCE_VALUE_DISABLED
|
||||
var disconnectionStatus = Constants.PREFERENCE_VALUE_DISABLED
|
||||
|
||||
// First check for general devices
|
||||
when (action) {
|
||||
ACTION_ACL_CONNECTED -> {
|
||||
connectionStatus = PREFERENCE_VALUE_ALL
|
||||
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
||||
connectionStatus = Constants.PREFERENCE_VALUE_ALL
|
||||
}
|
||||
ACTION_ACL_DISCONNECTED,
|
||||
ACTION_ACL_DISCONNECT_REQUESTED -> {
|
||||
disconnectionStatus = PREFERENCE_VALUE_ALL
|
||||
BluetoothDevice.ACTION_ACL_DISCONNECTED,
|
||||
BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED -> {
|
||||
disconnectionStatus = Constants.PREFERENCE_VALUE_ALL
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for A2DP devices
|
||||
when (state) {
|
||||
BluetoothA2dp.STATE_CONNECTED -> {
|
||||
connectionStatus = PREFERENCE_VALUE_A2DP
|
||||
connectionStatus = Constants.PREFERENCE_VALUE_A2DP
|
||||
}
|
||||
BluetoothA2dp.STATE_DISCONNECTED -> {
|
||||
disconnectionStatus = PREFERENCE_VALUE_A2DP
|
||||
disconnectionStatus = Constants.PREFERENCE_VALUE_A2DP
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,20 +66,20 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
||||
|
||||
// Now check the settings and set the appropriate flags
|
||||
when (Settings.resumeOnBluetoothDevice) {
|
||||
PREFERENCE_VALUE_ALL -> {
|
||||
shouldResume = (connectionStatus != PREFERENCE_VALUE_DISABLED)
|
||||
Constants.PREFERENCE_VALUE_ALL -> {
|
||||
shouldResume = (connectionStatus != Constants.PREFERENCE_VALUE_DISABLED)
|
||||
}
|
||||
PREFERENCE_VALUE_A2DP -> {
|
||||
shouldResume = (connectionStatus == PREFERENCE_VALUE_A2DP)
|
||||
Constants.PREFERENCE_VALUE_A2DP -> {
|
||||
shouldResume = (connectionStatus == Constants.PREFERENCE_VALUE_A2DP)
|
||||
}
|
||||
}
|
||||
|
||||
when (Settings.pauseOnBluetoothDevice) {
|
||||
PREFERENCE_VALUE_ALL -> {
|
||||
shouldPause = (disconnectionStatus != PREFERENCE_VALUE_DISABLED)
|
||||
Constants.PREFERENCE_VALUE_ALL -> {
|
||||
shouldPause = (disconnectionStatus != Constants.PREFERENCE_VALUE_DISABLED)
|
||||
}
|
||||
PREFERENCE_VALUE_A2DP -> {
|
||||
shouldPause = (disconnectionStatus == PREFERENCE_VALUE_A2DP)
|
||||
Constants.PREFERENCE_VALUE_A2DP -> {
|
||||
shouldPause = (disconnectionStatus == Constants.PREFERENCE_VALUE_A2DP)
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,24 +99,24 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BluetoothDevice?.getNameSafely(): String? {
|
||||
val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
(
|
||||
ActivityCompat.checkSelfPermission(
|
||||
UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
private fun BluetoothDevice?.getNameSafely(): String? {
|
||||
val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
(
|
||||
ActivityCompat.checkSelfPermission(
|
||||
UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
|
||||
return if (logBluetoothName) this?.name else "Unknown"
|
||||
}
|
||||
return if (logBluetoothName) this?.name else "Unknown"
|
||||
}
|
||||
|
||||
private fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||
private fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* UltrasonicIntentReceiver.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import timber.log.Timber
|
||||
|
||||
class UltrasonicIntentReceiver : BroadcastReceiver() {
|
||||
private val lifecycleSupport = inject<MediaPlayerLifecycleSupport>(
|
||||
MediaPlayerLifecycleSupport::class.java
|
||||
)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val intentAction = intent.action
|
||||
Timber.i("Received Ultrasonic Intent: %s", intentAction)
|
||||
try {
|
||||
lifecycleSupport.value.receiveIntent(intent)
|
||||
if (isOrderedBroadcast) {
|
||||
abortBroadcast()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* ExternalStorageMonitor.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Monitors the state of the mobile's external storage
|
||||
*/
|
||||
class ExternalStorageMonitor {
|
||||
private var ejectEventReceiver: BroadcastReceiver? = null
|
||||
var isExternalStorageAvailable = true
|
||||
private set
|
||||
|
||||
fun onCreate(ejectedCallback: Runnable) {
|
||||
// Stop when SD card is ejected.
|
||||
ejectEventReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
isExternalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED == intent.action
|
||||
if (!isExternalStorageAvailable) {
|
||||
Timber.i("External media is ejecting. Stopping playback.")
|
||||
ejectedCallback.run()
|
||||
} else {
|
||||
Timber.i("External media is available.")
|
||||
}
|
||||
}
|
||||
}
|
||||
val ejectFilter = IntentFilter(Intent.ACTION_MEDIA_EJECT)
|
||||
ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED)
|
||||
ejectFilter.addDataScheme("file")
|
||||
applicationContext().registerReceiver(ejectEventReceiver, ejectFilter)
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
applicationContext().unregisterReceiver(ejectEventReceiver)
|
||||
}
|
||||
}
|
@ -286,6 +286,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
setDeviceVolume(volume, 0)
|
||||
}
|
||||
|
@ -91,10 +91,12 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun hasPrevious(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun hasPreviousWindow(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -103,10 +105,12 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun previous() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun seekToPreviousWindow() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -115,10 +119,12 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun hasNext(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun hasNextWindow(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -127,10 +133,12 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun next() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun seekToNextWindow() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -163,10 +171,12 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getCurrentWindowIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getNextWindowIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -175,6 +185,7 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getPreviousWindowIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -183,14 +194,17 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun isCurrentWindowDynamic(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun isCurrentWindowLive(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun isCurrentWindowSeekable(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Scrobbler.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isScrobblingEnabled
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Scrobbles played songs to Last.fm.
|
||||
*/
|
||||
class Scrobbler : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
private var lastSubmission: String? = null
|
||||
private var lastNowPlaying: String? = null
|
||||
fun scrobble(song: Track?, submission: Boolean) {
|
||||
if (song == null || !isScrobblingEnabled()) return
|
||||
val id = song.id
|
||||
|
||||
// Avoid duplicate registrations.
|
||||
if (submission && id == lastSubmission) return
|
||||
if (!submission && id == lastNowPlaying) return
|
||||
if (submission) lastSubmission = id else lastNowPlaying = id
|
||||
|
||||
launch {
|
||||
val service = getMusicService()
|
||||
try {
|
||||
service.scrobble(id, submission)
|
||||
Timber.i(
|
||||
"Scrobbled '%s' for %s",
|
||||
if (submission) "submission" else "now playing",
|
||||
song
|
||||
)
|
||||
} catch (all: Exception) {
|
||||
Timber.i(
|
||||
all,
|
||||
"Failed to scrobble'%s' for %s",
|
||||
if (submission) "submission" else "now playing",
|
||||
song
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* ChatAdapter.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.view
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateFormat
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.ChatMessage
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
|
||||
class ChatAdapter(private val context: Context, private val messages: List<ChatMessage>) :
|
||||
ArrayAdapter<ChatMessage>(
|
||||
context, R.layout.chat_item, messages
|
||||
),
|
||||
KoinComponent {
|
||||
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
|
||||
override fun areAllItemsEnabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isEnabled(position: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return messages.size
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var view = convertView
|
||||
val message = getItem(position)
|
||||
val holder: ViewHolder
|
||||
val layout: Int
|
||||
val me = activeServerProvider.getActiveServer().userName
|
||||
|
||||
// Different layouts based on the message sender/recipient
|
||||
layout = if (message?.username == me) R.layout.chat_item_reverse else R.layout.chat_item
|
||||
|
||||
if (view == null || (view.tag as ViewHolder).layout != layout) {
|
||||
view = inflateView(layout, parent)
|
||||
holder = ViewHolder()
|
||||
} else {
|
||||
holder = view.tag as ViewHolder
|
||||
}
|
||||
|
||||
linkHolder(holder, layout, view)
|
||||
if (message != null) setData(holder, message)
|
||||
return view
|
||||
}
|
||||
|
||||
private fun inflateView(layout: Int, parent: ViewGroup): View {
|
||||
return LayoutInflater.from(context).inflate(layout, parent, false)
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
var layout = R.layout.chat_item
|
||||
var avatar: ImageView? = null
|
||||
var message: TextView? = null
|
||||
var username: TextView? = null
|
||||
var time: TextView? = null
|
||||
var chatMessage: ChatMessage? = null
|
||||
}
|
||||
|
||||
private fun linkHolder(holder: ViewHolder, layout: Int, view: View) {
|
||||
holder.layout = layout
|
||||
holder.avatar = view.findViewById(R.id.chat_avatar)
|
||||
holder.message = view.findViewById(R.id.chat_message)
|
||||
holder.message?.movementMethod = LinkMovementMethod.getInstance()
|
||||
holder.username = view.findViewById(R.id.chat_username)
|
||||
holder.time = view.findViewById(R.id.chat_time)
|
||||
view.tag = holder
|
||||
}
|
||||
|
||||
private fun setData(
|
||||
holder: ViewHolder,
|
||||
message: ChatMessage
|
||||
) {
|
||||
holder.chatMessage = message
|
||||
val timeFormat = DateFormat.getTimeFormat(context)
|
||||
val messageTimeFormatted = "[${timeFormat.format(message.time)}]"
|
||||
val imageLoader = imageLoaderProvider.getImageLoader()
|
||||
if (holder.avatar != null && !TextUtils.isEmpty(message.username)) {
|
||||
imageLoader.loadAvatarImage(holder.avatar!!, message.username)
|
||||
}
|
||||
holder.username?.text = message.username
|
||||
holder.message?.text = message.message
|
||||
holder.time?.text = messageTimeFormatted
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* GenreAdapter.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
package org.moire.ultrasonic.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.SectionIndexer
|
||||
import android.widget.TextView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
|
||||
class GenreAdapter(context: Context, genres: List<Genre>) :
|
||||
ArrayAdapter<Genre?>(context, R.layout.list_item_generic, genres), SectionIndexer {
|
||||
private val layoutInflater: LayoutInflater
|
||||
|
||||
// Both arrays are indexed by section ID.
|
||||
private val sections: Array<Any>
|
||||
private val positions: Array<Int>
|
||||
|
||||
init {
|
||||
layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val sectionSet: MutableCollection<String> = LinkedHashSet(INITIAL_CAPACITY)
|
||||
val positionList: MutableList<Int> = ArrayList(INITIAL_CAPACITY)
|
||||
for (i in genres.indices) {
|
||||
val (index) = genres[i]
|
||||
if (!sectionSet.contains(index)) {
|
||||
sectionSet.add(index)
|
||||
positionList.add(i)
|
||||
}
|
||||
}
|
||||
sections = sectionSet.toTypedArray()
|
||||
positions = positionList.toTypedArray()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var rowView = convertView
|
||||
if (rowView == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false)
|
||||
}
|
||||
(rowView as TextView?)!!.text = getItem(position)!!.name
|
||||
return rowView!!
|
||||
}
|
||||
|
||||
override fun getSections(): Array<Any> {
|
||||
return sections
|
||||
}
|
||||
|
||||
override fun getPositionForSection(section: Int): Int {
|
||||
return positions[section]
|
||||
}
|
||||
|
||||
override fun getSectionForPosition(pos: Int): Int {
|
||||
for (i in 0 until sections.size - 1) {
|
||||
if (pos < positions[i + 1]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return sections.size - 1
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INITIAL_CAPACITY: Int = 30
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* ShareAdapter.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Share
|
||||
|
||||
class ShareAdapter(private val context: Context, shares: List<Share>) : ArrayAdapter<Share>(
|
||||
context, R.layout.share_list_item, shares
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val entry = getItem(position)
|
||||
val view: View
|
||||
val holder: ViewHolder
|
||||
|
||||
if (convertView == null) {
|
||||
holder = ViewHolder()
|
||||
val inflater = LayoutInflater.from(context)
|
||||
view = inflater.inflate(R.layout.share_list_item, parent, false)
|
||||
holder.url = view.findViewById(R.id.share_url)
|
||||
holder.description = view.findViewById(R.id.share_description)
|
||||
view.tag = holder
|
||||
} else {
|
||||
view = convertView
|
||||
holder = view.tag as ViewHolder
|
||||
}
|
||||
|
||||
if (entry != null) setData(entry, holder)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun setData(entry: Share, holder: ViewHolder) {
|
||||
holder.url?.text = entry.name
|
||||
holder.description?.text = entry.description
|
||||
}
|
||||
|
||||
class ViewHolder {
|
||||
var url: TextView? = null
|
||||
var description: TextView? = null
|
||||
}
|
||||
}
|
@ -1,15 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
a:layout_width="0dp"
|
||||
a:layout_width="match_parent"
|
||||
a:layout_height="56dp"
|
||||
a:layout_gravity="center_vertical"
|
||||
a:layout_weight="1"
|
||||
a:minHeight="44dip"
|
||||
a:orientation="vertical"
|
||||
a:paddingStart="16dip"
|
||||
a:paddingEnd="16dip"
|
||||
tools:layout_width="match_parent">
|
||||
a:paddingEnd="16dip">
|
||||
|
||||
<LinearLayout
|
||||
a:layout_width="fill_parent"
|
||||
|
@ -8,7 +8,7 @@
|
||||
<string name="background_task.parse_error">Antwort nicht verstanden. Bitte die Serveradresse überprüfen.</string>
|
||||
<string name="background_task.ssl_cert_error">HTTPS Zertifikatsfehler: %1$s.</string>
|
||||
<string name="background_task.ssl_error">SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen.</string>
|
||||
<string name="background_task.wait">Bitte warten… </string>
|
||||
<string name="background_task.wait">Bitte warten…</string>
|
||||
<string name="button_bar.bookmarks">Lesezeichen</string>
|
||||
<string name="button_bar.browse">Medienbibliothek</string>
|
||||
<string name="button_bar.chat">Chat</string>
|
||||
@ -59,8 +59,8 @@
|
||||
<string name="download.jukebox_not_authorized">Fernbedienung ist nicht erlaubt. Bitte Jukebox Modus auf dem Subsonic Server in <b>Benutzer > Einstellungen</b> aktivieren.</string>
|
||||
<string name="download.jukebox_off">Fernbedienung ausgeschaltet. Musik wird auf dem Telefon wiedergegeben.</string>
|
||||
<string name="download.jukebox_offline">Fernbedienungs-Modus is Offline nicht verfügbar.</string>
|
||||
<string name="download.jukebox_on">Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben.</string>
|
||||
<string name="download.jukebox_server_too_old">Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren.</string>
|
||||
<string name="download.jukebox_on">Fernbedienung eingeschaltet. Musik wird auf dem Server wiedergegeben.</string>
|
||||
<string name="download.jukebox_server_too_old">Fernbedienungs Modus wird nicht unterstützt. Bitte aktualisiere den Subsonic Server.</string>
|
||||
<string name="download.menu_equalizer">Equalizer</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox Aus</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox An</string>
|
||||
@ -443,9 +443,15 @@
|
||||
<string name="albumArt">Album-Artwork</string>
|
||||
<string name="notification.permission_required">Benachrichtigungen sind für die Medienwiedergabe erforderlich. Du kannst die Erlaubnis jederzeit in den Android-Einstellungen erteilen.</string>
|
||||
<string name="settings.use_hw_offload_title">Hardware-Wiedergabe verwenden (experimentell)</string>
|
||||
<string name="settings.use_hw_offload_description">Versuche, die Medien mit dem Mediendecoder-Chip auf Deinem Telefon abzuspielen. Dadurch kann der Akku besser genutzt werden.</string>
|
||||
<string name="settings.use_hw_offload_description">Versuche, die Medien mit dem Mediendecoder-Chip auf Deinem Telefon abzuspielen. Dadurch kann der Akku besser genutzt werden. Einige Nutzer berichten über Stotterer beim Abspielen, wenn diese Option aktiv ist.</string>
|
||||
<string name="list_view">Liste</string>
|
||||
<string name="grid_view">Cover</string>
|
||||
<string name="supported_server_features">Unterstützte Funktionen</string>
|
||||
<string name="jukebox">Jukebox</string>
|
||||
<string name="shortcut_play_random_songs_long">Zufällige Titel abspielen</string>
|
||||
<string name="shortcut_play_random_songs_short">Zufällige Titel</string>
|
||||
<plurals name="n_songs_added_play_now">
|
||||
<item quantity="one">Ein Titel wurde zur Wiedergabeliste hinzugefügt</item>
|
||||
<item quantity="other">%d Titel wurden zur Wiedergabeliste hinzugefügt</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -455,4 +455,9 @@
|
||||
<string name="shortcut_play_random_songs_short">Canciones aleatorias</string>
|
||||
<string name="shortcut_play_random_songs_long">Reproducir las canciones aleatoriamente</string>
|
||||
<string name="download.menu_unstar">No me gusta</string>
|
||||
<plurals name="n_songs_added_play_now">
|
||||
<item quantity="one">%d canción añadida a la cola de reproducción</item>
|
||||
<item quantity="many">%d canciones añadidas a la cola de reproducción</item>
|
||||
<item quantity="other">%d canciones añadidas a la cola de reproducción</item>
|
||||
</plurals>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user