Deb setup
ci/woodpecker/push/devel Pipeline was successful Details
ci/woodpecker/push/master Pipeline was successful Details
ci/woodpecker/tag/devel Pipeline was successful Details
ci/woodpecker/tag/master Pipeline failed Details

This commit is contained in:
Mike Nolan 2024-08-10 13:32:16 -05:00
parent b3ae68232d
commit fe83c39604
62 changed files with 1882 additions and 115 deletions

1
.gitignore vendored
View File

@ -484,3 +484,4 @@ $RECYCLE.BIN/
*.swp *.swp
help.txt help.txt
data/ data/
out/

8
.woodpecker/devel.yml Normal file
View File

@ -0,0 +1,8 @@
steps:
- name: devel
image: mcr.microsoft.com/dotnet/sdk:8.0
when:
event: push
branch: devel
commands:
- bash test.sh

11
.woodpecker/master.yml Normal file
View File

@ -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

4
README.md Normal file
View File

@ -0,0 +1,4 @@
TessesCMS
=========
> :warning: **THIS IS UNDER CONSTRUCTION** don't use this software yet

View File

@ -2,13 +2,61 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android.Media;
using Avalonia.Android;
using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using LibVLCSharp.Platforms.Android;
using Newtonsoft.Json; using Newtonsoft.Json;
using Tesses.VirtualFilesystem; using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions; using Tesses.VirtualFilesystem.Extensions;
using Tesses.VirtualFilesystem.Filesystems; using Tesses.VirtualFilesystem.Filesystems;
namespace Tesses.CMS.Avalonia.Android; 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 internal class MobilePlatform : IPlatform
{ {
string configpath; string configpath;
@ -39,7 +87,12 @@ internal class MobilePlatform : IPlatform
internal IVirtualFilesystem? virtualFilesystem; internal IVirtualFilesystem? virtualFilesystem;
public IVirtualFilesystem? DownloadFilesystem => 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() 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);
}
} }

View File

@ -19,7 +19,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Android" Version="$(AvaloniaVersion)" /> <PackageReference Include="Avalonia.Android" Version="$(AvaloniaVersion)" />
<PackageReference Include="LibVlcSharp" Version="3.8.5" />
<PackageReference Include="MimeTypesMap" Version="1.0.8" /> <PackageReference Include="MimeTypesMap" Version="1.0.8" />
<PackageReference Include="VideoLAN.LibVLC.Android" Version="3.5.3" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.1" /> <PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.1" />
</ItemGroup> </ItemGroup>

View File

@ -1,3 +1,5 @@
using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -34,7 +36,12 @@ internal class DesktopPlatform : IPlatform
IVirtualFilesystem? virtualFilesystem; IVirtualFilesystem? virtualFilesystem;
public IVirtualFilesystem? DownloadFilesystem => 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() public async Task BrowseForDownloadDirectoryAsync()
{ {
@ -75,21 +82,28 @@ internal class DesktopPlatform : IPlatform
public Control CreatePlayer() public Control CreatePlayer()
{ {
return new VideoPlayer(); return new VideoView();
} }
public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer) public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer)
{ {
var ctrl = control as VideoPlayer; var ctrl = control as VideoView;
if(ctrl != null) if(ctrl != null)
ctrl.MediaPlayer = mediaPlayer; ctrl.MediaPlayer = mediaPlayer;
} }
public MediaPlayer? GetMediaPlayer(Control control) public MediaPlayer? GetMediaPlayer(Control control)
{ {
var ctrl = control as VideoPlayer; var ctrl = control as VideoView;
if(ctrl != null) if(ctrl != null)
return ctrl.MediaPlayer; return ctrl.MediaPlayer;
return null; return null;
} }
public void LaunchUrl(string url)
{
Process p = new Process();
p.StartInfo.UseShellExecute=true;
p.StartInfo.FileName = url;
p.Start();
}
} }

View File

@ -6,12 +6,24 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<AssemblyName>tcms-desktop</AssemblyName>
<HomePage>https://tesses.net/apps/TessesCMS</HomePage>
<Maintainer>Mike Nolan &lt;tesses@tesses.net&gt;</Maintainer>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Content Include="tcms-desktop.desktop" CopyToPublishDirectory="PreserveNewest" LinuxFileMode="1755">
<LinuxPath>/usr/share/applications/tcms-desktop.desktop</LinuxPath>
</Content>
<Content Include="icon.png" CopyToPublishDirectory="PreserveNewest" LinuxFileMode="1755">
<LinuxPath>/usr/share/icons/tcms.png</LinuxPath>
</Content>
<DebDependency Include="libvlc-dev (&gt;= 2.2.2)" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" /> <PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@ -20,6 +32,10 @@
<PackageReference Include="Tesses.VirtualFilesystem" Version="1.0.2" /> <PackageReference Include="Tesses.VirtualFilesystem" Version="1.0.2" />
<PackageReference Include="Tesses.VirtualFilesystem.Base" Version="1.0.2" /> <PackageReference Include="Tesses.VirtualFilesystem.Base" Version="1.0.2" />
<PackageReference Include="Tesses.Virtualfilesystem.Local" Version="1.0.1" /> <PackageReference Include="Tesses.Virtualfilesystem.Local" Version="1.0.1" />
<PackageReference Include="Packaging.Targets">
<Version>0.1.226-*</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -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;

