diff --git a/build.gradle b/build.gradle
index c44d7f84..efa20eed 100644
--- a/build.gradle
+++ b/build.gradle
@@ -17,6 +17,7 @@ buildscript {
         classpath libs.kotlin
         classpath libs.ktlintGradle
         classpath libs.detekt
+        classpath libs.navigationSafeArgs
     }
 }
 
diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt
index 6d58eb0d..8d399c2a 100644
--- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt
+++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt
@@ -16,7 +16,8 @@ enum class AlbumListType(val typeName: String) {
     SORTED_BY_ARTIST("alphabeticalByArtist"),
     STARRED("starred"),
     BY_YEAR("byYear"),
-    BY_GENRE("byGenre");
+    BY_GENRE("byGenre"),
+    BY_ARTIST("albumsByArtist");
 
     override fun toString(): String {
         return typeName
@@ -35,6 +36,7 @@ enum class AlbumListType(val typeName: String) {
             in STARRED.typeName -> STARRED
             in BY_YEAR.typeName -> BY_YEAR
             in BY_GENRE.typeName -> BY_GENRE
+            in BY_ARTIST.typeName -> BY_ARTIST
             else -> throw IllegalArgumentException("Unknown type: $typeName")
         }
 
diff --git a/detekt-baseline.xml b/detekt-baseline.xml
index 205ff394..b625cb3f 100644
--- a/detekt-baseline.xml
+++ b/detekt-baseline.xml
@@ -9,7 +9,9 @@
     <ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
     <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:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</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&lt;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>
@@ -19,7 +21,6 @@
     <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>TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) )</ID>
     <ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
     <ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
   </CurrentIssues>
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d6912a5c..b3fb3732 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -64,6 +64,7 @@ navigationUi            = { module = "androidx.navigation:navigation-ui", versio
 navigationFragmentKtx   = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
 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"}
 preferences             = { module = "androidx.preference:preference", version.ref = "preferences" }
 media                   = { module = "androidx.media:media", version.ref = "media" }
 media3exoplayer         = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle
index 16892319..bb6ae42f 100644
--- a/ultrasonic/build.gradle
+++ b/ultrasonic/build.gradle
@@ -1,6 +1,7 @@
 apply plugin: 'com.android.application'
 apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-kapt'
+apply plugin: "androidx.navigation.safeargs.kotlin"
 apply from: "../gradle_scripts/code_quality.gradle"
 
 android {
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java
deleted file mode 100644
index ba7e136e..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java
+++ /dev/null
@@ -1,342 +0,0 @@
-package org.moire.ultrasonic.fragment;
-
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.method.LinkMovementMethod;
-import android.text.util.Linkify;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.CheckBox;
-import android.widget.EditText;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.Navigation;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-
-import org.jetbrains.annotations.NotNull;
-import org.moire.ultrasonic.R;
-import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
-import org.moire.ultrasonic.data.ActiveServerProvider;
-import org.moire.ultrasonic.domain.Playlist;
-import org.moire.ultrasonic.service.MusicService;
-import org.moire.ultrasonic.service.MusicServiceFactory;
-import org.moire.ultrasonic.service.OfflineException;
-import org.moire.ultrasonic.subsonic.DownloadHandler;
-import org.moire.ultrasonic.util.BackgroundTask;
-import org.moire.ultrasonic.util.CacheCleaner;
-import org.moire.ultrasonic.util.CancellationToken;
-import org.moire.ultrasonic.util.Constants;
-import org.moire.ultrasonic.util.LoadingTask;
-import org.moire.ultrasonic.util.FragmentBackgroundTask;
-import org.moire.ultrasonic.util.Util;
-import org.moire.ultrasonic.view.PlaylistAdapter;
-
-import java.util.List;
-
-import kotlin.Lazy;
-
-import static org.koin.java.KoinJavaComponent.inject;
-
-/**
- * Displays the playlists stored on the server
- */
-public class PlaylistsFragment extends Fragment {
-
-    private SwipeRefreshLayout refreshPlaylistsListView;
-    private ListView playlistsListView;
-    private View emptyTextView;
-    private PlaylistAdapter playlistAdapter;
-
-    private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
-    private CancellationToken cancellationToken;
-
-    @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.select_playlist, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
-        cancellationToken = new CancellationToken();
-
-        refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh);
-        playlistsListView = view.findViewById(R.id.select_playlist_list);
-
-        refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
-        {
-            @Override
-            public void onRefresh() {
-                load(true);
-            }
-        });
-
-        emptyTextView = view.findViewById(R.id.select_playlist_empty);
-        playlistsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                Playlist playlist = (Playlist) parent.getItemAtPosition(position);
-
-                if (playlist == null)
-                {
-                    return;
-                }
-
-                Bundle bundle = new Bundle();
-                bundle.putString(Constants.INTENT_ID, playlist.getId());
-                bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId());
-                bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName());
-                Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
-            }
-        });
-        registerForContextMenu(playlistsListView);
-        FragmentTitle.Companion.setTitle(this, R.string.playlist_label);
-
-        load(false);
-    }
-
-    @Override
-    public void onDestroyView() {
-        cancellationToken.cancel();
-        super.onDestroyView();
-    }
-
-    private void load(final boolean refresh)
-    {
-        BackgroundTask<List<Playlist>> task = new FragmentBackgroundTask<List<Playlist>>(getActivity(), true, refreshPlaylistsListView, cancellationToken)
-        {
-            @Override
-            protected List<Playlist> doInBackground() throws Throwable
-            {
-                MusicService musicService = MusicServiceFactory.getMusicService();
-                List<Playlist> playlists = musicService.getPlaylists(refresh);
-
-                if (!ActiveServerProvider.Companion.isOffline())
-                    new CacheCleaner().cleanPlaylists(playlists);
-                return playlists;
-            }
-
-            @Override
-            protected void done(List<Playlist> result)
-            {
-                playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(getContext(), result));
-                emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
-            }
-        };
-        task.execute();
-    }
-
-    @Override
-    public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
-    {
-        super.onCreateContextMenu(menu, view, menuInfo);
-
-        MenuInflater inflater = getActivity().getMenuInflater();
-        if (ActiveServerProvider.Companion.isOffline()) inflater.inflate(R.menu.select_playlist_context_offline, menu);
-        else inflater.inflate(R.menu.select_playlist_context, menu);
-
-        MenuItem downloadMenuItem = menu.findItem(R.id.playlist_menu_download);
-
-        if (downloadMenuItem != null)
-        {
-            downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline());
-        }
-    }
-
-    @Override
-    public boolean onContextItemSelected(MenuItem menuItem)
-    {
-        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
-        if (info == null)
-        {
-            return false;
-        }
-
-        Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
-        if (playlist == null)
-        {
-            return false;
-        }
-
-        Bundle bundle;
-        int itemId = menuItem.getItemId();
-        if (itemId == R.id.playlist_menu_pin) {
-            downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), true, true, false, false, true, false, false);
-        } else if (itemId == R.id.playlist_menu_unpin) {
-            downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, true);
-        } else if (itemId == R.id.playlist_menu_download) {
-            downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, false);
-        } else if (itemId == R.id.playlist_menu_play_now) {
-            bundle = new Bundle();
-            bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId());
-            bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName());
-            bundle.putBoolean(Constants.INTENT_AUTOPLAY, true);
-            Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
-        } else if (itemId == R.id.playlist_menu_play_shuffled) {
-            bundle = new Bundle();
-            bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId());
-            bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName());
-            bundle.putBoolean(Constants.INTENT_AUTOPLAY, true);
-            bundle.putBoolean(Constants.INTENT_SHUFFLE, true);
-            Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
-        } else if (itemId == R.id.playlist_menu_delete) {
-            deletePlaylist(playlist);
-        } else if (itemId == R.id.playlist_info) {
-            displayPlaylistInfo(playlist);
-        } else if (itemId == R.id.playlist_update_info) {
-            updatePlaylistInfo(playlist);
-        } else {
-            return super.onContextItemSelected(menuItem);
-        }
-        return true;
-    }
-
-    private void deletePlaylist(final Playlist playlist)
-    {
-        new AlertDialog.Builder(getContext()).setIcon(R.drawable.ic_baseline_warning).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
-        {
-            @Override
-            public void onClick(DialogInterface dialog, int which)
-            {
-                new LoadingTask<Void>(getActivity(), refreshPlaylistsListView, cancellationToken)
-                {
-                    @Override
-                    protected Void doInBackground() throws Throwable
-                    {
-                        MusicService musicService = MusicServiceFactory.getMusicService();
-                        musicService.deletePlaylist(playlist.getId());
-                        return null;
-                    }
-
-                    @Override
-                    protected void done(Void result)
-                    {
-                        playlistAdapter.remove(playlist);
-                        playlistAdapter.notifyDataSetChanged();
-                        Util.toast(getContext(), getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
-                    }
-
-                    @Override
-                    protected void error(Throwable error)
-                    {
-                        String msg;
-                        msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error));
-
-                        Util.toast(getContext(), msg, false);
-                    }
-                }.execute();
-            }
-
-        }).setNegativeButton(R.string.common_cancel, null).show();
-    }
-
-    private void displayPlaylistInfo(final Playlist playlist)
-    {
-        final TextView textView = new TextView(getContext());
-        textView.setPadding(5, 5, 5, 5);
-
-        final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " +
-            ((playlist.getComment() == null) ? "" : playlist.getComment()) +
-            "\nSong Count: " + playlist.getSongCount() +
-            ((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' ')))));
-
-        Linkify.addLinks(message, Linkify.WEB_URLS);
-        textView.setText(message);
-        textView.setMovementMethod(LinkMovementMethod.getInstance());
-
-        new AlertDialog.Builder(getContext()).setTitle(playlist.getName()).setCancelable(true).setIcon(R.drawable.ic_baseline_info).setView(textView).show();
-    }
-
-    private void updatePlaylistInfo(final Playlist playlist)
-    {
-        View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
-
-        if (dialogView == null)
-        {
-            return;
-        }
-
-        final EditText nameBox = dialogView.findViewById(R.id.get_playlist_name);
-        final EditText commentBox = dialogView.findViewById(R.id.get_playlist_comment);
-        final CheckBox publicBox = dialogView.findViewById(R.id.get_playlist_public);
-
-        nameBox.setText(playlist.getName());
-        commentBox.setText(playlist.getComment());
-        Boolean pub = playlist.getPublic();
-
-        if (pub == null)
-        {
-            publicBox.setEnabled(false);
-        }
-        else
-        {
-            publicBox.setChecked(pub);
-        }
-
-        AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
-
-        alertDialog.setIcon(R.drawable.ic_baseline_warning);
-        alertDialog.setTitle(R.string.playlist_update_info);
-        alertDialog.setView(dialogView);
-        alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
-        {
-            @Override
-            public void onClick(DialogInterface dialog, int which)
-            {
-                new LoadingTask<Void>(getActivity(), refreshPlaylistsListView, cancellationToken)
-                {
-                    @Override
-                    protected Void doInBackground() throws Throwable
-                    {
-                        Editable nameBoxText = nameBox.getText();
-                        Editable commentBoxText = commentBox.getText();
-                        String name = nameBoxText != null ? nameBoxText.toString() : null;
-                        String comment = commentBoxText != null ? commentBoxText.toString() : null;
-
-                        MusicService musicService = MusicServiceFactory.getMusicService();
-                        musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked());
-                        return null;
-                    }
-
-                    @Override
-                    protected void done(Void result)
-                    {
-                        load(true);
-                        Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, playlist.getName()));
-                    }
-
-                    @Override
-                    protected void error(Throwable error)
-                    {
-                        String msg;
-                        msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error));
-
-                        Util.toast(getContext(), msg, false);
-                    }
-                }.execute();
-            }
-
-        });
-        alertDialog.setNegativeButton(R.string.common_cancel, null);
-        alertDialog.show();
-    }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java
deleted file mode 100644
index 00210dd0..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package org.moire.ultrasonic.fragment;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ListView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.Navigation;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-
-import org.moire.ultrasonic.R;
-import org.moire.ultrasonic.domain.PodcastsChannel;
-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.Constants;
-import org.moire.ultrasonic.util.FragmentBackgroundTask;
-import org.moire.ultrasonic.util.Util;
-import org.moire.ultrasonic.view.PodcastsChannelsAdapter;
-
-import java.util.List;
-
-/**
- * Displays the podcasts available on the server
- */
-public class PodcastFragment extends Fragment {
-
-    private View emptyTextView;
-    ListView channelItemsListView = null;
-    private CancellationToken cancellationToken;
-    private SwipeRefreshLayout swipeRefresh;
-
-    @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.podcasts, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-        cancellationToken = new CancellationToken();
-        swipeRefresh = view.findViewById(R.id.podcasts_refresh);
-        swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
-        {
-            @Override
-            public void onRefresh() {
-                load(view.getContext(), true);
-            }
-        });
-
-        FragmentTitle.Companion.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(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position);
-                if (pc == null) {
-                    return;
-                }
-
-                Bundle bundle = new Bundle();
-                bundle.putString(Constants.INTENT_PODCAST_CHANNEL_ID, pc.getId());
-                Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
-            }
-        });
-
-        load(view.getContext(), false);
-    }
-
-    @Override
-    public void onDestroyView() {
-        cancellationToken.cancel();
-        super.onDestroyView();
-    }
-
-    private void load(final Context context, final boolean refresh)
-    {
-        BackgroundTask<List<PodcastsChannel>> task = new FragmentBackgroundTask<List<PodcastsChannel>>(getActivity(), true, swipeRefresh, cancellationToken)
-        {
-            @Override
-            protected List<PodcastsChannel> doInBackground() throws Throwable
-            {
-                MusicService musicService = MusicServiceFactory.getMusicService();
-                return musicService.getPodcastsChannels(refresh);
-            }
-
-            @Override
-            protected void done(List<PodcastsChannel> result)
-            {
-                channelItemsListView.setAdapter(new PodcastsChannelsAdapter(context, result));
-                emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
-            }
-        };
-        task.execute();
-    }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java
deleted file mode 100644
index dc6f3382..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package org.moire.ultrasonic.fragment;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ListView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.Navigation;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-
-import org.moire.ultrasonic.R;
-import org.moire.ultrasonic.domain.Genre;
-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.Constants;
-import org.moire.ultrasonic.util.FragmentBackgroundTask;
-import org.moire.ultrasonic.util.Settings;
-import org.moire.ultrasonic.util.Util;
-import org.moire.ultrasonic.view.GenreAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import timber.log.Timber;
-
-/**
- * Displays the available genres in the media library
- */
-public class SelectGenreFragment extends Fragment {
-
-    private SwipeRefreshLayout refreshGenreListView;
-    private ListView genreListView;
-    private View emptyView;
-    private CancellationToken cancellationToken;
-
-    @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.select_genre, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
-        cancellationToken = new CancellationToken();
-        refreshGenreListView = view.findViewById(R.id.select_genre_refresh);
-        genreListView = view.findViewById(R.id.select_genre_list);
-
-        refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
-        {
-            @Override
-            public void onRefresh()
-            {
-                load(true);
-            }
-        });
-
-        genreListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                Genre genre = (Genre) parent.getItemAtPosition(position);
-
-                if (genre != null)
-                {
-                    Bundle bundle = new Bundle();
-                    bundle.putString(Constants.INTENT_GENRE_NAME, genre.getName());
-                    bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.getMaxSongs());
-                    bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0);
-                    Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
-                }
-            }
-        });
-
-        emptyView = view.findViewById(R.id.select_genre_empty);
-        registerForContextMenu(genreListView);
-
-        FragmentTitle.Companion.setTitle(this, R.string.main_genres_title);
-        load(false);
-    }
-
-    @Override
-    public void onDestroyView() {
-        cancellationToken.cancel();
-        super.onDestroyView();
-    }
-
-    private void load(final boolean refresh)
-    {
-        BackgroundTask<List<Genre>> task = new FragmentBackgroundTask<List<Genre>>(getActivity(), true, refreshGenreListView, cancellationToken)
-        {
-            @Override
-            protected List<Genre> doInBackground()
-            {
-                MusicService musicService = MusicServiceFactory.getMusicService();
-
-                List<Genre> genres = new ArrayList<>();
-
-                try
-                {
-                    genres = musicService.getGenres(refresh);
-                }
-                catch (Exception x)
-                {
-                    Timber.e(x, "Failed to load genres");
-                }
-
-                return genres;
-            }
-
-            @Override
-            protected void done(List<Genre> result)
-            {
-                emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
-
-                if (result != null)
-                {
-                    genreListView.setAdapter(new GenreAdapter(getContext(), result));
-                }
-            }
-        };
-        task.execute();
-    }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java
deleted file mode 100644
index 4abc6df6..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java
+++ /dev/null
@@ -1,331 +0,0 @@
-package org.moire.ultrasonic.fragment;
-
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.method.LinkMovementMethod;
-import android.text.util.Linkify;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.Navigation;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-
-import org.jetbrains.annotations.NotNull;
-import org.moire.ultrasonic.R;
-import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
-import org.moire.ultrasonic.domain.Share;
-import org.moire.ultrasonic.service.MusicService;
-import org.moire.ultrasonic.service.MusicServiceFactory;
-import org.moire.ultrasonic.service.OfflineException;
-import org.moire.ultrasonic.subsonic.DownloadHandler;
-import org.moire.ultrasonic.util.BackgroundTask;
-import org.moire.ultrasonic.util.CancellationToken;
-import org.moire.ultrasonic.util.Constants;
-import org.moire.ultrasonic.util.LoadingTask;
-import org.moire.ultrasonic.util.FragmentBackgroundTask;
-import org.moire.ultrasonic.util.TimeSpan;
-import org.moire.ultrasonic.util.TimeSpanPicker;
-import org.moire.ultrasonic.util.Util;
-import org.moire.ultrasonic.view.ShareAdapter;
-
-import java.util.List;
-
-import kotlin.Lazy;
-
-import static org.koin.java.KoinJavaComponent.inject;
-
-/**
- * Displays the shares in the media library
- */
-public class SharesFragment extends Fragment {
-
-    private SwipeRefreshLayout refreshSharesListView;
-    private ListView sharesListView;
-    private View emptyTextView;
-    private ShareAdapter shareAdapter;
-
-    private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
-    private CancellationToken cancellationToken;
-
-    @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.select_share, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
-        cancellationToken = new CancellationToken();
-
-        refreshSharesListView = view.findViewById(R.id.select_share_refresh);
-        sharesListView = view.findViewById(R.id.select_share_list);
-
-        refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
-        {
-            @Override
-            public void onRefresh()
-            {
-                load(true);
-            }
-        });
-
-        emptyTextView = view.findViewById(R.id.select_share_empty);
-        sharesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                Share share = (Share) parent.getItemAtPosition(position);
-
-                if (share == null)
-                {
-                    return;
-                }
-
-                Bundle bundle = new Bundle();
-                bundle.putString(Constants.INTENT_SHARE_ID, share.getId());
-                bundle.putString(Constants.INTENT_SHARE_NAME, share.getName());
-                Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
-            }
-        });
-        registerForContextMenu(sharesListView);
-        FragmentTitle.Companion.setTitle(this, R.string.button_bar_shares);
-
-        load(false);
-    }
-
-    @Override
-    public void onDestroyView() {
-        cancellationToken.cancel();
-        super.onDestroyView();
-    }
-
-    private void load(final boolean refresh)
-    {
-        BackgroundTask<List<Share>> task = new FragmentBackgroundTask<List<Share>>(getActivity(), true, refreshSharesListView, cancellationToken)
-        {
-            @Override
-            protected List<Share> doInBackground() throws Throwable
-            {
-                MusicService musicService = MusicServiceFactory.getMusicService();
-                return musicService.getShares(refresh);
-            }
-
-            @Override
-            protected void done(List<Share> result)
-            {
-                sharesListView.setAdapter(shareAdapter = new ShareAdapter(getContext(), result));
-                emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
-            }
-        };
-        task.execute();
-    }
-
-    @Override
-    public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
-    {
-        super.onCreateContextMenu(menu, view, menuInfo);
-
-        MenuInflater inflater = getActivity().getMenuInflater();
-        inflater.inflate(R.menu.select_share_context, menu);
-    }
-
-    @Override
-    public boolean onContextItemSelected(MenuItem menuItem)
-    {
-        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
-        if (info == null) return false;
-
-        Share share = (Share) sharesListView.getItemAtPosition(info.position);
-        if (share == null || share.getId() == null) return false;
-
-        int itemId = menuItem.getItemId();
-        if (itemId == R.id.share_menu_pin) {
-            downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), true, true, false, false, true, false, false);
-        } else if (itemId == R.id.share_menu_unpin) {
-            downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, true);
-        } else if (itemId == R.id.share_menu_download) {
-            downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, false);
-        } else if (itemId == R.id.share_menu_play_now) {
-            downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, false, false, false, false);
-        } else if (itemId == R.id.share_menu_play_shuffled) {
-            downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, true, false, false, false);
-        } else if (itemId == R.id.share_menu_delete) {
-            deleteShare(share);
-        } else if (itemId == R.id.share_info) {
-            displayShareInfo(share);
-        } else if (itemId == R.id.share_update_info) {
-            updateShareInfo(share);
-        } else {
-            return super.onContextItemSelected(menuItem);
-        }
-        return true;
-    }
-
-    private void deleteShare(final Share share)
-    {
-        new AlertDialog.Builder(getContext()).setIcon(R.drawable.ic_baseline_warning).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
-        {
-            @Override
-            public void onClick(DialogInterface dialog, int which)
-            {
-                new LoadingTask<Void>(getActivity(), refreshSharesListView, cancellationToken)
-                {
-                    @Override
-                    protected Void doInBackground() throws Throwable
-                    {
-                        MusicService musicService = MusicServiceFactory.getMusicService();
-                        musicService.deleteShare(share.getId());
-                        return null;
-                    }
-
-                    @Override
-                    protected void done(Void result)
-                    {
-                        shareAdapter.remove(share);
-                        shareAdapter.notifyDataSetChanged();
-                        Util.toast(getContext(), getResources().getString(R.string.menu_deleted_share, share.getName()));
-                    }
-
-                    @Override
-                    protected void error(Throwable error)
-                    {
-                        String msg;
-                        msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error));
-
-                        Util.toast(getContext(), msg, false);
-                    }
-                }.execute();
-            }
-
-        }).setNegativeButton(R.string.common_cancel, null).show();
-    }
-
-    private void displayShareInfo(final Share share)
-    {
-        final TextView textView = new TextView(getContext());
-        textView.setPadding(5, 5, 5, 5);
-
-        final Spannable message = new SpannableString("Owner: " + share.getUsername() +
-            "\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) +
-            "\nURL: " + share.getUrl() +
-            "\nEntry Count: " + share.getEntries().size() +
-            "\nVisit Count: " + share.getVisitCount() +
-            ((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) +
-            ((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) +
-            ((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' '))));
-
-        Linkify.addLinks(message, Linkify.WEB_URLS);
-        textView.setText(message);
-        textView.setMovementMethod(LinkMovementMethod.getInstance());
-
-        new AlertDialog.Builder(getContext()).setTitle("Share Details").setCancelable(true).setIcon(R.drawable.ic_baseline_info).setView(textView).show();
-    }
-
-    private void updateShareInfo(final Share share)
-    {
-        View dialogView = getLayoutInflater().inflate(R.layout.share_details, null);
-        if (dialogView == null)
-        {
-            return;
-        }
-
-        final EditText shareDescription = dialogView.findViewById(R.id.share_description);
-        final TimeSpanPicker timeSpanPicker = dialogView.findViewById(R.id.date_picker);
-
-        shareDescription.setText(share.getDescription());
-
-        CheckBox hideDialogCheckBox = dialogView.findViewById(R.id.hide_dialog);
-        CheckBox saveAsDefaultsCheckBox = dialogView.findViewById(R.id.save_as_defaults);
-        CheckBox noExpirationCheckBox = dialogView.findViewById(R.id.timeSpanDisableCheckBox);
-
-        noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
-        {
-            @Override
-            public void onCheckedChanged(CompoundButton compoundButton, boolean b)
-            {
-                timeSpanPicker.setEnabled(!b);
-            }
-        });
-
-        noExpirationCheckBox.setChecked(true);
-
-        timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration));
-
-        hideDialogCheckBox.setVisibility(View.GONE);
-        saveAsDefaultsCheckBox.setVisibility(View.GONE);
-
-        AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
-
-        alertDialog.setIcon(R.drawable.ic_baseline_warning);
-        alertDialog.setTitle(R.string.playlist_update_info);
-        alertDialog.setView(dialogView);
-        alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
-        {
-            @Override
-            public void onClick(DialogInterface dialog, int which)
-            {
-                new LoadingTask<Void>(getActivity(), refreshSharesListView, cancellationToken)
-                {
-                    @Override
-                    protected Void doInBackground() throws Throwable
-                    {
-                        long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds();
-
-                        if (millis > 0)
-                        {
-                            millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds();
-                        }
-
-                        Editable shareDescriptionText = shareDescription.getText();
-                        String description = shareDescriptionText != null ? shareDescriptionText.toString() : null;
-
-                        MusicService musicService = MusicServiceFactory.getMusicService();
-                        musicService.updateShare(share.getId(), description, millis);
-                        return null;
-                    }
-
-                    @Override
-                    protected void done(Void result)
-                    {
-                        load(true);
-                        Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, share.getName()));
-                    }
-
-                    @Override
-                    protected void error(Throwable error)
-                    {
-                        String msg;
-                        msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error));
-
-                        Util.toast(getContext(), msg, false);
-                    }
-                }.execute();
-            }
-        });
-
-        alertDialog.setNegativeButton(R.string.common_cancel, null);
-        alertDialog.show();
-    }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
index d3ae8caf..a0c607c0 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
@@ -44,6 +44,7 @@ import com.google.android.material.navigation.NavigationView
 import io.reactivex.rxjava3.disposables.CompositeDisposable
 import org.koin.android.ext.android.inject
 import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.moire.ultrasonic.NavigationGraphDirections
 import org.moire.ultrasonic.R
 import org.moire.ultrasonic.app.UApp
 import org.moire.ultrasonic.data.ActiveServerProvider
