diff --git a/ArchitectureOverview.xopp b/ArchitectureOverview.xopp new file mode 100644 index 0000000..67e418a Binary files /dev/null and b/ArchitectureOverview.xopp differ diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..19d85b7 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + diff --git a/QuickPlay.sln b/QuickPlay.sln index 54c5126..d3d5f69 100644 --- a/QuickPlay.sln +++ b/QuickPlay.sln @@ -5,10 +5,13 @@ VisualStudioVersion = 16.0.31112.23 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickPlay", "QuickPlay\QuickPlay.csproj", "{FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MpcCore", "..\..\Third-party\mpcCore\src\MpcCore\MpcCore.csproj", "{D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Release-Stable|Any CPU = Release-Stable|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -17,6 +20,15 @@ Global {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.ActiveCfg = Release|Any CPU {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.Build.0 = Release|Any CPU {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU + {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.Build.0 = Release|Any CPU + {FDBCCBF8-7CA5-4719-8CBB-8E33C464B27C}.Release-Stable|Any CPU.Deploy.0 = Release|Any CPU + {D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release|Any CPU.Build.0 = Release|Any CPU + {D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU + {D0A5AD05-B98C-45E6-B61D-4700F7AA72CF}.Release-Stable|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/QuickPlay/Configuration.cs b/QuickPlay/Configuration.cs index 723e4b4..188b8db 100644 --- a/QuickPlay/Configuration.cs +++ b/QuickPlay/Configuration.cs @@ -1,14 +1,133 @@ using System; +using System.Net.Http; using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; -namespace QuickPlayer +namespace QuickPlay { +#pragma warning disable CS0659 // This is basically an almost-singleton, which will never be put in any hash table. /// /// Application (global) configuration data class /// - class AppConfiguration + [Serializable] + public sealed class AppConfiguration { + // XXX: All the fields need to be checked in overriden Equals method + // Also: sensible defaults should be provided in defaultConfiguration field + public string playerConfigUrl; + // XXX: Any other fields of this model need [NonSerialized] attribute + [NonSerialized] + public static readonly string configFilePath = + Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), + "appConfig.xml" + ); + public static AppConfiguration loadConfiguration() + { + var cfg = loadSavedConfiguration(); + if (cfg == null) + { + cfg = defaultConfiguration; + } + return cfg; + } + public static AppConfiguration loadSavedConfiguration() + { + var serializer = new System.Xml.Serialization.XmlSerializer(typeof(AppConfiguration)); + AppConfiguration result = null; + try + { + using (var stream = new System.IO.FileStream(configFilePath, System.IO.FileMode.Open)) + { + result = (AppConfiguration)serializer.Deserialize(stream); + + } + } catch (FileNotFoundException) + { + // This is fine, we will return null anyway. + } catch (InvalidCastException) { + // This is not so much fine, TODO log it. + // Still, we cannot supply config, so treat it as if it does not exist. + } + return result; + } + + public void saveConfiguration() + { + // First we save the config + var serializer = new System.Xml.Serialization.XmlSerializer(typeof(AppConfiguration)); + using (var stream = new System.IO.FileStream(configFilePath, System.IO.FileMode.Create)) + { + serializer.Serialize(stream, this); + } + + // Make sure that the configuration is same + var newConfig = AppConfiguration.loadSavedConfiguration(); + if (this != newConfig) throw new InvalidDataException("Saved configuration is different from the supplied one."); + } + + [NonSerialized] + public static readonly AppConfiguration defaultConfiguration = new AppConfiguration + { + playerConfigUrl = "http://www.ms.mff.cuni.cz/~turinskp/znelky.ini", + }; + public async Task GetPlayerConfig() + { + IPlayerConfigurationProvider cfgProvider; + if (playerConfigUrl.StartsWith("http:") || playerConfigUrl.StartsWith("https:")) + { + cfgProvider = new HttpConfigurationProvider(playerConfigUrl); + } else if (playerConfigUrl.StartsWith("file://")) + { + string filename = playerConfigUrl.Substring("file://".Length); + var reader = new StreamReader(filename); + cfgProvider = new FileConfigurationProvider(reader); + } else if (playerConfigUrl == "default") { + cfgProvider = new DummyConfigurationProvider(); + } else + { + throw new InvalidOperationException("Schema of player config URL not supported"); + } + PlayerConfiguration playercfg; + try + { + playercfg = await cfgProvider.GetConfigurationAsync(); + } + // FIXME: These are provider-specific exceptions! + catch (Exception e) when ( + e is Java.Net.UnknownHostException + || e is OperationCanceledException + || e is HttpRequestException + || e is System.Net.WebException + ) + { + var ctx = Android.App.Application.Context; + var t = Android.Widget.Toast.MakeText(ctx, "Could not load config:" + e.Message, Android.Widget.ToastLength.Long); + t.Show(); + cfgProvider = new DummyConfigurationProvider(); + playercfg = await cfgProvider.GetConfigurationAsync(); + } + return playercfg; + } + + // We want to compare by values. + // It might actually be a bit more sensible to compare serialized + // versions of the objects, but that seems unneccessarily hard. + public override bool Equals(object obj) + { + var other = (AppConfiguration)obj; + // These fields have to match... + if (this.playerConfigUrl != other.playerConfigUrl) return false; + return true; + } + } +#pragma warning restore CS0659 + + public enum PlayerType + { + MPD, } /// @@ -16,27 +135,77 @@ namespace QuickPlayer /// /// Contains details about connection, configured songs &c. /// - class PlayerConfiguration + public class PlayerConfiguration { + public Dictionary songs = new Dictionary(); + public string playerName = ""; + public PlayerType playerType; + public string playerConnectionDetails = ""; // This is opaque to this class, the player will make sense of it. + public static PlayerConfiguration FromFile(StreamReader reader) { - return null; // FIXME: Implement + var parser = new IniParser(reader); + var ini = parser.Parse(); + var result = new PlayerConfiguration(); + foreach (var elem in ini) + { + // Two options: either general options, or song descriptions + if (elem.Key == "general") + { + result.playerName = elem.Value["name"]; + result.playerType = PlayerType.MPD; //FIXME: not always. + result.playerConnectionDetails = elem.Value["connection"]; + } else + { + var song = new Song(elem.Key, elem.Value); + result.songs[elem.Key] = song; + } + } + return result; + } + + public IPlayer GetPlayer() + { + IPlayer result; + switch (playerType) + { + case PlayerType.MPD: + result = new MpdPlayer(this); + break; + default: + throw new InvalidOperationException("Cannot happen: Player had no type."); + } + return result; } } interface IPlayerConfigurationProvider { - PlayerConfiguration GetConfiguration(); + Task GetConfigurationAsync(); } - sealed class NetworkConfigurationProvider : IPlayerConfigurationProvider + sealed class HttpConfigurationProvider : IPlayerConfigurationProvider { - + readonly string configUrl; + public HttpConfigurationProvider(string url) + { + configUrl = url; + } + public async Task GetConfigurationAsync() + { + return await Task.Run(async () => + { + var client = new HttpClient(); + var resp = await client.GetStreamAsync(configUrl); + var sr = new StreamReader(resp); + return PlayerConfiguration.FromFile(sr); + }); + } } sealed class FileConfigurationProvider : IPlayerConfigurationProvider { - public readonly StreamReader reader; + StreamReader reader; public FileConfigurationProvider(StreamReader reader) { this.reader = reader; @@ -46,14 +215,36 @@ namespace QuickPlayer { this.reader = new StreamReader(filename); } - public PlayerConfiguration GetConfiguration() + public Task GetConfigurationAsync() { - return PlayerConfiguration.FromFile(reader); + return Task.FromResult(PlayerConfiguration.FromFile(reader)); } } sealed class DummyConfigurationProvider : IPlayerConfigurationProvider { - public PlayerConfiguration GetConfiguration() => null; + public Task GetConfigurationAsync() + { + PlayerConfiguration cfg = new PlayerConfiguration(); + cfg.playerType = PlayerType.MPD; + cfg.playerName = "Dummy player"; + cfg.playerConnectionDetails = "127.0.0.1"; + + return Task.FromResult(cfg); + } + } + + class Song: IPlayable + { + public string Identifier { get; private set; } + public PlayableMetadata Metadata { get; private set; } + public Song(string id, Dictionary data) { + Identifier = id; + Metadata = new PlayableMetadata + { + usualPlayingTime = TimeSpan.Parse(data["time"]), + filePath = data["path"], + }; + } } } \ No newline at end of file diff --git a/QuickPlay/IniParser.cs b/QuickPlay/IniParser.cs new file mode 100644 index 0000000..4a20be6 --- /dev/null +++ b/QuickPlay/IniParser.cs @@ -0,0 +1,67 @@ +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; + +namespace QuickPlay +{ + /// + /// This class implements (yet another) INI-style parser. + /// + /// INI specification: everything is in sections, comments using both '#' + /// or ';', space around '=' is trimmed, last value assigned overrides + /// previous assignments, sections may not repeat. Section names may only + /// contain reasonable characters (at least [a-zA-Z0-9. ], not really + /// enforced). Everything is case-sensitive. + /// + /// Both CRLF and just LF should be supported as line ends. + /// + class IniParser + { + StreamReader reader; + string currentSection = null; + public IniParser(StreamReader sr) + { + reader = sr; + } + + // The return type is dictionary by sections, which in turn holds the section key-value dictionary + // Maybe the code is a bit more Pythonic than it should be... + public Dictionary> Parse() + { + Dictionary> result = new Dictionary>(); + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + line = StripComments(line).Trim(); + if (line.Length == 0) continue; + if (line.StartsWith('[')) + { + // This does not really do the right thing, but for QuickPlay's use it is good enough. + currentSection = line.Split(new char[] { '[', ']' })[1]; + if (result.ContainsKey(currentSection)) throw new InvalidOperationException("Multiple sections with same name"); + result[currentSection] = new Dictionary(); + } + else + { + // Other lines may only be assignments + int equalSignPosition = line.IndexOf('='); + string key = line.Substring(0, equalSignPosition).Trim(); + string value = line.Substring(equalSignPosition + 1).Trim(); + result[currentSection][key] = value; + } + } + return result; + } + string StripComments(string line) + { + return line.Split(new char[] { ';', '#' })[0]; + } + } +} \ No newline at end of file diff --git a/QuickPlay/Interfaces.cs b/QuickPlay/Interfaces.cs index dd4c473..acf3783 100644 --- a/QuickPlay/Interfaces.cs +++ b/QuickPlay/Interfaces.cs @@ -1,44 +1,65 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; // MPD client abstractions and simplifications -namespace QuickPlayer +namespace QuickPlay { /// /// Simplified abstraction of possible players. Only methods needed are included. /// /// That means that the interface may be extended in the future, which is sad. /// - interface IPlayer + public interface IPlayer { - void Play(string identifier); + public enum PlayerStatus + { + Disconnected, Playing, Stopped + } + Dictionary Songs { get; } + string PlayerName { get; } + Task Play(IPlayable playable); float CurrentProgress { get; } - bool IsReady { get; } - void SetReasonableOptions(); //TODO + PlayerStatus Status { get; } + Task SetReasonableOptions(); + /// + /// Attach to the real player. + /// + /// Since this operation can be asynchronous, we cannot put it in th + /// constructor. And this allows for some tweaks before connecting. + /// + /// + Task ConnectAsync(); } /// /// A simple dataclass to hold auxiliary data of the Playable objects. /// // This is not really an interface, but since it is a dataclass, I treat it // more as a part of interface, so it belongs to this file. - class PlayableMetadata + + // Also, in order not to need boilerplate code, this should be semantically + // treated as immutable even though it's not. + public class PlayableMetadata { - public readonly TimeSpan usualPlayingTime; + public TimeSpan usualPlayingTime; + public string filePath; } - interface IPlayable + public interface IPlayable { - void Play(); string Identifier { get; } PlayableMetadata Metadata { get; } } - interface ILayout + interface ILayoutStrategy { - // TODO + List LayOut(ICollection playables); } - interface ILayoutStrategy + + public class CannotConnectException: Exception { - ILayout LayOut(ICollection playables); + public CannotConnectException(string msg, Exception e) : base(msg, e) { } + public CannotConnectException(string msg) : base(msg) { } + public CannotConnectException() : base() { } } } \ No newline at end of file diff --git a/QuickPlay/LayoutStrategies.cs b/QuickPlay/LayoutStrategies.cs new file mode 100644 index 0000000..728996f --- /dev/null +++ b/QuickPlay/LayoutStrategies.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace QuickPlay +{ + class LexicographicLayoutStrategy : ILayoutStrategy + { + public List LayOut(ICollection playables) + { + List list = new List(playables); + list.Sort((IPlayable x, IPlayable y) => x.Identifier.CompareTo(y.Identifier)); + return list; + } + } +} \ No newline at end of file diff --git a/QuickPlay/MainActivity.cs b/QuickPlay/MainActivity.cs index 531edb4..d371843 100644 --- a/QuickPlay/MainActivity.cs +++ b/QuickPlay/MainActivity.cs @@ -1,60 +1,139 @@ using System; using Android.App; using Android.OS; -using Android.Runtime; using Android.Views; -using AndroidX.AppCompat.Widget; -using AndroidX.AppCompat.App; -using Google.Android.Material.FloatingActionButton; -using Google.Android.Material.Snackbar; +using Android.Widget; +using Android.Support.V7.App; +using Toolbar = Android.Support.V7.Widget.Toolbar; +using GridLayoutManager = Android.Support.V7.Widget.GridLayoutManager; namespace QuickPlay { - [Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)] + [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)] public class MainActivity : AppCompatActivity { - protected override void OnCreate(Bundle savedInstanceState) + private AppConfiguration appConfig; + private Android.Support.V7.Widget.RecyclerView recyclerView; + private IPlayer currentPlayer; + + protected override async void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); - Xamarin.Essentials.Platform.Init(this, savedInstanceState); + + + // App initialization + appConfig = AppConfiguration.loadConfiguration(); + var playerConfig = await appConfig.GetPlayerConfig(); + currentPlayer = playerConfig.GetPlayer(); + try + { + await currentPlayer.ConnectAsync(); + } catch (CannotConnectException e) + { + //TODO: View a toast with details and change some colors? + var t = Toast.MakeText(this, e.Message + ": " + e.InnerException.Message ?? "", ToastLength.Long); + t.Show(); + } + + // UI initialization SetContentView(Resource.Layout.activity_main); - Toolbar toolbar = FindViewById(Resource.Id.toolbar); + // UI Toolbar initialization + var toolbar = FindViewById(Resource.Id.toolbar); SetSupportActionBar(toolbar); + SupportActionBar.Title = "My Toolbar"; + + // Hide the play bar by default + var bar = FindViewById(Resource.Id.currentSongBar); + bar.Visibility = ViewStates.Invisible; + + // Initialize the RecyclerView + // Since this is rather complicated, it is in a separate method + InitializeRecyclerView(); - FloatingActionButton fab = FindViewById(Resource.Id.fab); - fab.Click += FabOnClick; + // FIXME: This should be in OnResume... + // Refresh player info + OnPlayerUpdate(); + } + + private void InitializeRecyclerView() + { + recyclerView = FindViewById(Resource.Id.recyclerView1); + var layoutStrategy = new LexicographicLayoutStrategy(); + var adapter = new SongRecyclerAdapter(currentPlayer, layoutStrategy); + adapter.SongClick += OnSongClick; + recyclerView.SetAdapter(adapter); + var layoutManager = new GridLayoutManager(this, 2, GridLayoutManager.Vertical, false); + recyclerView.SetLayoutManager(layoutManager); } public override bool OnCreateOptionsMenu(IMenu menu) { MenuInflater.Inflate(Resource.Menu.menu_main, menu); - return true; + return base.OnCreateOptionsMenu(menu); } public override bool OnOptionsItemSelected(IMenuItem item) { - int id = item.ItemId; - if (id == Resource.Id.action_settings) + if (item.ItemId == Resource.Id.action_settings) { - return true; + // Show the play bar + var bar = FindViewById(Resource.Id.currentSongBar); + bar.Visibility = ViewStates.Visible; } + if (item.ItemId == Resource.Id.action_edit) + { + // Hide the play bar + var bar = FindViewById(Resource.Id.currentSongBar); + bar.Visibility = ViewStates.Invisible; + } return base.OnOptionsItemSelected(item); } - - private void FabOnClick(object sender, EventArgs eventArgs) + + /// + /// A callback for all player updates. + /// + /// The update should be easy on resources, so no need to have separate + /// partial updates. This simply performs full update of the activity + /// state and views. + /// + public void OnPlayerUpdate() + { + TextView playerName = FindViewById(Resource.Id.playerNameText); + playerName.Text = currentPlayer.PlayerName; + var bar = FindViewById(Resource.Id.currentSongBar); + switch (currentPlayer.Status) + { + case IPlayer.PlayerStatus.Disconnected: + Toolbar tb = FindViewById(Resource.Id.toolbar); + tb.SetBackgroundColor(Android.Graphics.Color.Red); + bar.Visibility = ViewStates.Invisible; + break; + case IPlayer.PlayerStatus.Playing: + ResetToolbarColor(); + // Show progress bar + bar.Visibility = ViewStates.Visible; + break; + case IPlayer.PlayerStatus.Stopped: + ResetToolbarColor(); + bar.Visibility = ViewStates.Invisible; + break; + } + } + private void ResetToolbarColor() { - View view = (View) sender; - Snackbar.Make(view, "Replace with your own action", Snackbar.LengthLong) - .SetAction("Action", (View.IOnClickListener)null).Show(); + Toolbar tb = FindViewById(Resource.Id.toolbar); + tb.SetBackgroundColor( + new Android.Graphics.Color( + Android.Support.V4.Content.ContextCompat.GetColor(this, Resource.Color.colorPrimary) + ) + ); } - public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) + public async void OnSongClick(object sender, IPlayable song) { - Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); - - base.OnRequestPermissionsResult(requestCode, permissions, grantResults); - } - } + await currentPlayer.Play(song); + } + } } diff --git a/QuickPlay/MpdMonitorService.cs b/QuickPlay/MpdMonitorService.cs new file mode 100644 index 0000000..641b80e --- /dev/null +++ b/QuickPlay/MpdMonitorService.cs @@ -0,0 +1,32 @@ +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using System; + +namespace QuickPlay +{ + [Service] + class MpdMonitorService : Service + { + public override void OnCreate() + { + base.OnCreate(); + // TODO: Create the watching thread + } + public override IBinder OnBind(Intent intent) + { + throw new NotImplementedException(); + } + public override bool OnUnbind(Intent intent) + { + return base.OnUnbind(intent); + } + public override void OnDestroy() + { + base.OnDestroy(); + } + } +} \ No newline at end of file diff --git a/QuickPlay/MpdPlayer.cs b/QuickPlay/MpdPlayer.cs new file mode 100644 index 0000000..8bbdad2 --- /dev/null +++ b/QuickPlay/MpdPlayer.cs @@ -0,0 +1,68 @@ +using Android.Content; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MpcCore; +using System.Net.Sockets; + +namespace QuickPlay +{ + class MpdPlayer: IPlayer + { + MpcCoreClient mpd; + // MpcCore uses strings, so be it + string mpdIP, mpdPort; + public IPlayer.PlayerStatus Status { get; private set; } + + public Dictionary Songs { get; private set; } + public string PlayerName { get; private set; } + public float CurrentProgress { get { + throw new NotImplementedException(); + } } + public async Task Play(IPlayable song) + { + await mpd.SendAsync(new MpcCore.Commands.Player.Stop()); + await SetReasonableOptions(); + await mpd.SendAsync(new MpcCore.Commands.Queue.ClearQueue()); + await mpd.SendAsync(new MpcCore.Commands.Queue.AddToQueue(song.Metadata.filePath)); + await mpd.SendAsync(new MpcCore.Commands.Player.Play()); + } + public async Task SetReasonableOptions() + { + await mpd.SendAsync(new MpcCore.Commands.Options.SetConsume(true)); + await mpd.SendAsync(new MpcCore.Commands.Options.SetRandom(false)); + await mpd.SendAsync(new MpcCore.Commands.Options.SetRepeat(false)); + await mpd.SendAsync(new MpcCore.Commands.Options.SetSingle(false)); + } + public MpdPlayer(PlayerConfiguration cfg) + { + // Populate known fields/properties + Songs = cfg.songs; + PlayerName = cfg.playerName; + // NOTE: We separate the port by '@', since ':' could be part of IPv6 and we do not want to complicate parsing. + var connDetails = cfg.playerConnectionDetails.Split('@'); + if (connDetails.Length > 2) throw new InvalidOperationException("Bad connection details"); + mpdIP = connDetails[0]; + mpdPort = connDetails.Length >=2 ? connDetails[1] : "6600"; // XXX: Unneccessary default here... + // Connecting and monitoring remote player is done in ConnectAsync. + // Therefore, we start disconnected + Status = IPlayer.PlayerStatus.Disconnected; + } + public async Task ConnectAsync() + { + // Create a persistent connection + var conn = new MpcCoreConnection(mpdIP, mpdPort); + mpd = new MpcCoreClient(conn); + try + { + await mpd.ConnectAsync(); + } catch (SocketException e) + { + throw new CannotConnectException("MPD connect failed", e); + } + // Start the monitoring service + var ctx = Android.App.Application.Context; + var intent = new Intent(ctx, typeof(MpdMonitorService)); + } + } +} \ No newline at end of file diff --git a/QuickPlay/Properties/AndroidManifest.xml b/QuickPlay/Properties/AndroidManifest.xml index 889595d..452677d 100644 --- a/QuickPlay/Properties/AndroidManifest.xml +++ b/QuickPlay/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - - + + + - \ No newline at end of file diff --git a/QuickPlay/Properties/AssemblyInfo.cs b/QuickPlay/Properties/AssemblyInfo.cs index b67884d..fb72a46 100644 --- a/QuickPlay/Properties/AssemblyInfo.cs +++ b/QuickPlay/Properties/AssemblyInfo.cs @@ -15,6 +15,12 @@ using Android.App; [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] +#if DEBUG +[assembly: Application(Debuggable=true)] +#else +[assembly: Application(Debuggable = false)] +#endif + // Version information for an assembly consists of the following four values: // diff --git a/QuickPlay/QuickPlay.csproj b/QuickPlay/QuickPlay.csproj index 0e5a4c9..ec15e2b 100644 --- a/QuickPlay/QuickPlay.csproj +++ b/QuickPlay/QuickPlay.csproj @@ -36,14 +36,17 @@ 4 True None - true + false false false false false + false + false + Xamarin.Android.Net.AndroidClientHandler - True + false portable True bin\Release\ @@ -52,11 +55,20 @@ 4 true False - SdkOnly - True + Full + true + false + false + false + false + r8 + + + d8 + @@ -64,9 +76,16 @@ + + + + + + + @@ -77,9 +96,6 @@ Designer - - Designer - @@ -105,15 +121,119 @@ - + - - - 2.1.0.8 + + 28.0.0.3 + + + 28.0.0.3 - - + + 28.0.0.3 + + + + + {d0a5ad05-b98c-45e6-b61d-4700f7aa72cf} + MpcCore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + android:layout_height="fill_parent" + android:layout_below="@id/commonTopBar" + android:orientation="vertical"> - + android:layout_height="match_parent" + android:id="@+id/recyclerView1" + /> - - - - - + + + + + + - + + \ No newline at end of file diff --git a/QuickPlay/Resources/layout/commonTopBar.axml b/QuickPlay/Resources/layout/commonTopBar.axml new file mode 100644 index 0000000..c78ecf0 --- /dev/null +++ b/QuickPlay/Resources/layout/commonTopBar.axml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/QuickPlay/Resources/layout/content_main.xml b/QuickPlay/Resources/layout/content_main.xml deleted file mode 100644 index 5662d6d..0000000 --- a/QuickPlay/Resources/layout/content_main.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/QuickPlay/Resources/layout/songLayout.xml b/QuickPlay/Resources/layout/songLayout.xml new file mode 100644 index 0000000..e785907 --- /dev/null +++ b/QuickPlay/Resources/layout/songLayout.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/QuickPlay/Resources/menu/menu_main.xml b/QuickPlay/Resources/menu/menu_main.xml index ace8c65..7a0f775 100644 --- a/QuickPlay/Resources/menu/menu_main.xml +++ b/QuickPlay/Resources/menu/menu_main.xml @@ -5,5 +5,12 @@ android:id="@+id/action_settings" android:orderInCategory="100" android:title="@string/action_settings" - app:showAsAction="never" /> + android:icon="@mipmap/material_settings" + app:showAsAction="ifRoom" /> + diff --git a/QuickPlay/Resources/mipmap-anydpi-v26/material_edit.xml b/QuickPlay/Resources/mipmap-anydpi-v26/material_edit.xml new file mode 100644 index 0000000..fbe5335 --- /dev/null +++ b/QuickPlay/Resources/mipmap-anydpi-v26/material_edit.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/QuickPlay/Resources/mipmap-anydpi-v26/material_settings.xml b/QuickPlay/Resources/mipmap-anydpi-v26/material_settings.xml new file mode 100644 index 0000000..9d8ad76 --- /dev/null +++ b/QuickPlay/Resources/mipmap-anydpi-v26/material_settings.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/QuickPlay/Resources/mipmap-hdpi/material_edit.png b/QuickPlay/Resources/mipmap-hdpi/material_edit.png new file mode 100644 index 0000000..0c198df Binary files /dev/null and b/QuickPlay/Resources/mipmap-hdpi/material_edit.png differ diff --git a/QuickPlay/Resources/mipmap-hdpi/material_settings.png b/QuickPlay/Resources/mipmap-hdpi/material_settings.png new file mode 100644 index 0000000..89b0a0a Binary files /dev/null and b/QuickPlay/Resources/mipmap-hdpi/material_settings.png differ diff --git a/QuickPlay/Resources/mipmap-mdpi/material_edit.png b/QuickPlay/Resources/mipmap-mdpi/material_edit.png new file mode 100644 index 0000000..09b5c62 Binary files /dev/null and b/QuickPlay/Resources/mipmap-mdpi/material_edit.png differ diff --git a/QuickPlay/Resources/mipmap-mdpi/material_settings.png b/QuickPlay/Resources/mipmap-mdpi/material_settings.png new file mode 100644 index 0000000..d5ec9c9 Binary files /dev/null and b/QuickPlay/Resources/mipmap-mdpi/material_settings.png differ diff --git a/QuickPlay/Resources/mipmap-xhdpi/material_edit.png b/QuickPlay/Resources/mipmap-xhdpi/material_edit.png new file mode 100644 index 0000000..6e0eee3 Binary files /dev/null and b/QuickPlay/Resources/mipmap-xhdpi/material_edit.png differ diff --git a/QuickPlay/Resources/mipmap-xhdpi/material_settings.png b/QuickPlay/Resources/mipmap-xhdpi/material_settings.png new file mode 100644 index 0000000..d09235a Binary files /dev/null and b/QuickPlay/Resources/mipmap-xhdpi/material_settings.png differ diff --git a/QuickPlay/Resources/mipmap-xxhdpi/material_edit.png b/QuickPlay/Resources/mipmap-xxhdpi/material_edit.png new file mode 100644 index 0000000..1d393f6 Binary files /dev/null and b/QuickPlay/Resources/mipmap-xxhdpi/material_edit.png differ diff --git a/QuickPlay/Resources/mipmap-xxhdpi/material_settings.png b/QuickPlay/Resources/mipmap-xxhdpi/material_settings.png new file mode 100644 index 0000000..fe3cd78 Binary files /dev/null and b/QuickPlay/Resources/mipmap-xxhdpi/material_settings.png differ diff --git a/QuickPlay/Resources/mipmap-xxxhdpi/material_edit.png b/QuickPlay/Resources/mipmap-xxxhdpi/material_edit.png new file mode 100644 index 0000000..813515b Binary files /dev/null and b/QuickPlay/Resources/mipmap-xxxhdpi/material_edit.png differ diff --git a/QuickPlay/Resources/mipmap-xxxhdpi/material_settings.png b/QuickPlay/Resources/mipmap-xxxhdpi/material_settings.png new file mode 100644 index 0000000..eafb20f Binary files /dev/null and b/QuickPlay/Resources/mipmap-xxxhdpi/material_settings.png differ diff --git a/QuickPlay/Resources/values/strings.xml b/QuickPlay/Resources/values/strings.xml index 7903abe..057196d 100644 --- a/QuickPlay/Resources/values/strings.xml +++ b/QuickPlay/Resources/values/strings.xml @@ -1,4 +1,7 @@ QuickPlay + Not Connected! + Settings + Edit diff --git a/QuickPlay/Resources/values/styles.xml b/QuickPlay/Resources/values/styles.xml index fef37f9..c712d3d 100644 --- a/QuickPlay/Resources/values/styles.xml +++ b/QuickPlay/Resources/values/styles.xml @@ -1,11 +1,13 @@ - -