View File

@ -15,15 +15,23 @@ using Avalonia.Platform;
using Tesses.VirtualFilesystem; using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions; using Tesses.VirtualFilesystem.Extensions;
using System.IO; 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; namespace Tesses.CMS.Avalonia;
public partial class App : Application public partial class App : Application
{ {
public static OpenedCtrls OpenedControls {get;}=new OpenedCtrls();
public static IPlatform Platform {get;set;} = new NullPlatform(); public static IPlatform Platform {get;set;} = new NullPlatform();
public static MainWindow? Window {get;set;} public static MainWindow? Window {get;set;}
public static TessesCMSClient Client {get;} = new TessesCMSClient(); public static TessesCMSClient Client {get;} = new TessesCMSClient();
public static MainView? MainView {get;set;}
public override void Initialize() public override void Initialize()
{ {
@ -31,6 +39,20 @@ public partial class App : Application
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
public static List<ActiveDownload> Downloads {get;}=new List<ActiveDownload>();
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() public override void OnFrameworkInitializationCompleted()
{ {
Client.RootUrl = Platform.Configuration.ServerUrl; Client.RootUrl = Platform.Configuration.ServerUrl;
@ -46,11 +68,11 @@ public partial class App : Application
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{ {
MainView = new MainView
singleViewPlatform.MainView = new MainView
{ {
DataContext = new MainViewModel(title) DataContext = new MainViewModel(title)
}; };
singleViewPlatform.MainView = MainView;
} }
base.OnFrameworkInitializationCompleted(); 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"))); return new Bitmap(AssetLoader.Open(new Uri("avares://Tesses.CMS.Avalonia/Assets/no-media-icon.png")));
} }
internal static async Task<IImage> 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) 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<double>(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<FavoritesItem> items = new List<FavoritesItem>();
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<ActiveDownload> GetActiveDownloads()
{
lock(Downloads) {
return Downloads.ToList();
}
}
internal static IEnumerable<FavoritesItem> 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<List<FavoritesItem>>(text);
if(res != null)
foreach(var item in res)
yield return item;
}
internal static IEnumerable<FavoritesItem> 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<List<FavoritesItem>>(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<FavoritesItem> items = new List<FavoritesItem>();
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<FavoritesItem> items = new List<FavoritesItem>();
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<double>? ProgressChanged=null;
public event Action? Done = null;
public void UpdateProgress(double d)
{
Progress = d;
ProgressChanged?.Invoke(d);
}
public void SendDone()
{
Done?.Invoke();
}
public Func<Task> Start {get;set;}=async()=>{await Task.CompletedTask;};
}
public class OpenedCtrls
{
List<IDisposable> disposables=new List<IDisposable>();
public void Register(IDisposable disposable)
{
disposables.Add(disposable);
}
public void Destroy()
{
foreach(var item in disposables) item.Dispose();
disposables.Clear();
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 318 B

View File

@ -11,6 +11,9 @@ public interface IPlatform
bool PlatformUsesNormalPathsForDownload {get;} bool PlatformUsesNormalPathsForDownload {get;}
IVirtualFilesystem? DownloadFilesystem {get;} IVirtualFilesystem? DownloadFilesystem {get;}
bool CanTakeScreenShots { get; }
string ScreenshotPath { get; }
bool MustMoveScreenshot { get; }
Task WriteConfigurationAsync(); Task WriteConfigurationAsync();
@ -21,6 +24,8 @@ public interface IPlatform
void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer); void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer);
MediaPlayer? GetMediaPlayer(Control control); MediaPlayer? GetMediaPlayer(Control control);
void LaunchUrl(string url);
} }
@ -38,6 +43,12 @@ public class NullPlatform : IPlatform
public Configuration Configuration => _conf; 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() public async Task BrowseForDownloadDirectoryAsync()
{ {
await Task.CompletedTask; await Task.CompletedTask;
@ -67,4 +78,9 @@ public class NullPlatform : IPlatform
{ {
return null; return null;
} }
public void LaunchUrl(string url)
{
}
} }

View File

@ -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<FavoritesItem,Task> 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;}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
});
}
}

View File

@ -1,19 +1,55 @@
namespace Tesses.CMS.Avalonia.ViewModels; namespace Tesses.CMS.Avalonia.ViewModels;
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using LibVLCSharp.Shared; 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"); foreach(var item in App.EnumerateDownloads())
{
Player=new MediaPlayer(new Media(vlc,"https://tytdarchive.site.tesses.net/content/PreMuxed/PzUKeGZiEl0.mp4",FromType.FromLocation)); 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] [ObservableProperty]
private MediaPlayer? _player;
private ObservableCollection<ViewModelBaseItem> _downloads=new ObservableCollection<ViewModelBaseItem>();
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();
}
} }

View File

@ -1,8 +1,33 @@
namespace Tesses.CMS.Avalonia.ViewModels; namespace Tesses.CMS.Avalonia.ViewModels;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Models;
using Tesses.CMS.Avalonia.Views;
public partial class FavoritesPageViewModel : ViewModelBase 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<FavoritesPageItem> Favorites {get;}=new ObservableCollection<FavoritesPageItem>();
} }

View File