@@ -76,6 +77,9 @@ class NavigationActivity : AppCompatActivity() {
     private var bookmarksMenuItem: MenuItem? = null
     private var sharesMenuItem: MenuItem? = null
     private var podcastsMenuItem: MenuItem? = null
+    private var playlistsMenuItem: MenuItem? = null
+    private var downloadsMenuItem: MenuItem? = null
+
     private var nowPlayingView: FragmentContainerView? = null
     private var nowPlayingHidden = false
     private var navigationView: NavigationView? = null
@@ -274,15 +278,25 @@ class NavigationActivity : AppCompatActivity() {
     private fun setupNavigationMenu(navController: NavController) {
         navigationView?.setupWithNavController(navController)
 
-        // The exit menu is handled here manually
-        val exitItem: MenuItem? = navigationView?.menu?.findItem(R.id.menu_exit)
-        exitItem?.setOnMenuItemClickListener { item ->
-            if (item.itemId == R.id.menu_exit) {
-                setResult(Constants.RESULT_CLOSE_ALL)
-                mediaPlayerController.stopJukeboxService()
-                finish()
-                exit()
+        // The fragments which expect SafeArgs need to be navigated to with SafeArgs (even when
+        // they are empty)!
+        navigationView?.setNavigationItemSelectedListener {
+            when (it.itemId) {
+                R.id.mediaLibraryFragment -> {
+                    navController.navigate(NavigationGraphDirections.toMediaLibrary())
+                }
+                R.id.bookmarksFragment -> {
+                    navController.navigate(NavigationGraphDirections.toBookmarks())
+                }
+                R.id.menu_exit -> {
+                    setResult(Constants.RESULT_CLOSE_ALL)
+                    mediaPlayerController.stopJukeboxService()
+                    finish()
+                    exit()
+                }
+                else -> navController.navigate(it.itemId)
             }
+            drawerLayout?.closeDrawer(GravityCompat.START)
             true
         }
 
@@ -290,6 +304,9 @@ class NavigationActivity : AppCompatActivity() {
         bookmarksMenuItem = navigationView?.menu?.findItem(R.id.bookmarksFragment)
         sharesMenuItem = navigationView?.menu?.findItem(R.id.sharesFragment)
         podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment)
+        playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment)
+        downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment)
+
         selectServerButton =
             navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
         selectServerButton?.setOnClickListener {
@@ -457,17 +474,17 @@ class NavigationActivity : AppCompatActivity() {
     }
 
     private fun setMenuForServerCapabilities() {
-        if (ActiveServerProvider.isOffline()) {
-            chatMenuItem?.isVisible = false
-            bookmarksMenuItem?.isVisible = false
-            sharesMenuItem?.isVisible = false
-            podcastsMenuItem?.isVisible = false
-            return
-        }
+        val isOnline = !ActiveServerProvider.isOffline()
         val activeServer = activeServerProvider.getActiveServer()
+
+        // Note: Offline capabilities are defined in ActiveServerProvider, OFFLINE_DB.
+        // If you add Offline support for some of these features you need
+        // to switch the boolean to true there.
         chatMenuItem?.isVisible = activeServer.chatSupport != false
         bookmarksMenuItem?.isVisible = activeServer.bookmarkSupport != false
         sharesMenuItem?.isVisible = activeServer.shareSupport != false
         podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
+        playlistsMenuItem?.isVisible = isOnline
+        downloadsMenuItem?.isVisible = isOnline
     }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt
index d7b50e7a..bba08b76 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt
@@ -207,7 +207,11 @@ class ActiveServerProvider(
             allowSelfSignedCertificate = false,
             ldapSupport = false,
             musicFolderId = "",
-            minimumApiVersion = null
+            minimumApiVersion = null,
+            bookmarkSupport = false,
+            podcastSupport = false,
+            shareSupport = false,
+            chatSupport = false
         )
 
         /**
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt
index 2557aea4..ea918f33 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt
@@ -1,23 +1,28 @@
 /*
  * AlbumListFragment.kt
- * Copyright (C) 2009-2021 Ultrasonic developers
+ * Copyright (C) 2009-2022 Ultrasonic developers
  *
  * Distributed under terms of the GNU GPLv3 license.
  */
 
+@file:Suppress("NAME_SHADOWING")
+
 package org.moire.ultrasonic.fragment
 
 import android.os.Bundle
 import android.view.View
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.LiveData
+import androidx.lifecycle.viewModelScope
 import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
 import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.launch
 import org.moire.ultrasonic.R
 import org.moire.ultrasonic.adapters.AlbumRowBinder
+import org.moire.ultrasonic.api.subsonic.models.AlbumListType
 import org.moire.ultrasonic.domain.Album
 import org.moire.ultrasonic.model.AlbumListModel
-import org.moire.ultrasonic.util.Constants
 
 /**
  * Displays a list of Albums from the media library
@@ -39,33 +44,58 @@ class AlbumListFragment : EntryListFragment<Album>() {
      */
     override val refreshOnCreation: Boolean = false
 
+    private val navArgs: AlbumListFragmentArgs by navArgs()
+
     /**
      * The central function to pass a query to the model and return a LiveData object
      */
     override fun getLiveData(
-        args: Bundle?,
         refresh: Boolean
     ): LiveData<List<Album>> {
-        if (args == null) throw IllegalArgumentException("Required arguments are missing")
+        fetchAlbums(refresh)
 
-        val refresh2 = args.getBoolean(Constants.INTENT_REFRESH) || refresh
-        val append = args.getBoolean(Constants.INTENT_APPEND)
-
-        return listModel.getAlbumList(refresh2 or append, refreshListView!!, args)
+        return listModel.list
     }
 
+    private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
+        val refresh = navArgs.refresh || refresh
+
+        listModel.viewModelScope.launch(handler) {
+            refreshListView?.isRefreshing = true
+
+            if (navArgs.type == AlbumListType.BY_ARTIST) {
+                listModel.getAlbumsOfArtist(
+                    refresh = navArgs.refresh,
+                    id = navArgs.id!!,
+                    name = navArgs.title
+                )
+            } else {
+                listModel.getAlbums(
+                    albumListType = navArgs.type,
+                    size = navArgs.size,
+                    offset = navArgs.offset,
+                    append = append,
+                    refresh = refresh or append
+                )
+            }
+            refreshListView?.isRefreshing = false
+        }
+    }
+
+    // TODO: Make generic
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
+        setTitle(navArgs.title)
+
         // Attach our onScrollListener
         listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
             val scrollListener = object : EndlessScrollListener(viewManager) {
                 override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                     // Triggered only when new data needs to be appended to the list
                     // Add whatever code is needed to append new items to the bottom of the list
-                    val appendArgs = getArgumentsClone()
-                    appendArgs.putBoolean(Constants.INTENT_APPEND, true)
-                    getLiveData(appendArgs)
+                    fetchAlbums(append = true)
                 }
             }
             addOnScrollListener(scrollListener)
@@ -83,11 +113,12 @@ class AlbumListFragment : EntryListFragment<Album>() {
     }
 
     override fun onItemClick(item: Album) {
-        val bundle = Bundle()
-        bundle.putString(Constants.INTENT_ID, item.id)
-        bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory)
-        bundle.putString(Constants.INTENT_NAME, item.title)
-        bundle.putString(Constants.INTENT_PARENT_ID, item.parent)
-        findNavController().navigate(R.id.trackCollectionFragment, bundle)
+        val action = AlbumListFragmentDirections.albumListToTrackCollection(
+            item.id,
+            isAlbum = item.isDirectory,
+            name = item.title,
+            parentId = item.parent
+        )
+        findNavController().navigate(action)
     }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt
index 2446e05a..eef035d9 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt
@@ -1,18 +1,25 @@
+/*
+ * ArtistListFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.fragment
 
 import android.os.Bundle
 import android.view.View
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.LiveData
-import androidx.navigation.NavController
 import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
 import org.moire.ultrasonic.R
 import org.moire.ultrasonic.adapters.ArtistRowBinder
+import org.moire.ultrasonic.api.subsonic.models.AlbumListType
 import org.moire.ultrasonic.domain.Artist
 import org.moire.ultrasonic.domain.ArtistOrIndex
 import org.moire.ultrasonic.domain.Index
 import org.moire.ultrasonic.model.ArtistListModel
-import org.moire.ultrasonic.util.Constants
 
 /**
  * Displays the list of Artists or Indexes (folders) from the media library
@@ -29,16 +36,18 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
      */
     override val mainLayout = R.layout.list_layout_generic
 
+    private val navArgs: ArtistListFragmentArgs by navArgs()
+
     /**
      * The central function to pass a query to the model and return a LiveData object
      */
-    override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<ArtistOrIndex>> {
-        val refresh2 = args?.getBoolean(Constants.INTENT_REFRESH) ?: false || refresh
-        return listModel.getItems(refresh2, refreshListView!!)
+    override fun getLiveData(refresh: Boolean): LiveData<List<ArtistOrIndex>> {
+        return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+        setTitle(navArgs.title)
 
         viewAdapter.register(
             ArtistRowBinder(
@@ -55,29 +64,24 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
      * If we are showing artists, we need to go to AlbumList
      */
     override fun onItemClick(item: ArtistOrIndex) {
-        Companion.onItemClick(item, findNavController())
-    }
-
-    companion object {
-        fun onItemClick(item: ArtistOrIndex, navController: NavController) {
-            val bundle = Bundle()
-
-            // Common arguments
-            bundle.putString(Constants.INTENT_ID, item.id)
-            bundle.putString(Constants.INTENT_NAME, item.name)
-            bundle.putString(Constants.INTENT_PARENT_ID, item.id)
-            bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist))
-
-            // Check type
-            if (item is Index) {
-                navController.navigate(R.id.artistsListToTrackCollection, bundle)
-            } else {
-                bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
-                bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name)
-                bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000)
-                bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
-                navController.navigate(R.id.artistsListToAlbumsList, bundle)
-            }
+        // Check type
+        val action = if (item is Index) {
+            ArtistListFragmentDirections.artistsListToTrackCollection(
+                id = item.id,
+                name = item.name,
+                parentId = item.id,
+                isArtist = (item is Artist)
+            )
+        } else {
+            ArtistListFragmentDirections.artistsListToAlbumsList(
+                type = AlbumListType.BY_ARTIST,
+                id = item.id,
+                title = item.name,
+                size = 1000,
+                offset = 0
+            )
         }
+
+        findNavController().navigate(action)
     }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt
index 25aa3b98..8cbf1a74 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt
@@ -37,7 +37,6 @@ class BookmarksFragment : TrackCollectionFragment() {
     }
 
     override fun getLiveData(
-        args: Bundle?,
         refresh: Boolean
     ): LiveData<List<MusicDirectory.Child>> {
         listModel.viewModelScope.launch(handler) {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
index 3b6b302b..8a93da28 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
@@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment<Track>() {
     /**
      * The central function to pass a query to the model and return a LiveData object
      */
-    override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<Track>> {
+    override fun getLiveData(refresh: Boolean): LiveData<List<Track>> {
         return listModel.getList()
     }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt
index a527b6d8..ed66cef3 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt
@@ -1,3 +1,10 @@
+/*
+ * EditServerFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.fragment
 
 import android.os.Bundle
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt
index 4e12491a..4b8664c4 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt
@@ -24,7 +24,7 @@ abstract class EndlessScrollListener : RecyclerView.OnScrollListener {
 
     // Sets the starting page index
     private val startingPageIndex = 0
-    var thisManager: RecyclerView.LayoutManager
+    private var thisManager: RecyclerView.LayoutManager
 
     constructor(layoutManager: LinearLayoutManager) {
         thisManager = layoutManager
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt
index 395d2d6c..0cd26e32 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt
@@ -1,3 +1,10 @@
+/*
+ * EntryListFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.fragment
 
 import android.os.Bundle
@@ -13,7 +20,6 @@ import org.moire.ultrasonic.domain.GenericEntry
 import org.moire.ultrasonic.domain.Identifiable
 import org.moire.ultrasonic.service.RxBus
 import org.moire.ultrasonic.subsonic.DownloadHandler
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.Settings
 
 /**
@@ -26,9 +32,9 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
     /**
      * Whether to show the folder selector
      */
-    fun showFolderHeader(): Boolean {
-        return listModel.showSelectFolderHeader(arguments) &&
-            !listModel.isOffline() && !Settings.shouldUseId3Tags
+    private fun showFolderHeader(): Boolean {
+        return listModel.showSelectFolderHeader() && !listModel.isOffline() &&
+            !Settings.shouldUseId3Tags
     }
 
     override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
@@ -38,12 +44,14 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
     }
 
     override fun onItemClick(item: T) {
-        val bundle = Bundle()
-        bundle.putString(Constants.INTENT_ID, item.id)
-        bundle.putString(Constants.INTENT_NAME, item.name)
-        bundle.putString(Constants.INTENT_PARENT_ID, item.id)
-        bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist))
-        findNavController().navigate(R.id.trackCollectionFragment, bundle)
+        val action = EntryListFragmentDirections.entryListToTrackCollection(
+            id = item.id,
+            name = item.name,
+            parentId = item.id,
+            isArtist = (item is Artist),
+        )
+
+        findNavController().navigate(action)
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -59,7 +67,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
                 currentSetting.musicFolderId = it
                 serverSettingsModel.updateItem(currentSetting)
             }
-            listModel.refresh(refreshListView!!, arguments)
+            listModel.refresh(refreshListView!!)
         }
 
         viewAdapter.register(
@@ -71,7 +79,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
      * 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() && !(refreshListView?.isRefreshing ?: false)
 
         if (showFolderHeader()) {
             val list = mutableListOf<Identifiable>(folderHeader)
@@ -92,12 +100,11 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
         )
 
         listModel.musicFolders.observe(
-            viewLifecycleOwner,
-            {
-                header.folders = it
-                viewAdapter.notifyItemChanged(0)
-            }
-        )
+            viewLifecycleOwner
+        ) {
+            header.folders = it
+            viewAdapter.notifyItemChanged(0)
+        }
 
         header
     }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt
index caa1def0..75f07df5 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt
@@ -1,3 +1,10 @@
+/*
+ * MainFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.fragment
 
 import android.os.Bundle
@@ -7,12 +14,12 @@ import android.view.ViewGroup
 import android.widget.TextView
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
-import androidx.navigation.Navigation
+import androidx.navigation.fragment.findNavController
 import org.koin.core.component.KoinComponent
 import org.moire.ultrasonic.R
+import org.moire.ultrasonic.api.subsonic.models.AlbumListType
 import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
 import org.moire.ultrasonic.databinding.MainBinding
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.Settings
 import org.moire.ultrasonic.util.Util
 
@@ -118,7 +125,7 @@ class MainFragment : Fragment(), KoinComponent {
         musicTitle.isVisible = true
         artistsButton.isVisible = true
         albumsButton.isVisible = isOnline || useId3Offline
-        genresButton.isVisible = true
+        genresButton.isVisible = isOnline
 
         // Songs
         songsTitle.isVisible = true
@@ -143,35 +150,35 @@ class MainFragment : Fragment(), KoinComponent {
 
     private fun setupClickListener() {
         albumsNewestButton.setOnClickListener {
-            showAlbumList("newest", R.string.main_albums_newest)
+            showAlbumList(AlbumListType.NEWEST, R.string.main_albums_newest)
         }
 
         albumsRandomButton.setOnClickListener {
-            showAlbumList("random", R.string.main_albums_random)
+            showAlbumList(AlbumListType.RANDOM, R.string.main_albums_random)
         }
 
         albumsHighestButton.setOnClickListener {
-            showAlbumList("highest", R.string.main_albums_highest)
+            showAlbumList(AlbumListType.HIGHEST, R.string.main_albums_highest)
         }
 
         albumsRecentButton.setOnClickListener {
-            showAlbumList("recent", R.string.main_albums_recent)
+            showAlbumList(AlbumListType.RECENT, R.string.main_albums_recent)
         }
 
         albumsFrequentButton.setOnClickListener {
-            showAlbumList("frequent", R.string.main_albums_frequent)
+            showAlbumList(AlbumListType.FREQUENT, R.string.main_albums_frequent)
         }
 
         albumsStarredButton.setOnClickListener {
-            showAlbumList(Constants.STARRED, R.string.main_albums_starred)
+            showAlbumList(AlbumListType.STARRED, R.string.main_albums_starred)
         }
 
         albumsAlphaByNameButton.setOnClickListener {
-            showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName)
+            showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_alphaByName)
         }
 
         albumsAlphaByArtistButton.setOnClickListener {
-            showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist)
+            showAlbumList(AlbumListType.SORTED_BY_ARTIST, R.string.main_albums_alphaByArtist)
         }
 
         songsStarredButton.setOnClickListener {
@@ -183,7 +190,7 @@ class MainFragment : Fragment(), KoinComponent {
         }
 
         albumsButton.setOnClickListener {
-            showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title)
+            showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_title)
         }
 
         randomSongsButton.setOnClickListener {
@@ -200,45 +207,47 @@ class MainFragment : Fragment(), KoinComponent {
     }
 
     private fun showStarredSongs() {
-        val bundle = Bundle()
-        bundle.putInt(Constants.INTENT_STARRED, 1)
-        Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle)
+        val action = MainFragmentDirections.mainToTrackCollection(
+            getStarred = true,
+        )
+        findNavController().navigate(action)
     }
 
     private fun showRandomSongs() {
-        val bundle = Bundle()
-        bundle.putInt(Constants.INTENT_RANDOM, 1)
-        bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxSongs)
-        Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle)
+        val action = MainFragmentDirections.mainToTrackCollection(
+            getRandom = true,
+            size = Settings.maxSongs
+        )
+        findNavController().navigate(action)
     }
 
     private fun showArtists() {
-        val bundle = Bundle()
-        bundle.putString(
-            Constants.INTENT_ALBUM_LIST_TITLE,
-            requireContext().resources.getString(R.string.main_artists_title)
+        val action = MainFragmentDirections.mainToArtistList(
+            title = requireContext().resources.getString(R.string.main_artists_title)
         )
-        Navigation.findNavController(requireView()).navigate(R.id.mainToArtistList, bundle)
+        findNavController().navigate(action)
     }
 
-    private fun showAlbumList(type: String, titleIndex: Int) {
-        val bundle = Bundle()
+    private fun showAlbumList(type: AlbumListType, titleIndex: Int) {
         val title = requireContext().resources.getString(titleIndex, "")
-        bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, type)
-        bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, title)
-        bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxAlbums)
-        bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
-        Navigation.findNavController(requireView()).navigate(R.id.mainToAlbumList, bundle)
+        val action = MainFragmentDirections.mainToAlbumList(
+            type = type,
+            title = title,
+            size = Settings.maxAlbums,
+            offset = 0
+        )
+        findNavController().navigate(action)
     }
 
     private fun showGenres() {
-        Navigation.findNavController(requireView()).navigate(R.id.mainToSelectGenre)
+        findNavController().navigate(R.id.mainToSelectGenre)
     }
 
     private fun showVideos() {
-        val bundle = Bundle()
-        bundle.putInt(Constants.INTENT_VIDEOS, 1)
-        Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle)
+        val action = MainFragmentDirections.mainToTrackCollection(
+            getVideos = true,
+        )
+        findNavController().navigate(action)
     }
 
     companion object {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt
index fef74587..e1900806 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt
@@ -8,6 +8,8 @@
 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
@@ -22,6 +24,7 @@ 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.viewmodel.ext.android.viewModel
 import org.moire.ultrasonic.R
@@ -32,7 +35,7 @@ import org.moire.ultrasonic.model.GenericListModel
 import org.moire.ultrasonic.model.ServerSettingsModel
 import org.moire.ultrasonic.subsonic.DownloadHandler
 import org.moire.ultrasonic.subsonic.ImageLoaderProvider
-import org.moire.ultrasonic.util.Constants
+import org.moire.ultrasonic.util.CommunicationError
 import org.moire.ultrasonic.util.Util
 
 /**
@@ -67,12 +70,12 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
      * The LiveData containing the list provided by the model
      * Implement this as a getter
      */
-    internal lateinit var liveDataItems: LiveData<List<T>>
+    private lateinit var liveDataItems: LiveData<List<T>>
 
     /**
      * The central function to pass a query to the model and return a LiveData object
      */
-    open fun getLiveData(args: Bundle? = null, refresh: Boolean = false): LiveData<List<T>> {
+    open fun getLiveData(refresh: Boolean = false): LiveData<List<T>> {
         return MutableLiveData()
     }
 
@@ -94,6 +97,16 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
      */
     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(
@@ -118,17 +131,14 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        // Set the title if available
-        setTitle(arguments?.getString(Constants.INTENT_ALBUM_LIST_TITLE))
-
         // Setup refresh handler
         refreshListView = view.findViewById(refreshListId)
         refreshListView?.setOnRefreshListener {
-            listModel.refresh(refreshListView!!, arguments)
+            listModel.refresh(refreshListView!!)
         }
 
         // Populate the LiveData. This starts an API request in most cases
-        liveDataItems = getLiveData(arguments, refreshOnCreation)
+        liveDataItems = getLiveData(refreshOnCreation)
 
         // Link view to display text if the list is empty
         emptyView = view.findViewById(emptyViewId)
@@ -165,16 +175,4 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
     abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
 
     abstract fun onItemClick(item: T)
-
-    fun getArgumentsClone(): Bundle {
-        var bundle: Bundle
-
-        try {
-            bundle = arguments?.clone() as Bundle
-        } catch (ignored: Exception) {
-            bundle = Bundle()
-        }
-
-        return bundle
-    }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt
index 9f3254b5..e9b6ab8c 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt
@@ -17,15 +17,16 @@ import android.widget.ImageView
 import android.widget.TextView
 import androidx.fragment.app.Fragment
 import androidx.navigation.Navigation
+import androidx.navigation.fragment.findNavController
 import io.reactivex.rxjava3.disposables.Disposable
 import java.lang.Exception
 import kotlin.math.abs
 import org.koin.android.ext.android.inject
+import org.moire.ultrasonic.NavigationGraphDirections
 import org.moire.ultrasonic.R
 import org.moire.ultrasonic.service.MediaPlayerController
 import org.moire.ultrasonic.service.RxBus
 import org.moire.ultrasonic.subsonic.ImageLoaderProvider
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.Settings
 import org.moire.ultrasonic.util.Util.applyTheme
 import org.moire.ultrasonic.util.Util.getNotificationImageSize
@@ -35,7 +36,6 @@ import timber.log.Timber
 /**
  * Contains the mini-now playing information box displayed at the bottom of the screen
  */
-@Suppress("unused")
 class NowPlayingFragment : Fragment() {
 
     private var downX = 0f
@@ -107,21 +107,13 @@ class NowPlayingFragment : Fragment() {
                 nowPlayingArtist!!.text = artist
 
                 nowPlayingAlbumArtImage!!.setOnClickListener {
-                    val bundle = Bundle()
-
-                    if (Settings.shouldUseId3Tags) {
-                        bundle.putBoolean(Constants.INTENT_IS_ALBUM, true)
-                        bundle.putString(Constants.INTENT_ID, file.albumId)
-                    } else {
-                        bundle.putBoolean(Constants.INTENT_IS_ALBUM, false)
-                        bundle.putString(Constants.INTENT_ID, file.parent)
-                    }
-
-                    bundle.putString(Constants.INTENT_NAME, file.album)
-                    bundle.putString(Constants.INTENT_NAME, file.album)
-
-                    Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
-                        .navigate(R.id.trackCollectionFragment, bundle)
+                    val id3 = Settings.shouldUseId3Tags
+                    val action = NavigationGraphDirections.toTrackCollection(
+                        isAlbum = id3,
+                        id = if (id3) file.albumId else file.parent,
+                        name = file.album
+                    )
+                    findNavController().navigate(action)
                 }
             }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt
index cf7ecb32..c34ca2dd 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt
@@ -1,3 +1,10 @@
+/*
+ * OnBackPressedHandler.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.fragment
 
 /**
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
index 598d46e2..06221a4b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
@@ -43,6 +43,7 @@ import androidx.media3.common.Player
 import androidx.media3.common.Timeline
 import androidx.media3.session.SessionResult
 import androidx.navigation.Navigation
+import androidx.navigation.fragment.findNavController
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -160,7 +161,7 @@ class PlayerFragment :
     private val hollowStar = R.drawable.ic_star_hollow
     private val fullStar = R.drawable.ic_star_full
 
-    internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
+    private val viewAdapter: BaseAdapter<Identifiable> by lazy {
         BaseAdapter()
     }
 
@@ -205,7 +206,7 @@ class PlayerFragment :
         fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
     }
 
-    @Suppress("LongMethod")
+    @Suppress("LongMethod", "DEPRECATION")
     @SuppressLint("ClickableViewAccessibility")
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         cancellationToken = CancellationToken()
@@ -353,15 +354,6 @@ class PlayerFragment :
 
         registerForContextMenu(playlistView)
 
-        if (arguments != null && requireArguments().getBoolean(
-                Constants.INTENT_SHUFFLE,
-                false
-            )
-        ) {
-            networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
-            mediaPlayerController.isShufflePlayEnabled = true
-        }
-
         visualizerViewLayout.isVisible = false
         VisualizerController.get().observe(
             requireActivity()
@@ -641,11 +633,14 @@ class PlayerFragment :
                 if (track == null) return false
 
                 if (Settings.shouldUseId3Tags) {
+                    PlayerFragmentDirections.playerToSelectAlbum(
+                        id = track.artistId,
+                        name = track.artist,
+                        parentId = track.artistId,
+                        isArtist = true,
+                    )
                     bundle = Bundle()
-                    bundle.putString(Constants.INTENT_ID, track.artistId)
-                    bundle.putString(Constants.INTENT_NAME, track.artist)
-                    bundle.putString(Constants.INTENT_PARENT_ID, track.artistId)
-                    bundle.putBoolean(Constants.INTENT_ARTIST, true)
+
                     Navigation.findNavController(requireView())
                         .navigate(R.id.playerToSelectAlbum, bundle)
                 }
@@ -655,18 +650,19 @@ class PlayerFragment :
                 if (track == null) return false
 
                 val albumId = if (Settings.shouldUseId3Tags) track.albumId else track.parent
-                bundle = Bundle()
-                bundle.putString(Constants.INTENT_ID, albumId)
-                bundle.putString(Constants.INTENT_NAME, track.album)
-                bundle.putString(Constants.INTENT_PARENT_ID, track.parent)
-                bundle.putBoolean(Constants.INTENT_IS_ALBUM, true)
-                Navigation.findNavController(requireView())
-                    .navigate(R.id.playerToSelectAlbum, bundle)
+
+                val action = PlayerFragmentDirections.playerToSelectAlbum(
+                    id = albumId,
+                    name = track.album,
+                    parentId = track.parent,
+                    isAlbum = true
+                )
+
+                findNavController().navigate(action)
                 return true
             }
             R.id.menu_lyrics -> {
                 if (track == null) return false
-
                 bundle = Bundle()
                 bundle.putString(Constants.INTENT_ARTIST, track.artist)
                 bundle.putString(Constants.INTENT_TITLE, track.title)
@@ -815,7 +811,12 @@ class PlayerFragment :
                     val playlistEntry = item.toTrack()
                     tracks.add(playlistEntry)
                 }
-                shareHandler.createShare(this, tracks, null, cancellationToken)
+                shareHandler.createShare(
+                    this,
+                    tracks = tracks,
+                    swipe = null,
+                    cancellationToken = cancellationToken,
+                )
                 return true
             }
             R.id.menu_item_share_song -> {
@@ -824,7 +825,12 @@ class PlayerFragment :
                 val tracks: MutableList<Track?> = ArrayList()
                 tracks.add(currentSong)
 
-                shareHandler.createShare(this, tracks, null, cancellationToken)
+                shareHandler.createShare(
+                    this,
+                    tracks,
+                    swipe = null,
+                    cancellationToken = cancellationToken
+                )
                 return true
             }
             else -> return false
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
index 2f6d8196..d3b5d7f2 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
@@ -1,3 +1,10 @@
+/*
+ * SearchFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.fragment
 
 import android.app.SearchManager
@@ -11,7 +18,6 @@ import androidx.appcompat.widget.SearchView
 import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.viewModelScope
-import androidx.navigation.Navigation
 import androidx.navigation.fragment.findNavController
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 import kotlinx.coroutines.launch
@@ -24,6 +30,7 @@ import org.moire.ultrasonic.adapters.DividerBinder
 import org.moire.ultrasonic.adapters.MoreButtonBinder
 import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton
 import org.moire.ultrasonic.adapters.TrackViewBinder
+import org.moire.ultrasonic.api.subsonic.models.AlbumListType
 import org.moire.ultrasonic.domain.Album
 import org.moire.ultrasonic.domain.Artist
 import org.moire.ultrasonic.domain.ArtistOrIndex
@@ -47,6 +54,8 @@ import timber.log.Timber
 
 /**
  * Initiates a search on the media library and displays the results
+ *
+ * TODO: Move to SafeArgs
  */
 class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
     private var searchResult: SearchResult? = null
@@ -266,33 +275,37 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
     }
 
     private fun onArtistSelected(item: ArtistOrIndex) {
-        val bundle = Bundle()
-
-        // Common arguments
-        bundle.putString(Constants.INTENT_ID, item.id)
-        bundle.putString(Constants.INTENT_NAME, item.name)
-        bundle.putString(Constants.INTENT_PARENT_ID, item.id)
-        bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist))
-
-        // Check type
-        if (item is Index) {
-            findNavController().navigate(R.id.searchToTrackCollection, bundle)
+        // Create action based on type
+        val action = if (item is Index) {
+            SearchFragmentDirections.searchToTrackCollection(
+                id = item.id,
+                name = item.name,
+                parentId = item.id,
+                isArtist = (item is Artist)
+            )
         } else {
-            bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
-            bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name)
-            bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000)
-            bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
-            findNavController().navigate(R.id.searchToAlbumsList, bundle)
+            SearchFragmentDirections.searchToAlbumsList(
+                type = AlbumListType.BY_ARTIST,
+                id = item.id,
+                title = item.name,
+                size = 1000,
+                offset = 0
+            )
         }
+
+        // Lets go!
+        findNavController().navigate(action)
     }
 
     private fun onAlbumSelected(album: Album, autoplay: Boolean) {
-        val bundle = Bundle()
-        bundle.putString(Constants.INTENT_ID, album.id)
-        bundle.putString(Constants.INTENT_NAME, album.title)
-        bundle.putBoolean(Constants.INTENT_IS_ALBUM, album.isDirectory)
-        bundle.putBoolean(Constants.INTENT_AUTOPLAY, autoplay)
-        Navigation.findNavController(requireView()).navigate(R.id.searchToTrackCollection, bundle)
+
+        val action = SearchFragmentDirections.searchToTrackCollection(
+            id = album.id,
+            name = album.title,
+            autoPlay = autoplay,
+            isAlbum = true
+        )
+        findNavController().navigate(action)
     }
 
     private fun onSongSelected(song: Track, append: Boolean) {
@@ -366,7 +379,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
                     autoPlay = true,
                     playNext = false,
                     shuffle = false,
-                    songs = songs
+                    songs = songs,
+                    playlistName = null
                 )
             }
             R.id.song_menu_play_next -> {
@@ -378,7 +392,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
                     autoPlay = false,
                     playNext = true,
                     shuffle = false,
-                    songs = songs
+                    songs = songs,
+                    playlistName = null
                 )
             }
             R.id.song_menu_play_last -> {
@@ -390,7 +405,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
                     autoPlay = false,
                     playNext = false,
                     shuffle = false,
-                    songs = songs
+                    songs = songs,
+                    playlistName = null
                 )
             }
             R.id.song_menu_pin -> {
@@ -431,7 +447,13 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
             }
             R.id.song_menu_share -> {
                 songs.add(item)
-                shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!)
+                shareHandler.createShare(
+                    fragment = this,
+                    tracks = songs,
+                    swipe = searchRefresh,
+                    cancellationToken = cancellationToken!!,
+                    additionalId = null
+                )
             }
         }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
