diff --git a/.gitignore b/.gitignore index fc25db9..7c43ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -484,3 +484,4 @@ $RECYCLE.BIN/ *.swp help.txt data/ +out/ \ No newline at end of file diff --git a/.woodpecker/devel.yml b/.woodpecker/devel.yml new file mode 100644 index 0000000..51769e7 --- /dev/null +++ b/.woodpecker/devel.yml @@ -0,0 +1,8 @@ +steps: + - name: devel + image: mcr.microsoft.com/dotnet/sdk:8.0 + when: + event: push + branch: devel + commands: + - bash test.sh \ No newline at end of file diff --git a/.woodpecker/master.yml b/.woodpecker/master.yml new file mode 100644 index 0000000..8b736b3 --- /dev/null +++ b/.woodpecker/master.yml @@ -0,0 +1,11 @@ +steps: + - name: package + image: dotnet-android-docker:latest + when: + event: tag + branch: master + commands: + - bash package.sh + volumes: + - /mnt/20TB/Artifacts:/deploy_dir + - /mnt/20TB/DebServer/pool:/pool \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5640c2a --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +TessesCMS +========= + +> :warning: **THIS IS UNDER CONSTRUCTION** don't use this software yet \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs index fd98eb4..2916c47 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs @@ -2,13 +2,61 @@ using System; using System.IO; using System.Linq; using System.Threading.Tasks; +using Android.Media; +using Avalonia.Android; +using Avalonia.Controls; +using Avalonia.Platform; using Avalonia.Platform.Storage; +using LibVLCSharp.Platforms.Android; using Newtonsoft.Json; using Tesses.VirtualFilesystem; using Tesses.VirtualFilesystem.Extensions; using Tesses.VirtualFilesystem.Filesystems; namespace Tesses.CMS.Avalonia.Android; +public class NCH : NativeControlHost +{ + LibVLCSharp.Shared.MediaPlayer? mediaPlayer; + VideoView? view; + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + /*return Implementation?.CreateControl(IsSecond, parent, () => base.CreateNativeControlCore(parent)) + ?? base.CreateNativeControlCore(parent);*/ + var parentContext = (parent as AndroidViewControlHandle)?.View.Context + ?? global::Android.App.Application.Context; + + view = new VideoView(parentContext); + view.LayoutParameters = new global::Android.Views.ViewGroup.LayoutParams(global::Android.Views.ViewGroup.LayoutParams.MatchParent,global::Android.Views.ViewGroup.LayoutParams.MatchParent); + if(mediaPlayer != null) + { + view.MediaPlayer = mediaPlayer; + } + return new AndroidViewControlHandle(view); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + base.DestroyNativeControlCore(control); + } + + public LibVLCSharp.Shared.MediaPlayer? MediaPlayer { + get { + if(view != null) + { + return view.MediaPlayer; + } + return mediaPlayer; + } + set { + if(view != null) + { + view.MediaPlayer = value; + + } + mediaPlayer=value; + } + } +} internal class MobilePlatform : IPlatform { string configpath; @@ -39,7 +87,12 @@ internal class MobilePlatform : IPlatform internal IVirtualFilesystem? virtualFilesystem; public IVirtualFilesystem? DownloadFilesystem => virtualFilesystem; - + + public bool CanTakeScreenShots => DownloadFilesystem != null && !string.IsNullOrWhiteSpace(ScreenshotPath); + + public string ScreenshotPath => activity?.GetExternalCacheDirs()?[0].AbsolutePath ?? ""; + + public bool MustMoveScreenshot => true; public async Task BrowseForDownloadDirectoryAsync() { @@ -89,4 +142,31 @@ internal class MobilePlatform : IPlatform } + + public Control CreatePlayer() + { + return new NCH(); + // return new global::Android.Widget.Button(this.activity); + } + + public void SetMediaPlayer(Control control, global::LibVLCSharp.Shared.MediaPlayer? mediaPlayer) + { + var ctrl=control as NCH; + if(ctrl != null) + ctrl.MediaPlayer = mediaPlayer; + } + + public global::LibVLCSharp.Shared.MediaPlayer? GetMediaPlayer(Control control) + { + var ctrl=control as NCH; + if(ctrl != null) + return ctrl.MediaPlayer; + return null; + } + public void LaunchUrl(string url) + { + //https://stackoverflow.com/a/3004542 + //Intent intent = new Intent(Intent.ActionView); + + } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj index d3a34d9..413a0a1 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj @@ -19,7 +19,9 @@ + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs index 7088291..b3d6f29 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs @@ -1,3 +1,5 @@ +using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -34,7 +36,12 @@ internal class DesktopPlatform : IPlatform IVirtualFilesystem? virtualFilesystem; public IVirtualFilesystem? DownloadFilesystem => virtualFilesystem; - + + public bool CanTakeScreenShots => true; + + public string ScreenshotPath => DownloadFilesystem == null ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : Path.Combine(Configuration.DownloadPath,"Screenshots"); + + public bool MustMoveScreenshot => false; public async Task BrowseForDownloadDirectoryAsync() { @@ -75,21 +82,28 @@ internal class DesktopPlatform : IPlatform public Control CreatePlayer() { - return new VideoPlayer(); + return new VideoView(); } public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer) { - var ctrl = control as VideoPlayer; + var ctrl = control as VideoView; if(ctrl != null) ctrl.MediaPlayer = mediaPlayer; } public MediaPlayer? GetMediaPlayer(Control control) { - var ctrl = control as VideoPlayer; + var ctrl = control as VideoView; if(ctrl != null) return ctrl.MediaPlayer; return null; } + public void LaunchUrl(string url) + { + Process p = new Process(); + p.StartInfo.UseShellExecute=true; + p.StartInfo.FileName = url; + p.Start(); + } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj index 44b97dc..8138888 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj @@ -6,12 +6,24 @@ net8.0 enable true + tcms-desktop + https://tesses.net/apps/TessesCMS + Mike Nolan <tesses@tesses.net> app.manifest - + + + /usr/share/applications/tcms-desktop.desktop + + + + /usr/share/icons/tcms.png + + + @@ -20,6 +32,10 @@ + + 0.1.226-* + all + diff --git a/Tesses.CMS/Assets/android-chrome-512x512.png b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/icon.png similarity index 100% rename from Tesses.CMS/Assets/android-chrome-512x512.png rename to Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/icon.png diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/tcms-desktop.desktop b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/tcms-desktop.desktop new file mode 100644 index 0000000..48e518d --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/tcms-desktop.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=TessesCMS +Comment=The app for Tesses Studios +Exec=/usr/local/bin/tcms-desktop %U +Categories=AudioVideo;Player; +Icon=tcms +MimeType=application/ogg;application/x-ogg;audio/ogg;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/ogg;video/x-ogm;video/x-ogm+ogg;video/x-theora+ogg;video/x-theora;audio/x-speex;audio/opus;application/x-flac;audio/flac;audio/x-flac;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;audio/x-ms-wma;video/x-ms-asf;video/x-ms-asf-plugin;video/x-ms-asx;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvx;video/x-msvideo;audio/x-pn-windows-acm;video/divx;video/msvideo;video/vnd.divx;video/avi;video/x-avi;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/vnd.rn-realaudio;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;audio/x-real-audio;audio/x-realaudio;video/vnd.rn-realvideo;audio/mpeg;audio/mpg;audio/mp1;audio/mp2;audio/mp3;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpg;video/mp2t;video/mpeg;video/mpeg-system;video/x-mpeg;video/x-mpeg2;video/x-mpeg-system;application/mpeg4-iod;application/mpeg4-muxcodetable;application/x-extension-m4a;application/x-extension-mp4;audio/aac;audio/m4a;audio/mp4;audio/x-m4a;audio/x-aac;video/mp4;video/mp4v-es;video/x-m4v;application/x-quicktime-media-link;application/x-quicktimeplayer;video/quicktime;application/x-matroska;audio/x-matroska;video/x-matroska;video/webm;audio/webm;audio/3gpp;audio/3gpp2;audio/AMR;audio/AMR-WB;video/3gp;video/3gpp;video/3gpp2;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtsp;x-scheme-handler/rtp;x-scheme-handler/rtmp;x-scheme-handler/icy;x-scheme-handler/icyx;application/x-cd-image;x-content/video-vcd;x-content/video-svcd;x-content/video-dvd;x-content/audio-cdda;x-content/audio-player;application/ram;application/xspf+xml;audio/mpegurl;audio/x-mpegurl;audio/scpls;audio/x-scpls;text/google-video-pointer;text/x-google-video-pointer;video/vnd.mpegurl;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.ms-wpl;application/sdp;audio/dv;video/dv;audio/x-aiff;audio/x-pn-aiff;video/x-anim;video/x-nsv;video/fli;video/flv;video/x-flc;video/x-fli;video/x-flv;audio/wav;audio/x-pn-au;audio/x-pn-wav;audio/x-wav;audio/x-adpcm;audio/ac3;audio/eac3;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/basic;audio/midi;audio/x-ape;audio/x-gsm;audio/x-musepack;audio/x-tta;audio/x-wavpack;audio/x-shorten;application/x-shockwave-flash;application/x-flash-video;misc/ultravox;image/vnd.rn-realpix;audio/x-it;audio/x-mod;audio/x-s3m;audio/x-xm;application/mxf; \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs index c551c12..8d63468 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs @@ -15,15 +15,23 @@ using Avalonia.Platform; using Tesses.VirtualFilesystem; using Tesses.VirtualFilesystem.Extensions; using System.IO; +using System.Collections.Generic; +using System.Threading; +using Tesses.CMS.Avalonia.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Tesses.CMS.Avalonia; public partial class App : Application { + public static OpenedCtrls OpenedControls {get;}=new OpenedCtrls(); public static IPlatform Platform {get;set;} = new NullPlatform(); public static MainWindow? Window {get;set;} public static TessesCMSClient Client {get;} = new TessesCMSClient(); + + public static MainView? MainView {get;set;} public override void Initialize() { @@ -31,6 +39,20 @@ public partial class App : Application AvaloniaXamlLoader.Load(this); } + public static List Downloads {get;}=new List(); + + public static void AddActiveDownload(ActiveDownload dl) + { + lock(Downloads) + { + if(!Downloads.Any(e=>e.Path == dl.Path)) + { + Downloads.Add(dl); + dl.Start().Wait(0); + } + } + } + public override void OnFrameworkInitializationCompleted() { Client.RootUrl = Platform.Configuration.ServerUrl; @@ -46,11 +68,11 @@ public partial class App : Application } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { - - singleViewPlatform.MainView = new MainView + MainView = new MainView { DataContext = new MainViewModel(title) }; + singleViewPlatform.MainView = MainView; } base.OnFrameworkInitializationCompleted(); @@ -138,6 +160,75 @@ public partial class App : Application return new Bitmap(AssetLoader.Open(new Uri("avares://Tesses.CMS.Avalonia/Assets/no-media-icon.png"))); } + + internal static async Task GetShowThumbnailAsync(string username, string name) + { + //we need to cache the resource + if(Platform.Configuration.CacheResources && Platform.DownloadFilesystem != null) + { + UnixPath cacheDir = Special.Root / "Metadata" / "Cache" / username / "Shows" / name; + Log($"About to create directory: {cacheDir}"); + + await Platform.DownloadFilesystem.CreateDirectoryAsync(cacheDir); + Log($"Created directory: {cacheDir}"); + UnixPath thumbnailPath = cacheDir / "thumbnail.jpg"; + + if(await Platform.DownloadFilesystem.FileExistsAsync(thumbnailPath)) + { + Log($"Image exists: {thumbnailPath}"); + using(var sr = await Platform.DownloadFilesystem.OpenReadAsync(thumbnailPath)) + { + MemoryStream ms = new MemoryStream(); + sr.CopyTo(ms); + ms.Position=0; + Log($"Image read from file: {thumbnailPath}"); + return new Bitmap(ms); + } + + } + else + { + var metadata = await Client.Shows.GetShowContentMetadataAsync(username,name); + if(metadata.HasThumbnail) + { + using(var strm = await Platform.DownloadFilesystem.OpenAsync(thumbnailPath,System.IO.FileMode.Create,System.IO.FileAccess.Write,System.IO.FileShare.None)) + { + MemoryStream ms=new MemoryStream(); + Log($"About to read from network and save: {thumbnailPath}"); + + await Client.Shows.DownloadThumbnailAsync(username,name,ms); + ms.Position=0; + ms.CopyTo(strm); + ms.Position=0; + Log($"Image read from network and saved: {thumbnailPath}"); + return new Bitmap(ms); + } + } + + } + } + else + { + var metadata = await Client.Shows.GetShowContentMetadataAsync(username,name); + if(metadata.HasThumbnail) + { + MemoryStream ms=new MemoryStream(); + Log($"About to read from network: {username} {name}"); + + await Client.Shows.DownloadThumbnailAsync(username,name,ms); + ms.Position = 0; + Log($"Image read from network: {username} {name}"); + + return new Bitmap(ms); + + } + + } + Log($"Image does not exist: {username} {name}"); + + return new Bitmap(AssetLoader.Open(new Uri("avares://Tesses.CMS.Avalonia/Assets/no-media-icon.png"))); + } + public static void Log(string text) { @@ -157,4 +248,217 @@ public partial class App : Application } } } + + internal static void StartDownload(UserAccount account, Movie movie) + { + string username = account.Username; + + FavoritesItem item =new FavoritesItem() + { + Username = account.Username, + UserProperName = account.ProperName, + Account = account, + Media = JObject.FromObject(movie), + MediaName = movie.Name, + MediaProperName = movie.ProperName, + Type = FavoriteType.Movie + }; + + App.AddDownload(item); + + + UnixPath dir = Special.Root / "Downloads" / username / "Movies" / movie.Name; + UnixPath incomplete = dir / movie.ProperName + ".mp4.part"; + UnixPath filename = dir / movie.ProperName + ".mp4"; + Platform.DownloadFilesystem?.CreateDirectory(dir); + + if(Platform.DownloadFilesystem != null) + { + ActiveDownload download = new ActiveDownload(); + download.Name = movie.ProperName; + download.Path = filename.Path; + download.Start = async()=>{ + + if(!await Platform.DownloadFilesystem.FileExistsAsync(filename)) + { + + DateTime last = DateTime.Now; + + using(var strm = await Platform.DownloadFilesystem.OpenAsync(incomplete,FileMode.OpenOrCreate,FileAccess.Write,FileShare.None,download.TokenSource.Token)) + { + await Client.Movies.DownloadMovieAsync(username,movie.Name,strm,download.TokenSource.Token,new Progress(e=>{ + + var now = DateTime.Now; + + if(last.AddSeconds(0.5) < now) + { + + download.UpdateProgress(e); + last = now; + } + + + })); + + } + if(!download.TokenSource.IsCancellationRequested) + await Platform.DownloadFilesystem.MoveFileAsync(incomplete,filename); + RemoveActiveDownload(download); + } + }; + AddActiveDownload(download); + } + + } + + internal static void AddDownload(FavoritesItem item) + { + if(App.Platform.DownloadFilesystem == null) return; + List items = new List(); + + foreach(var _item in EnumerateDownloads()) + if(_item.Equals(item)) return; + else items.Add(_item); + + items.Add(item); + + App.Platform.DownloadFilesystem.CreateDirectory(Special.Root / "Metadata"); + App.Platform.DownloadFilesystem.WriteAllText(Special.Root / "Metadata" / "Downloads.json",JsonConvert.SerializeObject(items)); + + } + + private static void RemoveActiveDownload(ActiveDownload download) + { + lock(Downloads) + { + Downloads.Remove(download); + download.SendDone(); + + } + } + + internal static List GetActiveDownloads() + { + lock(Downloads) { + return Downloads.ToList(); + } + } + + internal static IEnumerable EnumerateFavorites() + { + if(App.Platform.DownloadFilesystem == null) yield break; + + if(!App.Platform.DownloadFilesystem.FileExists(Special.Root / "Metadata" / "Favorites.json")) yield break; + + App.Platform.DownloadFilesystem.CreateDirectory(Special.Root / "Metadata"); + + string text = App.Platform.DownloadFilesystem.ReadAllText(Special.Root / "Metadata" / "Favorites.json"); + + var res=JsonConvert.DeserializeObject>(text); + if(res != null) + foreach(var item in res) + yield return item; + } + internal static IEnumerable EnumerateDownloads() + { + if(App.Platform.DownloadFilesystem == null) yield break; + + if(!App.Platform.DownloadFilesystem.FileExists(Special.Root / "Metadata" / "Downloads.json")) yield break; + + App.Platform.DownloadFilesystem.CreateDirectory(Special.Root / "Metadata"); + + string text = App.Platform.DownloadFilesystem.ReadAllText(Special.Root / "Metadata" / "Downloads.json"); + + var res=JsonConvert.DeserializeObject>(text); + if(res != null) + foreach(var item in res) + yield return item; + } + internal static bool FavoritesExists(FavoritesItem item) + { + foreach(var _item in EnumerateFavorites()) + if(_item.Equals(item)) return true; + return false; + } + internal static void AddFavorite(FavoritesItem item) + { + if(App.Platform.DownloadFilesystem == null) return; + List items = new List(); + + foreach(var _item in EnumerateFavorites()) + if(_item.Equals(item)) return; + else items.Add(_item); + + items.Add(item); + + App.Platform.DownloadFilesystem.CreateDirectory(Special.Root / "Metadata"); + App.Platform.DownloadFilesystem.WriteAllText(Special.Root / "Metadata" / "Favorites.json",JsonConvert.SerializeObject(items)); + } + + internal static void RemoveFavorite(FavoritesItem item) + { + if(App.Platform.DownloadFilesystem == null) return; + List items = new List(); + + foreach(var _item in EnumerateFavorites()) + if(_item.Equals(item)) continue; + else items.Add(_item); + + App.Platform.DownloadFilesystem.CreateDirectory(Special.Root / "Metadata"); + App.Platform.DownloadFilesystem.WriteAllText(Special.Root / "Metadata" / "Favorites.json",JsonConvert.SerializeObject(items)); + + } + + internal static void LaunchUrl(string v) + { + Platform.LaunchUrl(v); + } +} + +public class ActiveDownload +{ + public CancellationTokenSource TokenSource {get;set;}=new CancellationTokenSource(); + + public string Path {get;set;}=""; + + + public string Name {get;set;}=""; + + public double Progress {get;set;}=0; + + public event Action? ProgressChanged=null; + + public event Action? Done = null; + + public void UpdateProgress(double d) + { + + Progress = d; + ProgressChanged?.Invoke(d); + } + + public void SendDone() + { + Done?.Invoke(); + } + + + public Func Start {get;set;}=async()=>{await Task.CompletedTask;}; +} + +public class OpenedCtrls +{ + List disposables=new List(); + + public void Register(IDisposable disposable) + { + disposables.Add(disposable); + } + + public void Destroy() + { + foreach(var item in disposables) item.Dispose(); + + disposables.Clear(); + } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico index da8d49f..9aad621 100644 Binary files a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico and b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico differ diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs index fd8c8c5..b2762d5 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs @@ -11,6 +11,9 @@ public interface IPlatform bool PlatformUsesNormalPathsForDownload {get;} IVirtualFilesystem? DownloadFilesystem {get;} + bool CanTakeScreenShots { get; } + string ScreenshotPath { get; } + bool MustMoveScreenshot { get; } Task WriteConfigurationAsync(); @@ -21,6 +24,8 @@ public interface IPlatform void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer); MediaPlayer? GetMediaPlayer(Control control); + + void LaunchUrl(string url); } @@ -38,6 +43,12 @@ public class NullPlatform : IPlatform public Configuration Configuration => _conf; + public bool CanTakeScreenShots => false; + + public string ScreenshotPath => throw new System.NotImplementedException(); + + public bool MustMoveScreenshot => throw new System.NotImplementedException(); + public async Task BrowseForDownloadDirectoryAsync() { await Task.CompletedTask; @@ -67,4 +78,9 @@ public class NullPlatform : IPlatform { return null; } + + public void LaunchUrl(string url) + { + + } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/FavoritePageItem.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/FavoritePageItem.cs new file mode 100644 index 0000000..435f2b9 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/FavoritePageItem.cs @@ -0,0 +1,28 @@ +using Avalonia.Media; +using Tesses.CMS.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System; +using ReactiveUI; +using System.Threading.Tasks; + +namespace Tesses.CMS.Avalonia.Models; + +public class FavoritesPageItem +{ + FavoritesItem item; + + public FavoritesPageItem(FavoritesItem item, Func fav) + { + this.item=item; + ViewCommand = ReactiveCommand.CreateFromTask(async()=>{ + await fav(item); + }); + } + public string Name => item.MediaProperName; + + public string AuthorName => item.UserProperName; + + public IReactiveCommand ViewCommand {get;} +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/FavoritesItem.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/FavoritesItem.cs new file mode 100644 index 0000000..37d01e5 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/FavoritesItem.cs @@ -0,0 +1,54 @@ +using Avalonia.Media; +using Tesses.CMS.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Newtonsoft.Json.Linq; + +namespace Tesses.CMS.Avalonia.Models; + +public enum FavoriteType { + Movie, + Show, + Season, + Episode, + Album, + AlbumTrack, + MusicVideo, + Program, + ProgramRelease, + Other, + OtherFile + +} + +public class FavoritesItem +{ + [JsonProperty("username")] + public string Username {get;set;}=""; + + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter),typeof(CamelCaseNamingStrategy))] + public FavoriteType Type {get;set;}=FavoriteType.Movie; + + [JsonProperty("userProperName")] + public string UserProperName {get;set;}=""; + + [JsonProperty("mediaName")] + public string MediaName {get;set;}=""; + + [JsonProperty("mediaProperName")] + public string MediaProperName {get;set;}=""; + + [JsonProperty("account")] + public UserAccount Account {get;set;}=new UserAccount(); + + [JsonProperty("media")] + public JToken Media {get;set;}=JValue.CreateNull(); + + public bool Equals(FavoritesItem item) + { + return Username == item.Username && Type == item.Type && MediaName == item.MediaName; + } + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/ShowItem.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/ShowItem.cs new file mode 100644 index 0000000..e611f56 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/ShowItem.cs @@ -0,0 +1,16 @@ +using Avalonia.Media; +using Tesses.CMS.Client; + +namespace Tesses.CMS.Avalonia.Models; + +public class ShowItem +{ + public ShowItem(Show show,IImage image) + { + Show = show; + Image = image; + } + public Show Show {get;} + public IImage Image {get;} + public string Name => Show.ProperName; +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ActiveDownloadViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ActiveDownloadViewModel.cs new file mode 100644 index 0000000..59b1e0a --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ActiveDownloadViewModel.cs @@ -0,0 +1,57 @@ +namespace Tesses.CMS.Avalonia.ViewModels; + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using global::Avalonia.Threading; +using LibVLCSharp.Shared; + +public partial class ActiveDownloadViewModel : ViewModelBase, IDisposable +{ + ActiveDownload download; + Action remove; + public ActiveDownloadViewModel(Action remove,ActiveDownload download) + { + this.remove = remove; + this.download=download; + this.download.ProgressChanged += ProgressChanged; + this.download.Done += Done; + Progress = (int)(this.download.Progress* 100); //for progress reasons + Name = download.Name; + } + + private void ProgressChanged(double obj) + { + + Dispatcher.UIThread.Post(()=>{ + Progress = (int)(obj * 100); + }); + } + + [ObservableProperty] + private int _progress=0; + + [ObservableProperty] + private string _name; + + [RelayCommand] + public void Cancel() + { + download.TokenSource.Cancel(); + download.TokenSource.Dispose(); + } + public void Dispose() + { + this.download.ProgressChanged -= ProgressChanged; + this.download.Done -= Done; + } + + private void Done() + { + Dispatcher.UIThread.Invoke(()=>{ + remove(); + }); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs index 6fce003..424f357 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs @@ -1,19 +1,55 @@ namespace Tesses.CMS.Avalonia.ViewModels; + +using System; +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using LibVLCSharp.Shared; -public partial class DownloadsPageViewModel : ViewModelBase +public partial class DownloadsPageViewModel : ViewModelBase, IDisposable { - public DownloadsPageViewModel() + public DownloadsPageViewModel(MainViewModel mvm) { - LibVLC vlc=new LibVLC("--input-repeat=65535"); - - Player=new MediaPlayer(new Media(vlc,"https://tytdarchive.site.tesses.net/content/PreMuxed/PzUKeGZiEl0.mp4",FromType.FromLocation)); - - + foreach(var item in App.EnumerateDownloads()) + { + ViewModelBaseItem vmbi=new ViewModelBaseItem(); + vmbi.Item = new SavedDownloadViewModel(item,mvm); + Downloads.Add(vmbi); + } + foreach(var item in App.GetActiveDownloads()) + { + var vmb=new ViewModelBaseItem(); + vmb.Item = new ActiveDownloadViewModel(()=>{ + if(Downloads.Contains(vmb)) Downloads.Remove(vmb); + },item); + Downloads.Add(vmb); + } } [ObservableProperty] - private MediaPlayer? _player; + + private ObservableCollection _downloads=new ObservableCollection(); + + public void Dispose() + { + foreach(var item in Downloads) + { + item.Dispose(); + } + } +} + +public class ViewModelBaseItem : IDisposable +{ + public ViewModelBaseItem() + { + + } + public ViewModelBase? Item {get;set;} + + public void Dispose() + { + var i = Item as IDisposable; + i?.Dispose(); + } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs index 3a6f72e..64dc894 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs @@ -1,8 +1,33 @@ namespace Tesses.CMS.Avalonia.ViewModels; + +using System.Collections.ObjectModel; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Avalonia.Views; + public partial class FavoritesPageViewModel : ViewModelBase { + MainViewModel mvm; + public FavoritesPageViewModel(MainViewModel mvm) + { + this.mvm=mvm; + Favorites.Clear(); + + foreach(var item in App.EnumerateFavorites()) + { + Favorites.Add(new FavoritesPageItem(item,async (fpg)=>{ + HomePageViewModel hpvm=new HomePageViewModel(); + await hpvm.NavigateToAsync(item); + mvm.SetHome(); + mvm.CurrentPage = hpvm; + })); + } + + + } + public ObservableCollection Favorites {get;}=new ObservableCollection(); } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs index 21829da..1b9b626 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs @@ -1,17 +1,38 @@ namespace Tesses.CMS.Avalonia.ViewModels; + +using System; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Avalonia.Models; using Tesses.CMS.Avalonia.ViewModels.HomePages; +using Tesses.CMS.Client; +using Tesses.VirtualFilesystem; +using Tesses.VirtualFilesystem.Extensions; public partial class HomePageViewModel : ViewModelBase { public HomePageViewModel() { - _currentPage = new HomeUserListPageViewModel(this); + _curPage = new HomeUserListPageViewModel(this); + } + private ViewModelBase _curPage; + public ViewModelBase CurrentPage + { + get + { + return _curPage; + } + set{ + App.OpenedControls.Destroy(); + _curPage = value; + var disp = value as IDisposable; + if(disp != null) App.OpenedControls.Register(disp); + OnPropertyChanged(nameof(CurrentPage)); + + } } - [ObservableProperty] - private ViewModelBase _currentPage; [RelayCommand] private void Back() @@ -26,4 +47,51 @@ public partial class HomePageViewModel : ViewModelBase //can't go back } } + + public async Task NavigateToAsync(FavoritesItem item) + { + switch(item.Type) + { + case FavoriteType.Movie: + var homeUserList = new HomeUserListPageViewModel(this); + + + + var homeUser = new HomeUserPageViewModel(this,homeUserList,item.Account); + + var homeMovieList = new HomeMovieListPageViewModel(this,homeUser); + Movie movie = item.Media.ToObject() ?? new Movie(){Name=item.MediaName,ProperName = item.MediaProperName}; + MovieItem movieItem=new MovieItem(movie,await App.GetMovieThumbnailAsync(item.Username,item.MediaName)); + + var homeMovie = new HomeMoviePageViewModel(this,homeMovieList,item.Account,movieItem); + CurrentPage = homeMovie; + break; + } + } + + public async Task PlayDownloadedAsync(FavoritesItem item) + { + if(App.Platform.DownloadFilesystem == null) return; + switch(item.Type) + { + case FavoriteType.Movie: + UnixPath dir = Special.Root / "Downloads" / item.Username / "Movies" / item.MediaName; + UnixPath filename = dir / item.MediaProperName + ".mp4"; + if(!await App.Platform.DownloadFilesystem.FileExistsAsync(filename)) return; + + var homeUserList = new HomeUserListPageViewModel(this); + + + + var homeUser = new HomeUserPageViewModel(this,homeUserList,item.Account); + + var homeMovieList = new HomeMovieListPageViewModel(this,homeUser); + Movie movie = item.Media.ToObject() ?? new Movie(){Name=item.MediaName,ProperName = item.MediaProperName}; + MovieItem movieItem=new MovieItem(movie,await App.GetMovieThumbnailAsync(item.Username,item.MediaName)); + + var homeMovie = new HomeMoviePageViewModel(this,homeMovieList,item.Account,movieItem); + CurrentPage = new HomeMovieVideoPlayerViewModel(this,homeMovie,await App.Platform.DownloadFilesystem.OpenReadAsync(filename)); + break; + } + } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs index b610270..6c77401 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs @@ -6,24 +6,27 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using global::Avalonia.Controls; using Tesses.CMS.Avalonia.Models; using Tesses.CMS.Avalonia.Views.HomePages; using Tesses.CMS.Client; public partial class HomeMovieListPageViewModel : ViewModelBase, IBackable { - HomeUserPageViewModel homePage; - public HomeMovieListPageViewModel(HomeUserPageViewModel homePage) + HomePageViewModel homePage; + HomeUserPageViewModel userPage; + public HomeMovieListPageViewModel(HomePageViewModel homePage,HomeUserPageViewModel userPage) { App.Log("In HomeMovieListPageViewModel::ctor block begin"); this.homePage = homePage; + this.userPage = userPage; Task.Run(async()=>{ App.Log("In HomeMovieListPageViewModel::ctor::async block begin"); - await foreach(var item in App.Client.Movies.GetMoviesAsync(homePage.Username)) + await foreach(var item in App.Client.Movies.GetMoviesAsync(userPage.Username)) { try{ - _movies.Add(new MovieItem(item,await App.GetMovieThumbnailAsync(homePage.Username,item.Name))); + _movies.Add(new MovieItem(item,await App.GetMovieThumbnailAsync(userPage.Username,item.Name))); }catch(Exception ex) { App.Log(ex.ToString()); @@ -42,12 +45,15 @@ public partial class HomeMovieListPageViewModel : ViewModelBase, IBackable { if (value is null) return; SelectedListItem=null; + homePage.CurrentPage = new HomeMoviePageViewModel(homePage,this,userPage.Account,value); //homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); + + } public ViewModelBase Back() { - return homePage; + return userPage; } } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMoviePageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMoviePageViewModel.cs new file mode 100644 index 0000000..17e529d --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMoviePageViewModel.cs @@ -0,0 +1,139 @@ + + +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Newtonsoft.Json.Linq; +using ReactiveUI; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Avalonia.Views.HomePages; +using Tesses.CMS.Client; +namespace Tesses.CMS.Avalonia.ViewModels.HomePages; + +public partial class HomeMoviePageViewModel : ViewModelBase, IBackable +{ + HomePageViewModel home; + + HomeMovieListPageViewModel movieList; + + Movie movie; + + [ObservableProperty] + private IImage? _thumbnail=null; + + [ObservableProperty] + private string _movieTitle; + + [ObservableProperty] + private string _movieCreator; + [ObservableProperty] + private string _description; + [ObservableProperty] + private string _published; + + [ObservableProperty] + private string _modified; + + string? watchUrl=null; + + + [ObservableProperty] + private bool _canWatch=false; + + [ObservableProperty] + private bool _canDownload=false; + + [ObservableProperty] + + private string _addOrRemoveFromFavorites=""; + private bool _favoriteExists=false; + public bool FavoriteExists { + get=>_favoriteExists; + set{ + _favoriteExists = value; + AddOrRemoveFromFavorites = value ? "Remove from Favorites" : "Add to Favorites"; + } + } + + string username; + UserAccount account; + + FavoritesItem fav; + + public HomeMoviePageViewModel(HomePageViewModel home,HomeMovieListPageViewModel movieList,UserAccount account,MovieItem movie) + { + this.account = account; + username = account.Username; + this.home = home; + this.movie = movie.Movie; + this.movieList = movieList; + + this.Thumbnail = movie.Image; + this.MovieTitle = movie.Movie.ProperName; + this.MovieCreator = account.ProperName; + this.Description = movie.Movie.Description; + this.Modified = $"Last Modified: {movie.Movie.LastUpdated.ToShortDateString()}"; + this.Published = $"Published: {movie.Movie.CreationTime.ToShortDateString()}"; + fav=new FavoritesItem() + { + Type = FavoriteType.Movie, + Username = account.Username, + UserProperName = account.ProperName, + MediaName = movie.Movie.Name, + MediaProperName = movie.Movie.ProperName, + Account = account, + Media = JObject.FromObject(movie.Movie) + }; + FavoriteExists = App.FavoritesExists(fav); + + Task.Run(async()=>{ + var res=await App.Client.Movies.GetMovieContentMetadataAsync(username,movie.Movie.Name); + if(res.HasBrowserStream) + watchUrl = res.BrowserStream; + CanWatch = res.HasBrowserStream; + + CanDownload = res.HasDownloadStream; + + + }).Wait(0); + } + + + public ViewModelBase Back() + { + return movieList; + } + + [RelayCommand] + public void Watch() + { + if(!string.IsNullOrWhiteSpace(watchUrl)) + { + home.CurrentPage = new HomeMovieVideoPlayerViewModel(home,this,watchUrl); + } + } + [RelayCommand] + public void Download() + { + if(CanDownload) + App.StartDownload(account,movie); + } + + [RelayCommand] + public void AddToFavorites() + { + if(FavoriteExists) + { + App.RemoveFavorite(fav); + } + else + { + App.AddFavorite(fav); + } + FavoriteExists = !FavoriteExists; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieVideoPlayerViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieVideoPlayerViewModel.cs new file mode 100644 index 0000000..c942b33 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieVideoPlayerViewModel.cs @@ -0,0 +1,75 @@ + + +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using LibVLCSharp.Shared; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Avalonia.Views.HomePages; +using Tesses.CMS.Client; +namespace Tesses.CMS.Avalonia.ViewModels.HomePages; + +public partial class HomeMovieVideoPlayerViewModel : ViewModelBase, IBackable, IDisposable +{ + HomePageViewModel home; + + HomeMoviePageViewModel moviePage; + + LibVLC vlc=new LibVLC("--input-repeat=65535"); + + public HomeMovieVideoPlayerViewModel(HomePageViewModel home,HomeMoviePageViewModel moviePage, string url) + { + + this.home = home; + this.moviePage = moviePage; + + + + + Player=new MediaPlayer(new Media(vlc,url,FromType.FromLocation)); + + Player.Play(); + } + public HomeMovieVideoPlayerViewModel(HomePageViewModel home,HomeMoviePageViewModel moviePage, Stream strm) + { + this.Stream = strm; + this.home = home; + this.moviePage = moviePage; + + + LibVLC vlc=new LibVLC("--input-repeat=65535"); + + Player=new MediaPlayer(new Media(vlc,new StreamMediaInput(strm))); + + Player.Play(); + } + [ObservableProperty] + private MediaPlayer? _player; + + + public ViewModelBase Back() + { + + return moviePage; + } + Stream? Stream=null; + + public void Dispose() + { + + Thread thrd=new Thread(()=>{ + Player?.Stop(); + Thread.Sleep(10000); + Player?.Dispose(); + Stream?.Dispose(); + vlc.Dispose(); + }); + thrd.Start(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeShowListPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeShowListPageViewModel.cs new file mode 100644 index 0000000..156d4a2 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeShowListPageViewModel.cs @@ -0,0 +1,59 @@ +namespace Tesses.CMS.Avalonia.ViewModels.HomePages; + +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using global::Avalonia.Controls; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Avalonia.Views.HomePages; +using Tesses.CMS.Client; + +public partial class HomeShowListPageViewModel : ViewModelBase, IBackable +{ + HomePageViewModel homePage; + HomeUserPageViewModel userPage; + public HomeShowListPageViewModel(HomePageViewModel homePage,HomeUserPageViewModel userPage) + { + App.Log("In HomeShowListPageViewModel::ctor block begin"); + this.homePage = homePage; + this.userPage = userPage; + Task.Run(async()=>{ + App.Log("In HomeShowListPageViewModel::ctor::async block begin"); + + await foreach(var item in App.Client.Shows.GetShowsAsync(userPage.Username)) + { + try{ + _shows.Add(new ShowItem(item,await App.GetShowThumbnailAsync(userPage.Username,item.Name))); + }catch(Exception ex) + { + App.Log(ex.ToString()); + } + } + App.Log("In HomeShowListPageViewModel::ctor::async block end"); + }).Wait(0); + App.Log("In HomeShowListPageViewModel::ctor block end"); + } + + [ObservableProperty] + private ObservableCollection _shows=new ObservableCollection(); + [ObservableProperty] + private MovieItem? _selectedListItem; + partial void OnSelectedListItemChanged(MovieItem? value) + { + if (value is null) return; + SelectedListItem=null; + //homePage.CurrentPage = new HomeMoviePageViewModel(homePage,this,userPage.Account,value); + //homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); + + + } + + public ViewModelBase Back() + { + return userPage; + } + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs index 6a0eb4f..a1faf11 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs @@ -34,4 +34,6 @@ public partial class HomeUserListPageViewModel : ViewModelBase SelectedListItem=null; homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); } + + } \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs index dd77db7..a7693e0 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs @@ -28,6 +28,9 @@ public partial class HomeUserPageViewModel : ViewModelBase, IBackable public string Username => account.Username; + public string ProperName => account.ProperName; + + public UserAccount Account => account; [ObservableProperty] private ObservableCollection _userItems=new ObservableCollection(); @@ -36,7 +39,8 @@ public partial class HomeUserPageViewModel : ViewModelBase, IBackable this.homePage = homePage; this.userList = userList; this.account = account; - UserItems.Add(new UserPageItem("Movies",new HomeMovieListPageViewModel(this))); + UserItems.Add(new UserPageItem("Movies",new HomeMovieListPageViewModel(homePage,this))); + UserItems.Add(new UserPageItem("Shows",new HomeShowListPageViewModel(homePage,this))); } [ObservableProperty] private UserPageItem? _selectedListItem; diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs index 0f8c82a..7a6dd4b 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs @@ -1,5 +1,6 @@ namespace Tesses.CMS.Avalonia.ViewModels; +using System; using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; @@ -11,24 +12,46 @@ public partial class MainViewModel : ViewModelBase { public MainViewModel(string title) { + _pages.Add(new Page("Home",()=>new HomePageViewModel())); + _pages.Add(new Page("Favorites",()=>new FavoritesPageViewModel(this))); + _pages.Add(new Page("Notifications",()=>new NotificationsPageViewModel())); + _pages.Add(new Page("Downloads",()=>new DownloadsPageViewModel(this))); + _pages.Add(new Page("Video Player",()=>new VideoPlayerViewModel())); _pages.Add(new Page("Settings",()=>new SettingsPageViewModel(this))); SelectedListItem = Pages.First(); this.Title = title; + + if(Environment.GetCommandLineArgs().Length > 1) + { + string path = Environment.GetCommandLineArgs()[1]; + Setting = true; + SelectedListItem = _pages.First(e=>e.Name == "Video Player"); + CurrentPage = new VideoPlayerViewModel(path); + PaneOpen=false; + Setting = false; + } } [ObservableProperty] private bool _paneOpen=true; - [ObservableProperty] - private ViewModelBase _currentPage = new HomePageViewModel(); + private ViewModelBase _curPage=new HomePageViewModel(); + public ViewModelBase CurrentPage + { + get + { + return _curPage; + } + set{ + App.OpenedControls.Destroy(); + _curPage = value; + var disp = value as IDisposable; + if(disp != null) App.OpenedControls.Register(disp); + OnPropertyChanged(nameof(CurrentPage)); + + } + } [ObservableProperty] - private ObservableCollection _pages=new ObservableCollection(){ - new Page("Home",()=>new HomePageViewModel()), - new Page("Favorites",()=>new FavoritesPageViewModel()), - new Page("Notifications",()=>new NotificationsPageViewModel()), - new Page("Downloads",()=>new DownloadsPageViewModel()), - - }; - + private ObservableCollection _pages=new ObservableCollection(); [ObservableProperty] private Page? _selectedListItem; [ObservableProperty] @@ -37,10 +60,12 @@ public partial class MainViewModel : ViewModelBase [ObservableProperty] private string _title; + public bool Setting {get;set;} + partial void OnSelectedListItemChanged(Page? value) { if (value is null) return; - + if(Setting) return; @@ -57,4 +82,9 @@ public partial class MainViewModel : ViewModelBase { App.Log("Login button"); } + + internal void SetHome() + { + SelectedListItem = Pages[0]; + } } diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SavedDownloadViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SavedDownloadViewModel.cs new file mode 100644 index 0000000..65ef640 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SavedDownloadViewModel.cs @@ -0,0 +1,71 @@ +namespace Tesses.CMS.Avalonia.ViewModels; + +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using global::Avalonia.Threading; +using LibVLCSharp.Shared; +using ReactiveUI; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Client; +using Tesses.VirtualFilesystem; + +public partial class SavedDownloadViewModel : ViewModelBase +{ + FavoritesItem item; + MainViewModel model; + + public SavedDownloadViewModel(FavoritesItem item,MainViewModel model) + { + this.item=item; + this.model = model; + DownloadOrViewCommand = ReactiveCommand.CreateFromTask(DownloadOrViewAsync); + UpdateDownloadButton(); + } + public string Name => item.MediaProperName; + + public string AuthorName => item.UserProperName; + + [ObservableProperty] + private string _downloadBtnText = "Download"; + + bool downloaded=false; + + private void UpdateDownloadButton() + { + switch(item.Type) + { + case FavoriteType.Movie: + UnixPath dir = Special.Root / "Downloads" / item.Username / "Movies" / item.MediaName; + UnixPath filename = dir / item.MediaProperName + ".mp4"; + downloaded=(App.Platform.DownloadFilesystem?.FileExists(filename) ?? false); + DownloadBtnText=downloaded ? "View" : "Download"; + break; + } + } + + public IReactiveCommand DownloadOrViewCommand {get;} + + public async Task DownloadOrViewAsync() + { + UpdateDownloadButton(); + if(downloaded) + { + model.SetHome(); + HomePageViewModel hpvm=new (); + model.CurrentPage = hpvm; + await hpvm.PlayDownloadedAsync(item); + return; + } + switch(item.Type) + { + case FavoriteType.Movie: + App.StartDownload(item.Account,item.Media.ToObject() ?? new Movie(){Name=item.MediaName,ProperName=item.MediaProperName}); + break; + } + UpdateDownloadButton(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/VideoPlayerViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/VideoPlayerViewModel.cs new file mode 100644 index 0000000..ad41908 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/VideoPlayerViewModel.cs @@ -0,0 +1,120 @@ + + +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using LibVLCSharp.Shared; +using ReactiveUI; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Avalonia.Views; +using Tesses.CMS.Client; +namespace Tesses.CMS.Avalonia.ViewModels; + +public partial class VideoPlayerViewModel : ViewModelBase, IDisposable +{ + bool played=false; + + public VideoPlayerViewModel() + { + BrowseCommand = ReactiveCommand.CreateFromTask(BrowseAsync); + /* + + + LibVLC vlc=new LibVLC("--input-repeat=65535"); + + Player=new MediaPlayer(new Media(vlc,new StreamMediaInput(strm))); + + Player.Play();*/ + } + + public VideoPlayerViewModel(string path) + { + BrowseCommand = ReactiveCommand.CreateFromTask(BrowseAsync); + played=true; + Stream = File.OpenRead(path); + Thread thrd=new Thread(()=>{ + Thread.Sleep(1000); + Player=new MediaPlayer(new Media(vlc,new StreamMediaInput(Stream))); + + Player.Play(); + }); + thrd.Start(); + } + LibVLC vlc=new LibVLC("--input-repeat=65535"); + [ObservableProperty] + private MediaPlayer? _player; + + + + Stream? Stream=null; + + public void Dispose() + { + + Thread thrd=new Thread(()=>{ + Player?.Stop(); + Thread.Sleep(10000); + Player?.Dispose(); + Stream?.Dispose(); + vlc.Dispose(); + }); + thrd.Start(); + } + + public IReactiveCommand BrowseCommand {get;} + public async Task BrowseAsync() + { + if(App.Window != null) + { + await OpenAsync(App.Window); + } + if(App.MainView != null) + { + await OpenAsync(TopLevel.GetTopLevel(App.MainView)); + } + } + + private async Task OpenAsync(TopLevel? topLevel) + { + if(topLevel != null) + { + var res=new global::Avalonia.Platform.Storage.FilePickerOpenOptions(); + res.AllowMultiple=false; + res.Title = "Browse for video"; + + var first = (await topLevel.StorageProvider.OpenFilePickerAsync(res)).FirstOrDefault(); + if(first != null) + { + if(played) + { + var player = Player; + var strm = Stream; + Thread thrd=new Thread(()=>{ + player?.Stop(); + Thread.Sleep(10000); + player?.Dispose(); + strm?.Dispose(); + + }); + thrd.Start(); + } + played=true; + + + Stream = await first.OpenReadAsync(); + + Player=new MediaPlayer(new Media(vlc,new StreamMediaInput(Stream))); + + Player.Play(); + } + } + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs index 3b35673..1001f22 100644 --- a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs @@ -5,4 +5,5 @@ namespace Tesses.CMS.Avalonia.ViewModels; public class ViewModelBase : ObservableObject { + } diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/ActiveDownloadView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/ActiveDownloadView.axaml new file mode 100644 index 0000000..e80c79b --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/ActiveDownloadView.axaml @@ -0,0 +1,25 @@ + + + + + + + + +