@ -1,17 +1,38 @@
namespace Tesses.CMS.Avalonia.ViewModels; namespace Tesses.CMS.Avalonia.ViewModels;
using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Models;
using Tesses.CMS.Avalonia.ViewModels.HomePages; using Tesses.CMS.Avalonia.ViewModels.HomePages;
using Tesses.CMS.Client;
using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions;
public partial class HomePageViewModel : ViewModelBase public partial class HomePageViewModel : ViewModelBase
{ {
public HomePageViewModel() 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] [RelayCommand]
private void Back() private void Back()
@ -26,4 +47,51 @@ public partial class HomePageViewModel : ViewModelBase
//can't go back //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<Movie>() ?? 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<Movie>() ?? 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;
}
}
} }

View File

@ -6,24 +6,27 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using global::Avalonia.Controls;
using Tesses.CMS.Avalonia.Models; using Tesses.CMS.Avalonia.Models;
using Tesses.CMS.Avalonia.Views.HomePages; using Tesses.CMS.Avalonia.Views.HomePages;
using Tesses.CMS.Client; using Tesses.CMS.Client;
public partial class HomeMovieListPageViewModel : ViewModelBase, IBackable public partial class HomeMovieListPageViewModel : ViewModelBase, IBackable
{ {
HomeUserPageViewModel homePage; HomePageViewModel homePage;
public HomeMovieListPageViewModel(HomeUserPageViewModel homePage) HomeUserPageViewModel userPage;
public HomeMovieListPageViewModel(HomePageViewModel homePage,HomeUserPageViewModel userPage)
{ {
App.Log("In HomeMovieListPageViewModel::ctor block begin"); App.Log("In HomeMovieListPageViewModel::ctor block begin");
this.homePage = homePage; this.homePage = homePage;
this.userPage = userPage;
Task.Run(async()=>{ Task.Run(async()=>{
App.Log("In HomeMovieListPageViewModel::ctor::async block begin"); 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{ 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) }catch(Exception ex)
{ {
App.Log(ex.ToString()); App.Log(ex.ToString());
@ -42,12 +45,15 @@ public partial class HomeMovieListPageViewModel : ViewModelBase, IBackable
{ {
if (value is null) return; if (value is null) return;
SelectedListItem=null; SelectedListItem=null;
homePage.CurrentPage = new HomeMoviePageViewModel(homePage,this,userPage.Account,value);
//homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); //homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value);
} }
public ViewModelBase Back() public ViewModelBase Back()
{ {
return homePage; return userPage;
} }
} }

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<ShowItem> _shows=new ObservableCollection<ShowItem>();
[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;
}
}

View File

@ -34,4 +34,6 @@ public partial class HomeUserListPageViewModel : ViewModelBase
SelectedListItem=null; SelectedListItem=null;
homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value);
} }
} }

View File

@ -28,6 +28,9 @@ public partial class HomeUserPageViewModel : ViewModelBase, IBackable
public string Username => account.Username; public string Username => account.Username;
public string ProperName => account.ProperName;
public UserAccount Account => account;
[ObservableProperty] [ObservableProperty]
private ObservableCollection<UserPageItem> _userItems=new ObservableCollection<UserPageItem>(); private ObservableCollection<UserPageItem> _userItems=new ObservableCollection<UserPageItem>();
@ -36,7 +39,8 @@ public partial class HomeUserPageViewModel : ViewModelBase, IBackable
this.homePage = homePage; this.homePage = homePage;
this.userList = userList; this.userList = userList;
this.account = account; 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] [ObservableProperty]
private UserPageItem? _selectedListItem; private UserPageItem? _selectedListItem;

View File

@ -1,5 +1,6 @@
namespace Tesses.CMS.Avalonia.ViewModels; namespace Tesses.CMS.Avalonia.ViewModels;
using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -11,24 +12,46 @@ public partial class MainViewModel : ViewModelBase
{ {
public MainViewModel(string title) 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))); _pages.Add(new Page("Settings",()=>new SettingsPageViewModel(this)));
SelectedListItem = Pages.First(); SelectedListItem = Pages.First();
this.Title = title; 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] [ObservableProperty]
private bool _paneOpen=true; private bool _paneOpen=true;
[ObservableProperty] private ViewModelBase _curPage=new HomePageViewModel();
private ViewModelBase _currentPage = 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] [ObservableProperty]
private ObservableCollection<Page> _pages=new ObservableCollection<Page>(){ private ObservableCollection<Page> _pages=new ObservableCollection<Page>();
new Page("Home",()=>new HomePageViewModel()),
new Page("Favorites",()=>new FavoritesPageViewModel()),
new Page("Notifications",()=>new NotificationsPageViewModel()),
new Page("Downloads",()=>new DownloadsPageViewModel()),
};
[ObservableProperty] [ObservableProperty]
private Page? _selectedListItem; private Page? _selectedListItem;
[ObservableProperty] [ObservableProperty]
@ -37,10 +60,12 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _title; private string _title;
public bool Setting {get;set;}
partial void OnSelectedListItemChanged(Page? value) partial void OnSelectedListItemChanged(Page? value)
{ {
if (value is null) return; if (value is null) return;
if(Setting) return;
@ -57,4 +82,9 @@ public partial class MainViewModel : ViewModelBase
{ {
App.Log("Login button"); App.Log("Login button");
} }
internal void SetHome()
{
SelectedListItem = Pages[0];
}
} }