index ddf9cc04..b4d9c021 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
@@ -8,8 +8,6 @@
 package org.moire.ultrasonic.fragment
 
 import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
 import android.view.Menu
 import android.view.MenuInflater
 import android.view.MenuItem
@@ -19,11 +17,11 @@ import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.viewModelScope
-import androidx.navigation.Navigation
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import java.util.Collections
-import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.launch
 import org.koin.android.ext.android.inject
 import org.moire.ultrasonic.R
@@ -45,9 +43,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
 import org.moire.ultrasonic.subsonic.ShareHandler
 import org.moire.ultrasonic.subsonic.VideoPlayer
 import org.moire.ultrasonic.util.CancellationToken
-import org.moire.ultrasonic.util.CommunicationError
 import org.moire.ultrasonic.util.ConfirmationDialog
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
 import org.moire.ultrasonic.util.Settings
 import org.moire.ultrasonic.util.Util
@@ -93,6 +89,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
      */
     override val mainLayout: Int = R.layout.list_layout_track
 
+    private val navArgs: TrackCollectionFragmentArgs by navArgs()
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         cancellationToken = CancellationToken()
@@ -102,7 +100,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         // Setup refresh handler
         refreshListView = view.findViewById(refreshListId)
         refreshListView?.setOnRefreshListener {
-            getLiveData(arguments, true)
+            handleRefresh()
         }
 
         setupButtons(view)
