mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-15 00:40:37 +03:00
Migrate remaining Java Code and modernize it
This commit is contained in:
parent
de523a6451
commit
442f622b35
@ -15,6 +15,7 @@ media3 = "1.1.1"
|
||||
androidSupport = "1.7.0"
|
||||
materialDesign = "1.9.0"
|
||||
constraintLayout = "2.1.4"
|
||||
activity = "1.8.0"
|
||||
multidex = "2.0.1"
|
||||
room = "2.5.2"
|
||||
kotlin = "1.9.10"
|
||||
@ -66,6 +67,7 @@ navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-kt
|
||||
navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
||||
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
|
||||
activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
|
||||
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
||||
media3common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
||||
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||
|
@ -1,9 +1,7 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues>
|
||||
<ID>TooManyFunctions:PlaybackService.kt$PlaybackService : MediaLibraryServiceKoinComponentCoroutineScope</ID>
|
||||
<ID>UnusedPrivateMember:UApp.kt$private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder</ID>
|
||||
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<CurrentIssues>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||
@ -11,19 +9,11 @@
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? )</ID>
|
||||
<ID>LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</ManuallySuppressedIssues>
|
||||
<CurrentIssues/>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
@ -22,20 +22,22 @@
|
||||
android:xlargeScreens="true"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:hasFragileUserData="true" tools:targetApi="q"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.Dark"
|
||||
android:name=".app.UApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/common.appname"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="false"
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.Dark"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:targetApi="q">
|
||||
<!-- Add for API 34 android:enableOnBackInvokedCallBack="true" -->
|
||||
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
@ -137,5 +139,4 @@
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedContentProvider" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -1,297 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.ChatMessage;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Settings;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.ChatAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
/**
|
||||
* Provides online chat functionality
|
||||
*/
|
||||
public class ChatFragment extends Fragment {
|
||||
|
||||
private ListView chatListView;
|
||||
private EditText messageEditText;
|
||||
private MaterialButton sendButton;
|
||||
private Timer timer;
|
||||
private volatile static Long lastChatMessageTime = (long) 0;
|
||||
private static final ArrayList<ChatMessage> messageList = new ArrayList<>();
|
||||
private CancellationToken cancellationToken;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.chat, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
swipeRefresh = view.findViewById(R.id.chat_refresh);
|
||||
swipeRefresh.setEnabled(false);
|
||||
|
||||
cancellationToken = new CancellationToken();
|
||||
messageEditText = view.findViewById(R.id.chat_edittext);
|
||||
sendButton = view.findViewById(R.id.chat_send);
|
||||
|
||||
sendButton.setOnClickListener(view1 -> sendMessage());
|
||||
|
||||
chatListView = view.findViewById(R.id.chat_entries_list);
|
||||
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
|
||||
chatListView.setStackFromBottom(true);
|
||||
|
||||
String serverName = activeServerProvider.getValue().getActiveServer().getName();
|
||||
String userName = activeServerProvider.getValue().getActiveServer().getUserName();
|
||||
String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName);
|
||||
|
||||
FragmentTitle.Companion.setTitle(this, title);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
|
||||
|
||||
messageEditText.addTextChangedListener(new TextWatcher()
|
||||
{
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable)
|
||||
{
|
||||
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
messageEditText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
{
|
||||
sendMessage();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
load();
|
||||
timerMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.chat, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
/*
|
||||
* Listen for option item selections so that we receive a notification
|
||||
* when the user requests a refresh by selecting the refresh action bar item.
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Check if user triggered a refresh:
|
||||
if (item.getItemId() == R.id.menu_refresh) {
|
||||
// Start the refresh background task.
|
||||
load();
|
||||
return true;
|
||||
}
|
||||
// User didn't trigger a refresh, let the superclass handle this action
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
|
||||
if (!messageList.isEmpty())
|
||||
{
|
||||
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
|
||||
chatListView.setAdapter(chatAdapter);
|
||||
}
|
||||
|
||||
if (timer == null)
|
||||
{
|
||||
timerMethod();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause()
|
||||
{
|
||||
super.onPause();
|
||||
|
||||
if (timer != null)
|
||||
{
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void timerMethod()
|
||||
{
|
||||
int refreshInterval = Settings.getChatRefreshInterval();
|
||||
|
||||
if (refreshInterval > 0)
|
||||
{
|
||||
timer = new Timer();
|
||||
|
||||
timer.schedule(new TimerTask()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
getActivity().runOnUiThread(() -> load());
|
||||
}
|
||||
}, refreshInterval, refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage()
|
||||
{
|
||||
if (messageEditText != null)
|
||||
{
|
||||
final String message;
|
||||
Editable text = messageEditText.getText();
|
||||
|
||||
if (text == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
message = text.toString();
|
||||
|
||||
if (!Util.isNullOrWhiteSpace(message))
|
||||
{
|
||||
messageEditText.setText("");
|
||||
|
||||
BackgroundTask<Void> task = new FragmentBackgroundTask<Void>(getActivity(), false, swipeRefresh, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
musicService.addChatMessage(message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Void result)
|
||||
{
|
||||
load();
|
||||
}
|
||||
};
|
||||
|
||||
task.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void load()
|
||||
{
|
||||
BackgroundTask<List<ChatMessage>> task = new FragmentBackgroundTask<List<ChatMessage>>(getActivity(), false, swipeRefresh, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected List<ChatMessage> doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
return musicService.getChatMessages(lastChatMessageTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(List<ChatMessage> result)
|
||||
{
|
||||
if (result != null && !result.isEmpty())
|
||||
{
|
||||
// Reset lastChatMessageTime if we have a newer message
|
||||
for (ChatMessage message : result)
|
||||
{
|
||||
if (message.getTime() > lastChatMessageTime)
|
||||
{
|
||||
lastChatMessageTime = message.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse results to show them on the bottom
|
||||
Collections.reverse(result);
|
||||
messageList.addAll(result);
|
||||
|
||||
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
|
||||
chatListView.setAdapter(chatAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error) {
|
||||
// Stop the timer in case of an error, otherwise it may repeat the error message forever
|
||||
if (timer != null)
|
||||
{
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
super.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
task.execute();
|
||||
}
|
||||
}
|
@ -1,72 +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.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public abstract class BackgroundTask<T> implements ProgressListener
|
||||
{
|
||||
private final Activity activity;
|
||||
private final Handler handler;
|
||||
|
||||
public BackgroundTask(Activity activity)
|
||||
{
|
||||
this.activity = activity;
|
||||
handler = new Handler();
|
||||
}
|
||||
|
||||
protected Activity getActivity()
|
||||
{
|
||||
return activity;
|
||||
}
|
||||
|
||||
protected Handler getHandler()
|
||||
{
|
||||
return handler;
|
||||
}
|
||||
|
||||
public abstract void execute();
|
||||
|
||||
protected abstract T doInBackground() throws Throwable;
|
||||
|
||||
protected abstract void done(T result);
|
||||
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
CommunicationError.handleError(error, activity);
|
||||
}
|
||||
|
||||
protected String getErrorMessage(Throwable error)
|
||||
{
|
||||
return CommunicationError.getErrorMessage(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract void updateProgress(final String message);
|
||||
|
||||
@Override
|
||||
public void updateProgress(int messageId)
|
||||
{
|
||||
updateProgress(activity.getResources().getString(messageId));
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class FragmentBackgroundTask<T> extends BackgroundTask<T>
|
||||
{
|
||||
private final boolean changeProgress;
|
||||
private final SwipeRefreshLayout swipe;
|
||||
private final CancellationToken cancel;
|
||||
|
||||
public FragmentBackgroundTask(Activity activity, boolean changeProgress,
|
||||
SwipeRefreshLayout swipe, CancellationToken cancel)
|
||||
{
|
||||
super(activity);
|
||||
this.changeProgress = changeProgress;
|
||||
this.swipe = swipe;
|
||||
this.cancel = cancel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
if (changeProgress)
|
||||
{
|
||||
if (swipe != null) swipe.setRefreshing(true);
|
||||
}
|
||||
|
||||
new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
final T result = doInBackground();
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if (changeProgress)
|
||||
{
|
||||
if (swipe != null) swipe.setRefreshing(false);
|
||||
}
|
||||
|
||||
done(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (final Throwable t)
|
||||
{
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
getHandler().post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if (changeProgress)
|
||||
{
|
||||
if (swipe != null) swipe.setRefreshing(false);
|
||||
}
|
||||
|
||||
error(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(final String message)
|
||||
{
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class LoadingTask<T> extends BackgroundTask<T>
|
||||
{
|
||||
private final SwipeRefreshLayout swipe;
|
||||
private final CancellationToken cancel;
|
||||
|
||||
public LoadingTask(Activity activity, SwipeRefreshLayout swipe, CancellationToken cancel)
|
||||
{
|
||||
super(activity);
|
||||
this.swipe = swipe;
|
||||
this.cancel = cancel;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
swipe.setRefreshing(true);
|
||||
|
||||
new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
final T result = doInBackground();
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(() -> {
|
||||
swipe.setRefreshing(false);
|
||||
done(result);
|
||||
});
|
||||
}
|
||||
catch (final Throwable t)
|
||||
{
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(() -> {
|
||||
swipe.setRefreshing(false);
|
||||
error(t);
|
||||
});
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(final String message)
|
||||
{
|
||||
}
|
||||
}
|
@ -1,29 +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.util;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public interface ProgressListener
|
||||
{
|
||||
void updateProgress(String message);
|
||||
|
||||
void updateProgress(int messageId);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import org.moire.ultrasonic.domain.Track;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Josh on 12/17/13.
|
||||
*/
|
||||
public class ShareDetails
|
||||
{
|
||||
public String Description;
|
||||
public boolean ShareOnServer;
|
||||
public long Expiration;
|
||||
public List<Track> Entries;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import androidx.preference.DialogPreference;
|
||||
import org.moire.ultrasonic.R;
|
||||
|
||||
/**
|
||||
* Created by Joshua Bahnsen on 12/22/13.
|
||||
*/
|
||||
public class TimeSpanPreference extends DialogPreference
|
||||
{
|
||||
Context context;
|
||||
|
||||
public TimeSpanPreference(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
this.context = context;
|
||||
|
||||
setPositiveButtonText(android.R.string.ok);
|
||||
setNegativeButtonText(android.R.string.cancel);
|
||||
|
||||
setDialogIcon(null);
|
||||
|
||||
}
|
||||
|
||||
public String getText()
|
||||
{
|
||||
String persisted = getPersistedString("");
|
||||
|
||||
if (!"".equals(persisted))
|
||||
{
|
||||
return persisted.replace(':', ' ');
|
||||
}
|
||||
|
||||
return this.context.getResources().getString(R.string.time_span_disabled);
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import java.util.Locale
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.Util.getVersionName
|
||||
|
||||
@ -56,7 +55,7 @@ class AboutFragment : Fragment() {
|
||||
versionName
|
||||
)
|
||||
|
||||
setTitle(this@AboutFragment, getString(R.string.menu_about))
|
||||
FragmentTitle.setTitle(this@AboutFragment, getString(R.string.menu_about))
|
||||
titleText?.text = title
|
||||
|
||||
webPageButton?.setOnClickListener {
|
||||
|
@ -5,8 +5,6 @@
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
@file:Suppress("NAME_SHADOWING")
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
@ -32,6 +30,7 @@ import org.moire.ultrasonic.domain.Album
|
||||
import org.moire.ultrasonic.model.AlbumListModel
|
||||
import org.moire.ultrasonic.util.LayoutType
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
import org.moire.ultrasonic.view.FilterButtonBar
|
||||
import org.moire.ultrasonic.view.SortOrder
|
||||
import org.moire.ultrasonic.view.ViewCapabilities
|
||||
@ -76,9 +75,10 @@ class AlbumListFragment(
|
||||
}
|
||||
|
||||
private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
|
||||
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView?.isRefreshing = true
|
||||
listModel.viewModelScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
swipeRefresh?.isRefreshing = true
|
||||
|
||||
if (navArgs.byArtist) {
|
||||
listModel.getAlbumsOfArtist(
|
||||
@ -95,7 +95,7 @@ class AlbumListFragment(
|
||||
refresh = refresh or append
|
||||
)
|
||||
}
|
||||
refreshListView?.isRefreshing = false
|
||||
swipeRefresh?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,8 +185,8 @@ class AlbumListFragment(
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
swipeRefresh = view.findViewById(refreshListId)
|
||||
swipeRefresh?.setOnRefreshListener {
|
||||
fetchAlbums(refresh = true)
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<ArtistOrIndex>> {
|
||||
return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
|
||||
return listModel.getItems(navArgs.refresh || refresh, swipeRefresh!!)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -16,8 +16,9 @@ import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
|
||||
/**
|
||||
* Lists the Bookmarks available on the server
|
||||
@ -40,10 +41,12 @@ class BookmarksFragment : TrackCollectionFragment() {
|
||||
refresh: Boolean,
|
||||
append: Boolean
|
||||
): LiveData<List<MusicDirectory.Child>> {
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView?.isRefreshing = true
|
||||
listModel.viewModelScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
swipeRefresh?.isRefreshing = true
|
||||
listModel.getBookmarks()
|
||||
refreshListView?.isRefreshing = false
|
||||
swipeRefresh?.isRefreshing = false
|
||||
}
|
||||
return listModel.currentList
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), Koi
|
||||
currentSetting.musicFolderId = it.id
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
listModel.refresh(refreshListView!!)
|
||||
listModel.refresh(swipeRefresh!!)
|
||||
}
|
||||
|
||||
viewAdapter.register(
|
||||
@ -90,7 +90,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), Koi
|
||||
* What to do when the list has changed
|
||||
*/
|
||||
override val defaultObserver: (List<T>) -> Unit = {
|
||||
emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing ?: false)
|
||||
emptyView.isVisible = it.isEmpty() && !(swipeRefresh?.isRefreshing ?: false)
|
||||
|
||||
if (showFolderHeader()) {
|
||||
val list = mutableListOf<Identifiable>(folderHeader)
|
||||
|
@ -26,7 +26,7 @@ import java.util.HashMap
|
||||
import java.util.Locale
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -7,24 +7,22 @@ import androidx.navigation.fragment.NavHostFragment
|
||||
/**
|
||||
* Contains utility functions related to Fragment title handling
|
||||
*/
|
||||
class FragmentTitle {
|
||||
companion object {
|
||||
fun setTitle(fragment: Fragment, title: CharSequence?) {
|
||||
// Only set the title if our fragment is a direct child of the NavHostFragment...
|
||||
if (fragment.parentFragment is NavHostFragment) {
|
||||
(fragment.activity as AppCompatActivity).supportActionBar?.title = title
|
||||
}
|
||||
}
|
||||
|
||||
fun setTitle(fragment: Fragment, id: Int) {
|
||||
// Only set the title if our fragment is a direct child of the NavHostFragment...
|
||||
if (fragment.parentFragment is NavHostFragment) {
|
||||
(fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTitle(fragment: Fragment): CharSequence? {
|
||||
return (fragment.activity as AppCompatActivity).supportActionBar?.title
|
||||
object FragmentTitle {
|
||||
fun setTitle(fragment: Fragment, title: CharSequence?) {
|
||||
// Only set the title if our fragment is a direct child of the NavHostFragment...
|
||||
if (fragment.parentFragment is NavHostFragment) {
|
||||
(fragment.activity as AppCompatActivity).supportActionBar?.title = title
|
||||
}
|
||||
}
|
||||
|
||||
fun setTitle(fragment: Fragment, id: Int) {
|
||||
// Only set the title if our fragment is a direct child of the NavHostFragment...
|
||||
if (fragment.parentFragment is NavHostFragment) {
|
||||
(fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTitle(fragment: Fragment): CharSequence? {
|
||||
return (fragment.activity as AppCompatActivity).supportActionBar?.title
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,6 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@ -23,7 +21,6 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.scope.ScopeFragment
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@ -34,18 +31,18 @@ import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.model.GenericListModel
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
/**
|
||||
* An abstract Model, which can be extended to display a list of items of type T from the API
|
||||
* @param T: The type of data which will be used (must extend GenericEntry)
|
||||
*/
|
||||
abstract class MultiListFragment<T : Identifiable> : ScopeFragment() {
|
||||
abstract class MultiListFragment<T : Identifiable> : ScopeFragment(), RefreshableFragment {
|
||||
internal val activeServerProvider: ActiveServerProvider by inject()
|
||||
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
internal val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
protected var refreshListView: SwipeRefreshLayout? = null
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
internal var listView: RecyclerView? = null
|
||||
internal lateinit var viewManager: LinearLayoutManager
|
||||
internal lateinit var emptyView: ConstraintLayout
|
||||
@ -95,16 +92,6 @@ abstract class MultiListFragment<T : Identifiable> : ScopeFragment() {
|
||||
*/
|
||||
open val refreshOnCreation: Boolean = true
|
||||
|
||||
/**
|
||||
* The default Exception Handler for Coroutines
|
||||
*/
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
CommunicationError.handleError(exception, context)
|
||||
}
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
|
||||
open fun setTitle(title: String?) {
|
||||
if (title == null) {
|
||||
FragmentTitle.setTitle(
|
||||
@ -122,7 +109,7 @@ abstract class MultiListFragment<T : Identifiable> : ScopeFragment() {
|
||||
* What to do when the list has changed
|
||||
*/
|
||||
internal open val defaultObserver: ((List<T>) -> Unit) = {
|
||||
emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing?:false)
|
||||
emptyView.isVisible = it.isEmpty() && !(swipeRefresh?.isRefreshing?:false)
|
||||
viewAdapter.submitList(it)
|
||||
}
|
||||
|
||||
@ -130,9 +117,9 @@ abstract class MultiListFragment<T : Identifiable> : ScopeFragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
listModel.refresh(refreshListView!!)
|
||||
swipeRefresh = view.findViewById(refreshListId)
|
||||
swipeRefresh?.setOnRefreshListener {
|
||||
listModel.refresh(swipeRefresh!!)
|
||||
}
|
||||
|
||||
// Populate the LiveData. This starts an API request in most cases
|
||||
|
@ -92,7 +92,7 @@ import org.moire.ultrasonic.databinding.CurrentPlayingBinding
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
@ -778,11 +778,8 @@ class PlayerFragment :
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_share -> {
|
||||
val tracks: MutableList<Track?> = ArrayList()
|
||||
val playlist = mediaPlayerManager.playlist
|
||||
for (item in playlist) {
|
||||
val playlistEntry = item.toTrack()
|
||||
tracks.add(playlistEntry)
|
||||
val tracks = mediaPlayerManager.playlist.map {
|
||||
it.toTrack()
|
||||
}
|
||||
shareHandler.createShare(
|
||||
this,
|
||||
@ -793,12 +790,9 @@ class PlayerFragment :
|
||||
R.id.menu_item_share_song -> {
|
||||
if (track == null) return true
|
||||
|
||||
val tracks: MutableList<Track?> = ArrayList()
|
||||
tracks.add(track)
|
||||
|
||||
shareHandler.createShare(
|
||||
this,
|
||||
tracks,
|
||||
listOf(track),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
@ -34,39 +34,32 @@ import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.Index
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.model.SearchListModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
|
||||
/**
|
||||
* Initiates a search on the media library and displays the results
|
||||
|
||||
*/
|
||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent {
|
||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, RefreshableFragment {
|
||||
private var searchResult: SearchResult? = null
|
||||
private var searchRefresh: SwipeRefreshLayout? = null
|
||||
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
private val navArgs by navArgs<SearchFragmentArgs>()
|
||||
|
||||
override val listModel: SearchListModel by viewModels()
|
||||
|
||||
override val mainLayout: Int = R.layout.search
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
cancellationToken = CancellationToken()
|
||||
setTitle(this, R.string.search_title)
|
||||
|
||||
listModel.searchResult.observe(
|
||||
@ -79,8 +72,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent {
|
||||
}
|
||||
}
|
||||
|
||||
searchRefresh = view.findViewById(R.id.swipe_refresh_view)
|
||||
searchRefresh!!.isEnabled = false
|
||||
swipeRefresh = view.findViewById(R.id.swipe_refresh_view)
|
||||
swipeRefresh!!.isEnabled = false
|
||||
|
||||
registerForContextMenu(listView!!)
|
||||
|
||||
@ -129,17 +122,17 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent {
|
||||
|
||||
override fun onDestroyView() {
|
||||
Util.hideKeyboard(activity)
|
||||
cancellationToken?.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun search(query: String, autoplay: Boolean) {
|
||||
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
||||
refreshListView?.isRefreshing = true
|
||||
listModel.search(query)
|
||||
refreshListView?.isRefreshing = false
|
||||
}.invokeOnCompletion {
|
||||
if (it == null && autoplay) {
|
||||
listModel.viewModelScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
swipeRefresh?.isRefreshing = true
|
||||
val result = listModel.search(query)
|
||||
swipeRefresh?.isRefreshing = false
|
||||
if (result != null && autoplay) {
|
||||
autoplay()
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.log.FileLoggerTree
|
||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.deleteLogFiles
|
||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileNumber
|
||||
|
@ -41,7 +41,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.model.TrackCollectionModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
@ -56,6 +56,7 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util.navigateToCurrent
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
import org.moire.ultrasonic.view.SortOrder
|
||||
import org.moire.ultrasonic.view.ViewCapabilities
|
||||
import timber.log.Timber
|
||||
@ -107,8 +108,8 @@ open class TrackCollectionFragment(
|
||||
albumButtons = view.findViewById(R.id.menu_album)
|
||||
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
swipeRefresh = view.findViewById(refreshListId)
|
||||
swipeRefresh?.setOnRefreshListener {
|
||||
handleRefresh()
|
||||
}
|
||||
|
||||
@ -284,7 +285,7 @@ open class TrackCollectionFragment(
|
||||
} else if (item.itemId == R.id.menu_item_share) {
|
||||
shareHandler.createShare(
|
||||
fragment = this@TrackCollectionFragment,
|
||||
tracks = getSelectedTracks(),
|
||||
tracks = getSelectedOrAllTracks(),
|
||||
additionalId = navArgs.id
|
||||
)
|
||||
return true
|
||||
@ -338,7 +339,7 @@ open class TrackCollectionFragment(
|
||||
} else {
|
||||
mediaPlayerManager.suggestedPlaylistName = navArgs.playlistName
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
songs = getAllSongs(),
|
||||
songs = getAllTracks(),
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||
shuffle = shuffle
|
||||
@ -357,26 +358,20 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
|
||||
private fun downloadSelectedOrAllTracks(save: Boolean) {
|
||||
var tracks = getSelectedTracks()
|
||||
if (tracks.isEmpty()) tracks = getAllSongs()
|
||||
|
||||
DownloadUtil.justDownload(
|
||||
action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD,
|
||||
fragment = this,
|
||||
tracks = tracks
|
||||
tracks = getSelectedOrAllTracks()
|
||||
)
|
||||
}
|
||||
|
||||
private fun playSelectedOrAllTracks(
|
||||
insertionMode: MediaPlayerManager.InsertionMode
|
||||
) {
|
||||
var tracks = getSelectedTracks()
|
||||
if (tracks.isEmpty()) tracks = getAllSongs()
|
||||
|
||||
mediaPlayerManager.playTracksAndToast(
|
||||
fragment = this,
|
||||
insertionMode = insertionMode,
|
||||
tracks = tracks
|
||||
tracks = getSelectedOrAllTracks()
|
||||
)
|
||||
}
|
||||
|
||||
@ -388,13 +383,6 @@ open class TrackCollectionFragment(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun getAllSongs(): List<Track> {
|
||||
return viewAdapter.getCurrentList().filter {
|
||||
it is Track && !it.isDirectory
|
||||
} as List<Track>
|
||||
}
|
||||
|
||||
private fun selectAllOrNone() {
|
||||
val someUnselected = viewAdapter.selectedSet.size < childCount
|
||||
selectAll(someUnselected)
|
||||
@ -503,6 +491,19 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun getAllTracks(): List<Track> {
|
||||
return viewAdapter.getCurrentList().filter {
|
||||
it is Track && !it.isDirectory
|
||||
} as List<Track>
|
||||
}
|
||||
|
||||
fun getSelectedOrAllTracks(): List<Track> {
|
||||
return getSelectedTracks().ifEmpty {
|
||||
getAllTracks()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTitle(title: String?) {
|
||||
setTitle(this@TrackCollectionFragment, title)
|
||||
}
|
||||
@ -534,8 +535,10 @@ open class TrackCollectionFragment(
|
||||
val offset = navArgs.offset
|
||||
val refresh2 = navArgs.refresh || refresh
|
||||
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView?.isRefreshing = true
|
||||
listModel.viewModelScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
swipeRefresh?.isRefreshing = true
|
||||
|
||||
if (playlistId != null) {
|
||||
setTitle(playlistName!!)
|
||||
@ -570,7 +573,7 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
}
|
||||
|
||||
refreshListView?.isRefreshing = false
|
||||
swipeRefresh?.isRefreshing = false
|
||||
}
|
||||
return listModel.currentList
|
||||
}
|
||||
|
@ -0,0 +1,214 @@
|
||||
/*
|
||||
* ChatFragment.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ListAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import java.util.Locale
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.model.ChatViewModel
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.Util.isNullOrWhiteSpace
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
import org.moire.ultrasonic.view.ChatAdapter
|
||||
|
||||
class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
|
||||
private lateinit var chatListView: ListView
|
||||
private lateinit var messageEditText: EditText
|
||||
private lateinit var sendButton: MaterialButton
|
||||
private var timer: Timer? = null
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private val chatViewModel: ChatViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applyTheme(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.chat, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Add the ChatMenuProvider for creating the menu
|
||||
(requireActivity() as MenuHost).addMenuProvider(
|
||||
menuProvider,
|
||||
viewLifecycleOwner,
|
||||
Lifecycle.State.RESUMED
|
||||
)
|
||||
|
||||
swipeRefresh = view.findViewById(R.id.chat_refresh)
|
||||
swipeRefresh?.isEnabled = false
|
||||
messageEditText = view.findViewById(R.id.chat_edittext)
|
||||
sendButton = view.findViewById(R.id.chat_send)
|
||||
sendButton.setOnClickListener { sendMessage() }
|
||||
chatListView = view.findViewById(R.id.chat_entries_list)
|
||||
chatListView.transcriptMode = ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL
|
||||
chatListView.isStackFromBottom = true
|
||||
val serverName = activeServerProvider.getActiveServer().name
|
||||
val userName = activeServerProvider.getActiveServer().userName
|
||||
val title = String.format(
|
||||
Locale.ROOT,
|
||||
"%s [%s@%s]",
|
||||
resources.getString(R.string.button_bar_chat),
|
||||
userName,
|
||||
serverName
|
||||
)
|
||||
setTitle(this, title)
|
||||
messageEditText.imeOptions = EditorInfo.IME_ACTION_SEND
|
||||
messageEditText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
sendButton.isEnabled = !isNullOrWhiteSpace(editable.toString())
|
||||
}
|
||||
})
|
||||
messageEditText.setOnEditorActionListener(
|
||||
OnEditorActionListener {
|
||||
_: TextView?,
|
||||
actionId: Int,
|
||||
event: KeyEvent ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
||||
(actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN)
|
||||
) {
|
||||
sendMessage()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
)
|
||||
load()
|
||||
timerMethod()
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.chat, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
if (menuItem.itemId == R.id.menu_refresh) {
|
||||
load()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
chatViewModel.chatMessages.observe(viewLifecycleOwner) { messages ->
|
||||
if (!messages.isNullOrEmpty()) {
|
||||
val chatAdapter: ListAdapter = ChatAdapter(requireContext(), messages)
|
||||
chatListView.adapter = chatAdapter
|
||||
}
|
||||
}
|
||||
if (timer == null) {
|
||||
timerMethod()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
}
|
||||
|
||||
private fun timerMethod() {
|
||||
val refreshInterval = Settings.chatRefreshInterval
|
||||
if (refreshInterval > 0) {
|
||||
timer = Timer()
|
||||
timer?.schedule(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
requireActivity().runOnUiThread { load() }
|
||||
}
|
||||
},
|
||||
refreshInterval.toLong(), refreshInterval.toLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessage() {
|
||||
val text = messageEditText.text ?: return
|
||||
val message = text.toString()
|
||||
if (!isNullOrWhiteSpace(message)) {
|
||||
messageEditText.setText("")
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
musicService.addChatMessage(message)
|
||||
}
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun load() {
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
musicService.getChatMessages(chatViewModel.lastChatMessageTime)?.filterNotNull()
|
||||
}
|
||||
swipeRefresh?.isRefreshing = false
|
||||
if (!result.isNullOrEmpty()) {
|
||||
for (message in result) {
|
||||
if (message.time > chatViewModel.lastChatMessageTime) {
|
||||
chatViewModel.lastChatMessageTime = message.time
|
||||
}
|
||||
}
|
||||
chatViewModel.updateChatMessages(result.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,35 +13,34 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Lyrics
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Displays the lyrics of a song
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class LyricsFragment : Fragment() {
|
||||
class LyricsFragment : Fragment(), RefreshableFragment {
|
||||
private var artistView: TextView? = null
|
||||
private var titleView: TextView? = null
|
||||
private var textView: TextView? = null
|
||||
private var swipe: SwipeRefreshLayout? = null
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
|
||||
private val navArgs by navArgs<LyricsFragmentArgs>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
applyTheme(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -53,42 +52,34 @@ class LyricsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.d("Lyrics set title")
|
||||
setTitle(this, R.string.download_menu_lyrics)
|
||||
swipe = view.findViewById(R.id.lyrics_refresh)
|
||||
swipe?.isEnabled = false
|
||||
swipeRefresh = view.findViewById(R.id.lyrics_refresh)
|
||||
swipeRefresh?.isEnabled = false
|
||||
artistView = view.findViewById(R.id.lyrics_artist)
|
||||
titleView = view.findViewById(R.id.lyrics_title)
|
||||
textView = view.findViewById(R.id.lyrics_text)
|
||||
load()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
val task: BackgroundTask<Lyrics> = object : FragmentBackgroundTask<Lyrics>(
|
||||
activity, true, swipe, cancellationToken
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Lyrics {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
return musicService.getLyrics(navArgs.artist, navArgs.title)!!
|
||||
musicService.getLyrics(navArgs.artist, navArgs.title)!!
|
||||
}
|
||||
|
||||
override fun done(result: Lyrics) {
|
||||
swipeRefresh?.isRefreshing = false
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result.artist != null) {
|
||||
artistView!!.text = result.artist
|
||||
titleView!!.text = result.title
|
||||
textView!!.text = result.text
|
||||
artistView?.text = result.artist
|
||||
titleView?.text = result.title
|
||||
textView?.text = result.text
|
||||
} else {
|
||||
artistView!!.setText(R.string.lyrics_nomatch)
|
||||
artistView?.setText(R.string.lyrics_nomatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
}
|
||||
|
@ -25,48 +25,46 @@ import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.androidx.scope.ScopeFragment
|
||||
import org.koin.core.component.KoinScopeComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Playlist
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.OfflineException
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CacheCleaner
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||
import org.moire.ultrasonic.util.DownloadAction
|
||||
import org.moire.ultrasonic.util.DownloadUtil
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.InfoDialog
|
||||
import org.moire.ultrasonic.util.LoadingTask
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
|
||||
/**
|
||||
* Displays the playlists stored on the server
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
private var refreshPlaylistsListView: SwipeRefreshLayout? = null
|
||||
@Suppress("InstanceOfCheckForException")
|
||||
class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment {
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
private var playlistsListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var playlistAdapter: ArrayAdapter<Playlist>? = null
|
||||
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
applyTheme(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -78,17 +76,16 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh)
|
||||
swipeRefresh = view.findViewById(R.id.select_playlist_refresh)
|
||||
playlistsListView = view.findViewById(R.id.select_playlist_list)
|
||||
refreshPlaylistsListView!!.setOnRefreshListener { load(true) }
|
||||
swipeRefresh?.setOnRefreshListener { load(true) }
|
||||
emptyTextView = view.findViewById(R.id.select_playlist_empty)
|
||||
playlistsListView!!.setOnItemClickListener { parent, _, position, _ ->
|
||||
val (id1, name) = parent.getItemAtPosition(position) as Playlist
|
||||
playlistsListView?.setOnItemClickListener { parent, _, position, _ ->
|
||||
val (id, name) = parent.getItemAtPosition(position) as Playlist
|
||||
|
||||
val action = NavigationGraphDirections.toTrackCollection(
|
||||
id = id1,
|
||||
playlistId = id1,
|
||||
id = id,
|
||||
playlistId = id,
|
||||
name = name,
|
||||
playlistName = name,
|
||||
)
|
||||
@ -99,33 +96,25 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<Playlist>> =
|
||||
object : FragmentBackgroundTask<List<Playlist>>(
|
||||
activity, true, refreshPlaylistsListView, cancellationToken
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): List<Playlist> {
|
||||
val musicService = getMusicService()
|
||||
val playlists = musicService.getPlaylists(refresh)
|
||||
val cacheCleaner: CacheCleaner by inject()
|
||||
if (!isOffline()) cacheCleaner.cleanPlaylists(playlists)
|
||||
return playlists
|
||||
}
|
||||
|
||||
override fun done(result: List<Playlist>) {
|
||||
playlistAdapter =
|
||||
ArrayAdapter(requireContext(), R.layout.list_item_generic, result)
|
||||
playlistsListView!!.adapter = playlistAdapter
|
||||
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
val cacheCleaner: CacheCleaner by inject()
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
val playlists = musicService.getPlaylists(refresh)
|
||||
playlists
|
||||
}
|
||||
task.execute()
|
||||
swipeRefresh?.isRefreshing = false
|
||||
withContext(Dispatchers.Main) {
|
||||
playlistAdapter =
|
||||
ArrayAdapter(requireContext(), R.layout.list_item_generic, result)
|
||||
playlistsListView?.adapter = playlistAdapter
|
||||
emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
if (!isOffline()) cacheCleaner.cleanPlaylists(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
||||
@ -143,7 +132,7 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
|
||||
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
||||
val info = menuItem.menuInfo as AdapterContextMenuInfo
|
||||
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
|
||||
val playlist = playlistsListView?.getItemAtPosition(info.position) as Playlist
|
||||
when (menuItem.itemId) {
|
||||
R.id.playlist_menu_pin -> {
|
||||
DownloadUtil.justDownload(
|
||||
@ -214,57 +203,45 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
.setTitle(R.string.common_confirm).setMessage(
|
||||
resources.getString(R.string.delete_playlist, playlist.name)
|
||||
).setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshPlaylistsListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler(
|
||||
resources.getString(
|
||||
R.string.menu_deleted_playlist_error,
|
||||
playlist.name
|
||||
)
|
||||
)
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
musicService.deletePlaylist(playlist.id)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
playlistAdapter!!.remove(playlist)
|
||||
playlistAdapter!!.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
playlistAdapter?.remove(playlist)
|
||||
playlistAdapter?.notifyDataSetChanged()
|
||||
toast(
|
||||
resources.getString(R.string.menu_deleted_playlist, playlist.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException)
|
||||
getErrorMessage(
|
||||
error
|
||||
) else String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.menu_deleted_playlist_error,
|
||||
playlist.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||
}
|
||||
|
||||
private fun displayPlaylistInfo(playlist: Playlist) {
|
||||
val textView = TextView(context)
|
||||
val textView = TextView(requireContext())
|
||||
textView.setPadding(5, 5, 5, 5)
|
||||
val message: Spannable = SpannableString(
|
||||
"""
|
||||
Owner: ${playlist.owner}
|
||||
Comments: ${playlist.comment}
|
||||
Song Count: ${playlist.songCount}
|
||||
Owner: ${playlist.owner}
|
||||
Comments: ${playlist.comment}
|
||||
Song Count: ${playlist.songCount}
|
||||
""".trimIndent() +
|
||||
if (playlist.public == null) "" else """
|
||||
|
||||
Public: ${playlist.public}
|
||||
|
||||
Public: ${playlist.public}
|
||||
""".trimIndent() + """
|
||||
|
||||
Creation Date: ${playlist.created.replace('T', ' ')}
|
||||
|
||||
Creation Date: ${playlist.created.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
)
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||
@ -293,42 +270,31 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
alertDialog.setTitle(R.string.playlist_update_info)
|
||||
alertDialog.setView(dialogView)
|
||||
alertDialog.setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshPlaylistsListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
val nameBoxText = nameBox.text
|
||||
val commentBoxText = commentBox.text
|
||||
val name = nameBoxText?.toString()
|
||||
val comment = commentBoxText?.toString()
|
||||
val musicService = getMusicService()
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler(
|
||||
resources.getString(
|
||||
R.string.playlist_updated_info_error,
|
||||
playlist.name
|
||||
)
|
||||
)
|
||||
) {
|
||||
val nameBoxText = nameBox.text
|
||||
val commentBoxText = commentBox.text
|
||||
val name = nameBoxText?.toString()
|
||||
val comment = commentBoxText?.toString()
|
||||
val musicService = getMusicService()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
musicService.updatePlaylist(playlist.id, name, comment, publicBox.isChecked)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
withContext(Dispatchers.Main) {
|
||||
load(true)
|
||||
toast(
|
||||
resources.getString(R.string.playlist_updated_info, playlist.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException)
|
||||
getErrorMessage(
|
||||
error
|
||||
) else String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.playlist_updated_info_error,
|
||||
playlist.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
}
|
||||
alertDialog.setNegativeButton(R.string.common_cancel, null)
|
||||
alertDialog.show()
|
||||
|
@ -13,35 +13,33 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.androidx.scope.ScopeFragment
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.PodcastsChannel
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
|
||||
/**
|
||||
* Displays the podcasts available on the server
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
* TODO: Use Coroutines
|
||||
*/
|
||||
class PodcastFragment : Fragment() {
|
||||
|
||||
class PodcastFragment : ScopeFragment(), RefreshableFragment {
|
||||
private var emptyTextView: View? = null
|
||||
var channelItemsListView: ListView? = null
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
private var swipeRefresh: SwipeRefreshLayout? = null
|
||||
private var channelItemsListView: ListView? = null
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
applyTheme(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -54,45 +52,33 @@ class PodcastFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
cancellationToken = CancellationToken()
|
||||
swipeRefresh = view.findViewById(R.id.podcasts_refresh)
|
||||
swipeRefresh!!.setOnRefreshListener { load(true) }
|
||||
swipeRefresh?.setOnRefreshListener { load(true) }
|
||||
setTitle(this, R.string.podcasts_label)
|
||||
emptyTextView = view.findViewById(R.id.select_podcasts_empty)
|
||||
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list)
|
||||
channelItemsListView!!.setOnItemClickListener { parent, _, position, _ ->
|
||||
val (id) = parent.getItemAtPosition(position) as PodcastsChannel
|
||||
val action = NavigationGraphDirections.toTrackCollection(
|
||||
podcastChannelId = id
|
||||
)
|
||||
|
||||
channelItemsListView?.setOnItemClickListener { parent, _, position, _ ->
|
||||
val id = (parent.getItemAtPosition(position) as PodcastsChannel).id
|
||||
val action = NavigationGraphDirections.toTrackCollection(podcastChannelId = id)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<PodcastsChannel>> =
|
||||
object : FragmentBackgroundTask<List<PodcastsChannel>>(
|
||||
activity, true, swipeRefresh, cancellationToken
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): List<PodcastsChannel> {
|
||||
val musicService = getMusicService()
|
||||
return musicService.getPodcastsChannels(refresh)
|
||||
}
|
||||
|
||||
override fun done(result: List<PodcastsChannel>) {
|
||||
channelItemsListView!!.adapter =
|
||||
ArrayAdapter(requireContext(), R.layout.list_item_generic, result)
|
||||
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
musicService.getPodcastsChannels(refresh)
|
||||
}
|
||||
task.execute()
|
||||
swipeRefresh?.isRefreshing = false
|
||||
withContext(Dispatchers.Main) {
|
||||
channelItemsListView?.adapter =
|
||||
ArrayAdapter(requireContext(), R.layout.list_item_generic, result)
|
||||
emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,3 @@
|
||||
/*
|
||||
* SelectGenreFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
@ -15,35 +8,34 @@ import android.widget.AdapterView
|
||||
import android.widget.ListView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.Settings.maxSongs
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
import org.moire.ultrasonic.view.GenreAdapter
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Displays the available genres in the media library
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class SelectGenreFragment : Fragment() {
|
||||
private var refreshGenreListView: SwipeRefreshLayout? = null
|
||||
class SelectGenreFragment : Fragment(), RefreshableFragment {
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
private var genreListView: ListView? = null
|
||||
private var emptyView: View? = null
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
applyTheme(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -55,15 +47,15 @@ class SelectGenreFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
refreshGenreListView = view.findViewById(R.id.select_genre_refresh)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
swipeRefresh = view.findViewById(R.id.select_genre_refresh)
|
||||
genreListView = view.findViewById(R.id.select_genre_list)
|
||||
refreshGenreListView!!.setOnRefreshListener { load(true) }
|
||||
swipeRefresh?.setOnRefreshListener { load(true) }
|
||||
|
||||
genreListView!!.setOnItemClickListener { parent: AdapterView<*>,
|
||||
_: View?,
|
||||
position: Int,
|
||||
_: Long ->
|
||||
genreListView?.setOnItemClickListener {
|
||||
parent: AdapterView<*>, _: View?,
|
||||
position: Int, _: Long
|
||||
->
|
||||
val genre = parent.getItemAtPosition(position) as Genre
|
||||
|
||||
val action = NavigationGraphDirections.toTrackCollection(
|
||||
@ -79,34 +71,19 @@ class SelectGenreFragment : Fragment() {
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
// TODO: Migrate to Coroutines
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<Genre>> = object : FragmentBackgroundTask<List<Genre>>(
|
||||
activity, true, refreshGenreListView, cancellationToken
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
override fun doInBackground(): List<Genre> {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val musicService = getMusicService()
|
||||
var genres: List<Genre> = ArrayList()
|
||||
try {
|
||||
genres = musicService.getGenres(refresh)
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all, "Failed to load genres")
|
||||
}
|
||||
return genres
|
||||
musicService.getGenres(refresh)
|
||||
}
|
||||
|
||||
override fun done(result: List<Genre>) {
|
||||
emptyView!!.isVisible = result.isEmpty()
|
||||
if (context != null) {
|
||||
genreListView!!.adapter = GenreAdapter(context!!, result)
|
||||
}
|
||||
swipeRefresh?.isRefreshing = false
|
||||
withContext(Dispatchers.Main) {
|
||||
emptyView?.isVisible = result.isEmpty()
|
||||
genreListView?.adapter = GenreAdapter(requireContext(), result)
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
}
|
||||
|
@ -24,29 +24,29 @@ import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.androidx.scope.ScopeFragment
|
||||
import org.koin.core.component.KoinScopeComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.domain.Share
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.OfflineException
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.DownloadAction
|
||||
import org.moire.ultrasonic.util.DownloadUtil
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.LoadingTask
|
||||
import org.moire.ultrasonic.util.RefreshableFragment
|
||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.util.launchWithToast
|
||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||
import org.moire.ultrasonic.view.ShareAdapter
|
||||
|
||||
/**
|
||||
@ -54,16 +54,16 @@ import org.moire.ultrasonic.view.ShareAdapter
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||
private var refreshSharesListView: SwipeRefreshLayout? = null
|
||||
class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment {
|
||||
override var swipeRefresh: SwipeRefreshLayout? = null
|
||||
private var sharesListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var shareAdapter: ShareAdapter? = null
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
Util.applyTheme(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -75,48 +75,41 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
refreshSharesListView = view.findViewById(R.id.select_share_refresh)
|
||||
swipeRefresh = view.findViewById(R.id.select_share_refresh)
|
||||
sharesListView = view.findViewById(R.id.select_share_list)
|
||||
refreshSharesListView!!.setOnRefreshListener { load(true) }
|
||||
swipeRefresh!!.setOnRefreshListener { load(true) }
|
||||
emptyTextView = view.findViewById(R.id.select_share_empty)
|
||||
sharesListView!!.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { parent, _, position, _ ->
|
||||
val share = parent.getItemAtPosition(position) as Share
|
||||
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
|
||||
parent, _,
|
||||
position, _ ->
|
||||
val share = parent.getItemAtPosition(position) as Share
|
||||
|
||||
val action = NavigationGraphDirections.toTrackCollection(
|
||||
shareId = share.id,
|
||||
shareName = share.name
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
val action = NavigationGraphDirections.toTrackCollection(
|
||||
shareId = share.id,
|
||||
shareName = share.name
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
registerForContextMenu(sharesListView!!)
|
||||
FragmentTitle.setTitle(this, R.string.button_bar_shares)
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<Share>> = object : FragmentBackgroundTask<List<Share>>(
|
||||
activity, true, refreshSharesListView, cancellationToken
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler()
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): List<Share> {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
return musicService.getShares(refresh)
|
||||
musicService.getShares(refresh)
|
||||
}
|
||||
|
||||
override fun done(result: List<Share>) {
|
||||
swipeRefresh?.isRefreshing = false
|
||||
withContext(Dispatchers.Main) {
|
||||
shareAdapter = ShareAdapter(requireContext(), result)
|
||||
sharesListView?.adapter = shareAdapter
|
||||
emptyTextView?.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
@ -202,45 +195,34 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||
.setTitle(R.string.common_confirm).setMessage(
|
||||
resources.getString(R.string.delete_playlist, share.name)
|
||||
).setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshSharesListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicService.deleteShare(share.id)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
shareAdapter!!.remove(share)
|
||||
shareAdapter!!.notifyDataSetChanged()
|
||||
toast(
|
||||
resources.getString(R.string.menu_deleted_share, share.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException) {
|
||||
getErrorMessage(
|
||||
error
|
||||
)
|
||||
} else {
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.menu_deleted_share_error,
|
||||
share.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
}
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
deleteShareOnServer(share)
|
||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||
}
|
||||
|
||||
private fun deleteShareOnServer(share: Share) {
|
||||
viewLifecycleOwner.lifecycleScope.launch(
|
||||
toastingExceptionHandler(
|
||||
resources.getString(
|
||||
R.string.menu_deleted_share_error,
|
||||
share.name
|
||||
)
|
||||
)
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicService.deleteShare(share.id)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
shareAdapter?.remove(share)
|
||||
shareAdapter?.notifyDataSetChanged()
|
||||
toast(
|
||||
resources.getString(R.string.menu_deleted_share, share.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayShareInfo(share: Share) {
|
||||
val textView = TextView(context)
|
||||
textView.setPadding(5, 5, 5, 5)
|
||||
@ -255,18 +237,18 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||
(
|
||||
if (share.created == null) "" else """
|
||||
|
||||
Creation Date: ${share.created!!.replace('T', ' ')}
|
||||
Creation Date: ${share.created!!.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
) +
|
||||
(
|
||||
if (share.lastVisited == null) "" else """
|
||||
|
||||
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
||||
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
) +
|
||||
if (share.expires == null) "" else """
|
||||
|
||||
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
||||
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
)
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||
@ -297,49 +279,31 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||
alertDialog.setTitle(R.string.playlist_update_info)
|
||||
alertDialog.setView(dialogView)
|
||||
alertDialog.setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshSharesListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
var millis = timeSpanPicker.getTimeSpan()
|
||||
if (millis > 0) {
|
||||
millis += System.currentTimeMillis()
|
||||
}
|
||||
val shareDescriptionText = shareDescription.text
|
||||
val description = shareDescriptionText?.toString()
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicService.updateShare(share.id, description, millis)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
load(true)
|
||||
toast(
|
||||
resources.getString(R.string.playlist_updated_info, share.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException) {
|
||||
getErrorMessage(
|
||||
error
|
||||
)
|
||||
} else {
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.playlist_updated_info_error,
|
||||
share.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
}
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
var millis = timeSpanPicker.getTimeSpan()
|
||||
if (millis > 0) {
|
||||
millis += System.currentTimeMillis()
|
||||
}
|
||||
updateShareOnServer(millis, shareDescription.text.toString(), share)
|
||||
}
|
||||
alertDialog.setNegativeButton(R.string.common_cancel, null)
|
||||
alertDialog.show()
|
||||
}
|
||||
|
||||
private fun updateShareOnServer(
|
||||
millis: Long,
|
||||
description: String,
|
||||
share: Share
|
||||
) {
|
||||
launchWithToast {
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicService.updateShare(share.id, description, millis)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
load(true)
|
||||
resources.getString(R.string.playlist_updated_info, share.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,6 +197,7 @@ class ImageLoader(
|
||||
* Download a cover art file of a Track and cache it on disk
|
||||
*/
|
||||
fun downloadCoverArt(track: Track) {
|
||||
if (track.coverArt == null) return
|
||||
downloadCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track))
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* ChatViewModel.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.model
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.moire.ultrasonic.domain.ChatMessage
|
||||
|
||||
class ChatViewModel : ViewModel() {
|
||||
|
||||
// MutableLiveData to store chat messages
|
||||
private val _chatMessages = MutableLiveData<List<ChatMessage>>()
|
||||
|
||||
// LiveData to observe chat messages
|
||||
val chatMessages: LiveData<List<ChatMessage>>
|
||||
get() = _chatMessages
|
||||
|
||||
// Last chat message time
|
||||
var lastChatMessageTime: Long = 0
|
||||
|
||||
// Function to update chat messages
|
||||
fun updateChatMessages(messages: List<ChatMessage>) {
|
||||
val updatedMessages = _chatMessages.value.orEmpty() + messages
|
||||
_chatMessages.postValue(updatedMessages)
|
||||
}
|
||||
}
|
@ -14,17 +14,18 @@ class SearchListModel(application: Application) : GenericListModel(application)
|
||||
|
||||
var searchResult: MutableLiveData<SearchResult?> = MutableLiveData()
|
||||
|
||||
suspend fun search(query: String) {
|
||||
suspend fun search(query: String): SearchResult? {
|
||||
val maxArtists = Settings.maxArtists
|
||||
val maxAlbums = Settings.maxAlbums
|
||||
val maxSongs = Settings.maxSongs
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val criteria = SearchCriteria(query, maxArtists, maxAlbums, maxSongs)
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
val result = service.search(criteria)
|
||||
|
||||
if (result != null) searchResult.postValue(result)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,7 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
|
||||
Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile)
|
||||
|
||||
// TODO: Check if the dependency on the image loader could be removed.
|
||||
// TODO: This method can be called outside of our regular lifecycle, where Koin might not exist yet
|
||||
imageLoaderProvider.executeOn {
|
||||
it.downloadCoverArt(parts[0], albumArtFile)
|
||||
}
|
||||
|
@ -510,12 +510,16 @@ class DownloadService : Service(), KoinComponent {
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
val context = UApp.applicationContext()
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
try {
|
||||
val context = UApp.applicationContext()
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
Timber.w(e, "Failed to start download service: the app is in the background")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,7 +256,7 @@ class DownloadTask(
|
||||
|
||||
// Download the largest size that we can display in the UI
|
||||
imageLoaderProvider.executeOn { imageLoader ->
|
||||
imageLoader.downloadCoverArt(this)
|
||||
imageLoader.downloadCoverArt(this@cacheMetadataAndArtwork)
|
||||
// Cache small copies of the Artist picture
|
||||
directArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||
compilationArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||
|
@ -11,7 +11,6 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
@ -502,9 +501,8 @@ class MediaPlayerManager(
|
||||
shuffle: Boolean = false,
|
||||
isArtist: Boolean = false
|
||||
) {
|
||||
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
|
||||
|
||||
scope.launchWithToast {
|
||||
fragment.launchWithToast {
|
||||
|
||||
val list: List<Track> =
|
||||
tracks.ifEmpty {
|
||||
|
@ -12,6 +12,7 @@ import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.imageloader.ImageLoaderConfig
|
||||
import org.moire.ultrasonic.util.CoroutinePatterns
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
@ -55,7 +56,7 @@ class ImageLoaderProvider :
|
||||
}
|
||||
|
||||
fun executeOn(cb: (iL: ImageLoader) -> Unit) {
|
||||
launch {
|
||||
launch(CoroutinePatterns.loggingExceptionHandler) {
|
||||
val iL = getImageLoader()
|
||||
withContext(Dispatchers.Main) {
|
||||
cb(iL)
|
||||
|
@ -68,14 +68,14 @@ class ShareHandler {
|
||||
|
||||
val ids: MutableList<String> = ArrayList()
|
||||
|
||||
if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1)
|
||||
if (!shareDetails.shareOnServer && shareDetails.entries.size == 1)
|
||||
return@withContext null
|
||||
if (shareDetails.Entries.isEmpty()) {
|
||||
if (shareDetails.entries.isEmpty()) {
|
||||
additionalId.ifNotNull {
|
||||
ids.add(it)
|
||||
}
|
||||
} else {
|
||||
for ((id) in shareDetails.Entries) {
|
||||
for ((id) in shareDetails.entries) {
|
||||
ids.add(id)
|
||||
}
|
||||
}
|
||||
@ -83,15 +83,21 @@ class ShareHandler {
|
||||
val musicService = getMusicService()
|
||||
var timeInMillis: Long = 0
|
||||
|
||||
if (shareDetails.Expiration != 0L) {
|
||||
timeInMillis = shareDetails.Expiration
|
||||
if (shareDetails.expiration != 0L) {
|
||||
timeInMillis = shareDetails.expiration
|
||||
}
|
||||
|
||||
val shares =
|
||||
musicService.createShare(ids, shareDetails.Description, timeInMillis)
|
||||
val shares = musicService.createShare(
|
||||
ids = ids,
|
||||
description = shareDetails.description,
|
||||
expires = timeInMillis
|
||||
)
|
||||
|
||||
// Return the share
|
||||
shares[0]
|
||||
if (shares.isNotEmpty())
|
||||
shares[0]
|
||||
else
|
||||
null
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
@ -120,15 +126,15 @@ class ShareHandler {
|
||||
val textBuilder = StringBuilder()
|
||||
textBuilder.appendLine(Settings.shareGreeting)
|
||||
|
||||
if (!shareDetails.Entries[0].title.isNullOrEmpty())
|
||||
if (!shareDetails.entries[0].title.isNullOrEmpty())
|
||||
textBuilder.append(getString(R.string.common_title))
|
||||
.append(": ").appendLine(shareDetails.Entries[0].title)
|
||||
if (!shareDetails.Entries[0].artist.isNullOrEmpty())
|
||||
.append(": ").appendLine(shareDetails.entries[0].title)
|
||||
if (!shareDetails.entries[0].artist.isNullOrEmpty())
|
||||
textBuilder.append(getString(R.string.common_artist))
|
||||
.append(": ").appendLine(shareDetails.Entries[0].artist)
|
||||
if (!shareDetails.Entries[0].album.isNullOrEmpty())
|
||||
.append(": ").appendLine(shareDetails.entries[0].artist)
|
||||
if (!shareDetails.entries[0].album.isNullOrEmpty())
|
||||
textBuilder.append(getString(R.string.common_album))
|
||||
.append(": ").append(shareDetails.Entries[0].album)
|
||||
.append(": ").append(shareDetails.entries[0].album)
|
||||
|
||||
intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString())
|
||||
}
|
||||
@ -144,17 +150,17 @@ class ShareHandler {
|
||||
|
||||
fun createShare(
|
||||
fragment: Fragment,
|
||||
tracks: List<Track?>?,
|
||||
tracks: List<Track>,
|
||||
additionalId: String? = null
|
||||
) {
|
||||
if (tracks.isEmpty()) return
|
||||
val askForDetails = Settings.shouldAskForShareDetails
|
||||
val shareDetails = ShareDetails()
|
||||
shareDetails.Entries = tracks
|
||||
val shareDetails = ShareDetails(tracks)
|
||||
if (askForDetails) {
|
||||
showDialog(fragment, shareDetails, additionalId)
|
||||
} else {
|
||||
shareDetails.Description = Settings.defaultShareDescription
|
||||
shareDetails.Expiration = System.currentTimeMillis() +
|
||||
shareDetails.description = Settings.defaultShareDescription
|
||||
shareDetails.expiration = System.currentTimeMillis() +
|
||||
Settings.defaultShareExpirationInMillis
|
||||
share(fragment, shareDetails, additionalId)
|
||||
}
|
||||
@ -177,12 +183,12 @@ class ShareHandler {
|
||||
noExpirationCheckBox = timeSpanPicker!!.findViewById<View>(
|
||||
R.id.timeSpanDisableCheckBox
|
||||
) as CheckBox
|
||||
textViewComment = layout.findViewById<View>(R.id.textViewComment) as TextView
|
||||
textViewExpiration = layout.findViewById<View>(R.id.textViewExpiration) as TextView
|
||||
textViewComment = layout.findViewById<View>(R.id.commentHeading) as TextView
|
||||
textViewExpiration = layout.findViewById<View>(R.id.expirationHeading) as TextView
|
||||
}
|
||||
|
||||
// Handle the visibility based on shareDetails.Entries size
|
||||
if (shareDetails.Entries.size == 1) {
|
||||
if (shareDetails.entries.size == 1) {
|
||||
shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ ->
|
||||
updateVisibility()
|
||||
}
|
||||
@ -238,11 +244,11 @@ class ShareHandler {
|
||||
builder.setPositiveButton(R.string.menu_share) { _, _ ->
|
||||
if (!noExpirationCheckBox!!.isChecked) {
|
||||
val timeSpan: Long = timeSpanPicker!!.getTimeSpan()
|
||||
shareDetails.Expiration = System.currentTimeMillis() + timeSpan
|
||||
shareDetails.expiration = System.currentTimeMillis() + timeSpan
|
||||
}
|
||||
|
||||
shareDetails.Description = shareDescription!!.text.toString()
|
||||
shareDetails.ShareOnServer = shareOnServerCheckBox!!.isChecked
|
||||
shareDetails.description = shareDescription!!.text.toString()
|
||||
shareDetails.shareOnServer = shareOnServerCheckBox!!.isChecked
|
||||
|
||||
if (hideDialogCheckBox!!.isChecked) {
|
||||
Settings.shouldAskForShareDetails = false
|
||||
@ -255,8 +261,8 @@ class ShareHandler {
|
||||
if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0)
|
||||
String.format("%d:%s", timeSpanAmount, timeSpanType) else ""
|
||||
|
||||
Settings.defaultShareDescription = shareDetails.Description
|
||||
Settings.shareOnServer = shareDetails.ShareOnServer
|
||||
Settings.defaultShareDescription = shareDetails.description!!
|
||||
Settings.shareOnServer = shareDetails.shareOnServer
|
||||
}
|
||||
|
||||
share(fragment, shareDetails, additionalId)
|
||||
|
@ -8,7 +8,9 @@ package org.moire.ultrasonic.util
|
||||
|
||||
/**
|
||||
* This class contains a very simple implementation of a CancellationToken
|
||||
*/
|
||||
* TODO: Remove this class and refactor all user to coroutines
|
||||
**/
|
||||
|
||||
class CancellationToken {
|
||||
var isCancellationRequested: Boolean = false
|
||||
|
||||
|
@ -130,8 +130,7 @@ object ContextMenuUtil : KoinComponent {
|
||||
val shareHandler: ShareHandler by inject()
|
||||
shareHandler.createShare(
|
||||
fragment = fragment,
|
||||
tracks = tracks,
|
||||
additionalId = null
|
||||
tracks = tracks
|
||||
)
|
||||
}
|
||||
else -> return false
|
||||
|
@ -9,13 +9,16 @@ package org.moire.ultrasonic.util
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.util.CommunicationError.getErrorMessage
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import timber.log.Timber
|
||||
|
||||
object CoroutinePatterns {
|
||||
@ -28,24 +31,43 @@ object CoroutinePatterns {
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchWithToast(
|
||||
fun Fragment.toastingExceptionHandler(
|
||||
prefix: String = ""
|
||||
): CoroutineExceptionHandler {
|
||||
return CoroutineExceptionHandler { _, exception ->
|
||||
// Stop the spinner if applicable
|
||||
if (this is RefreshableFragment) {
|
||||
this.swipeRefresh?.isRefreshing = false
|
||||
}
|
||||
toast("$prefix ${getErrorMessage(exception)}", shortDuration = false)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Launch a coroutine with a toast
|
||||
* This extension can be only started from a fragment
|
||||
* because it needs the fragments scope to create the toast
|
||||
*/
|
||||
fun Fragment.launchWithToast(
|
||||
block: suspend CoroutineScope.() -> String?
|
||||
) {
|
||||
// Get the scope
|
||||
val scope = activity?.lifecycleScope ?: lifecycleScope
|
||||
|
||||
// Launch the Job
|
||||
val deferred = async(CoroutinePatterns.loggingExceptionHandler, block = block)
|
||||
val deferred = scope.async(block = block)
|
||||
|
||||
// Setup a handler when the job is done
|
||||
deferred.invokeOnCompletion {
|
||||
val toastString = if (it != null && it !is CancellationException) {
|
||||
CommunicationError.getErrorMessage(it)
|
||||
getErrorMessage(it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val successString = toastString ?: deferred.await()
|
||||
if (successString != null) {
|
||||
Util.toast(successString, UApp.applicationContext())
|
||||
this@launchWithToast.toast(successString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import java.util.LinkedList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -36,10 +35,8 @@ object DownloadUtil {
|
||||
tracks: List<Track>? = null
|
||||
) {
|
||||
|
||||
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
|
||||
|
||||
// Launch the Job
|
||||
scope.launchWithToast {
|
||||
fragment.launchWithToast {
|
||||
val tracksToDownload: List<Track> = tracks
|
||||
?: getTracksFromServerAsync(isArtist, id!!, isDirectory, name, isShare)
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* RefreshableFragment.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
||||
interface RefreshableFragment {
|
||||
var swipeRefresh: SwipeRefreshLayout?
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
|
||||
/**
|
||||
* Created by Josh on 12/17/13.
|
||||
*/
|
||||
data class ShareDetails(val entries: List<Track>) {
|
||||
var description: String? = null
|
||||
var shareOnServer = false
|
||||
var expiration: Long = 0
|
||||
}
|
@ -31,7 +31,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty
|
||||
AdapterView.OnItemSelectedListener {
|
||||
private val timeSpanEditText: EditText
|
||||
private val timeSpanSpinner: Spinner
|
||||
val timeSpanDisableCheckbox: CheckBox
|
||||
private val timeSpanDisableCheckbox: CheckBox
|
||||
private var mTimeSpan: Long = -1L
|
||||
private val adapter: ArrayAdapter<CharSequence>
|
||||
private val dialog: View
|
||||
@ -49,7 +49,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty
|
||||
dialog = inflater.inflate(R.layout.time_span_dialog, this, true)
|
||||
timeSpanEditText = dialog.findViewById<View>(R.id.timeSpanEditText) as EditText
|
||||
timeSpanEditText.setText("0")
|
||||
timeSpanSpinner = dialog.findViewById<View>(R.id.timeSpanSpinner) as Spinner
|
||||
timeSpanSpinner = dialog.findViewById<View>(R.id.timeSpanUnitSelector) as Spinner
|
||||
timeSpanDisableCheckbox =
|
||||
dialog.findViewById<View>(R.id.timeSpanDisableCheckBox) as CheckBox
|
||||
timeSpanDisableCheckbox.setOnCheckedChangeListener { _, b ->
|
||||
@ -128,7 +128,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty
|
||||
companion object {
|
||||
fun getTimeSpanFromDialog(context: Context, dialog: View): Long {
|
||||
val timeSpanEditText = dialog.findViewById<View>(R.id.timeSpanEditText) as EditText
|
||||
val timeSpanSpinner = dialog.findViewById<View>(R.id.timeSpanSpinner) as Spinner
|
||||
val timeSpanSpinner = dialog.findViewById<View>(R.id.timeSpanUnitSelector) as Spinner
|
||||
val timeSpanType = timeSpanSpinner.selectedItem as String
|
||||
Timber.i("SELECTED ITEM: %d", timeSpanSpinner.selectedItemId)
|
||||
val text = timeSpanEditText.text
|
||||
|
@ -0,0 +1,27 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.DialogPreference
|
||||
import org.moire.ultrasonic.R
|
||||
|
||||
/**
|
||||
* Created by Joshua Bahnsen on 12/22/13.
|
||||
*/
|
||||
class TimeSpanPreference(mContext: Context, attrs: AttributeSet?) : DialogPreference(
|
||||
mContext, attrs
|
||||
) {
|
||||
init {
|
||||
setPositiveButtonText(android.R.string.ok)
|
||||
setNegativeButtonText(android.R.string.cancel)
|
||||
dialogIcon = null
|
||||
}
|
||||
|
||||
val text: String
|
||||
get() {
|
||||
val persisted = getPersistedString("")
|
||||
return if ("" != persisted) {
|
||||
persisted.replace(':', ' ')
|
||||
} else context.resources.getString(R.string.time_span_disabled)
|
||||
}
|
||||
}
|
@ -30,7 +30,6 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.text.TextUtils
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Gravity
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
@ -87,7 +86,6 @@ object Util {
|
||||
// Used by hexEncode()
|
||||
private val HEX_DIGITS =
|
||||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
|
||||
private var toast: Toast? = null
|
||||
|
||||
// Retrieves an instance of the application Context
|
||||
fun appContext(): Context {
|
||||
@ -145,19 +143,11 @@ object Util {
|
||||
// some background processing, our context might have expired!
|
||||
fun toast(message: CharSequence, shortDuration: Boolean, context: Context?) {
|
||||
try {
|
||||
if (toast == null) {
|
||||
toast = Toast.makeText(
|
||||
context,
|
||||
message,
|
||||
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
|
||||
)
|
||||
toast!!.setGravity(Gravity.CENTER, 0, 0)
|
||||
} else {
|
||||
toast!!.setText(message)
|
||||
toast!!.duration =
|
||||
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
|
||||
}
|
||||
toast!!.show()
|
||||
Toast.makeText(
|
||||
context,
|
||||
message,
|
||||
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
|
||||
).show()
|
||||
} catch (all: Exception) {
|
||||
Timber.w(all)
|
||||
}
|
||||
@ -277,15 +267,16 @@ object Util {
|
||||
* @param s The string to encode.
|
||||
* @return The encoded string.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
fun utf8HexEncode(s: String?): String? {
|
||||
if (s == null) {
|
||||
return null
|
||||
}
|
||||
val utf8: ByteArray = try {
|
||||
s.toByteArray(charset(Constants.UTF_8))
|
||||
} catch (x: UnsupportedEncodingException) {
|
||||
throw RuntimeException(x)
|
||||
} catch (all: UnsupportedEncodingException) {
|
||||
// TODO: Why is it needed to change the exception type here?
|
||||
throw RuntimeException(all)
|
||||
}
|
||||
return hexEncode(utf8)
|
||||
}
|
||||
@ -299,7 +290,7 @@ object Util {
|
||||
* @return A string containing hexadecimal characters.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun hexEncode(data: ByteArray): String {
|
||||
private fun hexEncode(data: ByteArray): String {
|
||||
val length = data.size
|
||||
val out = CharArray(length shl 1)
|
||||
var j = 0
|
||||
@ -319,15 +310,16 @@ object Util {
|
||||
* @return MD5 digest as a hex string.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
fun md5Hex(s: String?): String? {
|
||||
return if (s == null) {
|
||||
null
|
||||
} else try {
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
hexEncode(md5.digest(s.toByteArray(charset(Constants.UTF_8))))
|
||||
} catch (x: Exception) {
|
||||
throw RuntimeException(x.message, x)
|
||||
} catch (all: Exception) {
|
||||
// TODO: Why is it needed to change the exception type here?
|
||||
throw RuntimeException(all.message, all)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/chip_view_toggle"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_default="wrap">
|
||||
>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||
a:id="@+id/sort_order_menu_options"
|
||||
|
@ -58,6 +58,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/header_select_server"
|
||||
app:layout_constraintTop_toTopOf="@id/header_select_server"
|
||||
app:strokeColor="@null"
|
||||
app:tint="@color/selected_menu_dark" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,70 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ScrollView xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:id="@+id/share_details"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content">
|
||||
a:id="@+id/share_details"
|
||||
a:layout_width="match_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
a:layout_width="match_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:orientation="vertical"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content">
|
||||
a:padding="16dp">
|
||||
|
||||
<CheckBox
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
a:id="@+id/share_on_server"
|
||||
a:text="@string/share_on_server"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_marginTop="4dip"
|
||||
a:layout_marginBottom="4dip" />
|
||||
a:text="@string/share_on_server" />
|
||||
|
||||
<TextView
|
||||
a:id="@+id/commentHeading"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_marginTop="8dip"
|
||||
a:labelFor="@id/share_description"
|
||||
a:text="@string/share_comment"
|
||||
a:theme="@style/Ultrasonic.AllCapsLabel"
|
||||
a:id="@+id/textViewComment"
|
||||
a:layout_gravity="center_horizontal"/>
|
||||
a:theme="@style/Ultrasonic.AllCapsLabel" />
|
||||
|
||||
<EditText
|
||||
a:id="@+id/share_description"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:inputType="text"
|
||||
a:singleLine="false"
|
||||
a:layout_height="50dp"
|
||||
a:layout_marginTop="4dip"
|
||||
a:layout_marginBottom="4dip" />
|
||||
a:layout_marginBottom="16dip"
|
||||
a:inputType="text"
|
||||
a:singleLine="false" />
|
||||
|
||||
|
||||
<TextView
|
||||
a:id="@+id/expirationHeading"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:labelFor="@id/date_picker"
|
||||
a:text="@string/settings.share_expiration"
|
||||
a:theme="@style/Ultrasonic.AllCapsLabel"
|
||||
a:id="@+id/textViewExpiration"
|
||||
a:layout_gravity="center_horizontal"/>
|
||||
a:theme="@style/Ultrasonic.AllCapsLabel" />
|
||||
|
||||
|
||||
<org.moire.ultrasonic.util.TimeSpanPicker
|
||||
a:id="@+id/date_picker"
|
||||
a:layout_width="match_parent"
|
||||
a:layout_height="wrap_content" />
|
||||
|
||||
|
||||
<TextView
|
||||
a:id="@+id/optionsHeading"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_marginTop="4dip"
|
||||
a:layout_marginBottom="4dip" />
|
||||
a:layout_marginTop="16dip"
|
||||
a:labelFor="@id/share_description"
|
||||
a:text="@string/settings.share_options"
|
||||
a:theme="@style/Ultrasonic.AllCapsLabel" />
|
||||
|
||||
<CheckBox
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
a:id="@+id/hide_dialog"
|
||||
a:text="@string/do_not_show_dialog_again"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_marginTop="4dip"
|
||||
a:layout_marginBottom="4dip" />
|
||||
a:layout_marginTop="0dp"
|
||||
a:layout_marginBottom="0dp"
|
||||
a:text="@string/do_not_show_dialog_again" />
|
||||
|
||||
<CheckBox
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
a:id="@+id/save_as_defaults"
|
||||
a:text="@string/save_as_defaults"
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_marginTop="4dip"
|
||||
a:layout_marginBottom="4dip" />
|
||||
a:layout_marginTop="0dp"
|
||||
a:layout_marginBottom="8dp"
|
||||
a:text="@string/save_as_defaults" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
@ -3,11 +3,11 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<CheckBox
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<EditText
|
||||
android:id="@+id/timeSpanEditText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
@ -38,9 +38,10 @@
|
||||
android:inputType="numberSigned" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/timeSpanSpinner"
|
||||
android:id="@+id/timeSpanUnitSelector"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_alignTop="@+id/timeSpanEditText"
|
||||
android:layout_alignBottom="@+id/timeSpanEditText"
|
||||
android:layout_toEndOf="@+id/timeSpanEditText" />
|
||||
</RelativeLayout>
|
||||
|
@ -232,7 +232,7 @@
|
||||
android:name="org.moire.ultrasonic.fragment.BookmarksFragment" />
|
||||
<fragment
|
||||
android:id="@+id/chatFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.ChatFragment" />
|
||||
android:name="org.moire.ultrasonic.fragment.legacy.ChatFragment" />
|
||||
<fragment
|
||||
android:id="@+id/podcastFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment">
|
||||
|
@ -356,6 +356,7 @@
|
||||
<string name="save_as_defaults">Save as default</string>
|
||||
<string name="share_comment">Comment</string>
|
||||
<string name="settings.share_expiration">Time To Expiration</string>
|
||||
<string name="settings.share_options">Options</string>
|
||||
<string name="download_song_removed">\"%s\" was removed from playlist</string>
|
||||
<string name="download.share_playlist">Share Playlist</string>
|
||||
<string name="download.share_song">Share Current Song</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user