View File

@ -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<Movie>() ?? new Movie(){Name=item.MediaName,ProperName=item.MediaProperName});
break;
}
UpdateDownloadButton();
}
}

View File

@ -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();
}
}
}
}

View File

@ -5,4 +5,5 @@ namespace Tesses.CMS.Avalonia.ViewModels;
public class ViewModelBase : ObservableObject public class ViewModelBase : ObservableObject
{ {
} }

View File

@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.ActiveDownloadView"
x:DataType="vm:ActiveDownloadViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:DownloadsPageViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding Name}" />
<Button Grid.Column="1" Content="Cancel" Command="{Binding CancelCommand}"/>
</Grid>
<ProgressBar Grid.Row="1" Margin="0 10" Height="20"
Minimum="0" Maximum="100" Value="{Binding Progress}"
Foreground="Red"
ShowProgressText="True"/>
</Grid>
</UserControl>

View File

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Tesses.CMS.Avalonia.Views;
public partial class ActiveDownloadView : UserControl
{
public ActiveDownloadView()
{
InitializeComponent();
}
}

View File

@ -12,6 +12,13 @@
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) --> to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:DownloadsPageViewModel /> <vm:DownloadsPageViewModel />
</Design.DataContext> </Design.DataContext>
<ScrollViewer>
<v:VideoPlayerWrapper MediaPlayer="{Binding Player}" /> <ItemsControl ItemsSource="{Binding Downloads}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TransitioningContentControl Content="{Binding Item}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl> </UserControl>

View File

@ -11,5 +11,20 @@
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) --> to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:FavoritesPageViewModel /> <vm:FavoritesPageViewModel />
</Design.DataContext> </Design.DataContext>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Favorites}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock FontSize="24" FontWeight="Bold" Grid.Column="0" Text="{Binding Name}" />
<Button Grid.Column="1" Content="View" Command="{Binding ViewCommand}"/>
</Grid>
<TextBlock Grid.Row="1" Text="{Binding AuthorName}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl> </UserControl>

View File