@@ -155,6 +153,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         }
     }
 
+    internal open fun handleRefresh() {
+        getLiveData(true)
+    }
+
     internal open fun setupButtons(view: View) {
         selectButton = view.findViewById(R.id.select_album_select)
         playNowButton = view.findViewById(R.id.select_album_play_now)
@@ -178,7 +180,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
             downloadHandler.download(
                 this@TrackCollectionFragment, append = true,
                 save = false, autoPlay = false, playNext = true, shuffle = false,
-                songs = getSelectedSongs()
+                songs = getSelectedSongs(),
+                playlistName = navArgs.playlistName
             )
         }
 
@@ -219,13 +222,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         }
     }
 
-    val handler = CoroutineExceptionHandler { _, exception ->
-        Handler(Looper.getMainLooper()).post {
-            CommunicationError.handleError(exception, context)
-        }
-        refreshListView?.isRefreshing = false
-    }
-
     override fun onPrepareOptionsMenu(menu: Menu) {
         super.onPrepareOptionsMenu(menu)
         playAllButton = menu.findItem(R.id.select_album_play_all)
@@ -254,7 +250,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         } else if (itemId == R.id.menu_item_share) {
             shareHandler.createShare(
                 this, getSelectedSongs(),
-                refreshListView, cancellationToken!!
+                refreshListView, cancellationToken!!,
+                navArgs.id
             )
             return true
         }