@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels.HomePages"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.HomePages.HomeMoviePageView"
x:DataType="vm:HomeMoviePageViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:HomeMoviePageViewModel />
</Design.DataContext>
<ScrollViewer>
<Grid RowDefinitions="Auto,Auto,*" Margin="20">
<Grid ColumnDefinitions="Auto,*" Grid.Row="0" Margin="0 0 0 20">
<Image Grid.Column="0" Height="107" Width="60" Source="{Binding Thumbnail}" />
<StackPanel Margin="20 0 0 0" Grid.Column="1" Orientation="Vertical">
<TextBlock FontSize="24" FontWeight="Bold" Text="{Binding MovieTitle}"/>
<TextBlock FontSize="18" Text="{Binding MovieCreator}"/>
<TextBlock FontSize="18" Text="{Binding Published}"/>
<TextBlock FontSize="18" Text="{Binding Modified}"/>
</StackPanel>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto">
<Button Grid.Column="0" Command="{Binding WatchCommand}" Content="Watch" IsVisible="{Binding CanWatch}"/>
<Button Grid.Column="1" Command="{Binding DownloadCommand}" Content="Download" IsVisible="{Binding CanDownload}"/>
<Button Grid.Column="2" Command="{Binding AddToFavoritesCommand}" Content="{Binding AddOrRemoveFromFavorites}"/>
</Grid>
<v:InlineText Margin="0 20 0 0" Grid.Row="2" Text="{Binding Description}" />
</Grid>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views.HomePages;
public partial class HomeMoviePageView : UserControl
{
public HomeMoviePageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels.HomePages"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.HomePages.HomeMovieVideoPlayerView"
x:DataType="vm:HomeMovieVideoPlayerViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:HomeMovieVideoPlayerViewModel />
</Design.DataContext>
<v:VideoPlayer MediaPlayer="{Binding Player}" />
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views.HomePages;
public partial class HomeMovieVideoPlayerView : UserControl
{
public HomeMovieVideoPlayerView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels.HomePages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.HomePages.HomeShowListPageView"
x:DataType="vm:HomeShowListPageViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:HomeMovieListPageViewModel />
</Design.DataContext>
<ListBox ItemsSource="{Binding Shows}" SelectedItem="{Binding SelectedListItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Margin="20" Height="107" Width="60" Source="{Binding Image}" />
<TextBlock Grid.Column="1" Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views.HomePages;
public partial class HomeShowListPageView : UserControl
{
public HomeShowListPageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,103 @@
using System;
using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Data;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Tesses.CMS.Avalonia.Views;
public class InlineText : WrapPanel
{
/// <summary>
/// MediaPlayer Data Bound property
/// </summary>
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<InlineText, string> TextProperty =
AvaloniaProperty.RegisterDirect<InlineText, string>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
string text="";
public string Text {
get => text;
set{
try{
text = value;
SetText();
}catch(Exception ex)
{
_=ex;
Console.WriteLine(ex);
}
}
}
private void SetText()
{
this.Children.Clear();
StringBuilder b = new StringBuilder();
for (int i = 0; i < text.Length; i++)
{
if (StartsWithAt(text, i, "http:") || StartsWithAt(text, i, "https:") || StartsWithAt(text, i, "ftp:") || StartsWithAt(text, i, "ftps:") || StartsWithAt(text, i, "sftp:") || StartsWithAt(text, i, "magnet:"))
{
if(b.Length > 0)
{
this.Children.Add(new TextBlock(){Text = b.ToString()});
b.Clear();
}
StringBuilder b2 = new StringBuilder();
for (; i < text.Length; i++)
{
if (text[i] == '\n' || text[i] == ' ')
{
b.Append(text[i]);
break;
}
b2.Append(text[i]);
}
var tb = new TextBlock(){Text = b2.ToString()};
tb.Foreground = new SolidColorBrush(Color.FromRgb(0,0,255));
tb.TextDecorations = TextDecorations.Underline;
tb.Tapped += (sender,e)=>{
App.LaunchUrl(b2.ToString());
};
this.Children.Add(tb);
}
else
{
b.Append(text[i]);
}
}
if(b.Length > 0)
{
this.Children.Add(new TextBlock(){Text = b.ToString()});
b.Clear();
}
}
private static bool StartsWithAt(string str, int indexof, string startsWith)
{
if (str.Length - indexof < startsWith.Length) return false;
for (int i = 0; i < startsWith.Length; i++)
{
if (str[i + indexof] != startsWith[i]) return false;
}
return true;
}
public InlineText()
{
}
}

View File

@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.SavedDownloadView"
x:DataType="vm:SavedDownloadViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:SavedDownloadViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,Auto">
<Grid RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock FontSize="24" FontWeight="Bold" Grid.Column="0" Text="{Binding Name}" />
<Button Grid.Column="1" Content="{Binding DownloadBtnText}" Command="{Binding DownloadOrViewCommand}"/>
</Grid>
<TextBlock Grid.Row="1" Text="{Binding AuthorName}" />
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views;
public partial class SavedDownloadView : UserControl
{
public SavedDownloadView()
{
InitializeComponent();
}
}

View File

@ -1,16 +1,51 @@
using System; using System;
using System.IO; using System.IO;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Threading; using Avalonia.Threading;
using LibVLCSharp.Avalonia;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using Tesses.CMS.Avalonia; using Tesses.CMS.Avalonia;
using Tesses.CMS.Avalonia.Views;
using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions;
namespace Tesses.CMS.Avalonia.Views;
public static class MMS
{
public static void MoveScreenshot(this IPlatform p,string path)
{
if(p.DownloadFilesystem == null) return;
p.DownloadFilesystem.CreateDirectory(Special.Root/"Screenshots");
using(var ms = File.OpenRead(path))
{
using(var f = p.DownloadFilesystem.OpenWrite(Special.Root/"Screenshots"/Path.GetFileName(path)))
{
ms.CopyTo(f);
}
}
File.Delete(path);
}
}
public class VideoPlayer : Grid public class VideoPlayer : Grid
{ {
/// <summary>
/// MediaPlayer Data Bound property
/// </summary>
/// <summary>
/// Defines the <see cref="MediaPlayer"/> property.
/// </summary>
public static readonly DirectProperty<VideoPlayer, MediaPlayer?> MediaPlayerProperty =
AvaloniaProperty.RegisterDirect<VideoPlayer, MediaPlayer?>(
nameof(MediaPlayer),
o => o.MediaPlayer,
(o, v) => o.MediaPlayer = v,
defaultBindingMode: BindingMode.TwoWay);
public VideoPlayer() public VideoPlayer()
{ {
view=new VideoView(); view=App.Platform.CreatePlayer();
@ -23,12 +58,19 @@ public class VideoPlayer : Grid
ss.Content = "SS"; ss.Content = "SS";
ss.Click += (sender,e)=>{ ss.Click += (sender,e)=>{
//take screenshot //take screenshot
string realDir=App.Platform.DownloadFilesystem == null ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : Path.Combine(App.Platform.Configuration.DownloadPath,"Screenshots"); App.Log($"CanTakeScreenShots: {App.Platform.CanTakeScreenShots}");
if(!App.Platform.CanTakeScreenShots) return;
string realDir =App.Platform.ScreenshotPath; //App.Platform.DownloadFilesystem == null ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : Path.Combine(App.Platform.Configuration.DownloadPath,"Screenshots");
App.Log($"ScreenshotPath: {App.Platform.ScreenshotPath}");
Directory.CreateDirectory(realDir); Directory.CreateDirectory(realDir);
if(MediaPlayer == null) return; if(MediaPlayer == null) return;
string name = Path.Combine(realDir,$"TessesCMS-Screenshot-{DateTime.Now.ToString("yyyyMMdd_HHmmss")}-{TimeSpan.FromMilliseconds(MediaPlayer.Position*MediaPlayer.Length).ToString().Replace(":","_")}.png"); string name = Path.Combine(realDir,$"TessesCMS-Screenshot-{DateTime.Now.ToString("yyyyMMdd_HHmmss")}-{TimeSpan.FromMilliseconds(MediaPlayer.Position*MediaPlayer.Length).ToString().Replace(":","_")}.png");
App.Log($"name: {name}");
MediaPlayer.TakeSnapshot(0,name,0,0); MediaPlayer.TakeSnapshot(0,name,0,0);
if(App.Platform.MustMoveScreenshot)
App.Platform.MoveScreenshot(name);
App.Log("Screenshot taken");
}; };
slider = new Slider(); slider = new Slider();
slider.Minimum = 0; slider.Minimum = 0;
@ -57,6 +99,8 @@ public class VideoPlayer : Grid
} }
} }
}; };
seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star)); seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
@ -71,23 +115,24 @@ public class VideoPlayer : Grid
} }
Slider slider; Slider slider;
Button playBtn; Button playBtn;
VideoView view; Control view;
public MediaPlayer? MediaPlayer public MediaPlayer? MediaPlayer
{ {
get=>view.MediaPlayer; get=>App.Platform.GetMediaPlayer(view);
set{ set{
if(view.MediaPlayer != null) var mp = App.Platform.GetMediaPlayer(view);
if(mp != null)
{ {
view.MediaPlayer.Paused -= Paused; mp.Paused -= Paused;
view.MediaPlayer.Playing -= Playing; mp.Playing -= Playing;
view.MediaPlayer.PositionChanged -= PositionChanged; mp.PositionChanged -= PositionChanged;
} }
view.MediaPlayer=value; App.Platform.SetMediaPlayer(view,value);
if(view.MediaPlayer != null) if(value != null)
{ {
view.MediaPlayer.Paused += Paused; value.Paused += Paused;
view.MediaPlayer.Playing += Playing; value.Playing += Playing;
view.MediaPlayer.PositionChanged += PositionChanged; value.PositionChanged += PositionChanged;
} }
} }
} }