@@ -274,7 +271,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         if (selectedSongs.isNotEmpty()) {
             downloadHandler.download(
                 this, append, false, !append, playNext = false,
-                shuffle = false, songs = selectedSongs
+                shuffle = false, songs = selectedSongs, null
             )
         } else {
             playAll(false, append)
@@ -304,10 +301,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
             }
         }
 
-        val isArtist = arguments?.getBoolean(Constants.INTENT_ARTIST, false) ?: false
-        val id = arguments?.getString(Constants.INTENT_ID)
+        val isArtist = navArgs.isArtist
+        val id = navArgs.id
 
-        if (hasSubFolders && id != null) {
+        if (hasSubFolders) {
             downloadHandler.downloadRecursively(
                 fragment = this,
                 id = id,
@@ -328,7 +325,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
                 autoPlay = !append,
                 playNext = false,
                 shuffle = shuffle,
-                songs = getAllSongs()
+                songs = getAllSongs(),
+                playlistName = navArgs.playlistName
             )
         }
     }
@@ -465,7 +463,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
             }
         }
 
-        val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0
+        val listSize = navArgs.size
 
         // Hide select button for video lists and singular selection lists
         selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0
@@ -475,9 +473,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
                 moreButton!!.visibility = View.GONE
             } else {
                 moreButton!!.visibility = View.VISIBLE
-                if ((arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0) > 0) {
+                if (navArgs.getRandom) {
                     moreRandomTracks()
-                } else if ((arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "") != "") {
+                } else if (navArgs.genreName != null) {
                     moreSongsForGenre()
                 }
             }
@@ -488,9 +486,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
 
         enableButtons()
 
-        val isAlbumList = arguments?.containsKey(
-            Constants.INTENT_ALBUM_LIST_TYPE
-        ) ?: false
+        val isAlbumList = (navArgs.albumListType != null)
 
         playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
         shareButtonVisible = !isOffline() && songCount > 0
@@ -499,7 +495,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         shareButton?.isVisible = shareButtonVisible
 
         if (songCount > 0 && listModel.showHeader) {
-            val intentAlbumName = arguments?.getString(Constants.INTENT_NAME, "")
+            val intentAlbumName = navArgs.name
             val albumHeader = AlbumHeader(it, intentAlbumName)
             val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
             mixedList.addAll(entryList)
@@ -508,11 +504,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
             viewAdapter.submitList(entryList)
         }
 
-        val playAll = arguments?.getBoolean(Constants.INTENT_AUTOPLAY, false) ?: false
+        val playAll = navArgs.autoPlay
 
         if (playAll && songCount > 0) {
             playAll(
-                arguments?.getBoolean(Constants.INTENT_SHUFFLE, false) ?: false,
+                navArgs.shuffle,
                 false
             )
         }
@@ -522,37 +518,30 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
         Timber.i("Processed list")
     }
 
-    private fun moreSongsForGenre(args: Bundle = requireArguments()) {
+    private fun moreSongsForGenre() {
         moreButton!!.setOnClickListener {
-            val theGenre = args.getString(Constants.INTENT_GENRE_NAME)
-            val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0)
-            val theOffset = args.getInt(
-                Constants.INTENT_ALBUM_LIST_OFFSET, 0
-            ) + size
-            val bundle = Bundle()
-            bundle.putString(Constants.INTENT_GENRE_NAME, theGenre)
-            bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, size)
-            bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, theOffset)
-
-            Navigation.findNavController(requireView())
-                .navigate(R.id.trackCollectionFragment, bundle)
+            val action = TrackCollectionFragmentDirections.loadMoreTracks(
+                genreName = navArgs.genreName,
+                size = navArgs.size,
+                offset = navArgs.offset + navArgs.size
+            )
+            findNavController().navigate(action)
         }
     }
 
     private fun moreRandomTracks() {
-        val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0
+
+        val listSize = navArgs.size
 
         moreButton!!.setOnClickListener {
-            val offset = requireArguments().getInt(
-                Constants.INTENT_ALBUM_LIST_OFFSET, 0
-            ) + listSize
-            val bundle = Bundle()
-            bundle.putInt(Constants.INTENT_RANDOM, 1)
-            bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, listSize)
-            bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, offset)
-            Navigation.findNavController(requireView()).navigate(
-                R.id.trackCollectionFragment, bundle
+            val offset = navArgs.offset + listSize
+
+            val action = TrackCollectionFragmentDirections.loadMoreTracks(
+                getRandom = true,
+                size = listSize,
+                offset = offset
             )
+            findNavController().navigate(action)
         }
     }
 
@@ -576,27 +565,25 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
 
     @Suppress("LongMethod")
     override fun getLiveData(
-        args: Bundle?,
         refresh: Boolean
     ): LiveData<List<MusicDirectory.Child>> {
         Timber.i("Starting gathering track collection data...")
-        if (args == null) return listModel.currentList
-        val id = args.getString(Constants.INTENT_ID)
-        val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false)
-        val name = args.getString(Constants.INTENT_NAME)
-        val playlistId = args.getString(Constants.INTENT_PLAYLIST_ID)
-        val podcastChannelId = args.getString(Constants.INTENT_PODCAST_CHANNEL_ID)
-        val playlistName = args.getString(Constants.INTENT_PLAYLIST_NAME)
-        val shareId = args.getString(Constants.INTENT_SHARE_ID)
-        val shareName = args.getString(Constants.INTENT_SHARE_NAME)
-        val genreName = args.getString(Constants.INTENT_GENRE_NAME)
+        val id = navArgs.id
+        val isAlbum = navArgs.isAlbum
+        val name = navArgs.name
+        val playlistId = navArgs.playlistId
+        val podcastChannelId = navArgs.podcastChannelId
+        val playlistName = navArgs.playlistName
+        val shareId = navArgs.shareId
+        val shareName = navArgs.shareName
+        val genreName = navArgs.genreName
 