View File

@ -0,0 +1,20 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Tesses.CMS.Avalonia.ViewModels"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.VideoPlayerView"
x:DataType="vm:VideoPlayerViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:VideoPlayerViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<Button Command="{Binding BrowseCommand}" Content="Open"/>
<v:VideoPlayer Grid.Row="1" MediaPlayer="{Binding Player}" />
</Grid>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views;
public partial class VideoPlayerView : UserControl
{
public VideoPlayerView()
{
InitializeComponent();
}
}

View File

@ -1,41 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using LibVLCSharp.Shared;
namespace Tesses.CMS.Avalonia.Views;
public class VideoPlayerWrapper : Grid
{
public VideoPlayerWrapper()
{
RowDefinitions.Add(new RowDefinition(GridLength.Star));
ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
Player= App.Platform.CreatePlayer();
this.Children.Add(Player);
}
public Control Player {get;}
public MediaPlayer? MediaPlayer
{
get=>App.Platform.GetMediaPlayer(Player);
set=>App.Platform.SetMediaPlayer(Player,value);
}
/// <summary>
/// MediaPlayer Data Bound property
/// </summary>
/// <summary>
/// Defines the <see cref="MediaPlayer"/> property.
/// </summary>
public static readonly DirectProperty<VideoPlayerWrapper, MediaPlayer?> MediaPlayerProperty =
AvaloniaProperty.RegisterDirect<VideoPlayerWrapper, MediaPlayer?>(
nameof(MediaPlayer),
o => o.MediaPlayer,
(o, v) => o.MediaPlayer = v,
defaultBindingMode: BindingMode.TwoWay);
}

View File

@ -7,13 +7,20 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="ReadLine" Version="2.0.1" /> <PackageReference Include="ReadLine" Version="2.0.1" />
<PackageReference Include="Packaging.Targets">
<Version>0.1.226-*</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>tcms-cli</AssemblyName>
<HomePage>https://tesses.net/apps/TessesCMS</HomePage>
<Maintainer>Mike Nolan &lt;tesses@tesses.net&gt;</Maintainer>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -151,6 +151,7 @@ namespace Tesses.CMS.Client
byte[] buffer=new byte[1024]; byte[] buffer=new byte[1024];
using(var srcStrm = await resp.Content.ReadAsStreamAsync()) using(var srcStrm = await resp.Content.ReadAsStreamAsync())
{
do { do {
if(token.IsCancellationRequested) return; if(token.IsCancellationRequested) return;
read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token); read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
@ -158,8 +159,9 @@ namespace Tesses.CMS.Client
await dest.WriteAsync(buffer,0,read,token); await dest.WriteAsync(buffer,0,read,token);
offset += read; offset += read;
if(total > 0) if(total > 0)
progress?.Report((double)offset / (double)read); progress?.Report((double)offset / (double)total);
} while(read>0); } while(read>0);
}
resp.Dispose(); resp.Dispose();
} }
public ShowClient Shows => new ShowClient(this); public ShowClient Shows => new ShowClient(this);
@ -356,7 +358,18 @@ namespace Tesses.CMS.Client
yield return new ShowWithSeasonsAndEpisodes(show,seasons); yield return new ShowWithSeasonsAndEpisodes(show,seasons);
} }
} }
public async Task<ShowContentMetaData> GetShowContentMetadataAsync(string user,string show)
{
return JsonConvert.DeserializeObject<ShowContentMetaData>(await client.client.GetStringAsync($"{client.rooturl.TrimEnd('/')}/api/v1/ShowFile?show={show}&user={user}&type=json"));
}
public async Task DownloadThumbnailAsync(string user,string show,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/show/{show}/thumbnail.jpg",dest,token,progress);
}
public async Task DownloadThumbnailAsync(string user,string show,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/show/{show}/thumbnail.jpg",dest,token,progress);
}
} }
public class MovieClient public class MovieClient