-        val getStarredTracks = args.getInt(Constants.INTENT_STARRED, 0)
-        val getVideos = args.getInt(Constants.INTENT_VIDEOS, 0)
-        val getRandomTracks = args.getInt(Constants.INTENT_RANDOM, 0)
-        val albumListSize = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0)
-        val albumListOffset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
-        val refresh2 = args.getBoolean(Constants.INTENT_REFRESH, true) || refresh
+        val getStarredTracks = navArgs.getStarred
+        val getVideos = navArgs.getVideos
+        val getRandomTracks = navArgs.getRandom
+        val albumListSize = navArgs.size
+        val albumListOffset = navArgs.offset
+        val refresh2 = navArgs.refresh || refresh
 
         listModel.viewModelScope.launch(handler) {
             refreshListView?.isRefreshing = true
@@ -613,13 +600,13 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
             } else if (genreName != null) {
                 setTitle(genreName)
                 listModel.getSongsForGenre(genreName, albumListSize, albumListOffset)
-            } else if (getStarredTracks != 0) {
+            } else if (getStarredTracks) {
                 setTitle(getString(R.string.main_songs_starred))
                 listModel.getStarred()
-            } else if (getVideos != 0) {
+            } else if (getVideos) {
                 setTitle(R.string.main_videos)
                 listModel.getVideos(refresh2)
-            } else if (getRandomTracks != 0) {
+            } else if (getRandomTracks) {
                 setTitle(R.string.main_songs_random)
                 listModel.getRandom(albumListSize)
             } else {
@@ -659,7 +646,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
                     autoPlay = false,
                     playNext = true,
                     shuffle = false,
-                    songs = songs
+                    songs = songs,
+                    playlistName = navArgs.playlistName
                 )
             }
             R.id.song_menu_play_last -> {
@@ -681,8 +669,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
             R.id.song_menu_share -> {
                 if (item is Track) {
                     shareHandler.createShare(
-                        this, listOf(item), refreshListView,
-                        cancellationToken!!
+                        this,
+                        tracks = listOf(item),
+                        swipe = refreshListView,
+                        cancellationToken = cancellationToken!!,
+                        additionalId = navArgs.id
                     )
                 }
             }
@@ -706,15 +697,13 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
     override fun onItemClick(item: MusicDirectory.Child) {
         when {
             item.isDirectory -> {
-                val bundle = Bundle()
-                bundle.putString(Constants.INTENT_ID, item.id)
-                bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory)
-                bundle.putString(Constants.INTENT_NAME, item.title)
-                bundle.putString(Constants.INTENT_PARENT_ID, item.parent)
-                Navigation.findNavController(requireView()).navigate(
-                    R.id.trackCollectionFragment,
-                    bundle
+                val action = TrackCollectionFragmentDirections.loadMoreTracks(
+                    id = item.id,
+                    isAlbum = true,
+                    name = item.title,
+                    parentId = item.parent
                 )
+                findNavController().navigate(action)
             }
             item is Track && item.isVideo -> {
                 VideoPlayer.playVideo(requireContext(), item)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt
new file mode 100644
index 00000000..0bd9e4cd
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt
@@ -0,0 +1,345 @@
+/*
+ * PlaylistsFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
+package org.moire.ultrasonic.fragment.legacy
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.os.Bundle
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.method.LinkMovementMethod
+import android.text.util.Linkify
+import android.view.ContextMenu
+import android.view.ContextMenu.ContextMenuInfo
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView.AdapterContextMenuInfo
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.ListView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import java.util.Locale
+import org.koin.java.KoinJavaComponent.inject
+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.service.MusicServiceFactory.getMusicService
+import org.moire.ultrasonic.service.OfflineException
+import org.moire.ultrasonic.subsonic.DownloadHandler
+import org.moire.ultrasonic.util.BackgroundTask
+import org.moire.ultrasonic.util.CacheCleaner
+import org.moire.ultrasonic.util.CancellationToken
+import org.moire.ultrasonic.util.FragmentBackgroundTask
+import org.moire.ultrasonic.util.LoadingTask
+import org.moire.ultrasonic.util.Util.applyTheme
+import org.moire.ultrasonic.util.Util.toast
+import org.moire.ultrasonic.view.PlaylistAdapter
+
+/**
+ * Displays the playlists stored on the server
+ *
+ * TODO: This file has been converted from Java, but not modernized yet.
+ */
+class PlaylistsFragment : Fragment() {
+    private var refreshPlaylistsListView: SwipeRefreshLayout? = null
+    private var playlistsListView: ListView? = null
+    private var emptyTextView: View? = null
+    private var playlistAdapter: PlaylistAdapter? = null
+    private val downloadHandler = inject<DownloadHandler>(
+        DownloadHandler::class.java
+    )
+    private var cancellationToken: CancellationToken? = null
+    override fun onCreate(savedInstanceState: Bundle?) {
+        applyTheme(this.context)
+        super.onCreate(savedInstanceState)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.select_playlist, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        cancellationToken = CancellationToken()
+        refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh)
+        playlistsListView = view.findViewById(R.id.select_playlist_list)
+        refreshPlaylistsListView!!.setOnRefreshListener { load(true) }
+        emptyTextView = view.findViewById(R.id.select_playlist_empty)
+        playlistsListView!!.setOnItemClickListener { parent, _, position, _ ->
+            val (id1, name) = parent.getItemAtPosition(position) as Playlist
+
+            val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
+                id = id1,
+                playlistId = id1,
+                name = name,
+                playlistName = name,
+            )
+            findNavController().navigate(action)
+        }
+        registerForContextMenu(playlistsListView!!)
+        setTitle(this, R.string.playlist_label)
+        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)
+                    if (!isOffline()) CacheCleaner().cleanPlaylists(playlists)
+                    return playlists
+                }
+
+                override fun done(result: List<Playlist>) {
+                    playlistsListView!!.adapter =
+                        PlaylistAdapter(context, result).also { playlistAdapter = it }
+                    emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
+                }
+            }
+        task.execute()
+    }
+
+    override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
+        super.onCreateContextMenu(menu, view, menuInfo)
+        val inflater = requireActivity().menuInflater
+        if (isOffline()) inflater.inflate(
+            R.menu.select_playlist_context_offline,
+            menu
+        ) else inflater.inflate(R.menu.select_playlist_context, menu)
+        val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
+        if (downloadMenuItem != null) {
+            downloadMenuItem.isVisible = !isOffline()
+        }
+    }
+
+    override fun onContextItemSelected(menuItem: MenuItem): Boolean {
+        val info = menuItem.menuInfo as AdapterContextMenuInfo
+        val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
+        when (menuItem.itemId) {
+            R.id.playlist_menu_pin -> {
+                downloadHandler.value.downloadPlaylist(
+                    this,
+                    id = playlist.id,
+                    name = playlist.name,
+                    save = true,
+                    append = true,
+                    autoplay = false,
+                    shuffle = false,
+                    background = true,
+                    playNext = false,
+                    unpin = false
+                )
+            }
+            R.id.playlist_menu_unpin -> {
+                downloadHandler.value.downloadPlaylist(
+                    this,
+                    id = playlist.id,
+                    name = playlist.name,
+                    save = false,
+                    append = false,
+                    autoplay = false,
+                    shuffle = false,
+                    background = true,
+                    playNext = false,
+                    unpin = true
+                )
+            }
+            R.id.playlist_menu_download -> {
+                downloadHandler.value.downloadPlaylist(
+                    this,
+                    id = playlist.id,
+                    name = playlist.name,
+                    save = false,
+                    append = false,
+                    autoplay = false,
+                    shuffle = false,
+                    background = true,
+                    playNext = false,
+                    unpin = false
+                )
+            }
+            R.id.playlist_menu_play_now -> {
+                val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
+                    playlistId = playlist.id,
+                    playlistName = playlist.name,
+                    autoPlay = true
+                )
+                findNavController().navigate(action)
+            }
+            R.id.playlist_menu_play_shuffled -> {
+                val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
+                    playlistId = playlist.id,
+                    playlistName = playlist.name,
+                    autoPlay = true,
+                    shuffle = true
+                )
+
+                findNavController().navigate(action)
+            }
+            R.id.playlist_menu_delete -> {
+                deletePlaylist(playlist)
+            }
+            R.id.playlist_info -> {
+                displayPlaylistInfo(playlist)
+            }
+            R.id.playlist_update_info -> {
+                updatePlaylistInfo(playlist)
+            }
+            else -> {
+                return super.onContextItemSelected(menuItem)
+            }
+        }
+        return true
+    }
+
+    private fun deletePlaylist(playlist: Playlist) {
+        AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning)
+            .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? {
+                        val musicService = getMusicService()
+                        musicService.deletePlaylist(playlist.id)
+                        return null
+                    }
+
+                    override fun done(result: Any?) {
+                        playlistAdapter!!.remove(playlist)
+                        playlistAdapter!!.notifyDataSetChanged()
+                        toast(
+                            context,
+                            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(context, msg, false)
+                    }
+                }.execute()
+            }.setNegativeButton(R.string.common_cancel, null).show()
+    }
+
+    private fun displayPlaylistInfo(playlist: Playlist) {
+        val textView = TextView(context)
+        textView.setPadding(5, 5, 5, 5)
+        val message: Spannable = SpannableString(
+            """
+                  Owner: ${playlist.owner}
+                  Comments: ${playlist.comment}
+                  Song Count: ${playlist.songCount}
+            """.trimIndent() +
+                if (playlist.public == null) "" else """
+     
+     Public: ${playlist.public}
+                """.trimIndent() + """
+          
+          Creation Date: ${playlist.created.replace('T', ' ')}
+                """.trimIndent()
+        )
+        Linkify.addLinks(message, Linkify.WEB_URLS)
+        textView.text = message
+        textView.movementMethod = LinkMovementMethod.getInstance()
+        AlertDialog.Builder(context).setTitle(playlist.name).setCancelable(true)
+            .setIcon(R.drawable.ic_baseline_info).setView(textView).show()
+    }
+
+    @SuppressLint("InflateParams")
+    private fun updatePlaylistInfo(playlist: Playlist) {
+        val dialogView = layoutInflater.inflate(R.layout.update_playlist, null) ?: return
+        val nameBox = dialogView.findViewById<EditText>(R.id.get_playlist_name)
+        val commentBox = dialogView.findViewById<EditText>(R.id.get_playlist_comment)
+        val publicBox = dialogView.findViewById<CheckBox>(R.id.get_playlist_public)
+        nameBox.setText(playlist.name)
+        commentBox.setText(playlist.comment)
+        val pub = playlist.public
+        if (pub == null) {
+            publicBox.isEnabled = false
+        } else {
+            publicBox.isChecked = pub
+        }
+        val alertDialog = AlertDialog.Builder(context)
+        alertDialog.setIcon(R.drawable.ic_baseline_warning)
+        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()
+                    musicService.updatePlaylist(playlist.id, name, comment, publicBox.isChecked)
+                    return null
+                }
+
+                override fun done(result: Any?) {
+                    load(true)
+                    toast(
+                        context,
+                        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(context, msg, false)
+                }
+            }.execute()
+        }
+        alertDialog.setNegativeButton(R.string.common_cancel, null)
+        alertDialog.show()
+    }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt
new file mode 100644
index 00000000..b8fec24b
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt
@@ -0,0 +1,94 @@
+/*
+ * PodcastFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
+package org.moire.ultrasonic.fragment.legacy
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ListView
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import org.moire.ultrasonic.R
+import org.moire.ultrasonic.domain.PodcastsChannel
+import org.moire.ultrasonic.fragment.FragmentTitle.Companion.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.Util.applyTheme
+import org.moire.ultrasonic.view.PodcastsChannelsAdapter
+
+/**
+ * Displays the podcasts available on the server
+ *
+ * TODO: This file has been converted from Java, but not modernized yet.
+ */
+class PodcastFragment : Fragment() {
+    private var emptyTextView: View? = null
+    var channelItemsListView: ListView? = null
+    private var cancellationToken: CancellationToken? = null
+    private var swipeRefresh: SwipeRefreshLayout? = null
+    override fun onCreate(savedInstanceState: Bundle?) {
+        applyTheme(this.context)
+        super.onCreate(savedInstanceState)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.podcasts, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        cancellationToken = CancellationToken()
+        swipeRefresh = view.findViewById(R.id.podcasts_refresh)
+        swipeRefresh!!.setOnRefreshListener { load(view.context, 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 = PodcastFragmentDirections.podcastToTrackCollection(
+                podcastChannelId = id
+            )
+
+            findNavController().navigate(action)
+        }
+        load(view.context, false)
+    }
+
+    override fun onDestroyView() {
+        cancellationToken!!.cancel()
+        super.onDestroyView()
+    }
+
+    private fun load(context: Context, 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 = PodcastsChannelsAdapter(context, result)
+                    emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
+                }
+            }
+        task.execute()
+    }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt
new file mode 100644
index 00000000..9f038b30
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt
@@ -0,0 +1,107 @@
+/*
+ * 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
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.ListView
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import org.moire.ultrasonic.R
+import org.moire.ultrasonic.domain.Genre
+import org.moire.ultrasonic.fragment.FragmentTitle.Companion.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.Settings.maxSongs
+import org.moire.ultrasonic.util.Util.applyTheme
+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
+    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)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.select_genre, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        cancellationToken = CancellationToken()
+        refreshGenreListView = view.findViewById(R.id.select_genre_refresh)
+        genreListView = view.findViewById(R.id.select_genre_list)
+        refreshGenreListView!!.setOnRefreshListener { load(true) }
+        genreListView!!.setOnItemClickListener { parent: AdapterView<*>,
+            _: View?,
+            position: Int,
+            _: Long ->
+            val genre = parent.getItemAtPosition(position) as Genre
+
+            val action = SelectGenreFragmentDirections.selectGenreToTrackCollection(
+                genreName = genre.name,
+                size = maxSongs,
+                offset = 0
+            )
+            findNavController().navigate(action)
+        }
+        emptyView = view.findViewById(R.id.select_genre_empty)
+        registerForContextMenu(genreListView!!)
+        setTitle(this, R.string.main_genres_title)
+        load(false)
+    }
+
+    override fun onDestroyView() {
+        cancellationToken!!.cancel()
+        super.onDestroyView()
+    }
+
+    private fun load(refresh: Boolean) {
+        val task: BackgroundTask<List<Genre>> = object : FragmentBackgroundTask<List<Genre>>(
+            activity, true, refreshGenreListView, cancellationToken
+        ) {
+            override fun doInBackground(): List<Genre> {
+                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
+            }
+
+            override fun done(result: List<Genre>) {
+                emptyView!!.isVisible = result.isEmpty()
+                genreListView!!.adapter = GenreAdapter(context, result)
+            }
+        }
+        task.execute()
+    }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt
new file mode 100644
index 00000000..7753d079
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt
@@ -0,0 +1,364 @@
+/*
+ * SharesFragment.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
+package org.moire.ultrasonic.fragment.legacy
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.os.Bundle
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.method.LinkMovementMethod
+import android.text.util.Linkify
+import android.view.ContextMenu
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.ListView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import java.util.Locale
+import org.koin.java.KoinJavaComponent
+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.MusicServiceFactory
+import org.moire.ultrasonic.service.OfflineException
+import org.moire.ultrasonic.subsonic.DownloadHandler
+import org.moire.ultrasonic.util.BackgroundTask
+import org.moire.ultrasonic.util.CancellationToken
+import org.moire.ultrasonic.util.FragmentBackgroundTask
+import org.moire.ultrasonic.util.LoadingTask
+import org.moire.ultrasonic.util.TimeSpan
+import org.moire.ultrasonic.util.TimeSpanPicker
+import org.moire.ultrasonic.util.Util
+import org.moire.ultrasonic.view.ShareAdapter
+
+/**
+ * Displays the shares in the media library
+ *
+ * TODO: This file has been converted from Java, but not modernized yet.
+ */
+class SharesFragment : Fragment() {
+    private var refreshSharesListView: SwipeRefreshLayout? = null
+    private var sharesListView: ListView? = null
+    private var emptyTextView: View? = null
+    private var shareAdapter: ShareAdapter? = null
+    private val downloadHandler = KoinJavaComponent.inject<DownloadHandler>(
+        DownloadHandler::class.java
+    )
+    private var cancellationToken: CancellationToken? = null
+    override fun onCreate(savedInstanceState: Bundle?) {
+        Util.applyTheme(this.context)
+        super.onCreate(savedInstanceState)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.select_share, container, false)
+    }
+
+    @Suppress("NAME_SHADOWING")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        cancellationToken = CancellationToken()
+        refreshSharesListView = view.findViewById(R.id.select_share_refresh)
+        sharesListView = view.findViewById(R.id.select_share_list)
+        refreshSharesListView!!.setOnRefreshListener { load(true) }
+        emptyTextView = view.findViewById(R.id.select_share_empty)
+        sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
+            parent, _, position, _ ->
+            val share = parent.getItemAtPosition(position) as Share
+
+            val action = SharesFragmentDirections.sharesToTrackCollection(
+                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
+        ) {
+            @Throws(Throwable::class)
+            override fun doInBackground(): List<Share> {
+                val musicService = MusicServiceFactory.getMusicService()
+                return musicService.getShares(refresh)
+            }
+
+            override fun done(result: List<Share>) {
+                sharesListView!!.adapter = ShareAdapter(context, result).also { shareAdapter = it }
+                emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
+            }
+        }
+        task.execute()
+    }
+
+    override fun onCreateContextMenu(
+        menu: ContextMenu,
+        view: View,
+        menuInfo: ContextMenu.ContextMenuInfo?
+    ) {
+        super.onCreateContextMenu(menu, view, menuInfo)
+        val inflater = requireActivity().menuInflater
+        inflater.inflate(R.menu.select_share_context, menu)
+    }
+
+    override fun onContextItemSelected(menuItem: MenuItem): Boolean {
+        val info = menuItem.menuInfo as AdapterView.AdapterContextMenuInfo
+        val share = sharesListView!!.getItemAtPosition(info.position) as Share
+        when (menuItem.itemId) {
+            R.id.share_menu_pin -> {
+                downloadHandler.value.downloadShare(
+                    this,
+                    share.id,
+                    share.name,
+                    save = true,
+                    append = true,
+                    autoplay = false,
+                    shuffle = false,
+                    background = true,
+                    playNext = false,
+                    unpin = false
+                )
+            }
+            R.id.share_menu_unpin -> {
+                downloadHandler.value.downloadShare(
+                    this,
+                    share.id,
+                    share.name,
+                    save = false,
+                    append = false,
+                    autoplay = false,
+                    shuffle = false,
+                    background = true,
+                    playNext = false,
+                    unpin = true
+                )
+            }
+            R.id.share_menu_download -> {
+                downloadHandler.value.downloadShare(
+                    this,
+                    share.id,
+                    share.name,
+                    save = false,
+                    append = false,
+                    autoplay = false,
+                    shuffle = false,
+                    background = true,
+                    playNext = false,
+                    unpin = false
+                )
+            }
+            R.id.share_menu_play_now -> {
+                downloadHandler.value.downloadShare(
+                    this,
+                    share.id,
+                    share.name,
+                    save = false,
+                    append = false,
+                    autoplay = true,
+                    shuffle = false,
+                    background = false,
+                    playNext = false,
+                    unpin = false
+                )
+            }
+            R.id.share_menu_play_shuffled -> {
+                downloadHandler.value.downloadShare(
+                    this,
+                    share.id,
+                    share.name,
+                    save = false,
+                    append = false,
+                    autoplay = true,
+                    shuffle = true,
+                    background = false,
+                    playNext = false,
+                    unpin = false
+                )
+            }
+            R.id.share_menu_delete -> {
+                deleteShare(share)
+            }
+            R.id.share_info -> {
+                displayShareInfo(share)
+            }
+            R.id.share_update_info -> {
+                updateShareInfo(share)
+            }
+            else -> {
+                return super.onContextItemSelected(menuItem)
+            }
+        }
+        return true
+    }
+
+    private fun deleteShare(share: Share) {
+        AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning)
+            .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()
+                        Util.toast(
+                            context,
+                            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)
+                                )
+                            }
+                        Util.toast(context, msg, false)
+                    }
+                }.execute()
+            }.setNegativeButton(R.string.common_cancel, null).show()
+    }
+
+    private fun displayShareInfo(share: Share) {
+        val textView = TextView(context)
+        textView.setPadding(5, 5, 5, 5)
+        val message: Spannable = SpannableString(
+            """
+                  Owner: ${share.username}
+                  Comments: ${if (share.description == null) "" else share.description}
+                  URL: ${share.url}
+                  Entry Count: ${share.getEntries().size}
+                  Visit Count: ${share.visitCount}
+            """.trimIndent() +
+                (
+                    if (share.created == null) "" else """
+     
+     Creation Date: ${share.created!!.replace('T', ' ')}
+                    """.trimIndent()
+                    ) +
+                (
+                    if (share.lastVisited == null) "" else """
+     
+     Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
+                    """.trimIndent()
+                    ) +
+                if (share.expires == null) "" else """
+     
+     Expiration Date: ${share.expires!!.replace('T', ' ')}
+                """.trimIndent()
+        )
+        Linkify.addLinks(message, Linkify.WEB_URLS)
+        textView.text = message
+        textView.movementMethod = LinkMovementMethod.getInstance()
+        AlertDialog.Builder(context).setTitle("Share Details").setCancelable(true)
+            .setIcon(R.drawable.ic_baseline_info).setView(textView).show()
+    }
+
+    @SuppressLint("InflateParams")
+    private fun updateShareInfo(share: Share) {
+        val dialogView = layoutInflater.inflate(R.layout.share_details, null) ?: return
+        val shareDescription = dialogView.findViewById<EditText>(R.id.share_description)
+        val timeSpanPicker = dialogView.findViewById<TimeSpanPicker>(R.id.date_picker)
+        shareDescription.setText(share.description)
+        val hideDialogCheckBox = dialogView.findViewById<CheckBox>(R.id.hide_dialog)
+        val saveAsDefaultsCheckBox = dialogView.findViewById<CheckBox>(R.id.save_as_defaults)
+        val noExpirationCheckBox = dialogView.findViewById<CheckBox>(R.id.timeSpanDisableCheckBox)
+        noExpirationCheckBox.setOnCheckedChangeListener { _, b ->
+            timeSpanPicker.isEnabled = !b
+        }
+        noExpirationCheckBox.isChecked = true
+        timeSpanPicker.setTimeSpanDisableText(resources.getText(R.string.no_expiration))
+        hideDialogCheckBox.visibility = View.GONE
+        saveAsDefaultsCheckBox.visibility = View.GONE
+        val alertDialog = AlertDialog.Builder(context)
+        alertDialog.setIcon(R.drawable.ic_baseline_warning)
+        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.timeSpan.totalMilliseconds
+                    if (millis > 0) {
+                        millis = TimeSpan.getCurrentTime().add(millis).totalMilliseconds
+                    }
+                    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)
+                    Util.toast(
+                        context,
+                        resources.getString(R.string.playlist_updated_info, share.name)
+                    )
+                }
+
+                override fun error(error: Throwable) {
+                    val msg: String
+                    msg = 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)
+                        )
+                    }
+                    Util.toast(context, msg, false)
+                }
+            }.execute()
+        }
+        alertDialog.setNegativeButton(R.string.common_cancel, null)
+        alertDialog.show()
+    }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt
index 95c6ff49..ae08cb55 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt
@@ -1,124 +1,104 @@
+/*
+ * AlbumListModel.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.model
 
 import android.app.Application
-import android.os.Bundle
-import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import org.moire.ultrasonic.api.subsonic.models.AlbumListType
 import org.moire.ultrasonic.domain.Album
-import org.moire.ultrasonic.service.MusicService
-import org.moire.ultrasonic.util.Constants
+import org.moire.ultrasonic.service.MusicServiceFactory
 import org.moire.ultrasonic.util.Settings
 
 class AlbumListModel(application: Application) : GenericListModel(application) {
 
     val list: MutableLiveData<List<Album>> = MutableLiveData()
-    var lastType: String? = null
+    private var lastType: AlbumListType? = null
     private var loadedUntil: Int = 0
 
-    fun getAlbumList(
-        refresh: Boolean,
-        swipe: SwipeRefreshLayout,
-        args: Bundle
-    ): LiveData<List<Album>> {
-        // Don't reload the data if navigating back to the view that was active before.
-        // This way, we keep the scroll position
-        val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!!
-
-        if (refresh || list.value?.isEmpty() != false || albumListType != lastType) {
-            lastType = albumListType
-            backgroundLoadFromServer(refresh, swipe, args)
-        }
-        return list
-    }
-
-    private fun getAlbumsOfArtist(
-        musicService: MusicService,
+    suspend fun getAlbumsOfArtist(
         refresh: Boolean,
         id: String,
         name: String?
     ) {
-        list.postValue(musicService.getAlbumsOfArtist(id, name, refresh))
+        withContext(Dispatchers.IO) {
+            val service = MusicServiceFactory.getMusicService()
+            list.postValue(service.getAlbumsOfArtist(id, name, refresh))
+        }
     }
 
-    override fun load(
-        isOffline: Boolean,
-        useId3Tags: Boolean,
-        musicService: MusicService,
-        refresh: Boolean,
-        args: Bundle
+    @Suppress("NAME_SHADOWING")
+    suspend fun getAlbums(
+        albumListType: AlbumListType,
+        size: Int = 0,
+        offset: Int = 0,
+        append: Boolean = false,
+        refresh: Boolean
     ) {
-        super.load(isOffline, useId3Tags, musicService, refresh, args)
-
-        val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!!
-        val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0)
-        var offset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
-        val append = args.getBoolean(Constants.INTENT_APPEND, false)
-
-        val musicDirectory: List<Album>
-        val musicFolderId = if (showSelectFolderHeader(args)) {
-            activeServerProvider.getActiveServer().musicFolderId
-        } else {
-            null
+        // Don't reload the data if navigating back to the view that was active before.
+        // This way, we keep the scroll position
+        if ((!refresh && list.value?.isEmpty() == false && albumListType == lastType)) {
+            return
         }
+        lastType = albumListType
 
-        // If we are refreshing the random list, we want to avoid items moving across the screen,
-        // by clearing the list first
-        if (refresh && !append && albumListType == "random") {
-            list.postValue(listOf())
-        }
+        withContext(Dispatchers.IO) {
+            val service = MusicServiceFactory.getMusicService()
+            var offset = offset
 
-        // Handle the logic for endless scrolling:
-        // If appending the existing list, set the offset from where to load
-        if (append) offset += (size + loadedUntil)
+            val musicDirectory: List<Album>
+            val musicFolderId = if (showSelectFolderHeader()) {
+                activeServerProvider.getActiveServer().musicFolderId
+            } else {
+                null
+            }
 
-        if (albumListType == Constants.ALBUMS_OF_ARTIST) {
-            return getAlbumsOfArtist(
-                musicService,
-                refresh,
-                args.getString(Constants.INTENT_ID, ""),
-                args.getString(Constants.INTENT_NAME, "")
-            )
-        }
+            // If we are refreshing the random list, we want to avoid items moving across the screen,
+            // by clearing the list first
+            if (refresh && !append && albumListType == AlbumListType.RANDOM) {
+                list.postValue(listOf())
+            }
 
-        val type = AlbumListType.fromName(albumListType)
+            // Handle the logic for endless scrolling:
+            // If appending the existing list, set the offset from where to load
+            if (append) offset += (size + loadedUntil)
 
-        if (useId3Tags) {
-            musicDirectory =
-                musicService.getAlbumList2(
-                    type, size,
+            musicDirectory = if (Settings.shouldUseId3Tags) {
+                service.getAlbumList2(
+                    albumListType, size,
                     offset, musicFolderId
                 )
-        } else {
-            musicDirectory = musicService.getAlbumList(
-                type, size,
-                offset, musicFolderId
-            )
+            } else {
+                service.getAlbumList(
+                    albumListType, size,
+                    offset, musicFolderId
+                )
+            }
+
+            currentListIsSortable = isCollectionSortable(albumListType)
+
+            if (append && list.value != null) {
+                val newList = ArrayList<Album>()
+                newList.addAll(list.value!!)
+                newList.addAll(musicDirectory)
+                list.postValue(newList)
+            } else {
+                list.postValue(musicDirectory)
+            }
+
+            loadedUntil = offset
         }
-
-        currentListIsSortable = isCollectionSortable(type)
-
-        if (append && list.value != null) {
-            val newList = ArrayList<Album>()
-            newList.addAll(list.value!!)
-            newList.addAll(musicDirectory)
-            list.postValue(newList)
-        } else {
-            list.postValue(musicDirectory)
-        }
-
-        loadedUntil = offset
     }
 
-    override fun showSelectFolderHeader(args: Bundle?): Boolean {
-        if (args == null) return false
-
-        // TODO: Use proper type here
-        val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!!
-
-        val isAlphabetical = (albumListType == AlbumListType.SORTED_BY_NAME.typeName) ||
-            (albumListType == AlbumListType.SORTED_BY_ARTIST.typeName)
+    override fun showSelectFolderHeader(): Boolean {
+        val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) ||
+            (lastType == AlbumListType.SORTED_BY_ARTIST)
 
         return !isOffline() && !Settings.shouldUseId3Tags && isAlphabetical
     }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt
index e41c6bbc..6dd042ce 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt
@@ -7,7 +7,6 @@
 package org.moire.ultrasonic.model
 
 import android.app.Application
-import android.os.Bundle
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -38,10 +37,9 @@ class ArtistListModel(application: Application) : GenericListModel(application)
         isOffline: Boolean,
         useId3Tags: Boolean,
         musicService: MusicService,
-        refresh: Boolean,
-        args: Bundle
+        refresh: Boolean
     ) {
-        super.load(isOffline, useId3Tags, musicService, refresh, args)
+        super.load(isOffline, useId3Tags, musicService, refresh)
 
         val musicFolderId = activeServer.musicFolderId
 
@@ -54,7 +52,7 @@ class ArtistListModel(application: Application) : GenericListModel(application)
         artists.postValue(result.toMutableList().sortedWith(comparator))
     }
 
-    override fun showSelectFolderHeader(args: Bundle?): Boolean {
+    override fun showSelectFolderHeader(): Boolean {
         return true
     }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt
index 2d4b4aea..813dcce8 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt
@@ -1,8 +1,14 @@
+/*
+ * GenericListModel.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.model
 
 import android.app.Application
 import android.content.Context
-import android.os.Bundle
 import android.os.Handler
 import android.os.Looper
 import androidx.lifecycle.AndroidViewModel
@@ -41,7 +47,7 @@ open class GenericListModel(application: Application) :
 
     val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
 
-    open fun showSelectFolderHeader(args: Bundle?): Boolean {
+    open fun showSelectFolderHeader(): Boolean {
         return false
     }
 
@@ -55,8 +61,8 @@ open class GenericListModel(application: Application) :
     /**
      * Refreshes the cached items from the server
      */