View File

@ -1,8 +1,38 @@
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic;
namespace Tesses.CMS.Client namespace Tesses.CMS.Client
{ {
public class ShowContentMetaData
{
[JsonProperty("show_info")]
public Show Info {get;set;}
[JsonProperty("has_show_torrent")]
public bool HasShowTorrent{get;set;}
[JsonProperty("show_torrent_url")]
public string ShowTorrentUrl {get;set;}
[JsonProperty("has_show_with_extras_torrent")]
public bool HasShowWithExtrasTorrent{get;set;}
[JsonProperty("show_with_extras_torrent_url")]
public string ShowWithExtrasTorrentUrl {get;set;}
[JsonProperty("has_poster")]
public bool HasPoster {get;set;}
[JsonProperty("poster_url")]
public string PosterUrl {get;set;}
[JsonProperty("has_thumbnail")]
public bool HasThumbnail {get;set;}
[JsonProperty("thumbnail_url")]
public string ThumbnailUrl {get;set;}
[JsonProperty("extra_streams")]
public List<ExtraDataStream> ExtraStreams {get;set;}=new List<ExtraDataStream>();
}
public class Show public class Show
{ {
[JsonProperty("proper_name")] [JsonProperty("proper_name")]
@ -17,4 +47,4 @@ namespace Tesses.CMS.Client
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;}=""; public string Description {get;set;}="";
} }
} }

View File

@ -5,14 +5,35 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>tcms-server</AssemblyName>
<HomePage>https://tesses.net/apps/TessesCMS</HomePage>
<Maintainer>Mike Nolan &lt;tesses@tesses.net&gt;</Maintainer>
<InstallService>true</InstallService>
<CreateUser>true</CreateUser>
<UserName>tcms-server</UserName>
<PostInstallScript>chown -R tcms-server:tcms-server /var/lib/tcms-server</PostInstallScript>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="PlaylistsNET" Version="1.4.0" /> <PackageReference Include="PlaylistsNET" Version="1.4.0" />
<PackageReference Include="Tesses.WebServer.EasyServer" Version="1.0.1" /> <PackageReference Include="Tesses.WebServer.EasyServer" Version="1.0.1" />
<PackageReference Include="Packaging.Targets">
<Version>0.1.226-*</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="tcms-server.service" CopyToPublishDirectory="PreserveNewest" LinuxFileMode="1755">
<LinuxPath>/etc/systemd/system/tcms-server.service</LinuxPath>
</Content>
<Content Include="config.json" CopyToPublishDirectory="PreserveNewest" LinuxFileMode="1755">
<LinuxPath>/var/lib/tcms-server/config.json</LinuxPath>
</Content>
<DebDependency Include="ffmpeg (&gt;= 7:2.8.6)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Tesses.CMS\Tesses.CMS.csproj" /> <ProjectReference Include="..\Tesses.CMS\Tesses.CMS.csproj" />
<ProjectReference Include="..\Tesses.CMS.Providers.LiteDb\Tesses.CMS.Providers.LiteDb.csproj" /> <ProjectReference Include="..\Tesses.CMS.Providers.LiteDb\Tesses.CMS.Providers.LiteDb.csproj" />

View File

@ -0,0 +1,14 @@
{
"Title": "YourBranding",
"Urls": [{"Url":"https://example.com", "Text":"Example Url"}],
"Root": "/",
"Email":{
"Host": "YourSMTPDomain",
"User": "YourUser",
"Pass": "YourEmailPassword",
"Port": 587,
"Encryption": "StartTls",
"Email": "YourUser@YourDomain"
},
"Publish": "NoRestriction"
}

View File

@ -0,0 +1,11 @@
[Unit]
Description=Tesses.CMS
[Service]
Type=simple
User=tcms-server
Group=tcms-server
ExecStart=/usr/local/bin/tcms-server "/var/lib/tcms-server"
[Install]
WantedBy=multi-user.target

View File

@ -23,9 +23,9 @@ namespace Tesses.CMS
[JsonIgnore] [JsonIgnore]
public long UserId {get;set;} public long UserId {get;set;}
[JsonProperty("creation_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}=DateTime.Now;
[JsonProperty("last_updated_time")] [JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;}=DateTime.Now;
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;} public string Description {get;set;}

View File

@ -22,9 +22,9 @@ namespace Tesses.CMS
[JsonIgnore] [JsonIgnore]
public long UserId {get;set;} public long UserId {get;set;}
[JsonProperty("creation_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}=DateTime.Now;
[JsonProperty("last_updated_time")] [JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;}=DateTime.Now;
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;}=""; public string Description {get;set;}="";

View File

@ -14,10 +14,10 @@ namespace Tesses.CMS
public string Name {get;set;}=""; public string Name {get;set;}="";
[JsonIgnore] [JsonIgnore]
public long UserId {get;set;} public long UserId {get;set;}
[JsonProperty("created_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}=DateTime.Now;
[JsonProperty("last_updated_time")] [JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;}=DateTime.Now;
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;}=""; public string Description {get;set;}="";

View File

@ -16,9 +16,9 @@ namespace Tesses.CMS
[JsonIgnore] [JsonIgnore]
public long UserId {get;set;} public long UserId {get;set;}
[JsonProperty("creation_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}=DateTime.Now;
[JsonProperty("last_updated_time")] [JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;}=DateTime.Now;
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;}=""; public string Description {get;set;}="";
[JsonProperty("source")] [JsonProperty("source")]

View File

@ -23,7 +23,7 @@ namespace Tesses.CMS
[JsonProperty("platforms")] [JsonProperty("platforms")]
public List<ProjectReleasePlatform> Platforms {get;set;}=new List<ProjectReleasePlatform>(); public List<ProjectReleasePlatform> Platforms {get;set;}=new List<ProjectReleasePlatform>();
[JsonProperty("creation_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}=DateTime.Now;
} }
public enum CPUArchitecture public enum CPUArchitecture
{ {

View File

@ -18,9 +18,9 @@ namespace Tesses.CMS
[JsonIgnore] [JsonIgnore]
public long UserId {get;set;} public long UserId {get;set;}
[JsonProperty("creation_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}=DateTime.Now;
[JsonProperty("last_updated_time")] [JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;}=DateTime.Now;
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;}=""; public string Description {get;set;}="";

View File

@ -15,9 +15,9 @@ namespace Tesses.CMS
[JsonIgnore] [JsonIgnore]
public long UserId {get;set;} public long UserId {get;set;}
[JsonProperty("creation_time")] [JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;} = DateTime.Now;
[JsonProperty("last_updated_time")] [JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;} = DateTime.Now;
[JsonProperty("description")] [JsonProperty("description")]
public string Description {get;set;}=""; public string Description {get;set;}="";

58
package.sh Normal file
View File

@ -0,0 +1,58 @@
#!/bin/bash
dotnet tool install --global dotnet-deb
. /deploy_dir/setpath.sh
ln -s "$DEPLOY_DIR" publish
cd Tesses.CMS.Server
dotnet deb -c Release -o out
cd out
for f in *.deb; do mv -- "$f" "${f%.deb}_all.deb";done
cp *.deb /pool/
cd ../
mkdir ../publish/server/linux-{x64,arm,arm64}
dotnet publish -c Release -o ../publish/server/linux-x64 -r linux-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/server/linux-arm -r linux-arm --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/server/linux-arm64 -r linux-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
mkdir ../publish/server/win-{x86,x64,arm64}
dotnet publish -c Release -o ../publish/server/win-x86 -r win-x86 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/server/win-x64 -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/server/win-arm64 -r win-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
mkdir ../publish/server/osx-{x64,arm64}
dotnet publish -c Release -o ../publish/server/osx-x64 -r osx-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/server/osx-arm64 -r osx-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
cd ../Tesses.CMS.Cli
dotnet deb -c Release -o out
cd out
for f in *.deb; do mv -- "$f" "${f%.deb}_all.deb";done
cp *.deb /pool/
cd ../
mkdir ../publish/cli/linux-{x64,arm,arm64}
dotnet publish -c Release -o ../publish/cli/linux-x64 -r linux-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/cli/linux-arm -r linux-arm --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/cli/linux-arm64 -r linux-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
mkdir ../publish/cli/win-{x86,x64,arm64}
dotnet publish -c Release -o ../publish/cli/win-x86 -r win-x86 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/cli/win-x64 -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/cli/win-arm64 -r win-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
mkdir ../publish/cli/osx-{x64,arm64}
dotnet publish -c Release -o ../publish/cli/osx-x64 -r osx-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../publish/cli/osx-arm64 -r osx-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
cd ../Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop
dotnet deb -c Release -o out
cd out
for f in *.deb; do mv -- "$f" "${f%.deb}_all.deb";done
cp *.deb /pool/
cd ../
mkdir ../../publish/desktop/linux-{x64,arm,arm64}
dotnet publish -c Release -o ../../publish/desktop/linux-x64 -r linux-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../../publish/desktop/linux-arm -r linux-arm --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../../publish/desktop/linux-arm64 -r linux-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
#mkdir ../../publish/desktop/win-{x86,x64,arm64}
#dotnet publish -c Release -o ../../publish/desktop/win-x86 -r win-x86 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
#dotnet publish -c Release -o ../../publish/desktop/win-x64 -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
#dotnet publish -c Release -o ../../publish/desktop/win-arm64 -r win-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
mkdir ../../publish/desktop/osx-{x64,arm64}
dotnet publish -c Release -o ../../publish/desktop/osx-x64 -r osx-x64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
dotnet publish -c Release -o ../../publish/desktop/osx-arm64 -r osx-arm64 --self-contained -p:PublishSingleFile=true -p:PublishReadyToRun=true
cd ../Tesses.CMS.Avalonia.Android
mkdir ../../publish/android
dotnet publish -c Release -o ../../publish/android

9
test.sh Normal file
View File

@ -0,0 +1,9 @@
#!/bin/bash
cd Tesses.CMS.Server
dotnet build
cd ../Tesses.CMS.Cli
dotnet build
cd ../Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop
dotnet build
cd ../Tesses.CMS.Avalonia.Android
dotnet build