-    fun refresh(swipe: SwipeRefreshLayout, bundle: Bundle?) {
-        backgroundLoadFromServer(true, swipe, bundle ?: Bundle())
+    fun refresh(swipe: SwipeRefreshLayout) {
+        backgroundLoadFromServer(true, swipe)
     }
 
     /**
@@ -64,12 +70,11 @@ open class GenericListModel(application: Application) :
      */
     fun backgroundLoadFromServer(
         refresh: Boolean,
-        swipe: SwipeRefreshLayout,
-        bundle: Bundle = Bundle()
+        swipe: SwipeRefreshLayout
     ) {
         viewModelScope.launch {
             swipe.isRefreshing = true
-            loadFromServer(refresh, swipe, bundle)
+            loadFromServer(refresh, swipe)
             swipe.isRefreshing = false
         }
     }
@@ -77,18 +82,22 @@ open class GenericListModel(application: Application) :
     /**
      * Calls the load() function with error handling
      */
-    suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout, bundle: Bundle) =
+    private suspend fun loadFromServer(
+        refresh: Boolean,
+        swipe: SwipeRefreshLayout
+    ) {
         withContext(Dispatchers.IO) {
             val musicService = MusicServiceFactory.getMusicService()
             val isOffline = ActiveServerProvider.isOffline()
             val useId3Tags = Settings.shouldUseId3Tags
 
             try {
-                load(isOffline, useId3Tags, musicService, refresh, bundle)
+                load(isOffline, useId3Tags, musicService, refresh)
             } catch (all: Exception) {
                 handleException(all, swipe.context)
             }
         }
+    }
 
     private fun handleException(exception: Exception, context: Context) {
         Handler(Looper.getMainLooper()).post {
@@ -103,12 +112,11 @@ open class GenericListModel(application: Application) :
         isOffline: Boolean,
         useId3Tags: Boolean,
         musicService: MusicService,
-        refresh: Boolean,
-        args: Bundle
+        refresh: Boolean
     ) {
         // Update the list of available folders if enabled
         @Suppress("ComplexCondition")
-        if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) {
+        if (showSelectFolderHeader() && !isOffline && !useId3Tags && refresh) {
             musicFolders.postValue(
                 musicService.getMusicFolders(refresh)
             )
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt
index 252c48cb..84087b86 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt
@@ -1,14 +1,12 @@
 package org.moire.ultrasonic.model
 
 import android.app.Application
-import android.os.Bundle
 import androidx.lifecycle.MutableLiveData
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import org.moire.ultrasonic.domain.SearchCriteria
 import org.moire.ultrasonic.domain.SearchResult
 import org.moire.ultrasonic.fragment.SearchFragment
-import org.moire.ultrasonic.service.MusicService
 import org.moire.ultrasonic.service.MusicServiceFactory
 import org.moire.ultrasonic.util.Settings
 
@@ -16,16 +14,6 @@ class SearchListModel(application: Application) : GenericListModel(application)
 
     var searchResult: MutableLiveData<SearchResult?> = MutableLiveData()
 
-    override fun load(
-        isOffline: Boolean,
-        useId3Tags: Boolean,
-        musicService: MusicService,
-        refresh: Boolean,
-        args: Bundle
-    ) {
-        super.load(isOffline, useId3Tags, musicService, refresh, args)
-    }
-
     suspend fun search(query: String) {
         val maxArtists = Settings.maxArtists
         val maxAlbums = Settings.maxAlbums
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md
deleted file mode 100644
index 0019ee0b..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-UI:
-[x] Display tracks
-[x] On selection: Translate Tracks to MediaItems
-[x] Move playlist val to Controller: Keep it around for easier migration!!
-[x] Also make a LRU Cache to help with translation between MediaItem and DownloadFile
-[x] Hand MediaItems to Service
-[] If wanted also hand them to Downloader.kt
-[x] Service plays MediaItem through OkHttp
-[x] UI needs to receive info from service
-[x] Create a Cache Layer
-[] Translate AutoMediaBrowserService
-[] Add new shuffle icon....
-
-convertToPlaybackStateCompatState()
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
index 34af142b..d6312be7 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
@@ -1,6 +1,6 @@
 /*
  * CachedMusicService.kt
- * Copyright (C) 2009-2021 Ultrasonic developers
+ * Copyright (C) 2009-2022 Ultrasonic developers
  *
  * Distributed under terms of the GNU GPLv3 license.
  */
@@ -31,7 +31,6 @@ import org.moire.ultrasonic.domain.SearchResult
 import org.moire.ultrasonic.domain.Share
 import org.moire.ultrasonic.domain.Track
 import org.moire.ultrasonic.domain.UserInfo
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.LRUCache
 import org.moire.ultrasonic.util.Settings
 import org.moire.ultrasonic.util.TimeLimitedCache
@@ -393,7 +392,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
         var result = cachedGenres.get()
         if (result == null) {
             result = musicService.getGenres(refresh)
-            cachedGenres.set(result!!)
+            cachedGenres.set(result)
         }
 
         val sorted = result.toMutableList()
@@ -443,7 +442,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
     override fun getVideos(refresh: Boolean): MusicDirectory? {
         checkSettingsChanged()
         var cache =
-            if (refresh) null else cachedMusicDirectories[Constants.INTENT_VIDEOS]
+            if (refresh) null else cachedMusicDirectories[CACHE_KEY_VIDEOS]
         var dir = cache?.get()
         if (dir == null) {
             dir = musicService.getVideos(refresh)
@@ -451,7 +450,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
                 Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS
             )
             cache.set(dir)
-            cachedMusicDirectories.put(Constants.INTENT_VIDEOS, cache)
+            cachedMusicDirectories.put(CACHE_KEY_VIDEOS, cache)
         }
         return dir
     }
@@ -493,6 +492,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
 
     companion object {
         private const val MUSIC_DIR_CACHE_SIZE = 100
+        const val CACHE_KEY_VIDEOS = "VIDEOS"
     }
 
     init {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
index 4c21c547..d1e0a3a3 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
@@ -231,7 +231,7 @@ class MediaPlayerController(
 
     private fun publishPlaybackState() {
         val newState = RxBus.StateWithTrack(
-            track = currentMediaItem?.let { it.toTrack() },
+            track = currentMediaItem?.toTrack(),
             index = currentMediaItemIndex,
             isPlaying = isPlaying,
             state = playbackState
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
index ea9ed883..eb3c8bb9 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
@@ -1,6 +1,6 @@
 /*
  * MusicService.kt
- * Copyright (C) 2009-2021 Ultrasonic developers
+ * Copyright (C) 2009-2022 Ultrasonic developers
  *
  * Distributed under terms of the GNU GPLv3 license.
  */
@@ -36,7 +36,7 @@ interface MusicService {
     fun isLicenseValid(): Boolean
 
     @Throws(Exception::class)
-    fun getGenres(refresh: Boolean): List<Genre>?
+    fun getGenres(refresh: Boolean): List<Genre>
 
     @Throws(Exception::class)
     fun star(id: String?, albumId: String?, artistId: String?)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
index 5c438218..039e1de1 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
@@ -376,7 +376,7 @@ class OfflineMusicService : MusicService, KoinComponent {
     }
 
     @Throws(Exception::class)
-    override fun getGenres(refresh: Boolean): List<Genre>? {
+    override fun getGenres(refresh: Boolean): List<Genre> {
         throw OfflineException("Getting Genres not available in offline mode")
     }
 
@@ -572,7 +572,7 @@ class OfflineMusicService : MusicService, KoinComponent {
         title = name
 
         val albumArt = FileUtil.getAlbumArtFile(this)
-        if (albumArt != null && File(albumArt).exists()) {
+        if (File(albumArt).exists()) {
             coverArt = albumArt
         }
     }
@@ -628,10 +628,11 @@ class OfflineMusicService : MusicService, KoinComponent {
         if (string == null) return null
 
         val slashIndex = string.indexOf('/')
-        if (slashIndex > 0)
-            return string.substring(0, slashIndex).toIntOrNull()
+
+        return if (slashIndex > 0)
+            string.substring(0, slashIndex).toIntOrNull()
         else
-            return string.toIntOrNull()
+            string.toIntOrNull()
     }
 
     /*
@@ -642,10 +643,10 @@ class OfflineMusicService : MusicService, KoinComponent {
 
         val duration: Long? = string.toLongOrNull()
 
-        if (duration != null)
-            return TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
+        return if (duration != null)
+            TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
         else
-            return null
+            null
     }
 
     // TODO: Simplify this deeply nested and complicated function
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
index 5223c641..a6e2e034 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
@@ -574,7 +574,7 @@ open class RESTMusicService(
     @Throws(Exception::class)
     override fun getGenres(
         refresh: Boolean
-    ): List<Genre>? {
+    ): List<Genre> {
         val response = API.getGenres().execute().throwOnFailure()
 
         return response.body()!!.genresList.toDomainEntityList()
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt
index d5f0d14a..ff531995 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt
@@ -1,3 +1,10 @@
+/*
+ * DownloadHandler.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.subsonic
 
 import android.app.Activity
@@ -11,7 +18,6 @@ import org.moire.ultrasonic.domain.MusicDirectory
 import org.moire.ultrasonic.domain.Track
 import org.moire.ultrasonic.service.MediaPlayerController
 import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
 import org.moire.ultrasonic.util.ModalBackgroundTask
 import org.moire.ultrasonic.util.Settings
@@ -35,6 +41,7 @@ class DownloadHandler(
         playNext: Boolean,
         shuffle: Boolean,
         songs: List<Track>,
+        playlistName: String?,
     ) {
         val onValid = Runnable {
             // TODO: The logic here is different than in the controller...
@@ -52,9 +59,7 @@ class DownloadHandler(
                 shuffle,
                 insertionMode
             )
-            val playlistName: String? = fragment.arguments?.getString(
-                Constants.INTENT_PLAYLIST_NAME
-            )
+
             if (playlistName != null) {
                 mediaPlayerController.suggestedPlaylistName = playlistName
             }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt
index d0f8f596..f60ef15f 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt
@@ -1,3 +1,10 @@
+/*
+ * ShareHandler.kt
+ * Copyright (C) 2009-2022 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
 package org.moire.ultrasonic.subsonic
 
 import android.app.AlertDialog
@@ -20,7 +27,6 @@ import org.moire.ultrasonic.domain.Track
 import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
 import org.moire.ultrasonic.util.BackgroundTask
 import org.moire.ultrasonic.util.CancellationToken
-import org.moire.ultrasonic.util.Constants
 import org.moire.ultrasonic.util.FragmentBackgroundTask
 import org.moire.ultrasonic.util.Settings
 import org.moire.ultrasonic.util.ShareDetails
@@ -42,31 +48,12 @@ class ShareHandler(val context: Context) {
     private var textViewExpiration: TextView? = null
     private val pattern = Pattern.compile(":")
 
-    fun createShare(
-        fragment: Fragment,
-        tracks: List<Track?>?,
-        swipe: SwipeRefreshLayout?,
-        cancellationToken: CancellationToken
-    ) {
-        val askForDetails = Settings.shouldAskForShareDetails
-        val shareDetails = ShareDetails()
-        shareDetails.Entries = tracks
-        if (askForDetails) {
-            showDialog(fragment, shareDetails, swipe, cancellationToken)
-        } else {
-            shareDetails.Description = Settings.defaultShareDescription
-            shareDetails.Expiration = TimeSpan.getCurrentTime().add(
-                Settings.defaultShareExpirationInMillis
-            ).totalMilliseconds
-            share(fragment, shareDetails, swipe, cancellationToken)
-        }
-    }
-
     fun share(
         fragment: Fragment,
         shareDetails: ShareDetails,
         swipe: SwipeRefreshLayout?,
-        cancellationToken: CancellationToken
+        cancellationToken: CancellationToken,
+        additionalId: String?
     ) {
         val task: BackgroundTask<Share?> = object : FragmentBackgroundTask<Share?>(
             fragment.requireActivity(),
@@ -80,7 +67,7 @@ class ShareHandler(val context: Context) {
 
                 if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null
                 if (shareDetails.Entries.isEmpty()) {
-                    fragment.arguments?.getString(Constants.INTENT_ID).ifNotNull {
+                    additionalId.ifNotNull {
                         ids.add(it)
                     }
                 } else {
@@ -144,11 +131,33 @@ class ShareHandler(val context: Context) {
         task.execute()
     }
 
+    fun createShare(
+        fragment: Fragment,
+        tracks: List<Track?>?,
+        swipe: SwipeRefreshLayout?,
+        cancellationToken: CancellationToken,
+        additionalId: String? = null
+    ) {
+        val askForDetails = Settings.shouldAskForShareDetails
+        val shareDetails = ShareDetails()
+        shareDetails.Entries = tracks
+        if (askForDetails) {
+            showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId)
+        } else {
+            shareDetails.Description = Settings.defaultShareDescription
+            shareDetails.Expiration = TimeSpan.getCurrentTime().add(
+                Settings.defaultShareExpirationInMillis
+            ).totalMilliseconds
+            share(fragment, shareDetails, swipe, cancellationToken, additionalId)
+        }
+    }
+
     private fun showDialog(
         fragment: Fragment,
         shareDetails: ShareDetails,
         swipe: SwipeRefreshLayout?,
-        cancellationToken: CancellationToken
+        cancellationToken: CancellationToken,
+        additionalId: String?
     ) {
         val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
 
@@ -205,7 +214,7 @@ class ShareHandler(val context: Context) {
                 Settings.shareOnServer = shareDetails.ShareOnServer
             }
 
-            share(fragment, shareDetails, swipe, cancellationToken)
+            share(fragment, shareDetails, swipe, cancellationToken, additionalId)
         }
 
         builder.setNegativeButton(R.string.common_cancel) { dialog, _ ->
@@ -216,9 +225,7 @@ class ShareHandler(val context: Context) {
         builder.setCancelable(true)
 
         timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration))
-        noExpirationCheckBox!!.setOnCheckedChangeListener {
-            _,
-            b ->
+        noExpirationCheckBox!!.setOnCheckedChangeListener { _, b ->
             timeSpanPicker!!.isEnabled = !b
         }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
index b8b178fc..a1135e6b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
@@ -1,6 +1,6 @@
 /*
  * Constants.kt
- * Copyright (C) 2009-2021 Ultrasonic developers
+ * Copyright (C) 2009-2022 Ultrasonic developers
  *
  * Distributed under terms of the GNU GPLv3 license.
  */
@@ -15,32 +15,13 @@ object Constants {
     const val REST_PROTOCOL_VERSION = "1.7.0"
     const val REST_CLIENT_ID = "Ultrasonic"
 
-    // Names for intent extras.
-    const val INTENT_ID = "subsonic.id"
-    const val INTENT_NAME = "subsonic.name"
+    // Legacy names for intent extras, in those fragments which don't use SafeArgs yet.
     const val INTENT_ARTIST = "subsonic.artist"
     const val INTENT_TITLE = "subsonic.title"
     const val INTENT_AUTOPLAY = "subsonic.playall"
     const val INTENT_QUERY = "subsonic.query"
-    const val INTENT_PLAYLIST_ID = "subsonic.playlist.id"
-    const val INTENT_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id"
-    const val INTENT_PARENT_ID = "subsonic.parent.id"
-    const val INTENT_PLAYLIST_NAME = "subsonic.playlist.name"
-    const val INTENT_SHARE_ID = "subsonic.share.id"
-    const val INTENT_SHARE_NAME = "subsonic.share.name"
     const val INTENT_ALBUM_LIST_TYPE = "subsonic.albumlisttype"
-    const val INTENT_ALBUM_LIST_TITLE = "subsonic.albumlisttitle"
-    const val INTENT_ALBUM_LIST_SIZE = "subsonic.albumlistsize"
-    const val INTENT_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset"
-    const val INTENT_SHUFFLE = "subsonic.shuffle"
-    const val INTENT_REFRESH = "subsonic.refresh"
-    const val INTENT_STARRED = "subsonic.starred"
-    const val INTENT_RANDOM = "subsonic.random"
-    const val INTENT_GENRE_NAME = "subsonic.genre"
-    const val INTENT_IS_ALBUM = "subsonic.isalbum"
-    const val INTENT_VIDEOS = "subsonic.videos"
     const val INTENT_SHOW_PLAYER = "subsonic.showplayer"
-    const val INTENT_APPEND = "subsonic.append"
 
     // Names for Intent Actions
     const val CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE"
@@ -62,8 +43,5 @@ object Constants {
 
     const val FILENAME_PLAYLIST_SER = "downloadstate.ser"
     const val ALBUM_ART_FILE = "folder.jpeg"
-    const val STARRED = "starred"
-    const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
-    const val ALBUMS_OF_ARTIST = "albumsOfArtist"
     const val RESULT_CLOSE_ALL = 1337
 }
diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml
index 90f978a6..a00ade3a 100644
--- a/ultrasonic/src/main/res/navigation/navigation_graph.xml
+++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml
@@ -3,6 +3,13 @@
     xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation_graph"
     app:startDestination="@id/mainFragment">
 
+    <action android:id="@+id/toTrackCollection"
+        app:destination="@id/trackCollectionFragment"  />
+    <action android:id="@+id/toBookmarks"
+        app:destination="@id/bookmarksFragment"  />
+    <action android:id="@+id/toMediaLibrary"
+        app:destination="@id/mediaLibraryFragment"  />
+
     <fragment
         android:id="@+id/mainFragment"
         android:name="org.moire.ultrasonic.fragment.MainFragment"
@@ -19,6 +26,9 @@
         <action
             android:id="@+id/mainToSelectGenre"
             app:destination="@id/selectGenreFragment" />
+        <action
+            android:id="@+id/toTrackCollection"
+            app:destination="@id/trackCollectionFragment" />
     </fragment>
     <fragment
         android:id="@+id/mediaLibraryFragment"
@@ -31,6 +41,14 @@
             android:id="@+id/artistsListToTrackCollection"
             app:destination="@id/trackCollectionFragment" />
     </fragment>
+    <fragment
+        android:id="@+id/nowPlayingFragment"
+        android:name="org.moire.ultrasonic.fragment.NowPlayingFragment"
+        android:label="@string/button_bar.now_playing">
+        <action
+            android:id="@+id/toTrackCollection"
+            app:destination="@id/trackCollectionFragment" />
+    </fragment>
     <fragment
         android:id="@+id/artistListFragment"
         android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
@@ -40,21 +58,150 @@
         <action
             android:id="@+id/artistsListToTrackCollection"
             app:destination="@id/trackCollectionFragment" />
+        <argument
+            android:name="refresh"
+            app:argType="boolean"
+            android:defaultValue="false" />
+        <argument
+            android:name="title"
+            app:argType="string"
+            app:nullable="true"
+            android:defaultValue="@null"
+            />
     </fragment>
     <fragment
         android:id="@+id/trackCollectionFragment"
         android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment" >
         <argument
             android:name="id"
+            app:nullable="true"
+            android:defaultValue="@null"
             app:argType="string" />
         <argument
             android:name="isAlbum"
             app:argType="boolean"
-            android:defaultValue="false" />
+            android:defaultValue="false"/>
+        <argument
+            android:name="isArtist"
+            app:argType="boolean"
+            android:defaultValue="false"/>
+        <argument
+            android:name="getRandom"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="getStarred"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="getVideos"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="autoPlay"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="shuffle"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="refresh"
+            android:defaultValue="true"
+            app:argType="boolean" />
+        <argument android:name="name"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="parentId"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="genreName"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="shareId"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="playlistId"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="playlistName"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="shareName"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument android:name="podcastChannelId"
+            app:argType="string"
+            android:defaultValue="@null"
+            app:nullable="true"/>
+        <argument
+            android:name="albumListType"
+            app:argType="string"
+            app:nullable="true"
+            android:defaultValue="@null"/>
+        <argument
+            android:name="size"
+            app:argType="integer"
+            android:defaultValue="0"
+            />
+        <argument
+            android:name="offset"
+            app:argType="integer"
+            android:defaultValue="0"
+            />
+        <action
+            android:id="@+id/loadMoreTracks"
+            app:destination="@id/trackCollectionFragment" />
     </fragment>
     <fragment
         android:id="@+id/albumListFragment"
         android:name="org.moire.ultrasonic.fragment.AlbumListFragment" >
+        <argument
+            android:name="type"
+            app:argType="org.moire.ultrasonic.api.subsonic.models.AlbumListType"
+            />
+        <argument
+            android:name="title"
+            app:argType="string"
+            app:nullable="true" />
+        <argument
+            android:name="size"
+            app:argType="integer"
+        />
+        <argument
+            android:name="offset"
+            app:argType="integer"
+            />
+        <argument
+            android:name="append"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="refresh"
+            android:defaultValue="false"
+            app:argType="boolean" />
+        <argument
+            android:name="id"
+            android:defaultValue="@null"
+            app:nullable="true"
+            app:argType="string" />
+        <action
+            android:id="@+id/albumListToTrackCollection"
+            app:destination="@id/trackCollectionFragment" />
+    </fragment>
+    <fragment
+        android:id="@+id/entryListFragment"
+        android:name="org.moire.ultrasonic.fragment.EntryListFragment" >
+        <action
+            android:id="@+id/entryListToTrackCollection"
+            app:destination="@id/trackCollectionFragment" />
     </fragment>
     <fragment
         android:id="@+id/searchFragment"
@@ -68,9 +215,9 @@
     </fragment>
     <fragment
         android:id="@+id/playlistsFragment"
-        android:name="org.moire.ultrasonic.fragment.PlaylistsFragment" >
+        android:name="org.moire.ultrasonic.fragment.legacy.PlaylistsFragment" >
         <action
-            android:id="@+id/playlistsToSelectAlbum"
+            android:id="@+id/playlistsToTrackCollection"
             app:destination="@id/trackCollectionFragment" />
     </fragment>
     <fragment
@@ -78,9 +225,9 @@
         android:name="org.moire.ultrasonic.fragment.DownloadsFragment" />
     <fragment
         android:id="@+id/sharesFragment"
-        android:name="org.moire.ultrasonic.fragment.SharesFragment" >
+        android:name="org.moire.ultrasonic.fragment.legacy.SharesFragment" >
         <action
-            android:id="@+id/sharesToSelectAlbum"
+            android:id="@+id/sharesToTrackCollection"
             app:destination="@id/trackCollectionFragment" />
     </fragment>
     <fragment
@@ -91,9 +238,9 @@
         android:name="org.moire.ultrasonic.fragment.ChatFragment" />
     <fragment
         android:id="@+id/podcastFragment"
-        android:name="org.moire.ultrasonic.fragment.PodcastFragment" >
+        android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment" >
         <action
-            android:id="@+id/podcastToSelectAlbum"
+            android:id="@+id/podcastToTrackCollection"
             app:destination="@id/trackCollectionFragment" />
     </fragment>
     <fragment
@@ -105,7 +252,11 @@
         android:name="org.moire.ultrasonic.fragment.AboutFragment" />
     <fragment
         android:id="@+id/selectGenreFragment"
-        android:name="org.moire.ultrasonic.fragment.SelectGenreFragment" />
+        android:name="org.moire.ultrasonic.fragment.legacy.SelectGenreFragment">
+        <action
+            android:id="@+id/selectGenreToTrackCollection"
+            app:destination="@id/trackCollectionFragment" />
+    </fragment>
     <fragment
         android:id="@+id/playerFragment"
         android:name="org.moire.ultrasonic.fragment.PlayerFragment" >